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
118 changes: 118 additions & 0 deletions go/both_ref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ func bothRefEqual(a, b any) bool {
return false
}
}
// Meta: treat nil and empty map as equal
if len(av.Meta) != 0 || len(bv.Meta) != 0 {
if len(av.Meta) != len(bv.Meta) {
return false
}
for k, v := range av.Meta {
bval, exists := bv.Meta[k]
if !exists || !bothRefEqual(v, bval) {
return false
}
}
}
return true
case ListRef:
bv, ok := b.(ListRef)
Expand All @@ -63,6 +75,18 @@ func bothRefEqual(a, b any) bool {
return false
}
}
// Meta: treat nil and empty map as equal
if len(av.Meta) != 0 || len(bv.Meta) != 0 {
if len(av.Meta) != len(bv.Meta) {
return false
}
for k, v := range av.Meta {
bval, exists := bv.Meta[k]
if !exists || !bothRefEqual(v, bval) {
return false
}
}
}
return true
case map[string]any:
bv, ok := b.(map[string]any)
Expand Down Expand Up @@ -467,3 +491,97 @@ 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)))
}

// --- Meta map ---

func TestMapRefMetaInitialized(t *testing.T) {
// MapRef.Meta should be initialized as an empty map when MapRef is enabled.
j := Make(Options{MapRef: 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.Meta == nil {
t.Errorf("expected Meta to be initialized (non-nil), got nil")
}
if len(m.Meta) != 0 {
t.Errorf("expected Meta to be empty, got %#v", m.Meta)
}
}

func TestListRefMetaInitialized(t *testing.T) {
// ListRef.Meta should be initialized as an empty map when ListRef is enabled.
j := Make(Options{ListRef: boolPtr(true)})
got, err := j.Parse("[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 lr.Meta == nil {
t.Errorf("expected Meta to be initialized (non-nil), got nil")
}
if len(lr.Meta) != 0 {
t.Errorf("expected Meta to be empty, got %#v", lr.Meta)
}
}

func TestMapRefMetaAvailableInBOPhase(t *testing.T) {
// Verify that Meta is available during parsing (set in BO, accessible in BC).
j := Make(Options{MapRef: boolPtr(true)})

// Add a custom BO action that writes to Meta
j.Rule("map", func(rs *RuleSpec) {
rs.AddBO(func(r *Rule, ctx *Context) {
if mr, ok := r.Node.(MapRef); ok {
mr.Meta["created_in"] = "bo"
r.Node = mr
}
})
})

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.Meta["created_in"] != "bo" {
t.Errorf("expected Meta[\"created_in\"] = \"bo\", got %#v", m.Meta)
}
}

func TestListRefMetaAvailableInBOPhase(t *testing.T) {
// Verify that Meta is available during parsing (set in BO, accessible in BC).
j := Make(Options{ListRef: boolPtr(true)})

// Add a custom BO action that writes to Meta
j.Rule("list", func(rs *RuleSpec) {
rs.AddBO(func(r *Rule, ctx *Context) {
if lr, ok := r.Node.(ListRef); ok {
lr.Meta["created_in"] = "bo"
r.Node = lr
}
})
})

got, err := j.Parse("[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 lr.Meta["created_in"] != "bo" {
t.Errorf("expected Meta[\"created_in\"] = \"bo\", got %#v", lr.Meta)
}
}
92 changes: 69 additions & 23 deletions go/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,17 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {

// BO callbacks (JSON then Jsonic):
mapSpec.BO = []StateAction{
// JSON: create empty map
// JSON: create empty map (or MapRef if enabled)
func(r *Rule, ctx *Context) {
_ = ctx
r.Node = make(map[string]any)
if cfg.MapRef {
r.Node = MapRef{
Val: make(map[string]any),
Meta: make(map[string]any),
}
} else {
r.Node = make(map[string]any)
}
},
// Jsonic: increment dmap depth
func(r *Rule, ctx *Context) {
Expand All @@ -191,13 +198,14 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {

// BC callbacks:
mapSpec.BC = []StateAction{
// Wrap map in MapRef if option is enabled.
// Set Implicit on 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}
if mr, ok := r.Node.(MapRef); ok {
mr.Implicit = implicit
r.Node = mr
}
}
},
Expand Down Expand Up @@ -240,10 +248,17 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {

// BO callbacks (JSON then Jsonic):
listSpec.BO = []StateAction{
// JSON: create empty list
// JSON: create empty list (or ListRef if enabled)
func(r *Rule, ctx *Context) {
_ = ctx
r.Node = make([]any, 0)
if cfg.ListRef {
r.Node = ListRef{
Val: make([]any, 0),
Meta: make(map[string]any),
}
} else {
r.Node = make([]any, 0)
}
},
// Jsonic: increment dlist depth, handle implist
func(r *Rule, ctx *Context) {
Expand All @@ -256,13 +271,11 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {
// If previous rule was an implicit list, adopt its node
if r.Prev != NoRule && r.Prev != nil {
if implist, ok := r.Prev.U["implist"]; ok && implist == true {
arr := r.Node.([]any)
prevNode := r.Prev.Node
if IsUndefined(prevNode) {
prevNode = nil
}
arr = append(arr, prevNode)
r.Node = arr
r.Node = nodeListAppend(r.Node, prevNode)
r.Prev.Node = r.Node
}
}
Expand All @@ -271,17 +284,17 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {

// BC callbacks:
listSpec.BC = []StateAction{
// Wrap list in ListRef if option is enabled.
// Set Implicit and Child on 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 {
var child any
if lr, ok := r.Node.(ListRef); ok {
lr.Implicit = implicit
if c, ok := r.U["child$"]; ok {
child = c
lr.Child = c
}
r.Node = ListRef{Val: arr, Implicit: implicit, Child: child}
r.Node = lr
}
}
},
Expand Down Expand Up @@ -424,8 +437,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {
_ = ctx
done, _ := r.U["done"].(bool)
if !done && !IsUndefined(r.Child.Node) {
if arr, ok := r.Node.([]any); ok {
r.Node = append(arr, r.Child.Node)
if _, ok := nodeListVal(r.Node); ok {
r.Node = nodeListAppend(r.Node, r.Child.Node)
// Propagate updated slice to parent list rule
// (Go slices may reallocate on append, unlike JS arrays which are reference types)
if r.Parent != NoRule && r.Parent != nil {
Expand All @@ -447,8 +460,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {
val = nil
}
pairObj := map[string]any{key: val}
if arr, ok := r.Node.([]any); ok {
r.Node = append(arr, pairObj)
if _, ok := nodeListVal(r.Node); ok {
r.Node = nodeListAppend(r.Node, pairObj)
if r.Parent != NoRule && r.Parent != nil {
r.Parent.Node = r.Node
}
Expand Down Expand Up @@ -495,8 +508,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {
U: map[string]any{"done": true},
A: func(r *Rule, ctx *Context) {
_ = ctx
if arr, ok := r.Node.([]any); ok {
r.Node = append(arr, nil)
if _, ok := nodeListVal(r.Node); ok {
r.Node = nodeListAppend(r.Node, nil)
// Propagate to parent
if r.Parent != NoRule && r.Parent != nil {
r.Parent.Node = r.Node
Expand All @@ -508,8 +521,8 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {
U: map[string]any{"done": true},
A: func(r *Rule, ctx *Context) {
_ = ctx
if arr, ok := r.Node.([]any); ok {
r.Node = append(arr, nil)
if _, ok := nodeListVal(r.Node); ok {
r.Node = nodeListAppend(r.Node, nil)
// Propagate to parent
if r.Parent != NoRule && r.Parent != nil {
r.Parent.Node = r.Node
Expand Down Expand Up @@ -563,6 +576,39 @@ func Grammar(rsm map[string]*RuleSpec, cfg *LexConfig) {
rsm["elem"] = elemSpec
}

// nodeListAppend appends a value to a list node (plain []any or ListRef).
// Returns the updated node (must be reassigned since slices may reallocate).
func nodeListAppend(node any, val any) any {
if lr, ok := node.(ListRef); ok {
lr.Val = append(lr.Val, val)
return lr
}
if arr, ok := node.([]any); ok {
return append(arr, val)
}
return node
}

// nodeListVal extracts the []any from a list node (plain []any or ListRef).
func nodeListVal(node any) ([]any, bool) {
if lr, ok := node.(ListRef); ok {
return lr.Val, true
}
if arr, ok := node.([]any); ok {
return arr, true
}
return nil, false
}

// nodeListSetVal updates the []any inside a list node, returning the updated node.
func nodeListSetVal(node any, arr []any) any {
if lr, ok := node.(ListRef); ok {
lr.Val = arr
return lr
}
return arr
}

// nodeMapSet sets a key on a map node.
func nodeMapSet(node any, key any, val any) {
k, _ := key.(string)
Expand Down
12 changes: 12 additions & 0 deletions go/listref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ func listRefEqual(a, b any) bool {
return false
}
}
// Meta: treat nil and empty map as equal
if len(av.Meta) != 0 || len(bv.Meta) != 0 {
if len(av.Meta) != len(bv.Meta) {
return false
}
for k, v := range av.Meta {
bval, exists := bv.Meta[k]
if !exists || !listRefEqual(v, bval) {
return false
}
}
}
return true
case map[string]any:
bv, ok := b.(map[string]any)
Expand Down
12 changes: 12 additions & 0 deletions go/mapref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ func mapRefEqual(a, b any) bool {
return false
}
}
// Meta: treat nil and empty map as equal
if len(av.Meta) != 0 || len(bv.Meta) != 0 {
if len(av.Meta) != len(bv.Meta) {
return false
}
for k, v := range av.Meta {
bval, exists := bv.Meta[k]
if !exists || !mapRefEqual(v, bval) {
return false
}
}
}
return true
case map[string]any:
bv, ok := b.(map[string]any)
Expand Down
14 changes: 14 additions & 0 deletions go/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Text struct {
// 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.
// ListRef is created early (in the BO phase) so that custom parsers
// can store additional information in the Meta map during parsing.
type ListRef struct {
// Val is the list contents.
Val []any
Expand All @@ -31,11 +33,18 @@ type ListRef struct {
// Multiple child values are merged (deep merge if Map.Extend is true).
// Nil when no child value is present.
Child any

// Meta is a map for custom parsers to attach additional information
// during parsing. It is initialized when the ListRef is created in
// the BO (before-open) phase.
Meta map[string]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.
// MapRef is created early (in the BO phase) so that custom parsers
// can store additional information in the Meta map during parsing.
type MapRef struct {
// Val is the map contents.
Val map[string]any
Expand All @@ -44,4 +53,9 @@ type MapRef struct {
// (e.g. key:value pairs without braces),
// and false when braces were used explicitly.
Implicit bool

// Meta is a map for custom parsers to attach additional information
// during parsing. It is initialized when the MapRef is created in
// the BO (before-open) phase.
Meta map[string]any
}
Loading
Loading