Skip to content

Commit

Permalink
Path handler (#180)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
ohler55 authored Aug 10, 2024
1 parent d291cb2 commit 19c93df
Show file tree
Hide file tree
Showing 11 changed files with 633 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

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

## [1.24.0] - 2024-08-09
### Added
- 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.23.0] - 2024-07-07
### Added
- New script functions can now be added with `jp.RegisterUnaryFunction()` and `jp.RegisterBinaryFunction()`.
Expand Down
97 changes: 92 additions & 5 deletions cmd/oj/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"io"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -38,6 +38,8 @@ var (
safe = false
mongo = false
omit = false
dig = false
annotate = false

// If true wrap extracts with an array.
wrapExtract = false
Expand Down Expand Up @@ -72,13 +74,16 @@ func init() {
flag.BoolVar(&lazy, "z", lazy, "lazy mode accepts Simple Encoding Notation (quotes and commas mostly optional)")
flag.BoolVar(&senOut, "sen", senOut, "output in Simple Encoding Notation")
flag.BoolVar(&tab, "t", tab, "indent with tabs")
flag.BoolVar(&annotate, "annotate", annotate, "annotate dig extracts with a path comment")
flag.Var(&exValue{}, "x", "extract path")
flag.Var(&matchValue{}, "m", "match equation/script")
flag.Var(&delValue{}, "d", "delete path")
flag.BoolVar(&dig, "dig", dig, "dig into a large document using the tokenizer")
flag.BoolVar(&showVersion, "version", showVersion, "display version and exit")
flag.StringVar(&planDef, "a", planDef, "assembly plan or plan file using @<plan>")
flag.BoolVar(&showRoot, "r", showRoot, "print root if an assemble plan provided")
flag.StringVar(&prettyOpt, "p", prettyOpt, `pretty print with the width, depth, and align as <width>.<max-depth>.<align>`)
flag.StringVar(&prettyOpt, "p", prettyOpt,
`pretty print with the width, depth, and align as <width>.<max-depth>.<align>`)
flag.BoolVar(&html, "html", html, "output colored output as HTML")
flag.BoolVar(&safe, "safe", safe, "escape &, <, and > for HTML inclusion")
flag.StringVar(&confFile, "f", confFile, "configuration file (see -help-config), - indicates no file")
Expand Down Expand Up @@ -271,7 +276,7 @@ func run() (err error) {
if 0 < len(planDef) {
if planDef[0] != '[' {
var b []byte
if b, err = ioutil.ReadFile(planDef); err != nil {
if b, err = os.ReadFile(planDef); err != nil {
return err
}
planDef = string(b)
Expand All @@ -290,7 +295,11 @@ func run() (err error) {
var f *os.File
for _, file := range files {
if f, err = os.Open(file); err == nil {
_, err = p.ParseReader(f, write)
if dig {
err = digParse(f)
} else {
_, err = p.ParseReader(f, write)
}
_ = f.Close()
}
if err != nil {
Expand All @@ -304,7 +313,12 @@ func run() (err error) {
}
}
if len(files) == 0 && len(input) == 0 {
if _, err = p.ParseReader(os.Stdin, write); err != nil {
if dig {
err = digParse(os.Stdin)
} else {
_, err = p.ParseReader(os.Stdin, write)
}
if err != nil {
panic(err)
}
}
Expand All @@ -317,6 +331,79 @@ func run() (err error) {
return
}

func digParse(r io.Reader) error {
var fn func(path jp.Expr, data any)
annotateColor := ""

if color {
annotateColor = ojg.Gray
}
// Pick a function that satisfies omit, annotate, and senOut
// values. Determining the function before the actual calling means few
// conditional paths during the repeated calls later.
if omit {
if annotate {
if senOut {
fn = func(path jp.Expr, data any) {
if data != nil && data != "" {
fmt.Printf("%s// %s\n", annotateColor, path)
writeSEN(data)
}
}
} else {
fn = func(path jp.Expr, data any) {
if data != nil && data != "" {
fmt.Printf("%s// %s\n", annotateColor, path)
writeJSON(data)
}
}
}
} else {
if senOut {
fn = func(path jp.Expr, data any) {
if data != nil && data != "" {
writeSEN(data)
}
}
} else {
fn = func(path jp.Expr, data any) {
if data != nil && data != "" {
writeJSON(data)
}
}
}
}
} else {
if annotate {
if senOut {
fn = func(path jp.Expr, data any) {
fmt.Printf("%s// %s\n", annotateColor, path)
writeSEN(data)
}
} else {
fn = func(path jp.Expr, data any) {
fmt.Printf("%s// %s\n", annotateColor, path)
writeJSON(data)
}
}
} else {
if senOut {
fn = func(path jp.Expr, data any) {
writeSEN(data)
}
} else {
fn = func(path jp.Expr, data any) {
writeJSON(data)
}
}
}
}
if lazy {
return sen.MatchLoad(r, fn, extracts...)
}
return oj.MatchLoad(r, fn, extracts...)
}

func write(v any) bool {
if conv != nil {
v = conv.Convert(v)
Expand Down
86 changes: 86 additions & 0 deletions jp/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2024, Peter Ohler, All rights reserved.

package jp

// PathMatch returns true if the provided path would match the target
// expression. The path argument is expected to be a normalized path with only
// elements of Root ($), At (@), Child (string), or Nth (int). A Filter
// fragment in the target expression will match any value in path since it
// requires data from a JSON document to be evaluated. Slice fragments always
// return true as long as the path element is an Nth.
func PathMatch(target, path Expr) bool {
if 0 < len(target) {
switch target[0].(type) {
case Root, At:
target = target[1:]
}
}
if 0 < len(path) {
switch path[0].(type) {
case Root, At:
path = path[1:]
}
}
for i, f := range target {
if len(path) == 0 {
return false
}
switch path[0].(type) {
case Child, Nth:
default:
return false
}
switch tf := f.(type) {
case Child, Nth:
if tf != path[0] {
return false
}
path = path[1:]
case Bracket:
// ignore and don't advance path
case Wildcard:
path = path[1:]
case Union:
var ok bool
for _, u := range tf {
check:
switch tu := u.(type) {
case string:
if Child(tu) == path[0] {
ok = true
break check
}
case int64:
if Nth(tu) == path[0] {
ok = true
break check
}
}
}
if !ok {
return false
}
path = path[1:]
case Slice:
if _, ok := path[0].(Nth); !ok {
return false
}
path = path[1:]
case *Filter:
// Assume a match since there is no data for comparison.
path = path[1:]
case Descent:
rest := target[i+1:]
for 0 < len(path) {
if PathMatch(rest, path) {
return true
}
path = path[1:]
}
return false
default:
return false
}
}
return true
}
56 changes: 56 additions & 0 deletions jp/match_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// copyright (c) 2024, Peter Ohler, All rights reserved.

package jp_test

import (
"testing"

"github.com/ohler55/ojg/jp"
"github.com/ohler55/ojg/tt"
)

type matchData struct {
target string
path string
expect bool
}

func TestPathMatchCheck(t *testing.T) {
for i, md := range []*matchData{
{target: "$.a", path: "a", expect: true},
{target: "@.a", path: "a", expect: true},
{target: "a", path: "a", expect: true},
{target: "a", path: "$.a", expect: true},
{target: "a", path: "@.a", expect: true},
{target: "[1]", path: "[1]", expect: true},
{target: "[1]", path: "[0]", expect: false},
{target: "*", path: "[1]", expect: true},
{target: "[*]", path: "[1]", expect: true},
{target: "*", path: "a", expect: true},
{target: "[1,'a']", path: "a", expect: true},
{target: "[1,'a']", path: "[1]", expect: true},
{target: "[1,'a']", path: "b", expect: false},
{target: "[1,'a']", path: "[0]", expect: false},
{target: "$.x[1,'a']", path: "x[1]", expect: true},
{target: "..x", path: "a.b.x", expect: true},
{target: "..x", path: "a.b.c", expect: false},
{target: "x[1:5:2]", path: "x[2]", expect: true},
{target: "x[1:5:2]", path: "x.y", expect: false},
{target: "x[[email protected] == 2]", path: "x[2]", expect: true},
{target: "x.y.z", path: "x.y", expect: false},
} {
tt.Equal(t, md.expect, jp.PathMatch(jp.MustParseString(md.target), jp.MustParseString(md.path)),
"%d: %s %s", i, md.target, md.path)
}
}

func TestPathMatchDoubleRoot(t *testing.T) {
tt.Equal(t, false, jp.PathMatch(jp.R().R().C("a"), jp.C("a")))
tt.Equal(t, false, jp.PathMatch(jp.A().A().C("a"), jp.C("a")))
tt.Equal(t, false, jp.PathMatch(jp.C("a"), jp.R().R().C("a")))
tt.Equal(t, false, jp.PathMatch(jp.C("a"), jp.A().A().C("a")))
}

func TestPathMatchSkipBracket(t *testing.T) {
tt.Equal(t, true, jp.PathMatch(jp.B().C("a"), jp.C("a")))
}
Loading

0 comments on commit 19c93df

Please sign in to comment.