Skip to content

Commit

Permalink
Jp locate (#154)
Browse files Browse the repository at this point in the history
Add jp.Expr.Locate()
  • Loading branch information
ohler55 committed Dec 13, 2023
1 parent 4f98673 commit 8d4703e
Show file tree
Hide file tree
Showing 26 changed files with 1,119 additions and 23 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ 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.21.0] - unreleased
### Added
- Added the Expr function `BracketString` to force the use of bracket
notation to for normalized paths as described by the draft IETF
JSONPath document in section 2.7.
- Added `jp.Expr.Locate()` function that returns normalized paths for JSONPath expression.
### Fixed
- TBD Unmarshal now supports arrays such as `[4]int`.

## [1.20.3] - 2023-11-09
### Added
- Added an option to jp.Walk to just callback on leaves making the function more useable.
Expand Down
19 changes: 19 additions & 0 deletions alt/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package alt

import (
"fmt"
"reflect"
"time"
"unsafe"
Expand All @@ -18,6 +19,24 @@ var TimeTolerance = time.Millisecond
// is a wildcard that matches either.
type Path []any

// String representation of the Path.
func (p Path) String() string {
var b []byte

for i, a := range p {
switch ta := a.(type) {
case int:
b = fmt.Appendf(b, "[%d]", ta)
case string:
if 0 < i {
b = append(b, '.')
}
b = append(b, ta...)
}
}
return string(b)
}

// Diff returns the paths to the differences between two values. Any ignore
// paths are ignored in the comparison.
func Diff(v0, v1 any, ignores ...Path) (diffs []Path) {
Expand Down
4 changes: 4 additions & 0 deletions alt/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,7 @@ func TestDiffArrayIgnores2(t *testing.T) {
)
tt.Equal(t, "[[0 b]]", pretty.SEN(diffs))
}

func TestPathString(t *testing.T) {
tt.Equal(t, "a[3].b", alt.Path{"a", 3, "b"}.String())
}
4 changes: 2 additions & 2 deletions alt/example_diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func ExampleDiff() {
})
fmt.Printf("diff: %v\n", diffs)

// Output: diff: [[y] [z 1] [z 2]]
// Output: diff: [y z[1] z[2]]
}

func ExampleCompare() {
Expand All @@ -30,7 +30,7 @@ func ExampleCompare() {
)
fmt.Printf("diff: %v\n", diff)

// Output: diff: [z 1]
// Output: diff: z[1]
}

func ExampleMatch() {
Expand Down
7 changes: 7 additions & 0 deletions jp/at.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ func (f At) Append(buf []byte, bracket, first bool) []byte {
buf = append(buf, '@')
return buf
}

func (f At) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) {
if 0 < len(rest) {
locs = rest[0].locate(append(pp, f), data, rest[1:], max)
}
return
}
7 changes: 7 additions & 0 deletions jp/bracket.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ type Bracket byte
func (f Bracket) Append(buf []byte, bracket, first bool) []byte {
return buf
}

func (f Bracket) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) {
if 0 < len(rest) {
locs = rest[0].locate(pp, data, rest[1:], max)
}
return
}
21 changes: 21 additions & 0 deletions jp/child.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,24 @@ func (f Child) remove(value any) (out any, changed bool) {
}
return
}

func (f Child) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) {
var (
v any
has bool
)
switch td := data.(type) {
case map[string]any:
v, has = td[string(f)]
case gen.Object:
v, has = td[string(f)]
case Keyed:
v, has = td.ValueForKey(string(f))
default:
v, has = pp.reflectGetChild(td, string(f))
}
if has {
locs = locateNthChildHas(pp, f, v, rest, max)
}
return
}
131 changes: 131 additions & 0 deletions jp/descent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

package jp

import (
"reflect"

"github.com/ohler55/ojg/gen"
)

// Descent is used as a flag to indicate the path should be displayed in a
// recursive descent representation.
type Descent byte
Expand All @@ -16,3 +22,128 @@ func (f Descent) Append(buf []byte, bracket, first bool) []byte {
}
return buf
}

