From 028dafca37c7e4643ab167976c44fb7e6824dd4e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 18:40:01 +0000 Subject: [PATCH] Add ListRef option to return lists with implicit/explicit metadata Define a ListRef struct with Val ([]any) and Implicit (bool) fields. When the ListRef option is enabled, list values in the output are wrapped in ListRef instead of plain []any slices. The Implicit flag is true for lists created without brackets (comma-separated or space-separated values) and false for explicit bracket-delimited lists. The option is off by default. Also updates deepMerge and deepClone in utility.go to handle ListRef so that the extension (deep merge) feature works correctly when ListRef is enabled. Includes 21 unit tests covering explicit/implicit lists, nesting, maps, scalars, deep merge, combined with TextInfo, and more. https://claude.ai/code/session_01DmZRT53cbEtiRzmrhYQdfv --- go/grammar.go | 14 +++ go/lexer.go | 3 + go/listref_test.go | 276 +++++++++++++++++++++++++++++++++++++++++++++ go/options.go | 8 ++ go/text.go | 13 +++ go/utility.go | 21 ++++ 6 files changed, 335 insertions(+) create mode 100644 go/listref_test.go diff --git a/go/grammar.go b/go/grammar.go index ef39e0e..b50dcd2 100644 --- a/go/grammar.go +++ b/go/grammar.go @@ -255,6 +255,20 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) { }, } + // BC callbacks: + listSpec.BC = []StateAction{ + // Wrap list in ListRef if option is enabled. + func(r *Rule, ctx *Context) { + _ = ctx + 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} + } + } + }, + } + // list.Open ordering (Jsonic unshift + JSON + Jsonic append): // [0] implist condition (Jsonic, unshifted as single object) // [1] OS CS empty list (JSON) diff --git a/go/lexer.go b/go/lexer.go index b4f9e52..dfa9ac8 100644 --- a/go/lexer.go +++ b/go/lexer.go @@ -79,6 +79,9 @@ type LexConfig struct { // TextInfo wraps string/text output values in Text structs. TextInfo bool + // ListRef wraps list output values in ListRef structs. + ListRef 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/listref_test.go b/go/listref_test.go new file mode 100644 index 0000000..aaa72bc --- /dev/null +++ b/go/listref_test.go @@ -0,0 +1,276 @@ +package jsonic + +import ( + "testing" +) + +// expectListRef parses input with ListRef enabled and checks the result. +func expectListRef(t *testing.T, input string, expected any) { + t.Helper() + j := Make(Options{ListRef: boolPtr(true)}) + got, err := j.Parse(input) + if err != nil { + t.Errorf("Parse(%q) unexpected error: %v", input, err) + return + } + if !listRefEqual(got, expected) { + t.Errorf("Parse(%q)\n got: %#v\n expected: %#v", + input, got, expected) + } +} + +// listRefEqual compares values including ListRef structs. +func listRefEqual(a, b any) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + + switch av := a.(type) { + 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 !listRefEqual(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 || !listRefEqual(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 !listRefEqual(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 + } +} + +// lr is shorthand to create a ListRef value. +func lr(implicit bool, vals ...any) ListRef { + if vals == nil { + vals = []any{} + } + return ListRef{Val: vals, Implicit: implicit} +} + +func TestListRefOff(t *testing.T) { + // Default (ListRef off) - plain []any in output. + 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) + } +} + +func TestListRefExplicitOff(t *testing.T) { + // Explicitly setting ListRef to false. + j := Make(Options{ListRef: boolPtr(false)}) + 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 TestListRefExplicitList(t *testing.T) { + // Explicit list with brackets: not implicit. + expectListRef(t, "[1,2,3]", lr(false, 1.0, 2.0, 3.0)) +} + +func TestListRefExplicitEmpty(t *testing.T) { + // Empty explicit list. + expectListRef(t, "[]", lr(false)) +} + +func TestListRefImplicitComma(t *testing.T) { + // Implicit list via trailing comma: a,b + expectListRef(t, "a,b", lr(true, "a", "b")) +} + +func TestListRefImplicitTrailingComma(t *testing.T) { + // Trailing comma creates implicit list. + expectListRef(t, "a,", lr(true, "a")) +} + +func TestListRefImplicitSpace(t *testing.T) { + // Implicit list via space separation: a b c + expectListRef(t, "a b c", lr(true, "a", "b", "c")) +} + +func TestListRefImplicitLeadingComma(t *testing.T) { + // Leading comma creates implicit list with null first element. + expectListRef(t, ",a", lr(true, nil, "a")) +} + +func TestListRefImplicitCommaOnly(t *testing.T) { + // Single comma creates implicit list with null element. + expectListRef(t, ",", lr(true, nil)) +} + +func TestListRefNestedExplicit(t *testing.T) { + // Nested explicit lists. + expectListRef(t, "[[1],[2]]", lr(false, + lr(false, 1.0), + lr(false, 2.0), + )) +} + +func TestListRefExplicitInMap(t *testing.T) { + // Explicit list as map value. + expectListRef(t, "a:[1,2]", map[string]any{ + "a": lr(false, 1.0, 2.0), + }) +} + +func TestListRefMixedImplicitExplicit(t *testing.T) { + // Implicit list of explicit lists. + expectListRef(t, "[a],[b]", lr(true, + lr(false, "a"), + lr(false, "b"), + )) +} + +func TestListRefSpaceSeparatedLists(t *testing.T) { + // Space-separated explicit lists. + expectListRef(t, "[a] [b]", lr(true, + lr(false, "a"), + lr(false, "b"), + )) +} + +func TestListRefMapsUnaffected(t *testing.T) { + // Maps should not be wrapped in ListRef. + j := Make(Options{ListRef: boolPtr(true)}) + 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 TestListRefScalarsUnaffected(t *testing.T) { + // Scalars should not be affected. + j := Make(Options{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 %#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 TestListRefDeepMerge(t *testing.T) { + // Extension (deep merge) with ListRef enabled. + // a:[{b:1}],a:[{b:2}] merges the arrays. + expectListRef(t, "a:[{b:1}],a:[{b:2}]", map[string]any{ + "a": lr(false, map[string]any{"b": 2.0}), + }) +} + +func TestListRefSpaceSeparatedMaps(t *testing.T) { + // Space-separated maps create implicit list. + expectListRef(t, "{a:1} {b:2}", lr(true, + map[string]any{"a": 1.0}, + map[string]any{"b": 2.0}, + )) +} + +func TestListRefWithNumbers(t *testing.T) { + expectListRef(t, "[1,2,3]", lr(false, 1.0, 2.0, 3.0)) + expectListRef(t, "1,2,3", lr(true, 1.0, 2.0, 3.0)) +} + +func TestListRefImplicitNullCommas(t *testing.T) { + // Double comma creates null element. + expectListRef(t, "1,,", lr(true, 1.0, nil)) + expectListRef(t, "1,,,", lr(true, 1.0, nil, nil)) +} + +func TestListRefSingleElement(t *testing.T) { + // Single element in brackets. + expectListRef(t, "[a]", lr(false, "a")) +} + +func TestListRefCombinedWithTextInfo(t *testing.T) { + // Both ListRef and TextInfo enabled. + j := Make(Options{ListRef: boolPtr(true), TextInfo: boolPtr(true)}) + got, err := j.Parse(`["a",'b',c]`) + 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 lr.Implicit { + t.Errorf("expected Implicit=false for bracketed list") + } + if len(lr.Val) != 3 { + t.Fatalf("expected 3 elements, got %d", len(lr.Val)) + } + // Check elements are Text structs. + for i, expected := range []Text{ + {Quote: `"`, Str: "a"}, + {Quote: "'", Str: "b"}, + {Quote: "", Str: "c"}, + } { + if txt, ok := lr.Val[i].(Text); !ok || txt != expected { + t.Errorf("element %d: expected %#v, got %#v", i, expected, lr.Val[i]) + } + } +} diff --git a/go/options.go b/go/options.go index 69692a0..9a01f73 100644 --- a/go/options.go +++ b/go/options.go @@ -65,6 +65,11 @@ type Options struct { // that include the quote character used. Default: false. TextInfo *bool + // ListRef enables returning lists as ListRef structs instead of []any. + // When true, list values include an Implicit flag indicating whether + // the list was created implicitly (without brackets). Default: false. + ListRef *bool + // Tag is an instance identifier tag. Tag string } @@ -476,6 +481,9 @@ func buildConfig(o *Options) *LexConfig { // TextInfo cfg.TextInfo = boolVal(o.TextInfo, false) + // ListRef + cfg.ListRef = boolVal(o.ListRef, false) + // Apply config modifiers. if o.ConfigModify != nil { for _, mod := range o.ConfigModify { diff --git a/go/text.go b/go/text.go index be661b4..f49648d 100644 --- a/go/text.go +++ b/go/text.go @@ -12,3 +12,16 @@ type Text struct { // Str is the actual string value (with escapes processed for quoted strings). Str string } + +// ListRef wraps a list value with metadata about how it was created. +// When the ListRef option is enabled, list values in the output are +// returned as ListRef instead of plain []any slices. +type ListRef struct { + // Val is the list contents. + Val []any + + // Implicit is true when the list was created implicitly + // (e.g. comma-separated or space-separated values without brackets), + // and false when brackets were used explicitly. + Implicit bool +} diff --git a/go/utility.go b/go/utility.go index 14bbc8e..4fdf624 100644 --- a/go/utility.go +++ b/go/utility.go @@ -24,8 +24,19 @@ func deepMerge(base, over any) any { baseMap, baseIsMap := base.(map[string]any) overMap, overIsMap := over.(map[string]any) + // Extract arrays from ListRef if present. baseArr, baseIsArr := base.([]any) + baseLR, baseIsLR := base.(ListRef) + if baseIsLR { + baseArr = baseLR.Val + baseIsArr = true + } overArr, overIsArr := over.([]any) + overLR, overIsLR := over.(ListRef) + if overIsLR { + overArr = overLR.Val + overIsArr = true + } if baseIsMap && overIsMap { // Both maps: recursively merge @@ -59,6 +70,10 @@ func deepMerge(base, over any) any { result[i] = deepClone(baseArr[i]) } } + // Preserve ListRef wrapper if the over value was a ListRef. + if overIsLR { + return ListRef{Val: result, Implicit: overLR.Implicit} + } return result } @@ -87,6 +102,12 @@ func deepClone(val any) any { result[i] = deepClone(val) } return result + case ListRef: + result := make([]any, len(v.Val)) + for i, val := range v.Val { + result[i] = deepClone(val) + } + return ListRef{Val: result, Implicit: v.Implicit} default: return v }