From 71ccc42fb47f3dfdcff0f7f8036d1ff1baff9b71 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 16:47:12 +0000 Subject: [PATCH] Refactor Go multisource to use jsonic/directive/go and jsonic/path/go Align the Go implementation with the TypeScript version by using the directive package instead of manually implementing token registration, custom matchers, and rule modifications. Add jsonic/path/go as a test dependency for Path plugin compatibility testing. https://claude.ai/code/session_01UCcfE3sNcpkMr5eYdoR12u --- go/go.mod | 6 +- go/go.sum | 4 + go/multisource_test.go | 55 ++++++++ go/plugin.go | 301 ++++++++++++++++------------------------- 4 files changed, 181 insertions(+), 185 deletions(-) diff --git a/go/go.mod b/go/go.mod index e825e8d..305f764 100644 --- a/go/go.mod +++ b/go/go.mod @@ -2,4 +2,8 @@ module github.com/jsonicjs/multisource/go go 1.24.7 -require github.com/jsonicjs/jsonic/go v0.1.4 +require ( + github.com/jsonicjs/directive/go v0.1.0 + github.com/jsonicjs/jsonic/go v0.1.4 + github.com/jsonicjs/path/go v0.1.0 +) diff --git a/go/go.sum b/go/go.sum index dc99d17..2cf7b40 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,2 +1,6 @@ +github.com/jsonicjs/directive/go v0.1.0 h1:ajPq4SJ/iShncaO+rMVSpNEMKbQhZxMGUxRkf42epyY= +github.com/jsonicjs/directive/go v0.1.0/go.mod h1:QSlUJcNs1/hhJzjKdRu/iz+QQp2YRJtzFRcD6/0llGY= github.com/jsonicjs/jsonic/go v0.1.4 h1:V1KEzmg/jIwk25+JYj8ig1+B7190rHmH8WqZbT7XlgA= github.com/jsonicjs/jsonic/go v0.1.4/go.mod h1:ObNKlCG7esWoi4AHCpdgkILvPINV8bpvkbCd4llGGUg= +github.com/jsonicjs/path/go v0.1.0 h1:rByeAJ/Zd/1q9WzI3VOQjW3axIvHr+o4lUEodUYhKx4= +github.com/jsonicjs/path/go v0.1.0/go.mod h1:/LLj1pxpn/e0Xu3Oo91YzGL5owcPasKfICNDGHY8GEM= diff --git a/go/multisource_test.go b/go/multisource_test.go index 84d80c6..18ad5c2 100644 --- a/go/multisource_test.go +++ b/go/multisource_test.go @@ -8,6 +8,7 @@ import ( "testing" jsonic "github.com/jsonicjs/jsonic/go" + path "github.com/jsonicjs/path/go" ) // assert is a test helper that checks deep equality. @@ -307,3 +308,57 @@ func TestAbsolutePath(t *testing.T) { m, _ := r.(map[string]any) assert(t, "abs-path", m["cfg"], map[string]any{"env": "prod"}) } + +func TestPathPlugin(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + "b.jsonic": `{b:2}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + j.Use(path.Path, nil) + + r, err := j.Parse(`{x: @a.jsonic, y: @b.jsonic}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "path-a", m["x"], map[string]any{"a": float64(1)}) + assert(t, "path-b", m["y"], map[string]any{"b": float64(2)}) +} + +func TestMergeIntoMap(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`{x:2, @a.jsonic}`) + if err != nil { + t.Fatal(err) + } + m, _ := r.(map[string]any) + assert(t, "merge-x", m["x"], float64(2)) + assert(t, "merge-a", m["a"], float64(1)) +} + +func TestTopLevelRef(t *testing.T) { + files := map[string]string{ + "a.jsonic": `{a:1}`, + } + + j := MakeJsonic(MultiSourceOptions{ + Resolver: MakeMemResolver(files), + }) + + r, err := j.Parse(`@a.jsonic`) + if err != nil { + t.Fatal(err) + } + assert(t, "top-level", r, map[string]any{"a": float64(1)}) +} diff --git a/go/plugin.go b/go/plugin.go index 4708099..1a38dff 100644 --- a/go/plugin.go +++ b/go/plugin.go @@ -3,19 +3,12 @@ package multisource import ( - "strings" + "fmt" + directive "github.com/jsonicjs/directive/go" jsonic "github.com/jsonicjs/jsonic/go" ) -// lexMode tracks which kind of token the custom matchers should produce. -type lexMode int - -const ( - modeNormal lexMode = iota // Normal jsonic parsing. - modePath // Scanning a multisource path after @. -) - // MultiSource is a jsonic plugin that adds multisource reference support. // When '@path' is encountered in the input, the path is resolved using // the configured resolver and processed into a value. @@ -25,197 +18,137 @@ func MultiSource(j *jsonic.Jsonic, pluginOpts map[string]any) { if markChar == "" { markChar = "@" } - markByte := markChar[0] - - mode := modeNormal cfg := j.Config() - // Register the mark character as a fixed token. - AT := j.Token("#AT", markChar) - cfg.SortFixedTokens() - - // Register custom token type for multisource paths. - MP := j.Token("#MP") - - // Standard tokens. - ZZ := j.Token("#ZZ") - OB := j.Token("#OB") // { - CB := j.Token("#CB") // } - CL := j.Token("#CL") // : - CA := j.Token("#CA") // , - _ = ZZ - _ = OB - _ = CB - _ = CL - _ = CA - // Add the mark character to ender chars so built-in matchers stop there. if cfg.EnderChars == nil { cfg.EnderChars = make(map[rune]bool) } - cfg.EnderChars[rune(markByte)] = true - - // Path matcher: reads the path after @. - // Runs at priority < 2e6 so it executes before built-in matchers. - j.AddMatcher("msrcpath", 100000, func(lex *jsonic.Lex) *jsonic.Token { - if mode != modePath { - return nil - } - mode = modeNormal - - pnt := lex.Cursor() - src := lex.Src - sI := pnt.SI - cI := pnt.CI - - if sI >= pnt.Len { - return nil - } - - // Skip leading whitespace. - for sI < pnt.Len && (src[sI] == ' ' || src[sI] == '\t') { - sI++ - cI++ - } - - ch := src[sI] - // Don't match at delimiters or quotes. - if ch == ',' || ch == '}' || ch == ']' || ch == '{' || ch == '[' || - ch == '\n' || ch == '\r' || ch == markByte { - return nil - } - - // Handle quoted paths: "path" or 'path'. - if ch == '"' || ch == '\'' { - quote := ch - sI++ // skip opening quote - cI++ - startI := sI - for sI < pnt.Len && src[sI] != quote { - if src[sI] == '\\' && sI+1 < pnt.Len { - sI += 2 - cI += 2 - continue + cfg.EnderChars[rune(markChar[0])] = true + + // Define a directive that can load content from multiple sources. + dopts := directive.DirectiveOptions{ + Name: "multisource", + Open: markChar, + Rules: &directive.RulesOption{ + Open: map[string]*directive.RuleMod{ + "val": {}, + "pair": { + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.Lte("pk", 0) + }, + }, + }, + }, + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) { + spec := rule.Child.Node + + var pathStr string + switch v := spec.(type) { + case string: + pathStr = v + case map[string]any: + if p, ok := v["path"]; ok { + pathStr = fmt.Sprintf("%v", p) } - sI++ - cI++ } - pathStr := src[startI:sI] - raw := src[pnt.SI:sI] - if sI < pnt.Len { - sI++ // skip closing quote - cI++ - raw = src[pnt.SI:sI] - } - tkn := lex.Token("#MP", MP, pathStr, raw) - pnt.SI = sI - pnt.CI = cI - return tkn - } - - startI := sI - - // Read path until a delimiter. - for sI < pnt.Len { - c := src[sI] - if c == ' ' || c == '\t' || c == '\n' || c == '\r' || - c == ',' || c == '}' || c == ']' || c == ':' || - c == '{' || c == '[' || c == markByte { - break - } - sI++ - cI++ - } - if sI == startI { - return nil - } + res := resolveSource(pathStr, opts, j) - raw := src[startI:sI] - pathStr := strings.TrimSpace(raw) - - tkn := lex.Token("#MP", MP, pathStr, raw) - pnt.SI = sI - pnt.CI = cI - return tkn - }) + from := "" + if rule.Parent != nil && rule.Parent != jsonic.NoRule { + from = rule.Parent.Name + } - // resolveSource resolves a multisource path and sets the node value. - resolveSource := func(pathStr string) any { - spec := ResolvePathSpec(pathStr, opts.Path) - res := opts.Resolver(spec, opts) + // Handle the {@foo} case, injecting keys into parent map. + if from == "pair" { + if m, ok := res.(map[string]any); ok { + if rule.Parent.Parent != nil && rule.Parent.Parent != jsonic.NoRule { + if parent, ok := rule.Parent.Parent.Node.(map[string]any); ok { + for k, v := range m { + parent[k] = v + } + } + } + } + } else { + rule.Node = res + } + }, + Custom: func(j *jsonic.Jsonic, cfg directive.DirectiveConfig) { + OPEN := cfg.OPEN + name := cfg.Name + + // Handle special case of @foo first token - assume a map. + j.Rule("val", func(rs *jsonic.RuleSpec) { + rs.PrependOpen( + &jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] > 0 + }, + B: 1, + G: name + "_undive", + }, + &jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.D == 0 + }, + P: "map", + B: 1, + N: map[string]int{name + "_top": 1}, + }, + ) + }) + + j.Rule("map", func(rs *jsonic.RuleSpec) { + rs.PrependOpen(&jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.D == 1 && r.N[name+"_top"] == 1 + }, + P: "pair", + B: 1, + }) + rs.PrependClose(&jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] > 0 + }, + B: 1, + G: name + "_undive", + }) + }) + + j.Rule("pair", func(rs *jsonic.RuleSpec) { + rs.PrependClose(&jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] > 0 + }, + B: 1, + G: name + "_undive", + }) + }) + }, + } - if !res.Found { - return nil - } + directive.Apply(j, dopts) +} - proc := getProcessor(res.Kind, opts.Processor) - proc(&res, opts, j) +// resolveSource resolves a multisource path and returns the processed value. +func resolveSource(pathStr string, opts *MultiSourceOptions, j *jsonic.Jsonic) any { + spec := ResolvePathSpec(pathStr, opts.Path) + res := opts.Resolver(spec, opts) - return res.Val + if !res.Found { + return nil } - // Extend the val rule to handle @path in value position. - j.Rule("val", func(rs *jsonic.RuleSpec) { - newAlts := []*jsonic.AltSpec{ - // @path in value position: resolve and use as value. - { - S: [][]jsonic.Tin{{AT}}, - P: "msrc", - A: func(r *jsonic.Rule, ctx *jsonic.Context) { - mode = modePath - }, - }, - } - rs.Open = append(newAlts, rs.Open...) - }) - - // Extend the pair rule to handle @path in pair position (merge into map). - j.Rule("pair", func(rs *jsonic.RuleSpec) { - newAlts := []*jsonic.AltSpec{ - { - S: [][]jsonic.Tin{{AT}}, - P: "msrc", - U: map[string]any{"msrc_merge": true}, - A: func(r *jsonic.Rule, ctx *jsonic.Context) { - mode = modePath - }, - }, - } - rs.Open = append(newAlts, rs.Open...) - }) - - // The msrc rule handles resolving the multisource path. - j.Rule("msrc", func(rs *jsonic.RuleSpec) { - rs.Clear() - - rs.Open = []*jsonic.AltSpec{ - { - S: [][]jsonic.Tin{{MP}}, - A: func(r *jsonic.Rule, ctx *jsonic.Context) { - pathStr, _ := r.O0.Val.(string) - if pathStr == "" { - pathStr = r.O0.Src - } + proc := getProcessor(res.Kind, opts.Processor) + proc(&res, opts, j) - val := resolveSource(pathStr) - r.Node = val - - // If parent requested merge, merge the resolved map. - if r.Parent != nil && r.Parent != jsonic.NoRule { - if _, doMerge := r.Parent.U["msrc_merge"]; doMerge { - if m, ok := val.(map[string]any); ok { - if parent, ok := r.Parent.Node.(map[string]any); ok { - for k, v := range m { - parent[k] = v - } - } - } - } - } - }, - }, - } - }) + return res.Val }