Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions go/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions go/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
276 changes: 276 additions & 0 deletions go/listref_test.go
Original file line number Diff line number Diff line change
@@ -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])
}
}
}
8 changes: 8 additions & 0 deletions go/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions go/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions go/utility.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down
Loading