diff --git a/go/both_ref_test.go b/go/both_ref_test.go new file mode 100644 index 0000000..ab1f525 --- /dev/null +++ b/go/both_ref_test.go @@ -0,0 +1,469 @@ +package jsonic + +import ( + "testing" +) + +// expectBothRef parses input with both MapRef and ListRef enabled and checks the result. +func expectBothRef(t *testing.T, input string, expected any) { + t.Helper() + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse(input) + if err != nil { + t.Errorf("Parse(%q) unexpected error: %v", input, err) + return + } + if !bothRefEqual(got, expected) { + t.Errorf("Parse(%q)\n got: %#v\n expected: %#v", + input, got, expected) + } +} + +// bothRefEqual compares values including both ListRef and MapRef structs. +func bothRefEqual(a, b any) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + + switch av := a.(type) { + case MapRef: + bv, ok := b.(MapRef) + if !ok { + return false + } + if av.Implicit != bv.Implicit { + return false + } + if len(av.Val) != len(bv.Val) { + return false + } + for k, v := range av.Val { + bval, exists := bv.Val[k] + if !exists || !bothRefEqual(v, bval) { + return false + } + } + return true + case ListRef: + bv, ok := b.(ListRef) + if !ok { + return false + } + if av.Implicit != bv.Implicit { + return false + } + if len(av.Val) != len(bv.Val) { + return false + } + for i := range av.Val { + if !bothRefEqual(av.Val[i], bv.Val[i]) { + return false + } + } + return true + case map[string]any: + bv, ok := b.(map[string]any) + if !ok || len(av) != len(bv) { + return false + } + for k, v := range av { + bval, exists := bv[k] + if !exists || !bothRefEqual(v, bval) { + return false + } + } + return true + case []any: + bv, ok := b.([]any) + if !ok || len(av) != len(bv) { + return false + } + for i := range av { + if !bothRefEqual(av[i], bv[i]) { + return false + } + } + return true + case float64: + bv, ok := b.(float64) + return ok && av == bv + case bool: + bv, ok := b.(bool) + return ok && av == bv + case string: + bv, ok := b.(string) + return ok && av == bv + default: + return false + } +} + +// Shorthand helpers for combined tests. +// blr creates a ListRef (reuses lr name pattern). +func blr(implicit bool, vals ...any) ListRef { + if vals == nil { + vals = []any{} + } + return ListRef{Val: vals, Implicit: implicit} +} + +// bmr creates a MapRef from key-value pairs. +func bmr(implicit bool, pairs ...any) MapRef { + m := make(map[string]any) + for i := 0; i+1 < len(pairs); i += 2 { + k, _ := pairs[i].(string) + m[k] = pairs[i+1] + } + return MapRef{Val: m, Implicit: implicit} +} + +// --- Basic combined wrapping --- + +func TestBothRefExplicitMapExplicitList(t *testing.T) { + // {a:[1,2]} → explicit MapRef wrapping explicit ListRef + expectBothRef(t, "{a:[1,2]}", bmr(false, "a", blr(false, 1.0, 2.0))) +} + +func TestBothRefImplicitMapExplicitList(t *testing.T) { + // a:[1,2] → implicit MapRef wrapping explicit ListRef + expectBothRef(t, "a:[1,2]", bmr(true, "a", blr(false, 1.0, 2.0))) +} + +func TestBothRefExplicitListExplicitMaps(t *testing.T) { + // [{a:1},{b:2}] → explicit ListRef of explicit MapRefs + expectBothRef(t, "[{a:1},{b:2}]", blr(false, + bmr(false, "a", 1.0), + bmr(false, "b", 2.0), + )) +} + +func TestBothRefImplicitListExplicitMaps(t *testing.T) { + // {a:1},{b:2} → comma creates implicit ListRef of explicit MapRefs + expectBothRef(t, "{a:1},{b:2}", blr(true, + bmr(false, "a", 1.0), + bmr(false, "b", 2.0), + )) +} + +func TestBothRefSpaceSeparatedMaps(t *testing.T) { + // {a:1} {b:2} → space creates implicit ListRef of explicit MapRefs + expectBothRef(t, "{a:1} {b:2}", blr(true, + bmr(false, "a", 1.0), + bmr(false, "b", 2.0), + )) +} + +// --- Empty structures --- + +func TestBothRefEmptyMapInList(t *testing.T) { + // [{}] → explicit ListRef containing empty explicit MapRef + expectBothRef(t, "[{}]", blr(false, bmr(false))) +} + +func TestBothRefEmptyListInMap(t *testing.T) { + // {a:[]} → explicit MapRef containing empty explicit ListRef + expectBothRef(t, "{a:[]}", bmr(false, "a", blr(false))) +} + +func TestBothRefEmptyMapAndListValues(t *testing.T) { + // {a:[],b:{}} → explicit MapRef with mixed empty values + expectBothRef(t, "{a:[],b:{}}", bmr(false, "a", blr(false), "b", bmr(false))) +} + +// --- Deep nesting --- + +func TestBothRefTripleNestingMapListMap(t *testing.T) { + // {a:[{b:1}]} → MapRef > ListRef > MapRef + expectBothRef(t, "{a:[{b:1}]}", bmr(false, + "a", blr(false, bmr(false, "b", 1.0)), + )) +} + +func TestBothRefTripleNestingListMapList(t *testing.T) { + // [{a:[1,2]}] → ListRef > MapRef > ListRef + expectBothRef(t, "[{a:[1,2]}]", blr(false, + bmr(false, "a", blr(false, 1.0, 2.0)), + )) +} + +func TestBothRefQuadNesting(t *testing.T) { + // {a:[{b:[1]}]} → MapRef > ListRef > MapRef > ListRef + expectBothRef(t, "{a:[{b:[1]}]}", bmr(false, + "a", blr(false, + bmr(false, "b", blr(false, 1.0)), + ), + )) +} + +// --- Path dive --- + +func TestBothRefPathDive(t *testing.T) { + // a:b:1 → implicit MapRef(a: implicit MapRef(b: 1)) + expectBothRef(t, "a:b:1", bmr(true, "a", bmr(true, "b", 1.0))) +} + +func TestBothRefPathDiveWithList(t *testing.T) { + // a:b:[1,2] → implicit MapRef(a: implicit MapRef(b: explicit ListRef)) + expectBothRef(t, "a:b:[1,2]", bmr(true, "a", bmr(true, "b", blr(false, 1.0, 2.0)))) +} + +func TestBothRefExplicitPathDive(t *testing.T) { + // {a:b:1} → explicit MapRef(a: implicit MapRef(b: 1)) + expectBothRef(t, "{a:b:1}", bmr(false, "a", bmr(true, "b", 1.0))) +} + +// --- Deep merge with both --- + +func TestBothRefDeepMergeNestedMapLists(t *testing.T) { + // a:[{b:1}],a:[{b:2}] → deep merge: ListRef arrays merge, inner MapRef maps merge + expectBothRef(t, "a:[{b:1}],a:[{b:2}]", bmr(true, + "a", blr(false, bmr(false, "b", 2.0)), + )) +} + +func TestBothRefDeepMergeNestedMaps(t *testing.T) { + // a:{b:1},a:{c:2} → deep merge: inner MapRefs merge + expectBothRef(t, "a:{b:1},a:{c:2}", bmr(true, + "a", bmr(false, "b", 1.0, "c", 2.0), + )) +} + +func TestBothRefDeepMergeMapsWithListValues(t *testing.T) { + // a:{b:[1]},a:{b:[2]} → deep merge: maps merge, then list values merge + expectBothRef(t, "a:{b:[1]},a:{b:[2]}", bmr(true, + "a", bmr(false, "b", blr(false, 2.0)), + )) +} + +// --- Mixed content in same structure --- + +func TestBothRefMapWithMixedValues(t *testing.T) { + // {a:{x:1},b:[1,2],c:3} → MapRef with MapRef, ListRef, and scalar values + expectBothRef(t, "{a:{x:1},b:[1,2],c:3}", bmr(false, + "a", bmr(false, "x", 1.0), + "b", blr(false, 1.0, 2.0), + "c", 3.0, + )) +} + +func TestBothRefListWithMixedValues(t *testing.T) { + // [{a:1},[1,2],3] → ListRef with MapRef, ListRef, and scalar values + expectBothRef(t, "[{a:1},[1,2],3]", blr(false, + bmr(false, "a", 1.0), + blr(false, 1.0, 2.0), + 3.0, + )) +} + +// --- Implicit structures combined --- + +func TestBothRefImplicitListOfImplicitListsInMaps(t *testing.T) { + // {a:1} {b:2} → implicit ListRef of explicit MapRefs (already tested above, but + // verify types more explicitly) + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse("{a:1} {b:2}") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + outerList, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef at top level, got %T: %#v", got, got) + } + if !outerList.Implicit { + t.Errorf("expected top-level ListRef to be implicit") + } + if len(outerList.Val) != 2 { + t.Fatalf("expected 2 elements, got %d", len(outerList.Val)) + } + for i, expectedKey := range []string{"a", "b"} { + m, ok := outerList.Val[i].(MapRef) + if !ok { + t.Errorf("element %d: expected MapRef, got %T", i, outerList.Val[i]) + continue + } + if m.Implicit { + t.Errorf("element %d: expected explicit MapRef (braces)", i) + } + if _, exists := m.Val[expectedKey]; !exists { + t.Errorf("element %d: expected key %q", i, expectedKey) + } + } +} + +// --- Scalars still pass through unchanged --- + +func TestBothRefScalarNumber(t *testing.T) { + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse("42") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f, ok := got.(float64); !ok || f != 42.0 { + t.Errorf("expected 42.0, got %T: %#v", got, got) + } +} + +func TestBothRefScalarString(t *testing.T) { + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse(`"hello"`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s, ok := got.(string); !ok || s != "hello" { + t.Errorf("expected \"hello\", got %T: %#v", got, got) + } +} + +func TestBothRefScalarBool(t *testing.T) { + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse("true") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if b, ok := got.(bool); !ok || b != true { + t.Errorf("expected true, got %T: %#v", got, got) + } +} + +func TestBothRefScalarNull(t *testing.T) { + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse("null") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != nil { + t.Errorf("expected nil, got %T: %#v", got, got) + } +} + +// --- Pair in list --- + +func TestBothRefPairInList(t *testing.T) { + // [a:1,b:2] → explicit ListRef with key-value properties + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse("[a:1,b:2]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + outerList, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef, got %T: %#v", got, got) + } + if outerList.Implicit { + t.Errorf("expected explicit ListRef (brackets)") + } +} + +// --- JSON compat with both --- + +func TestBothRefStrictJSON(t *testing.T) { + // Standard JSON: {"a": [1, 2], "b": {"c": true}} + expectBothRef(t, `{"a": [1, 2], "b": {"c": true}}`, bmr(false, + "a", blr(false, 1.0, 2.0), + "b", bmr(false, "c", true), + )) +} + +func TestBothRefNestedJSONArrays(t *testing.T) { + // {"a": [[1], [2]]} → nested ListRefs inside MapRef + expectBothRef(t, `{"a": [[1], [2]]}`, bmr(false, + "a", blr(false, blr(false, 1.0), blr(false, 2.0)), + )) +} + +// --- Implicit list containing maps with list values --- + +func TestBothRefImplicitListMapsWithLists(t *testing.T) { + // {a:[1]} {b:[2]} → implicit ListRef of explicit MapRefs with ListRef values + expectBothRef(t, "{a:[1]} {b:[2]}", blr(true, + bmr(false, "a", blr(false, 1.0)), + bmr(false, "b", blr(false, 2.0)), + )) +} + +// --- Deep merge preserves Ref wrappers through merge --- + +func TestBothRefDeepMergePreservesMapRef(t *testing.T) { + // {a:{x:1}},{a:{y:2}} → implicit list but deep merge combines the maps + // Actually this is an implicit list of two maps, deep merge happens on dup keys inside each + // Let me use explicit map: {a:{x:1},a:{y:2}} + expectBothRef(t, "{a:{x:1},a:{y:2}}", bmr(false, + "a", bmr(false, "x", 1.0, "y", 2.0), + )) +} + +func TestBothRefDeepMergePreservesListRef(t *testing.T) { + // {a:[1,2],a:[3]} → deep merge of ListRef values inside MapRef + expectBothRef(t, "{a:[1,2],a:[3]}", bmr(false, + "a", blr(false, 3.0, 2.0), + )) +} + +// --- All three options combined --- + +func TestBothRefWithTextInfo(t *testing.T) { + // All three options: MapRef + ListRef + TextInfo + j := Make(Options{ + MapRef: boolPtr(true), + ListRef: boolPtr(true), + TextInfo: boolPtr(true), + }) + got, err := j.Parse(`{a:["x"]}`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m, ok := got.(MapRef) + if !ok { + t.Fatalf("expected MapRef, got %T: %#v", got, got) + } + if m.Implicit { + t.Errorf("expected explicit MapRef") + } + listVal, ok := m.Val["a"].(ListRef) + if !ok { + t.Fatalf("expected ListRef for key 'a', got %T: %#v", m.Val["a"], m.Val["a"]) + } + if listVal.Implicit { + t.Errorf("expected explicit ListRef") + } + if len(listVal.Val) != 1 { + t.Fatalf("expected 1 element, got %d", len(listVal.Val)) + } + txt, ok := listVal.Val[0].(Text) + if !ok { + t.Fatalf("expected Text, got %T: %#v", listVal.Val[0], listVal.Val[0]) + } + if txt.Str != "x" || txt.Quote != `"` { + t.Errorf("expected Text{Quote:`\"`, Str:\"x\"}, got %#v", txt) + } +} + +// --- Comma-separated explicit lists create implicit outer list --- + +func TestBothRefCommaSeparatedLists(t *testing.T) { + // [1],[2] → implicit ListRef of explicit ListRefs + expectBothRef(t, "[1],[2]", blr(true, + blr(false, 1.0), + blr(false, 2.0), + )) +} + +// --- Null values in combined structures --- + +func TestBothRefNullMapValue(t *testing.T) { + // {a:null} → explicit MapRef with null value + expectBothRef(t, "{a:null}", bmr(false, "a", nil)) +} + +func TestBothRefNullListElements(t *testing.T) { + // [null,{a:1}] → ListRef with null and MapRef + expectBothRef(t, "[null,{a:1}]", blr(false, nil, bmr(false, "a", 1.0))) +} + +func TestBothRefMapWithNullAndList(t *testing.T) { + // {a:null,b:[1]} → MapRef with null value and ListRef value + expectBothRef(t, "{a:null,b:[1]}", bmr(false, "a", nil, "b", blr(false, 1.0))) +} diff --git a/go/feature_tsv_test.go b/go/feature_tsv_test.go new file mode 100644 index 0000000..2490548 --- /dev/null +++ b/go/feature_tsv_test.go @@ -0,0 +1,227 @@ +package jsonic + +import ( + "encoding/json" + "fmt" + "path/filepath" + "testing" +) + +// stripRefs recursively unwraps ListRef, MapRef, and Text back to plain +// Go values ([]any, map[string]any, string) so they can be compared against +// JSON-unmarshaled expected values from TSV files. +func stripRefs(v any) any { + if v == nil { + return nil + } + switch val := v.(type) { + case ListRef: + result := make([]any, len(val.Val)) + for i, elem := range val.Val { + result[i] = stripRefs(elem) + } + return result + case MapRef: + result := make(map[string]any) + for k, elem := range val.Val { + result[k] = stripRefs(elem) + } + return result + case Text: + return val.Str + case map[string]any: + result := make(map[string]any) + for k, elem := range val { + result[k] = stripRefs(elem) + } + return result + case []any: + result := make([]any, len(val)) + for i, elem := range val { + result[i] = stripRefs(elem) + } + return result + default: + return v + } +} + +// --- Standard parser TSV runner with custom options --- + +// runParserTSV runs a standard 2-column TSV (input, expected) with a custom parser. +func runParserTSV(t *testing.T, file string, j *Jsonic) { + t.Helper() + path := filepath.Join(specDir(), file) + rows, err := loadTSV(path) + if err != nil { + t.Fatalf("failed to load %s: %v", file, err) + } + + for _, row := range rows { + if len(row.cols) < 2 { + continue + } + input := row.cols[0] + expectedStr := row.cols[1] + + expected, err := parseExpected(expectedStr) + if err != nil { + t.Errorf("line %d: failed to parse expected %q: %v", row.lineNo, expectedStr, err) + continue + } + + got, err := j.Parse(preprocessEscapes(input)) + if err != nil { + t.Errorf("line %d: Parse(%q) error: %v", row.lineNo, input, err) + continue + } + + // Strip ListRef/MapRef/Text wrappers for JSON comparison. + gotPlain := stripRefs(got) + if !valuesEqual(gotPlain, expected) { + t.Errorf("line %d: Parse(%q)\n got: %s\n expected: %s", + row.lineNo, input, formatValue(gotPlain), formatValue(expected)) + } + } +} + +// --- List-child TSV runner (3-column: input, expected_array, expected_child) --- + +// runListChildTSV runs a list-child TSV file. +func runListChildTSV(t *testing.T, file string, j *Jsonic) { + t.Helper() + path := filepath.Join(specDir(), file) + rows, err := loadTSV(path) + if err != nil { + t.Fatalf("failed to load %s: %v", file, err) + } + + for _, row := range rows { + if len(row.cols) < 2 { + continue + } + input := row.cols[0] + expectedArrStr := row.cols[1] + expectedChildStr := "" + if len(row.cols) >= 3 { + expectedChildStr = row.cols[2] + } + + expectedArr, err := parseExpected(expectedArrStr) + if err != nil { + t.Errorf("line %d: failed to parse expected_array %q: %v", row.lineNo, expectedArrStr, err) + continue + } + + var expectedChild any + if expectedChildStr != "" { + expectedChild, err = parseExpected(expectedChildStr) + if err != nil { + t.Errorf("line %d: failed to parse expected_child %q: %v", row.lineNo, expectedChildStr, err) + continue + } + } + + got, err := j.Parse(preprocessEscapes(input)) + if err != nil { + t.Errorf("line %d: Parse(%q) error: %v", row.lineNo, input, err) + continue + } + + lr, ok := got.(ListRef) + if !ok { + t.Errorf("line %d: Parse(%q) expected ListRef, got %T: %s", + row.lineNo, input, got, formatValue(got)) + continue + } + + // Compare Val (strip inner refs for JSON comparison). + gotArr := stripRefs(lr.Val) + if !valuesEqual(gotArr, expectedArr) { + t.Errorf("line %d: Parse(%q) Val\n got: %s\n expected: %s", + row.lineNo, input, formatValue(gotArr), formatValue(expectedArr)) + } + + // Compare Child. + gotChild := stripRefs(lr.Child) + if !valuesEqual(gotChild, expectedChild) { + t.Errorf("line %d: Parse(%q) Child\n got: %s\n expected: %s", + row.lineNo, input, formatValue(gotChild), formatValue(expectedChild)) + } + } +} + +// --- feature-list-child.tsv --- + +func TestTSVFeatureListChild(t *testing.T) { + j := Make(Options{List: &ListOptions{Child: boolPtr(true)}}) + runListChildTSV(t, "feature-list-child.tsv", j) +} + +// --- feature-list-child-deep.tsv --- + +func TestTSVFeatureListChildDeep(t *testing.T) { + j := Make(Options{List: &ListOptions{Child: boolPtr(true)}}) + runListChildTSV(t, "feature-list-child-deep.tsv", j) +} + +// --- feature-list-child-pair.tsv --- + +func TestTSVFeatureListChildPair(t *testing.T) { + j := Make(Options{List: &ListOptions{ + Child: boolPtr(true), + Pair: boolPtr(true), + }}) + runListChildTSV(t, "feature-list-child-pair.tsv", j) +} + +// --- feature-list-child-pair-deep.tsv --- + +func TestTSVFeatureListChildPairDeep(t *testing.T) { + j := Make(Options{List: &ListOptions{ + Child: boolPtr(true), + Pair: boolPtr(true), + }}) + runListChildTSV(t, "feature-list-child-pair-deep.tsv", j) +} + +// --- feature-list-pair.tsv --- + +func TestTSVFeatureListPair(t *testing.T) { + j := Make(Options{List: &ListOptions{Pair: boolPtr(true)}}) + runParserTSV(t, "feature-list-pair.tsv", j) +} + +// --- feature-map-child.tsv --- + +func TestTSVFeatureMapChild(t *testing.T) { + j := Make(Options{Map: &MapOptions{Child: boolPtr(true)}}) + runParserTSV(t, "feature-map-child.tsv", j) +} + +// --- feature-map-child-deep.tsv --- + +func TestTSVFeatureMapChildDeep(t *testing.T) { + // Deep tests combine map.child with list.child for nested structures. + j := Make(Options{ + Map: &MapOptions{Child: boolPtr(true)}, + List: &ListOptions{Child: boolPtr(true)}, + }) + runParserTSV(t, "feature-map-child-deep.tsv", j) +} + +// --- Verify formatValue handles ListRef for debugging --- + +func TestStripRefsBasic(t *testing.T) { + lr := ListRef{ + Val: []any{MapRef{Val: map[string]any{"a": 1.0}, Implicit: false}, "hello"}, + Implicit: true, + Child: Text{Str: "world", Quote: `"`}, + } + got := stripRefs(lr) + expected := []any{map[string]any{"a": 1.0}, "hello"} + if !valuesEqual(got, expected) { + b, _ := json.Marshal(got) + t.Errorf("stripRefs: got %s, expected %s", string(b), fmt.Sprintf("%v", expected)) + } +} diff --git a/go/grammar.go b/go/grammar.go index b50dcd2..1cd9aae 100644 --- a/go/grammar.go +++ b/go/grammar.go @@ -189,6 +189,20 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { }, } + // BC callbacks: + mapSpec.BC = []StateAction{ + // Wrap map in MapRef if option is enabled. + func(r *Rule, ctx *Context) { + _ = ctx + if cfg.MapRef { + implicit := !(r.O0 != NoToken && r.O0.Tin == TinOB) + if m, ok := r.Node.(map[string]any); ok { + r.Node = MapRef{Val: m, Implicit: implicit} + } + } + }, + } + // map.Open ordering (after Jsonic unshift + append): // [0] OB ZZ auto-close (Jsonic, unshifted) // [1] OB CB empty map (JSON) @@ -263,7 +277,11 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { if cfg.ListRef { implicit := !(r.O0 != NoToken && r.O0.Tin == TinOS) if arr, ok := r.Node.([]any); ok { - r.Node = ListRef{Val: arr, Implicit: implicit} + var child any + if c, ok := r.U["child$"]; ok { + child = c + } + r.Node = ListRef{Val: arr, Implicit: implicit, Child: child} } } }, @@ -318,17 +336,45 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { pairval(r, ctx) } }, + // Jsonic phase: map.child - bare colon :value stores as child$ key + func(r *Rule, ctx *Context) { + _ = ctx + if childFlag, ok := r.U["child"]; !ok || childFlag != true { + return + } + val := r.Child.Node + if IsUndefined(val) { + val = nil + } + prev, hasPrev := nodeMapGet(r.Node, "child$") + if !hasPrev { + nodeMapSet(r.Node, "child$", val) + } else if cfg.MapExtend { + nodeMapSet(r.Node, "child$", Deep(prev, val)) + } else { + nodeMapSet(r.Node, "child$", val) + } + }, } // pair.Open ordering (JSON + Jsonic append): // [0] KEY CL pair (JSON) // [1] CA ignore comma (Jsonic, appended) - pairSpec.Open = []*AltSpec{ + // [2] CL child value (Jsonic, optional - only when map.child enabled) + pairOpen := []*AltSpec{ // JSON: key:value pair {S: [][]Tin{KEY, {TinCL}}, P: "val", U: map[string]any{"pair": true}, A: pairkey}, // Jsonic: Ignore initial comma: {,a:1 {S: [][]Tin{{TinCA}}}, } + // Jsonic: map.child - bare colon :value stores as child$ key + if cfg.MapChild { + pairOpen = append(pairOpen, &AltSpec{ + S: [][]Tin{{TinCL}}, P: "val", + U: map[string]any{"done": true, "child": true}, + }) + } + pairSpec.Open = pairOpen // pair.Close ordering (after Jsonic unshift + delete:[0,1]): // Jsonic alternates unshifted, then JSON [0] and [1] deleted. @@ -390,19 +436,60 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { }, // Jsonic: handle pair-in-list func(r *Rule, ctx *Context) { - if pair, ok := r.U["pair"]; ok && pair == true { + if pair, ok := r.U["pair"]; !ok || pair != true { + return + } + if cfg.ListPair { + // list.pair: push pair as {key: val} object element + key := r.U["key"].(string) + val := r.Child.Node + if IsUndefined(val) { + val = nil + } + pairObj := map[string]any{key: val} + if arr, ok := r.Node.([]any); ok { + r.Node = append(arr, pairObj) + if r.Parent != NoRule && r.Parent != nil { + r.Parent.Node = r.Node + } + } + } else { r.U["prev"] = nodeMapGetVal(r.Node, r.U["key"]) pairval(r, ctx) } }, + // Jsonic: handle child value in list (bare colon :value) + func(r *Rule, ctx *Context) { + _ = ctx + if childFlag, ok := r.U["child"]; !ok || childFlag != true { + return + } + val := r.Child.Node + if IsUndefined(val) { + val = nil + } + // Store child value on parent list rule's U map. + // The list BC callback transfers it to ListRef.Child. + if r.Parent != NoRule && r.Parent != nil { + prev, hasPrev := r.Parent.U["child$"] + if !hasPrev { + r.Parent.U["child$"] = val + } else if cfg.MapExtend { + r.Parent.U["child$"] = Deep(prev, val) + } else { + r.Parent.U["child$"] = val + } + } + }, } // elem.Open ordering (Jsonic unshifted + JSON): // [0] CA CA double comma null (Jsonic, unshifted) // [1] CA single comma null (Jsonic, unshifted) // [2] KEY CL pair in list (Jsonic, unshifted) - // [3] p:val (JSON, original) - elemSpec.Open = []*AltSpec{ + // [3] CL child value (Jsonic, optional - only when list.child enabled) + // [4] p:val (JSON, original) + elemOpen := []*AltSpec{ // Jsonic: Empty commas insert null (CA CA) {S: [][]Tin{{TinCA}, {TinCA}}, B: 2, U: map[string]any{"done": true}, @@ -434,9 +521,17 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { N: map[string]int{"pk": 1, "dmap": 1}, U: map[string]any{"done": true, "pair": true, "list": true}, A: pairkey}, - // JSON: Element is a value - {P: "val"}, } + // Jsonic: list.child - bare colon `:value` stores child value + if cfg.ListChild { + elemOpen = append(elemOpen, &AltSpec{ + S: [][]Tin{{TinCL}}, P: "val", + U: map[string]any{"done": true, "child": true, "list": true}, + }) + } + // JSON: Element is a value (fallback - must be last) + elemOpen = append(elemOpen, &AltSpec{P: "val"}) + elemSpec.Open = elemOpen // elem.Close ordering (Jsonic unshifted + delete:[-1,-2]): // [0] CA CS/ZZ trailing comma (Jsonic, unshifted) @@ -470,19 +565,25 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { // nodeMapSet sets a key on a map node. func nodeMapSet(node any, key any, val any) { + k, _ := key.(string) if m, ok := node.(map[string]any); ok { - k, _ := key.(string) m[k] = val + } else if mr, ok := node.(MapRef); ok { + mr.Val[k] = val } } // nodeMapGet gets a value from a map node. func nodeMapGet(node any, key any) (any, bool) { + k, _ := key.(string) if m, ok := node.(map[string]any); ok { - k, _ := key.(string) v, exists := m[k] return v, exists } + if mr, ok := node.(MapRef); ok { + v, exists := mr.Val[k] + return v, exists + } return nil, false } diff --git a/go/lexer.go b/go/lexer.go index dfa9ac8..2921626 100644 --- a/go/lexer.go +++ b/go/lexer.go @@ -50,7 +50,9 @@ type LexConfig struct { // Map/List options MapExtend bool // Deep-merge duplicate keys. Default: true. + MapChild bool // Parse bare colon in maps as child$ key. Default: false. ListProperty bool // Allow named properties in arrays. Default: true. + ListPair bool // Push pairs as object elements in arrays. Default: false. // Safe options SafeKey bool // Prevent __proto__ keys. Default: true. @@ -82,6 +84,12 @@ type LexConfig struct { // ListRef wraps list output values in ListRef structs. ListRef bool + // ListChild enables bare colon (:value) syntax in lists to set a child value. + ListChild bool + + // MapRef wraps map output values in MapRef structs. + MapRef bool + // LexCheck callbacks allow plugins to intercept and override matchers. // Each returns nil to continue normal matching, or a LexCheckResult to short-circuit. FixedCheck LexCheck diff --git a/go/listchild_test.go b/go/listchild_test.go new file mode 100644 index 0000000..d4b571c --- /dev/null +++ b/go/listchild_test.go @@ -0,0 +1,407 @@ +package jsonic + +import ( + "testing" +) + +// makeChildParser creates a parser with List.Child enabled. +// ListRef is automatically enabled by list.child. +func makeChildParser() *Jsonic { + return Make(Options{List: &ListOptions{Child: boolPtr(true)}}) +} + +// expectChild parses input with list.child enabled and checks Val and Child. +func expectChild(t *testing.T, input string, expectedVal []any, expectedChild any) { + t.Helper() + j := makeChildParser() + got, err := j.Parse(input) + if err != nil { + t.Errorf("Parse(%q) unexpected error: %v", input, err) + return + } + lr, ok := got.(ListRef) + if !ok { + t.Errorf("Parse(%q) expected ListRef, got %T: %#v", input, got, got) + return + } + if !bothRefEqual(lr.Val, expectedVal) { + t.Errorf("Parse(%q) Val\n got: %#v\n expected: %#v", input, lr.Val, expectedVal) + } + if !bothRefEqual(lr.Child, expectedChild) { + t.Errorf("Parse(%q) Child\n got: %#v\n expected: %#v", input, lr.Child, expectedChild) + } +} + +// --- Basic child values --- + +func TestListChildNumber(t *testing.T) { + // [:1] → empty list with child=1 + expectChild(t, "[:1]", []any{}, 1.0) +} + +func TestListChildString(t *testing.T) { + // [:a] → empty list with child="a" + expectChild(t, "[:a]", []any{}, "a") +} + +func TestListChildQuotedString(t *testing.T) { + // [:"hello"] → empty list with child="hello" + expectChild(t, `[:"hello"]`, []any{}, "hello") +} + +func TestListChildTrue(t *testing.T) { + expectChild(t, "[:true]", []any{}, true) +} + +func TestListChildFalse(t *testing.T) { + expectChild(t, "[:false]", []any{}, false) +} + +func TestListChildNull(t *testing.T) { + expectChild(t, "[:null]", []any{}, nil) +} + +func TestListChildBareColon(t *testing.T) { + // [:] → empty list with child=null (bare colon, no value) + expectChild(t, "[:]", []any{}, nil) +} + +func TestListChildMap(t *testing.T) { + // [:{a:1}] → empty list with child={a:1} + expectChild(t, "[:{a:1}]", []any{}, map[string]any{"a": 1.0}) +} + +func TestListChildMapMultiKey(t *testing.T) { + // [:{a:1,b:2}] → empty list with child={a:1,b:2} + expectChild(t, "[:{a:1,b:2}]", []any{}, map[string]any{"a": 1.0, "b": 2.0}) +} + +func TestListChildEmptyMap(t *testing.T) { + expectChild(t, "[:{}]", []any{}, map[string]any{}) +} + +func TestListChildNestedMap(t *testing.T) { + expectChild(t, "[:{a:{b:1}}]", []any{}, map[string]any{"a": map[string]any{"b": 1.0}}) +} + +func TestListChildList(t *testing.T) { + // [:[1,2]] → empty list with child=ListRef([1,2]) + // ListRef is auto-enabled, so inner lists are also wrapped. + expectChild(t, "[:[1,2]]", []any{}, ListRef{Val: []any{1.0, 2.0}, Implicit: false}) +} + +func TestListChildEmptyList(t *testing.T) { + expectChild(t, "[:[]]", []any{}, ListRef{Val: []any{}, Implicit: false}) +} + +func TestListChildNestedList(t *testing.T) { + expectChild(t, "[:[[1],[2]]]", []any{}, + ListRef{Val: []any{ + ListRef{Val: []any{1.0}, Implicit: false}, + ListRef{Val: []any{2.0}, Implicit: false}, + }, Implicit: false}) +} + +// --- Mixed child and regular elements --- + +func TestListChildAfterElement(t *testing.T) { + // [1,:2] → [1] with child=2 + expectChild(t, "[1,:2]", []any{1.0}, 2.0) +} + +func TestListChildBeforeElement(t *testing.T) { + // [:1,2] → [2] with child=1 + expectChild(t, "[:1,2]", []any{2.0}, 1.0) +} + +func TestListChildMiddle(t *testing.T) { + // [1,:2,3] → [1,3] with child=2 + expectChild(t, "[1,:2,3]", []any{1.0, 3.0}, 2.0) +} + +func TestListChildAtEnd(t *testing.T) { + // [1,2,:3] → [1,2] with child=3 + expectChild(t, "[1,2,:3]", []any{1.0, 2.0}, 3.0) +} + +func TestListChildAtStartMultiple(t *testing.T) { + // [:1,2,3] → [2,3] with child=1 + expectChild(t, "[:1,2,3]", []any{2.0, 3.0}, 1.0) +} + +// --- Multiple child values (last wins for scalars, deep merge for maps) --- + +func TestListChildMultipleScalars(t *testing.T) { + // [:1,:2] → [] with child=2 (last scalar wins) + expectChild(t, "[:1,:2]", []any{}, 2.0) +} + +func TestListChildTripleScalars(t *testing.T) { + // [:1,:2,:3] → [] with child=3 + expectChild(t, "[:1,:2,:3]", []any{}, 3.0) +} + +func TestListChildMergeMaps(t *testing.T) { + // [:{a:1},:{b:2}] → [] with child={a:1,b:2} (deep merge) + expectChild(t, "[:{a:1},:{b:2}]", []any{}, map[string]any{"a": 1.0, "b": 2.0}) +} + +func TestListChildMergeThreeMaps(t *testing.T) { + // [:{a:1},:{b:2},:{c:3}] → [] with child={a:1,b:2,c:3} + expectChild(t, "[:{a:1},:{b:2},:{c:3}]", []any{}, map[string]any{"a": 1.0, "b": 2.0, "c": 3.0}) +} + +func TestListChildDeepMergeMaps(t *testing.T) { + // [:{a:{x:1}},:{a:{y:2}}] → [] with child={a:{x:1,y:2}} + expectChild(t, "[:{a:{x:1}},:{a:{y:2}}]", []any{}, map[string]any{ + "a": map[string]any{"x": 1.0, "y": 2.0}, + }) +} + +func TestListChildMergeDupKey(t *testing.T) { + // [:{a:1},:{a:2}] → [] with child={a:2} (dup key, over wins) + expectChild(t, "[:{a:1},:{a:2}]", []any{}, map[string]any{"a": 2.0}) +} + +// --- Pair in list with child --- + +func TestListChildWithPairProperty(t *testing.T) { + // [a:1,:2] → list with property a=1 and child=2 + j := makeChildParser() + got, err := j.Parse("[a:1,:2]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lr, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef, got %T: %#v", got, got) + } + if !bothRefEqual(lr.Child, 2.0) { + t.Errorf("Child: got %#v, expected 2.0", lr.Child) + } +} + +// --- Child path dive --- + +func TestListChildPathDive(t *testing.T) { + // [:a:b] → [] with child={a:"b"} (path dive from child) + expectChild(t, "[:a:b]", []any{}, map[string]any{"a": "b"}) +} + +func TestListChildDeepPathDive(t *testing.T) { + // [:a:b:1] → [] with child={a:{b:1}} + expectChild(t, "[:a:b:1]", []any{}, map[string]any{"a": map[string]any{"b": 1.0}}) +} + +// --- Trailing comma --- + +func TestListChildTrailingComma(t *testing.T) { + // [:1,] → [] with child=1 + expectChild(t, "[:1,]", []any{}, 1.0) +} + +func TestListChildElementThenChildTrailing(t *testing.T) { + // [1,:2,] → [1] with child=2 + expectChild(t, "[1,:2,]", []any{1.0}, 2.0) +} + +func TestListChildMultipleTrailing(t *testing.T) { + // [:1,:2,] → [] with child=2 + expectChild(t, "[:1,:2,]", []any{}, 2.0) +} + +// --- Leading comma (null element) --- + +func TestListChildLeadingCommaNull(t *testing.T) { + // [,:1] → [null] with child=1 + expectChild(t, "[,:1]", []any{nil}, 1.0) +} + +func TestListChildDoubleLeadingComma(t *testing.T) { + // [,,:1] → [null,null] with child=1 + expectChild(t, "[,,:1]", []any{nil, nil}, 1.0) +} + +// --- No child (regular lists unchanged) --- + +func TestListChildNone(t *testing.T) { + // [1,2,3] → [1,2,3] with child=nil (no child set) + expectChild(t, "[1,2,3]", []any{1.0, 2.0, 3.0}, nil) +} + +func TestListChildEmptyBrackets(t *testing.T) { + // [] → [] with child=nil + expectChild(t, "[]", []any{}, nil) +} + +// --- Mixed maps and child --- + +func TestListChildMapElementThenChild(t *testing.T) { + // [{a:1},:2] → [{a:1}] with child=2 + expectChild(t, "[{a:1},:2]", []any{map[string]any{"a": 1.0}}, 2.0) +} + +func TestListChildBeforeMapElement(t *testing.T) { + // [:1,{a:2}] → [{a:2}] with child=1 + expectChild(t, "[:1,{a:2}]", []any{map[string]any{"a": 2.0}}, 1.0) +} + +func TestListChildListElement(t *testing.T) { + // [[1,2],:3] → [ListRef([1,2])] with child=3 + expectChild(t, "[[1,2],:3]", []any{ListRef{Val: []any{1.0, 2.0}, Implicit: false}}, 3.0) +} + +func TestListChildBeforeListElement(t *testing.T) { + // [:1,[2,3]] → [ListRef([2,3])] with child=1 + expectChild(t, "[:1,[2,3]]", []any{ListRef{Val: []any{2.0, 3.0}, Implicit: false}}, 1.0) +} + +func TestListChildMapAroundChild(t *testing.T) { + // [{a:1},:2,{b:3}] → [{a:1},{b:3}] with child=2 + expectChild(t, "[{a:1},:2,{b:3}]", []any{map[string]any{"a": 1.0}, map[string]any{"b": 3.0}}, 2.0) +} + +// --- Bare colon creates null child --- + +func TestListChildBareColonThenElement(t *testing.T) { + // [:,1] → [1] with child=null + expectChild(t, "[:,1]", []any{1.0}, nil) +} + +func TestListChildElementThenBareColon(t *testing.T) { + // [1,:] → [1] with child=null + expectChild(t, "[1,:]", []any{1.0}, nil) +} + +// --- Multiple children interleaved with elements --- + +func TestListChildInterleaved(t *testing.T) { + // [:1,:2,3,:4] → [3] with child=4 + expectChild(t, "[:1,:2,3,:4]", []any{3.0}, 4.0) +} + +func TestListChildMultiInterleaved(t *testing.T) { + // [1,:2,3,:4,5] → [1,3,5] with child=4 + expectChild(t, "[1,:2,3,:4,5]", []any{1.0, 3.0, 5.0}, 4.0) +} + +// --- list.child auto-enables ListRef --- + +func TestListChildAutoEnablesListRef(t *testing.T) { + // Enabling list.child should auto-enable ListRef wrapping. + j := Make(Options{List: &ListOptions{Child: boolPtr(true)}}) + got, err := j.Parse("[1,2]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := got.(ListRef); !ok { + t.Errorf("expected ListRef (auto-enabled by list.child), got %T: %#v", got, got) + } +} + +// --- list.child disabled (default) --- + +func TestListChildDisabledDefault(t *testing.T) { + // Default: list.child disabled, bare colon is not special. + j := Make() + got, err := j.Parse("[1,2]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := got.([]any); !ok { + t.Errorf("expected []any, got %T: %#v", got, got) + } +} + +// --- list.child with MapRef --- + +func TestListChildWithMapRef(t *testing.T) { + // list.child + MapRef: child map should be MapRef + j := Make(Options{ + List: &ListOptions{Child: boolPtr(true)}, + MapRef: boolPtr(true), + }) + got, err := j.Parse("[:{a:1}]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lr, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef, got %T: %#v", got, got) + } + child, ok := lr.Child.(MapRef) + if !ok { + t.Fatalf("expected child to be MapRef, got %T: %#v", lr.Child, lr.Child) + } + if child.Val["a"] != 1.0 { + t.Errorf("expected child.Val[a]=1, got %#v", child.Val) + } +} + +// --- list.child with TextInfo --- + +func TestListChildWithTextInfo(t *testing.T) { + // list.child + TextInfo: child text should be Text struct + j := Make(Options{ + List: &ListOptions{Child: boolPtr(true)}, + TextInfo: boolPtr(true), + }) + got, err := j.Parse(`[:"hello"]`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lr, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef, got %T: %#v", got, got) + } + child, ok := lr.Child.(Text) + if !ok { + t.Fatalf("expected child to be Text, got %T: %#v", lr.Child, lr.Child) + } + if child.Str != "hello" || child.Quote != `"` { + t.Errorf("expected Text{Str:hello, Quote:\"}, got %#v", child) + } +} + +// --- Implicit flag --- + +func TestListChildExplicitBrackets(t *testing.T) { + j := makeChildParser() + got, err := j.Parse("[:1]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lr := got.(ListRef) + if lr.Implicit { + t.Error("expected Implicit=false for bracketed list") + } +} + +// --- Deep merge disabled --- + +func TestListChildNoExtend(t *testing.T) { + // With map.extend=false, multiple child values → last wins (no deep merge) + j := Make(Options{ + List: &ListOptions{Child: boolPtr(true)}, + Map: &MapOptions{Extend: boolPtr(false)}, + }) + got, err := j.Parse("[:{a:1},:{b:2}]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + lr, ok := got.(ListRef) + if !ok { + t.Fatalf("expected ListRef, got %T", got) + } + // Without extend, last child value wins entirely + child, ok := lr.Child.(map[string]any) + if !ok { + t.Fatalf("expected child to be map, got %T: %#v", lr.Child, lr.Child) + } + if _, hasA := child["a"]; hasA { + t.Errorf("expected child to only have key 'b' (last wins), got %#v", child) + } + if child["b"] != 2.0 { + t.Errorf("expected child[b]=2, got %#v", child) + } +} diff --git a/go/mapref_test.go b/go/mapref_test.go new file mode 100644 index 0000000..96471f0 --- /dev/null +++ b/go/mapref_test.go @@ -0,0 +1,271 @@ +package jsonic + +import ( + "testing" +) + +// expectMapRef parses input with MapRef enabled and checks the result. +func expectMapRef(t *testing.T, input string, expected any) { + t.Helper() + j := Make(Options{MapRef: boolPtr(true)}) + got, err := j.Parse(input) + if err != nil { + t.Errorf("Parse(%q) unexpected error: %v", input, err) + return + } + if !mapRefEqual(got, expected) { + t.Errorf("Parse(%q)\n got: %#v\n expected: %#v", + input, got, expected) + } +} + +// mapRefEqual compares values including MapRef structs. +func mapRefEqual(a, b any) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + + switch av := a.(type) { + case MapRef: + bv, ok := b.(MapRef) + if !ok { + return false + } + if av.Implicit != bv.Implicit { + return false + } + if len(av.Val) != len(bv.Val) { + return false + } + for k, v := range av.Val { + bval, exists := bv.Val[k] + if !exists || !mapRefEqual(v, bval) { + return false + } + } + return true + case map[string]any: + bv, ok := b.(map[string]any) + if !ok || len(av) != len(bv) { + return false + } + for k, v := range av { + bval, exists := bv[k] + if !exists || !mapRefEqual(v, bval) { + return false + } + } + return true + case []any: + bv, ok := b.([]any) + if !ok || len(av) != len(bv) { + return false + } + for i := range av { + if !mapRefEqual(av[i], bv[i]) { + return false + } + } + return true + case float64: + bv, ok := b.(float64) + return ok && av == bv + case bool: + bv, ok := b.(bool) + return ok && av == bv + case string: + bv, ok := b.(string) + return ok && av == bv + default: + return false + } +} + +// mr is shorthand to create a MapRef value. +func mr(implicit bool, pairs ...any) MapRef { + m := make(map[string]any) + for i := 0; i+1 < len(pairs); i += 2 { + k, _ := pairs[i].(string) + m[k] = pairs[i+1] + } + return MapRef{Val: m, Implicit: implicit} +} + +func TestMapRefOff(t *testing.T) { + // Default (MapRef off) - plain map[string]any in output. + j := Make() + got, err := j.Parse("{a:1}") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := got.(map[string]any); !ok { + t.Errorf("expected map[string]any, got %T: %#v", got, got) + } +} + +func TestMapRefExplicitOff(t *testing.T) { + // Explicitly setting MapRef to false. + j := Make(Options{MapRef: boolPtr(false)}) + got, err := j.Parse("{a:1}") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := got.(map[string]any); !ok { + t.Errorf("expected map[string]any, got %T: %#v", got, got) + } +} + +func TestMapRefExplicitMap(t *testing.T) { + // Explicit map with braces: not implicit. + expectMapRef(t, "{a:1,b:2}", mr(false, "a", 1.0, "b", 2.0)) +} + +func TestMapRefExplicitEmpty(t *testing.T) { + // Empty explicit map. + expectMapRef(t, "{}", mr(false)) +} + +func TestMapRefImplicitMap(t *testing.T) { + // Implicit map via key:value without braces. + expectMapRef(t, "a:1", mr(true, "a", 1.0)) +} + +func TestMapRefImplicitMultipleKeys(t *testing.T) { + // Implicit map with multiple keys. + expectMapRef(t, "a:1,b:2", mr(true, "a", 1.0, "b", 2.0)) +} + +func TestMapRefImplicitSpaceSeparated(t *testing.T) { + // Implicit map with space-separated pairs. + expectMapRef(t, "a:1 b:2", mr(true, "a", 1.0, "b", 2.0)) +} + +func TestMapRefNestedExplicit(t *testing.T) { + // Nested explicit maps. + expectMapRef(t, "{a:{b:1}}", mr(false, "a", mr(false, "b", 1.0))) +} + +func TestMapRefNestedImplicitInExplicit(t *testing.T) { + // Implicit map nested inside explicit map value (via pair dive). + expectMapRef(t, "{a:b:1}", mr(false, "a", mr(true, "b", 1.0))) +} + +func TestMapRefListsUnaffected(t *testing.T) { + // Lists should not be wrapped in MapRef. + j := Make(Options{MapRef: boolPtr(true)}) + got, err := j.Parse("[1,2]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := got.([]any); !ok { + t.Errorf("expected []any, got %T: %#v", got, got) + } +} + +func TestMapRefScalarsUnaffected(t *testing.T) { + // Scalars should not be affected. + j := Make(Options{MapRef: boolPtr(true)}) + + got, err := j.Parse("42") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if f, ok := got.(float64); !ok || f != 42.0 { + t.Errorf("expected 42.0, got %#v", got) + } + + got, err = j.Parse("true") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if b, ok := got.(bool); !ok || b != true { + t.Errorf("expected true, got %#v", got) + } +} + +func TestMapRefDeepMerge(t *testing.T) { + // Extension (deep merge) with MapRef enabled. + expectMapRef(t, "a:{b:1},a:{c:2}", mr(true, "a", mr(false, "b", 1.0, "c", 2.0))) +} + +func TestMapRefMapInList(t *testing.T) { + // MapRef inside a list. + j := Make(Options{MapRef: boolPtr(true)}) + got, err := j.Parse("[{a:1},{b:2}]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + arr, ok := got.([]any) + if !ok { + t.Fatalf("expected []any, got %T: %#v", got, got) + } + if len(arr) != 2 { + t.Fatalf("expected 2 elements, got %d", len(arr)) + } + m0, ok := arr[0].(MapRef) + if !ok { + t.Errorf("expected MapRef for element 0, got %T: %#v", arr[0], arr[0]) + } else if m0.Implicit || len(m0.Val) != 1 || m0.Val["a"] != 1.0 { + t.Errorf("element 0: expected {a:1} explicit, got %#v", m0) + } + m1, ok := arr[1].(MapRef) + if !ok { + t.Errorf("expected MapRef for element 1, got %T: %#v", arr[1], arr[1]) + } else if m1.Implicit || len(m1.Val) != 1 || m1.Val["b"] != 2.0 { + t.Errorf("element 1: expected {b:2} explicit, got %#v", m1) + } +} + +func TestMapRefWithStringValues(t *testing.T) { + expectMapRef(t, `{a:"hello",b:"world"}`, mr(false, "a", "hello", "b", "world")) +} + +func TestMapRefCombinedWithListRef(t *testing.T) { + // Both MapRef and ListRef enabled. + j := Make(Options{MapRef: boolPtr(true), ListRef: boolPtr(true)}) + got, err := j.Parse("{a:[1,2]}") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m, ok := got.(MapRef) + if !ok { + t.Fatalf("expected MapRef, got %T: %#v", got, got) + } + if m.Implicit { + t.Errorf("expected Implicit=false for braced map") + } + listVal, ok := m.Val["a"].(ListRef) + if !ok { + t.Fatalf("expected ListRef for key 'a', got %T: %#v", m.Val["a"], m.Val["a"]) + } + if listVal.Implicit { + t.Errorf("expected Implicit=false for bracketed list") + } + if len(listVal.Val) != 2 { + t.Fatalf("expected 2 elements, got %d", len(listVal.Val)) + } +} + +func TestMapRefCombinedWithTextInfo(t *testing.T) { + // Both MapRef and TextInfo enabled. + j := Make(Options{MapRef: boolPtr(true), TextInfo: boolPtr(true)}) + got, err := j.Parse(`{"a":1}`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + m, ok := got.(MapRef) + if !ok { + t.Fatalf("expected MapRef, got %T: %#v", got, got) + } + if m.Implicit { + t.Errorf("expected Implicit=false for braced map") + } +} + +func TestMapRefSingleKey(t *testing.T) { + // Single key explicit map. + expectMapRef(t, "{a:1}", mr(false, "a", 1.0)) +} diff --git a/go/options.go b/go/options.go index 9a01f73..43b40df 100644 --- a/go/options.go +++ b/go/options.go @@ -70,6 +70,11 @@ type Options struct { // the list was created implicitly (without brackets). Default: false. ListRef *bool + // MapRef enables returning maps as MapRef structs instead of map[string]any. + // When true, map values include an Implicit flag indicating whether + // the map was created implicitly (without braces). Default: false. + MapRef *bool + // Tag is an instance identifier tag. Tag string } @@ -138,11 +143,14 @@ type StringOptions struct { // MapOptions controls object/map behavior. type MapOptions struct { Extend *bool // Deep-merge duplicate keys. Default: true. + Child *bool // Parse bare colon as child$ key: {:1} → {"child$":1}. Default: false. } // ListOptions controls array/list behavior. type ListOptions struct { Property *bool // Allow named properties in arrays [a:1]. Default: true. + Pair *bool // Push pairs as object elements: [a:1] → [{"a":1}]. Default: false. + Child *bool // Parse bare colon as child value: [:1] → ListRef with Child=1. Default: false. } // ValueDef defines a keyword value. @@ -463,9 +471,12 @@ func buildConfig(o *Options) *LexConfig { // Map cfg.MapExtend = boolVal(optBool(o.Map, func(m *MapOptions) *bool { return m.Extend }), true) + cfg.MapChild = boolVal(optBool(o.Map, func(m *MapOptions) *bool { return m.Child }), false) // List cfg.ListProperty = boolVal(optBool(o.List, func(l *ListOptions) *bool { return l.Property }), true) + cfg.ListPair = boolVal(optBool(o.List, func(l *ListOptions) *bool { return l.Pair }), false) + cfg.ListChild = boolVal(optBool(o.List, func(l *ListOptions) *bool { return l.Child }), false) // Rule cfg.FinishRule = boolVal(optBool(o.Rule, func(r *RuleOptions) *bool { return r.Finish }), true) @@ -483,6 +494,13 @@ func buildConfig(o *Options) *LexConfig { // ListRef cfg.ListRef = boolVal(o.ListRef, false) + // list.child requires ListRef to store the child value on ListRef.Child. + if cfg.ListChild { + cfg.ListRef = true + } + + // MapRef + cfg.MapRef = boolVal(o.MapRef, false) // Apply config modifiers. if o.ConfigModify != nil { diff --git a/go/text.go b/go/text.go index f49648d..f9993f7 100644 --- a/go/text.go +++ b/go/text.go @@ -24,4 +24,24 @@ type ListRef struct { // (e.g. comma-separated or space-separated values without brackets), // and false when brackets were used explicitly. Implicit bool + + // Child is the optional child value set by bare colon syntax (:value) + // inside a list. Enabled by the List.Child option. + // For example, `[:1, a, b]` produces Val=[a, b] with Child=1. + // Multiple child values are merged (deep merge if Map.Extend is true). + // Nil when no child value is present. + Child any +} + +// MapRef wraps a map value with metadata about how it was created. +// When the MapRef option is enabled, map values in the output are +// returned as MapRef instead of plain map[string]any. +type MapRef struct { + // Val is the map contents. + Val map[string]any + + // Implicit is true when the map was created implicitly + // (e.g. key:value pairs without braces), + // and false when braces were used explicitly. + Implicit bool } diff --git a/go/utility.go b/go/utility.go index 4fdf624..f855b49 100644 --- a/go/utility.go +++ b/go/utility.go @@ -21,8 +21,19 @@ func Deep(base any, rest ...any) any { } func deepMerge(base, over any) any { + // Extract maps from MapRef if present. baseMap, baseIsMap := base.(map[string]any) + baseMR, baseIsMR := base.(MapRef) + if baseIsMR { + baseMap = baseMR.Val + baseIsMap = true + } overMap, overIsMap := over.(map[string]any) + overMR, overIsMR := over.(MapRef) + if overIsMR { + overMap = overMR.Val + overIsMap = true + } // Extract arrays from ListRef if present. baseArr, baseIsArr := base.([]any) @@ -51,6 +62,10 @@ func deepMerge(base, over any) any { result[k] = deepClone(v) } } + // Preserve MapRef wrapper if the over value was a MapRef. + if overIsMR { + return MapRef{Val: result, Implicit: overMR.Implicit} + } return result } @@ -72,7 +87,16 @@ func deepMerge(base, over any) any { } // Preserve ListRef wrapper if the over value was a ListRef. if overIsLR { - return ListRef{Val: result, Implicit: overLR.Implicit} + // Merge Child fields if both are ListRef. + var child any + if baseIsLR && baseLR.Child != nil && overLR.Child != nil { + child = deepMerge(baseLR.Child, overLR.Child) + } else if overLR.Child != nil { + child = deepClone(overLR.Child) + } else if baseIsLR { + child = deepClone(baseLR.Child) + } + return ListRef{Val: result, Implicit: overLR.Implicit, Child: child} } return result } @@ -107,7 +131,13 @@ func deepClone(val any) any { for i, val := range v.Val { result[i] = deepClone(val) } - return ListRef{Val: result, Implicit: v.Implicit} + return ListRef{Val: result, Implicit: v.Implicit, Child: deepClone(v.Child)} + case MapRef: + result := make(map[string]any) + for k, val := range v.Val { + result[k] = deepClone(val) + } + return MapRef{Val: result, Implicit: v.Implicit} default: return v }