Skip to content

Commit 19c93df

Browse files
authored
Path handler (#180)
- Added the `jp.PathMatch` function that compares a normalized JSONPath with a target JSONPath. - Added `jp.MatchHandler` a TokenHandler that can be used to build a path and data while processing a JSON document. - Added `oj.Match` and `sen.Match` functions.
1 parent d291cb2 commit 19c93df

File tree

11 files changed

+633
-6
lines changed

11 files changed

+633
-6
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
44

55
The structure and content of this file follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66

7+
## [1.24.0] - 2024-08-09
8+
### Added
9+
- Added the `jp.PathMatch` function that compares a normalized JSONPath with a target JSONPath.
10+
- Added `jp.MatchHandler` a TokenHandler that can be used to
11+
build a path and data while processing a JSON document.
12+
- Added `oj.Match` and `sen.Match` functions.
13+
714
## [1.23.0] - 2024-07-07
815
### Added
916
- New script functions can now be added with `jp.RegisterUnaryFunction()` and `jp.RegisterBinaryFunction()`.

cmd/oj/main.go

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package main
55
import (
66
"flag"
77
"fmt"
8-
"io/ioutil"
8+
"io"
99
"os"
1010
"path/filepath"
1111
"sort"
@@ -38,6 +38,8 @@ var (
3838
safe = false
3939
mongo = false
4040
omit = false
41+
dig = false
42+
annotate = false
4143

4244
// If true wrap extracts with an array.
4345
wrapExtract = false
@@ -72,13 +74,16 @@ func init() {
7274
flag.BoolVar(&lazy, "z", lazy, "lazy mode accepts Simple Encoding Notation (quotes and commas mostly optional)")
7375
flag.BoolVar(&senOut, "sen", senOut, "output in Simple Encoding Notation")
7476
flag.BoolVar(&tab, "t", tab, "indent with tabs")
77+
flag.BoolVar(&annotate, "annotate", annotate, "annotate dig extracts with a path comment")
7578
flag.Var(&exValue{}, "x", "extract path")
7679
flag.Var(&matchValue{}, "m", "match equation/script")
7780
flag.Var(&delValue{}, "d", "delete path")
81+
flag.BoolVar(&dig, "dig", dig, "dig into a large document using the tokenizer")
7882
flag.BoolVar(&showVersion, "version", showVersion, "display version and exit")
7983
flag.StringVar(&planDef, "a", planDef, "assembly plan or plan file using @<plan>")
8084
flag.BoolVar(&showRoot, "r", showRoot, "print root if an assemble plan provided")
81-
flag.StringVar(&prettyOpt, "p", prettyOpt, `pretty print with the width, depth, and align as <width>.<max-depth>.<align>`)
85+
flag.StringVar(&prettyOpt, "p", prettyOpt,
86+
`pretty print with the width, depth, and align as <width>.<max-depth>.<align>`)
8287
flag.BoolVar(&html, "html", html, "output colored output as HTML")
8388
flag.BoolVar(&safe, "safe", safe, "escape &, <, and > for HTML inclusion")
8489
flag.StringVar(&confFile, "f", confFile, "configuration file (see -help-config), - indicates no file")
@@ -271,7 +276,7 @@ func run() (err error) {
271276
if 0 < len(planDef) {
272277
if planDef[0] != '[' {
273278
var b []byte
274-
if b, err = ioutil.ReadFile(planDef); err != nil {
279+
if b, err = os.ReadFile(planDef); err != nil {
275280
return err
276281
}
277282
planDef = string(b)
@@ -290,7 +295,11 @@ func run() (err error) {
290295
var f *os.File
291296
for _, file := range files {
292297
if f, err = os.Open(file); err == nil {
293-
_, err = p.ParseReader(f, write)
298+
if dig {
299+
err = digParse(f)
300+
} else {
301+
_, err = p.ParseReader(f, write)
302+
}
294303
_ = f.Close()
295304
}
296305
if err != nil {
@@ -304,7 +313,12 @@ func run() (err error) {
304313
}
305314
}
306315
if len(files) == 0 && len(input) == 0 {
307-
if _, err = p.ParseReader(os.Stdin, write); err != nil {
316+
if dig {
317+
err = digParse(os.Stdin)
318+
} else {
319+
_, err = p.ParseReader(os.Stdin, write)
320+
}
321+
if err != nil {
308322
panic(err)
309323
}
310324
}
@@ -317,6 +331,79 @@ func run() (err error) {
317331
return
318332
}
319333

334+
func digParse(r io.Reader) error {
335+
var fn func(path jp.Expr, data any)
336+
annotateColor := ""
337+
338+
if color {
339+
annotateColor = ojg.Gray
340+
}
341+
// Pick a function that satisfies omit, annotate, and senOut
342+
// values. Determining the function before the actual calling means few
343+
// conditional paths during the repeated calls later.
344+
if omit {
345+
if annotate {
346+
if senOut {
347+
fn = func(path jp.Expr, data any) {
348+
if data != nil && data != "" {
349+
fmt.Printf("%s// %s\n", annotateColor, path)
350+
writeSEN(data)
351+
}
352+
}
353+
} else {
354+
fn = func(path jp.Expr, data any) {
355+
if data != nil && data != "" {
356+
fmt.Printf("%s// %s\n", annotateColor, path)
357+
writeJSON(data)
358+
}
359+
}
360+
}
361+
} else {
362+
if senOut {
363+
fn = func(path jp.Expr, data any) {
364+
if data != nil && data != "" {
365+
writeSEN(data)
366+
}
367+
}
368+
} else {
369+
fn = func(path jp.Expr, data any) {
370+
if data != nil && data != "" {
371+
writeJSON(data)
372+
}
373+
}
374+
}
375+
}
376+
} else {
377+
if annotate {
378+
if senOut {
379+
fn = func(path jp.Expr, data any) {
380+
fmt.Printf("%s// %s\n", annotateColor, path)
381+
writeSEN(data)
382+
}
383+
} else {
384+
fn = func(path jp.Expr, data any) {
385+
fmt.Printf("%s// %s\n", annotateColor, path)
386+
writeJSON(data)
387+
}
388+
}
389+
} else {
390+
if senOut {
391+
fn = func(path jp.Expr, data any) {
392+
writeSEN(data)
393+
}
394+
} else {
395+
fn = func(path jp.Expr, data any) {
396+
writeJSON(data)
397+
}
398+
}
399+
}
400+
}
401+
if lazy {
402+
return sen.MatchLoad(r, fn, extracts...)
403+
}
404+
return oj.MatchLoad(r, fn, extracts...)
405+
}
406+
320407
func write(v any) bool {
321408
if conv != nil {
322409
v = conv.Convert(v)

jp/match.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) 2024, Peter Ohler, All rights reserved.
2+
3+
package jp
4+
5+
// PathMatch returns true if the provided path would match the target
6+
// expression. The path argument is expected to be a normalized path with only
7+
// elements of Root ($), At (@), Child (string), or Nth (int). A Filter
8+
// fragment in the target expression will match any value in path since it
9+
// requires data from a JSON document to be evaluated. Slice fragments always
10+
// return true as long as the path element is an Nth.
11+
func PathMatch(target, path Expr) bool {
12+
if 0 < len(target) {
13+
switch target[0].(type) {
14+
case Root, At:
15+
target = target[1:]
16+
}
17+
}
18+
if 0 < len(path) {
19+
switch path[0].(type) {
20+
case Root, At:
21+
path = path[1:]
22+
}
23+
}
24+
for i, f := range target {
25+
if len(path) == 0 {
26+
return false
27+
}
28+
switch path[0].(type) {
29+
case Child, Nth:
30+
default:
31+
return false
32+
}
33+
switch tf := f.(type) {
34+
case Child, Nth:
35+
if tf != path[0] {
36+
return false
37+
}
38+
path = path[1:]
39+
case Bracket:
40+
// ignore and don't advance path
41+
case Wildcard:
42+
path = path[1:]
43+
case Union:
44+
var ok bool
45+
for _, u := range tf {
46+
check:
47+
switch tu := u.(type) {
48+
case string:
49+
if Child(tu) == path[0] {
50+
ok = true
51+
break check
52+
}
53+
case int64:
54+
if Nth(tu) == path[0] {
55+
ok = true
56+
break check
57+
}
58+
}
59+
}
60+
if !ok {
61+
return false
62+
}
63+
path = path[1:]
64+
case Slice:
65+
if _, ok := path[0].(Nth); !ok {
66+
return false
67+
}
68+
path = path[1:]
69+
case *Filter:
70+
// Assume a match since there is no data for comparison.
71+
path = path[1:]
72+
case Descent:
73+
rest := target[i+1:]
74+
for 0 < len(path) {
75+
if PathMatch(rest, path) {
76+
return true
77+
}
78+
path = path[1:]
79+
}
80+
return false
81+
default:
82+
return false
83+
}
84+
}
85+
return true
86+
}

jp/match_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// copyright (c) 2024, Peter Ohler, All rights reserved.
2+
3+
package jp_test
4+
5+
import (
6+
"testing"
7+
8+
"github.com/ohler55/ojg/jp"
9+
"github.com/ohler55/ojg/tt"
10+
)
11+
12+
type matchData struct {
13+
target string
14+
path string
15+
expect bool
16+
}
17+
18+
func TestPathMatchCheck(t *testing.T) {
19+
for i, md := range []*matchData{
20+
{target: "$.a", path: "a", expect: true},
21+
{target: "@.a", path: "a", expect: true},
22+
{target: "a", path: "a", expect: true},
23+
{target: "a", path: "$.a", expect: true},
24+
{target: "a", path: "@.a", expect: true},
25+
{target: "[1]", path: "[1]", expect: true},
26+
{target: "[1]", path: "[0]", expect: false},
27+
{target: "*", path: "[1]", expect: true},
28+
{target: "[*]", path: "[1]", expect: true},
29+
{target: "*", path: "a", expect: true},
30+
{target: "[1,'a']", path: "a", expect: true},
31+
{target: "[1,'a']", path: "[1]", expect: true},
32+
{target: "[1,'a']", path: "b", expect: false},
33+
{target: "[1,'a']", path: "[0]", expect: false},
34+
{target: "$.x[1,'a']", path: "x[1]", expect: true},
35+
{target: "..x", path: "a.b.x", expect: true},
36+
{target: "..x", path: "a.b.c", expect: false},
37+
{target: "x[1:5:2]", path: "x[2]", expect: true},
38+
{target: "x[1:5:2]", path: "x.y", expect: false},
39+
{target: "x[[email protected] == 2]", path: "x[2]", expect: true},
40+
{target: "x.y.z", path: "x.y", expect: false},
41+
} {
42+
tt.Equal(t, md.expect, jp.PathMatch(jp.MustParseString(md.target), jp.MustParseString(md.path)),
43+
"%d: %s %s", i, md.target, md.path)
44+
}
45+
}
46+
47+
func TestPathMatchDoubleRoot(t *testing.T) {
48+
tt.Equal(t, false, jp.PathMatch(jp.R().R().C("a"), jp.C("a")))
49+
tt.Equal(t, false, jp.PathMatch(jp.A().A().C("a"), jp.C("a")))
50+
tt.Equal(t, false, jp.PathMatch(jp.C("a"), jp.R().R().C("a")))
51+
tt.Equal(t, false, jp.PathMatch(jp.C("a"), jp.A().A().C("a")))
52+
}
53+
54+
func TestPathMatchSkipBracket(t *testing.T) {
55+
tt.Equal(t, true, jp.PathMatch(jp.B().C("a"), jp.C("a")))
56+
}

0 commit comments

Comments
 (0)