Skip to content

Commit

Permalink
Jp proc (#196)
Browse files Browse the repository at this point in the history
* Start on procedures

* Add jp-proc tests
  • Loading branch information
ohler55 authored Dec 31, 2024
1 parent 6b02545 commit c53cf6d
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 6 deletions.
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

0 comments on commit c53cf6d

Please sign in to comment.