diff --git a/functions/core/assert_contains_all.go b/functions/core/assert_contains_all.go new file mode 100644 index 00000000..7c5e378e --- /dev/null +++ b/functions/core/assert_contains_all.go @@ -0,0 +1,22 @@ +package core + +// AssertContainsAll verifica que haystack contiene todos los elementos de needles. +// Retorna (true, nil) si todos estan presentes, o (false, []string con los faltantes). +func AssertContainsAll(haystack, needles []string) (bool, []string) { + set := make(map[string]struct{}, len(haystack)) + for _, s := range haystack { + set[s] = struct{}{} + } + + var missing []string + for _, n := range needles { + if _, ok := set[n]; !ok { + missing = append(missing, n) + } + } + + if len(missing) == 0 { + return true, nil + } + return false, missing +} diff --git a/functions/core/assert_contains_all.md b/functions/core/assert_contains_all.md new file mode 100644 index 00000000..42f51f6e --- /dev/null +++ b/functions/core/assert_contains_all.md @@ -0,0 +1,53 @@ +--- +name: assert_contains_all +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func AssertContainsAll(haystack, needles []string) (bool, []string)" +description: "Verifica que el slice haystack contiene todos los elementos de needles. Retorna (true, nil) si todos estan presentes, o (false, missing) con los elementos faltantes." +tags: [testing, assert, slice, contains, pure] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: haystack + desc: "slice de strings donde se busca; es el conjunto completo a inspeccionar" + - name: needles + desc: "slice de strings que deben estar todos presentes en haystack" +output: "(ok bool, missing []string) — ok=true y missing=nil si todos los needles estan en haystack; ok=false y missing con los elementos ausentes si falta alguno" +tested: true +tests: + - "todos presentes retorna true y nil" + - "un elemento faltante retorna false con el faltante" + - "multiples faltantes retornan todos en missing" + - "needles vacio retorna true" + - "haystack vacio con needles no vacios retorna todos como faltantes" + - "duplicados en haystack no afectan el resultado" +test_file_path: "functions/core/assert_contains_all_test.go" +file_path: "functions/core/assert_contains_all.go" +--- + +## Ejemplo + +```go +ok, missing := AssertContainsAll( + []string{"a", "b", "c", "d"}, + []string{"a", "c"}, +) +// ok = true, missing = nil + +ok2, missing2 := AssertContainsAll( + []string{"a", "b"}, + []string{"a", "c", "d"}, +) +// ok2 = false, missing2 = ["c", "d"] +``` + +## Notas + +Funcion pura. Usa un mapa set para lookup O(1). El orden de los elementos en missing sigue el orden de needles. No elimina duplicados de needles. diff --git a/functions/core/assert_contains_all_test.go b/functions/core/assert_contains_all_test.go new file mode 100644 index 00000000..55fe4047 --- /dev/null +++ b/functions/core/assert_contains_all_test.go @@ -0,0 +1,58 @@ +package core + +import ( + "testing" +) + +func TestAssertContainsAll(t *testing.T) { + t.Run("todos presentes retorna true y nil", func(t *testing.T) { + ok, missing := AssertContainsAll([]string{"a", "b", "c"}, []string{"a", "b"}) + if !ok || missing != nil { + t.Errorf("got ok=%v missing=%v, want ok=true missing=nil", ok, missing) + } + }) + + t.Run("un elemento faltante retorna false con el faltante", func(t *testing.T) { + ok, missing := AssertContainsAll([]string{"a", "b"}, []string{"a", "c"}) + if ok { + t.Error("expected ok=false") + } + if len(missing) != 1 || missing[0] != "c" { + t.Errorf("got missing=%v, want [c]", missing) + } + }) + + t.Run("multiples faltantes retornan todos en missing", func(t *testing.T) { + ok, missing := AssertContainsAll([]string{"a"}, []string{"b", "c", "d"}) + if ok { + t.Error("expected ok=false") + } + if len(missing) != 3 { + t.Errorf("got missing=%v, want [b c d]", missing) + } + }) + + t.Run("needles vacio retorna true", func(t *testing.T) { + ok, missing := AssertContainsAll([]string{"a", "b"}, []string{}) + if !ok || missing != nil { + t.Errorf("got ok=%v missing=%v, want ok=true missing=nil", ok, missing) + } + }) + + t.Run("haystack vacio con needles no vacios retorna todos como faltantes", func(t *testing.T) { + ok, missing := AssertContainsAll([]string{}, []string{"x", "y"}) + if ok { + t.Error("expected ok=false") + } + if len(missing) != 2 { + t.Errorf("got missing=%v, want [x y]", missing) + } + }) + + t.Run("duplicados en haystack no afectan el resultado", func(t *testing.T) { + ok, missing := AssertContainsAll([]string{"a", "a", "b", "b"}, []string{"a", "b"}) + if !ok || missing != nil { + t.Errorf("got ok=%v missing=%v, want ok=true missing=nil", ok, missing) + } + }) +} diff --git a/functions/core/assert_json_equal.go b/functions/core/assert_json_equal.go new file mode 100644 index 00000000..90d50a8c --- /dev/null +++ b/functions/core/assert_json_equal.go @@ -0,0 +1,30 @@ +package core + +import ( + "encoding/json" + "fmt" + "reflect" +) + +// AssertJSONEqual compara dos payloads JSON por igualdad semantica. +// Retorna (true, "") si son equivalentes, o (false, diff) con una descripcion +// legible de la diferencia. No es sensible al orden de las claves en objetos. +func AssertJSONEqual(expected, actual []byte) (bool, string) { + var expVal, actVal any + + if err := json.Unmarshal(expected, &expVal); err != nil { + return false, fmt.Sprintf("expected JSON invalido: %v", err) + } + if err := json.Unmarshal(actual, &actVal); err != nil { + return false, fmt.Sprintf("actual JSON invalido: %v", err) + } + + if reflect.DeepEqual(expVal, actVal) { + return true, "" + } + + expPretty, _ := json.MarshalIndent(expVal, "", " ") + actPretty, _ := json.MarshalIndent(actVal, "", " ") + diff := fmt.Sprintf("expected:\n%s\n\nactual:\n%s", string(expPretty), string(actPretty)) + return false, diff +} diff --git a/functions/core/assert_json_equal.md b/functions/core/assert_json_equal.md new file mode 100644 index 00000000..0adf29a3 --- /dev/null +++ b/functions/core/assert_json_equal.md @@ -0,0 +1,54 @@ +--- +name: assert_json_equal +kind: function +lang: go +domain: core +version: "1.0.0" +purity: pure +signature: "func AssertJSONEqual(expected, actual []byte) (bool, string)" +description: "Compara dos payloads JSON por igualdad semantica. Insensible al orden de claves. Retorna (true, \"\") si son equivalentes o (false, diff) con descripcion legible de la diferencia." +tags: [testing, json, assert, diff, pure] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["encoding/json", "fmt", "reflect"] +params: + - name: expected + desc: "payload JSON esperado como slice de bytes" + - name: actual + desc: "payload JSON real a comparar contra el esperado" +output: "(ok bool, diff string) — ok=true si son semanticamente iguales, diff con formato legible si difieren" +tested: true +tests: + - "json identico retorna true y diff vacio" + - "objetos con claves en distinto orden son iguales" + - "valores distintos retorna false con diff" + - "json invalido en expected retorna false con mensaje de error" + - "json invalido en actual retorna false con mensaje de error" + - "arrays con mismo contenido son iguales" + - "arrays con distinto orden son distintos" +test_file_path: "functions/core/assert_json_equal_test.go" +file_path: "functions/core/assert_json_equal.go" +--- + +## Ejemplo + +```go +ok, diff := AssertJSONEqual( + []byte(`{"a":1,"b":2}`), + []byte(`{"b":2,"a":1}`), +) +// ok = true, diff = "" + +ok2, diff2 := AssertJSONEqual( + []byte(`{"a":1}`), + []byte(`{"a":2}`), +) +// ok2 = false, diff2 = "expected:\n{...}\n\nactual:\n{...}" +``` + +## Notas + +Funcion pura. Usa json.Unmarshal a `any` seguido de reflect.DeepEqual para comparacion semantica — no textual. El diff incluye ambos JSONs formateados con indentacion para facilitar la inspeccion manual. diff --git a/functions/core/assert_json_equal_test.go b/functions/core/assert_json_equal_test.go new file mode 100644 index 00000000..9727e868 --- /dev/null +++ b/functions/core/assert_json_equal_test.go @@ -0,0 +1,71 @@ +package core + +import ( + "testing" +) + +func TestAssertJSONEqual(t *testing.T) { + t.Run("json identico retorna true y diff vacio", func(t *testing.T) { + ok, diff := AssertJSONEqual([]byte(`{"a":1}`), []byte(`{"a":1}`)) + if !ok || diff != "" { + t.Errorf("got ok=%v diff=%q, want ok=true diff=\"\"", ok, diff) + } + }) + + t.Run("objetos con claves en distinto orden son iguales", func(t *testing.T) { + ok, diff := AssertJSONEqual( + []byte(`{"a":1,"b":2}`), + []byte(`{"b":2,"a":1}`), + ) + if !ok || diff != "" { + t.Errorf("got ok=%v diff=%q, want ok=true diff=\"\"", ok, diff) + } + }) + + t.Run("valores distintos retorna false con diff", func(t *testing.T) { + ok, diff := AssertJSONEqual([]byte(`{"a":1}`), []byte(`{"a":2}`)) + if ok { + t.Error("expected ok=false for different values") + } + if diff == "" { + t.Error("expected non-empty diff") + } + }) + + t.Run("json invalido en expected retorna false con mensaje de error", func(t *testing.T) { + ok, diff := AssertJSONEqual([]byte(`{not json}`), []byte(`{"a":1}`)) + if ok { + t.Error("expected ok=false for invalid expected JSON") + } + if diff == "" { + t.Error("expected error message in diff") + } + }) + + t.Run("json invalido en actual retorna false con mensaje de error", func(t *testing.T) { + ok, diff := AssertJSONEqual([]byte(`{"a":1}`), []byte(`{not json}`)) + if ok { + t.Error("expected ok=false for invalid actual JSON") + } + if diff == "" { + t.Error("expected error message in diff") + } + }) + + t.Run("arrays con mismo contenido son iguales", func(t *testing.T) { + ok, diff := AssertJSONEqual([]byte(`[1,2,3]`), []byte(`[1,2,3]`)) + if !ok || diff != "" { + t.Errorf("got ok=%v diff=%q, want ok=true diff=\"\"", ok, diff) + } + }) + + t.Run("arrays con distinto orden son distintos", func(t *testing.T) { + ok, diff := AssertJSONEqual([]byte(`[1,2,3]`), []byte(`[3,2,1]`)) + if ok { + t.Error("expected ok=false for different array order") + } + if diff == "" { + t.Error("expected non-empty diff for different arrays") + } + }) +}