func (f Descent) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) {
if len(rest) == 0 { // last one
loc := make(Expr, len(pp))
copy(loc, pp)
locs = append(locs, loc)
} else {
locs = locateContinueFrag(locs, pp, data, rest, max)
}
cp := append(pp, nil) // place holder
mx := max
switch td := data.(type) {
case map[string]any:
for k, v := range td {
cp[len(pp)] = Child(k)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, v, rest, mx)...)
}
case []any:
for i, v := range td {
cp[len(pp)] = Nth(i)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, v, rest, mx)...)
}
case gen.Object:
for k, v := range td {
cp[len(pp)] = Child(k)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, v, rest, mx)...)
}
case gen.Array:
for i, v := range td {
cp[len(pp)] = Nth(i)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, v, rest, mx)...)
}
case Keyed:
keys := td.Keys()
for _, k := range keys {
v, _ := td.ValueForKey(k)
cp[len(pp)] = Child(k)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, v, rest, mx)...)
}
case Indexed:
size := td.Size()
for i := 0; i < size; i++ {
v := td.ValueAtIndex(i)
cp[len(pp)] = Nth(i)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, v, rest, mx)...)
}
case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String,
int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int:
default:
rd := reflect.ValueOf(data)
rt := rd.Type()
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
rd = rd.Elem()
}
cp := append(pp, nil) // place holder
switch rt.Kind() {
case reflect.Struct:
for i := rd.NumField() - 1; 0 <= i; i-- {
rv := rd.Field(i)
if rv.CanInterface() {
cp[len(pp)] = Child(rt.Field(i).Name)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, rv.Interface(), rest, mx)...)
}
}
case reflect.Slice, reflect.Array:
for i := 0; i < rd.Len(); i++ {
rv := rd.Index(i)
if rv.CanInterface() {
cp[len(pp)] = Nth(i)
if 0 < max {
mx = max - len(locs)
if mx <= 0 {
break
}
}
locs = append(locs, f.locate(cp, rv.Interface(), rest, mx)...)
}
}
}
}
return
}
14 changes: 11 additions & 3 deletions jp/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

package jp

import "unsafe"
import (
"unsafe"
)

// Expr is a JSON path expression composed of fragments. An Expr implements
// JSONPath as described by https://goessner.net/articles/JsonPath. Where the
Expand All @@ -15,10 +17,16 @@ func (x Expr) String() string {
return string(x.Append(nil))
}

// BracketString returns a string representation of the expression using the
// bracket notation.
func (x Expr) BracketString() string {
return string(x.Append(nil, true))
}

// Append a string representation of the expression to a byte slice and return
// the expanded buffer.
func (x Expr) Append(buf []byte) []byte {
bracket := false
func (x Expr) Append(buf []byte, brackets ...bool) []byte {
bracket := 0 < len(brackets) && brackets[0]
for i, frag := range x {
if _, ok := frag.(Bracket); ok {
bracket = true
Expand Down
5 changes: 5 additions & 0 deletions jp/expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,8 @@ func TestExprBracket(t *testing.T) {
br := jp.Bracket('x')
tt.Equal(t, 0, len(br.Append([]byte{}, true, true)))
}

func TestExprBracketString(t *testing.T) {
x := jp.R().C("abc").N(1).C("def")
tt.Equal(t, "$['abc'][1]['def']", x.BracketString())
}
23 changes: 23 additions & 0 deletions jp/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,26 @@ func (f Filter) removeOne(value any) (out any, changed bool) {
}
return
}

func (f Filter) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) {
ns, lcs := f.evalWithRoot([]any{}, data, nil)
stack, _ := ns.([]any)
if len(rest) == 0 { // last one
for _, lc := range lcs {
locs = locateAppendFrag(locs, pp, lc)
if 0 < max && max <= len(locs) {
break
}
}
} else {
cp := append(pp, nil) // place holder
for i, lc := range lcs {
cp[len(pp)] = lc
locs = locateContinueFrag(locs, cp, stack[i], rest, max)
if 0 < max && max <= len(locs) {
break
}
}
}
return
}
2 changes: 2 additions & 0 deletions jp/frag.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ type Frag interface {
// Append a fragment string representation of the fragment to the buffer
// then returning the expanded buffer.
Append(buf []byte, bracket, first bool) []byte

locate(pp Expr, data any, rest Expr, max int) (locs []Expr)
}
6 changes: 4 additions & 2 deletions jp/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,8 @@ func (x Expr) Get(data any) (results []any) {
}
case *Filter:
before := len(stack)
stack, _ = tf.EvalWithRoot(stack, prev, data).([]any)
ns, _ := tf.evalWithRoot(stack, prev, data)
stack, _ = ns.([]any)
if int(fi) == len(x)-1 { // last one
for i := len(stack) - 1; before <= i; i-- {
results = append(results, stack[i])
Expand Down Expand Up @@ -1616,7 +1617,8 @@ func (x Expr) FirstFound(data any) (any, bool) {
}
case *Filter:
before := len(stack)
stack, _ = tf.EvalWithRoot(stack, prev, data).([]any)
ns, _ := tf.evalWithRoot(stack, prev, data)
stack, _ = ns.([]any)
if int(fi) == len(x)-1 { // last one
if before < len(stack) {
result := stack[len(stack)-1]
Expand Down
3 changes: 2 additions & 1 deletion jp/has.go
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,8 @@ func (x Expr) Has(data any) bool {
}
case *Filter:
before := len(stack)
stack, _ = tf.EvalWithRoot(stack, prev, data).([]any)
ns, _ := tf.evalWithRoot(stack, prev, data)
stack, _ = ns.([]any)
if int(fi) == len(x)-1 { // last one
if before < len(stack) {
stack = stack[:before]
Expand Down
Loading

0 comments on commit 8d4703e

Please sign in to comment.