Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jp proc #196

Merged
merged 2 commits into from
Dec 31, 2024
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ 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.26.0] - 2024-12-31
### Added
- Support for non-selector scripts added to the jp package. See the
`jp.CompileScript` variable along with the `Proc` fragment type and
`Procedure` interface.

## [1.25.1] - 2024-12-26
### Fixed
- Fixed precision loss with some fraction parsing.
Expand Down
44 changes: 44 additions & 0 deletions jp/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,28 @@ func (x Expr) Get(data any) (results []any) {
stack = stack[:before]
}
}
case *Proc:
got := tf.Procedure.Get(prev)
if int(fi) == len(x)-1 { // last one
results = append(results, got...)
} else {
for i := len(got) - 1; 0 <= i; i-- {
v = got[i]
switch v.(type) {
case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String,
int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int:
case map[string]any, []any, gen.Object, gen.Array, Keyed, Indexed:
stack = append(stack, v)
default:
if rt := reflect.TypeOf(v); rt != nil {
switch rt.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Array, reflect.Map:
stack = append(stack, v)
}
}
}
}
}
}
if int(fi) < len(x)-1 {
if _, ok := stack[len(stack)-1].(fragIndex); !ok {
Expand Down Expand Up @@ -1626,6 +1648,28 @@ func (x Expr) FirstFound(data any) (any, bool) {
return result, true
}
}
case *Proc:
if int(fi) == len(x)-1 { // last one
return tf.Procedure.First(prev), true
} else {
got := tf.Procedure.Get(prev)
for i := len(got) - 1; 0 <= i; i-- {
v = got[i]
switch v.(type) {
case nil, bool, string, float64, float32, gen.Bool, gen.Float, gen.String,
int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, gen.Int:
case map[string]any, []any, gen.Object, gen.Array, Keyed, Indexed:
stack = append(stack, v)
default:
if rt := reflect.TypeOf(v); rt != nil {
switch rt.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Array, reflect.Map:
stack = append(stack, v)
}
}
}
}
}
}
if int(fi) < len(x)-1 {
if _, ok := stack[len(stack)-1].(fragIndex); !ok {
Expand Down
15 changes: 14 additions & 1 deletion jp/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func (p *parser) afterBracket() Frag {
case '?':
return p.readFilter()
case '(':
p.raise("scripts not implemented yet")
return p.readProc()
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
var i int
i, b = p.readInt(b)
Expand Down Expand Up @@ -561,6 +561,19 @@ func (p *parser) readFilter() *Filter {
return eq.Filter()
}

func (p *parser) readProc() *Proc {
end := bytes.Index(p.buf, []byte{')', ']'})
if end < 0 {
p.raise("not terminated")
}
end++
code := p.buf[p.pos-1 : end]
p.pos = end + 1

return MustNewProc(code)

}

// Reads an equation by reading the left value first and then seeing if there
// is an operation after that. If so it reads the next equation and decides
// based on precedent which is contained in the other.
Expand Down
4 changes: 3 additions & 1 deletion jp/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type xdata struct {
}

func TestParse(t *testing.T) {
jp.CompileScript = nil
for i, d := range []xdata{
{src: "@", expect: "@"},
{src: "$", expect: "$"},
Expand Down Expand Up @@ -87,7 +88,8 @@ func TestParse(t *testing.T) {
{src: "[]", err: "parse error at 2 in []"},
{src: "[**", err: "not terminated at 4 in [**"},
{src: "['x'z]", err: "invalid bracket fragment at 6 in ['x'z]"},
{src: "[(x)]", err: "scripts not implemented yet at 3 in [(x)]"},
{src: "[(x)]", err: "jp.CompileScript has not been set"},
{src: "[(x)", err: "not terminated at 3 in [(x)"},
{src: "[-x]", err: "expected a number at 4 in [-x]"},
{src: "[0x]", err: "invalid bracket fragment at 4 in [0x]"},
{src: "[x]", err: "parse error at 2 in [x]"},
Expand Down
88 changes: 88 additions & 0 deletions jp/proc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2024, Peter Ohler, All rights reserved.

package jp

import (
"fmt"
)

// CompileScript if non-nil should return object that implments the Procedure
// interface. This function is called when a script notation bracketed by [(
// and )] is encountered. Note the string code argument will included the open
// and close parenthesis but not the square brackets.
var CompileScript func(code []byte) Procedure

// Proc is a script used as a procedure which is a script not limited to being
// a selector. While both Locate() and Walk() are supported the results may
// not be as expected since the procedure can modify the original
// data. Remove() is not supported with this fragment type.
type Proc struct {
Procedure Procedure
Script []byte
}

// MustNewProc creates a new Proc and panics on error.
func MustNewProc(code []byte) (p *Proc) {
if CompileScript == nil {
panic(fmt.Errorf("jp.CompileScript has not been set"))
}
return &Proc{
Procedure: CompileScript(code),
Script: code,
}
}

// String representation of the proc.
func (p *Proc) String() string {
return string(p.Append([]byte{}, true, false))
}

// Append a fragment string representation of the fragment to the buffer
// then returning the expanded buffer.
func (p *Proc) Append(buf []byte, _, _ bool) []byte {
buf = append(buf, "["...)
buf = append(buf, p.Script...)

return append(buf, ']')
}

func (p *Proc) locate(pp Expr, data any, rest Expr, max int) (locs []Expr) {
got := p.Procedure.Get(data)
if len(rest) == 0 { // last one
for i := range got {
locs = locateAppendFrag(locs, pp, Nth(i))
if 0 < max && max <= len(locs) {
break
}
}
} else {
cp := append(pp, nil) // place holder
for i, v := range got {
cp[len(pp)] = Nth(i)
locs = locateContinueFrag(locs, cp, v, rest, max)
if 0 < max && max <= len(locs) {
break
}
}
}
return
}

// Walk each element returned from the procedure call. Note that this may or
// may not correspond to the original data as the procedure can modify not only
// the elements in the original data but also the contents of each.
func (p *Proc) Walk(rest, path Expr, nodes []any, cb func(path Expr, nodes []any)) {
path = append(path, nil)
data := nodes[len(nodes)-1]
nodes = append(nodes, nil)

for i, v := range p.Procedure.Get(data) {
path[len(path)-1] = Nth(i)
nodes[len(nodes)-1] = v
if 0 < len(rest) {
rest[0].Walk(rest[1:], path, nodes, cb)
} else {
cb(path, nodes)
}
}
}
161 changes: 161 additions & 0 deletions jp/proc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2024, Peter Ohler, All rights reserved.

package jp_test

import (
"fmt"
"testing"

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

type mathProc struct {
op rune
left int
right int
}

func (mp *mathProc) Get(data any) []any {
return []any{mp.First(data)}
}

func (mp *mathProc) First(data any) any {
a, ok := data.([]any)
if ok {
var (
left int
right int
)
if mp.left < len(a) {
left, _ = a[mp.left].(int)
}
if mp.right < len(a) {
right, _ = a[mp.right].(int)
}
switch mp.op {
case '+':
return left + right
case '-':
return left - right
}
return 0
}
return nil
}

func compileMathProc(code []byte) jp.Procedure {
var mp mathProc
_, _ = fmt.Sscanf(string(code), "(%c %d %d)", &mp.op, &mp.left, &mp.right)

return &mp
}

type mapProc struct{}

func (mp mapProc) Get(data any) (result []any) {
a, _ := data.([]any)
for i, v := range a {
result = append(result, map[string]any{"i": i, "v": v})
}
return
}

func (mp mapProc) First(data any) any {
if a, _ := data.([]any); 0 < len(a) {
return map[string]any{"i": 0, "v": a[0]}
}
return nil
}

func compileMapProc(code []byte) jp.Procedure {
return mapProc{}
}

type mapIntProc struct{}

func (mip mapIntProc) Get(data any) (result []any) {
a, _ := data.([]int)
for i, v := range a {
result = append(result, map[string]int{"i": i, "v": v})
}
return
}

func (mip mapIntProc) First(data any) any {
if a, _ := data.([]int); 0 < len(a) {
return map[string]int{"i": 0, "v": a[0]}
}
return nil
}

func compileMapIntProc(code []byte) jp.Procedure {
return mapIntProc{}
}

func TestProcLast(t *testing.T) {
jp.CompileScript = compileMathProc

p := jp.MustNewProc([]byte("(+ 0 1)"))
tt.Equal(t, "[(+ 0 1)]", p.String())

x := jp.MustParseString("[(+ 0 1)]")
tt.Equal(t, "[(+ 0 1)]", x.String())

data := []any{2, 3, 4}
result := x.First(data)
tt.Equal(t, 5, result)

got := x.Get(data)
tt.Equal(t, []any{5}, got)

locs := x.Locate(data, 1)
tt.Equal(t, "[[0]]", pretty.SEN(locs))

var buf []byte
x.Walk(data, func(path jp.Expr, nodes []any) {
buf = fmt.Appendf(buf, "%s : %v\n", path, nodes)
})
tt.Equal(t, "[0] : [[2 3 4] 5]\n", string(buf))
}

func TestProcNotLast(t *testing.T) {
jp.CompileScript = compileMapProc

x := jp.MustParseString("[(quux)].v")
tt.Equal(t, "[(quux)].v", x.String())

data := []any{2, 3, 4}
result := x.First(data)
tt.Equal(t, 2, result)

got := x.Get(data)
tt.Equal(t, []any{2, 3, 4}, got)

locs := x.Locate(data, 2)
tt.Equal(t, "[[0 v] [1 v]]", pretty.SEN(locs))

var buf []byte
x.Walk(data, func(path jp.Expr, nodes []any) {
buf = fmt.Appendf(buf, "%s : %v\n", path, nodes)
})
tt.Equal(t, `[0].v : [[2 3 4] map[i:0 v:2] 2]
[1].v : [[2 3 4] map[i:1 v:3] 3]
[2].v : [[2 3 4] map[i:2 v:4] 4]
`, string(buf))
}

func TestProcNotLastReflect(t *testing.T) {
jp.CompileScript = compileMapIntProc

x := jp.MustParseString("[(quux)].v")
tt.Equal(t, "[(quux)].v", x.String())

data := []int{2, 3, 4}
result := x.First(data)
tt.Equal(t, 2, result)

got := x.Get(data)
tt.Equal(t, []any{2, 3, 4}, got)
}
14 changes: 14 additions & 0 deletions jp/procedure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2025, Peter Ohler, All rights reserved.

package jp

// Procedure defines the interface for functions for script fragments between
// [( and )] delimiters.
type Procedure interface {
// Get should return a list of matching in the data element.
Get(data any) []any

// First should return a single matching in the data element or nil if
// there are no matches.
First(data any) any
}
Loading
Loading