Skip to content

Commit

Permalink
Add jp functions (#178)
Browse files Browse the repository at this point in the history
New script functions can now be added with `jp.RegisterUnaryFunction()` and `jp.RegisterBinaryFunction()`.
  • Loading branch information
ohler55 authored Jul 7, 2024
1 parent 9dccb04 commit d291cb2
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 90 deletions.
19 changes: 2 additions & 17 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ run:
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
format: colored-line-number
formats: colored-line-number

# print lines of code with issue, default is true
print-issued-lines: true
Expand All @@ -70,17 +70,12 @@ linters-settings:
# default is false: such cases aren't reported by default.
check-blank: false

# [deprecated] comma-separated list of pairs of the form pkg:regex
# the regex is used to ignore names within pkg. (default "fmt:.*").
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
ignore: fmt:.*,io/ioutil:^Read.*

# path to a file containing a list of functions to exclude from checking
# see https://github.com/kisielk/errcheck#excluding-functions for details
#exclude: /path/to/file.txt
govet:
# report about shadowed variables
check-shadowing: true
shadow: true

# settings per analyzer
settings:
Expand Down Expand Up @@ -174,17 +169,14 @@ linters-settings:
rangeValCopy:
sizeThreshold: 32

# deadcode: Finds unused code [fast: true, auto-fix: false]
# errcheck: Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
# gosimple: Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false]
# govet: (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
# ineffassign: Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
# staticcheck: Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false]
# structcheck: Finds an unused struct fields [fast: true, auto-fix: false]
# typecheck: Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
# unparam: Reports unused function parameters [fast: false, auto-fix: false]
# unused: Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
# varcheck: Finds unused global variables and constants [fast: true, auto-fix: false]

# Disabled by your configuration linters:
# depguard: Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
Expand All @@ -197,7 +189,6 @@ linters-settings:
# gofmt: Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
# goimports: Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
# gosec (gas): Inspects source code for security problems [fast: true, auto-fix: false]
# interfacer: Linter that suggests narrower interface types [fast: false, auto-fix: false]
# lll: Reports long lines [fast: true, auto-fix: false]
# misspell: Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
# nakedret: Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
Expand All @@ -213,7 +204,6 @@ linters:
- goimports
- ineffassign
- lll
- megacheck
- misspell
- typecheck
- unconvert
Expand All @@ -226,26 +216,21 @@ linters:
- gochecknoinits
- gocyclo
- gosec
- interfacer
- nakedret
- prealloc
- exhaustive
- rowserrcheck # this is completely broken
- scopelint
- bodyclose # thinks all body closes have to happen in the same function as the request
- contextcheck # not ready for go1.18 yet
- gosimple # not ready for go1.18 yet
- nilerr # not ready for go1.18 yet
- noctx # not ready for go1.18 yet
- sqlclosecheck # not ready for go1.18 yet
- staticcheck # not ready for go1.18 yet
- structcheck # not ready for go1.18 yet
- stylecheck # not ready for go1.18 yet
- unparam # not ready for go1.18 yet
- unused # not ready for go1.18 yet
- asasalint # stupid rule that makes no sense
- varcheck # deprecated
- deadcode # deprecated
- reassign # removed because it makes no sense to flag an assignment of a public variable as an error.
- revive # complains about unexported return, calling it annoying
- musttag # absolutely not wanted as it insists on JSON annotation on any public struct that is unmarshalled
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ 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.23.0] - 2024-07-07
### Added
- New script functions can now be added with `jp.RegisterUnaryFunction()` and `jp.RegisterBinaryFunction()`.

## [1.22.1] - 2024-06-23
### Added
- Added the missing support of Keyed and Indexed in jp.Modify.
Expand Down
5 changes: 4 additions & 1 deletion jp/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ var (
{path: "[?(@[1].a > 230)][1].b", expect: []any{322, 422}},
{path: "[?(@ > 1)]", expect: []any{2, 3}, data: []any{1, 2, 3}},
{path: "$[?(1==1)]", expect: []any{1, 2, 3}, data: []any{1, 2, 3}},
{path: "$.*[*].a", expect: []any{111, 121, 131, 141, 211, 221, 231, 241, 311, 321, 331, 341, 411, 421, 431, 441}},
{
path: "$.*[*].a",
expect: []any{111, 121, 131, 141, 211, 221, 231, 241, 311, 321, 331, 341, 411, 421, 431, 441},
},
{path: `$['\\']`, expect: []any{3}, data: map[string]any{`\`: 3}},
{path: `$['\x41']`, expect: []any{3}, data: map[string]any{"A": 3}},
{path: `$['\x4A']`, expect: []any{3}, data: map[string]any{"J": 3}},
Expand Down
88 changes: 47 additions & 41 deletions jp/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -577,19 +577,6 @@ func (p *parser) readEq() (eq *Equation) {
p.pos++
s := p.readStr(b)
eq = &Equation{result: s}
case 'n':
p.readEqToken([]byte("null"))
eq = &Equation{result: nil}
case 'N':
p.readEqToken([]byte("Nothing"))
eq = &Equation{result: Nothing}
case 't':
p.readEqToken([]byte("true"))
eq = &Equation{result: true}

case 'f':
p.readEqToken([]byte("false"))
eq = &Equation{result: false}
case '@', '$':
x := p.readExpr()
eq = &Equation{result: x}
Expand All @@ -606,20 +593,27 @@ func (p *parser) readEq() (eq *Equation) {
case '/':
p.pos++
eq = &Equation{result: p.readRegex()}
case 'l':
eq = &Equation{}
p.readFunc(length, eq)
case 'c':
eq = &Equation{}
p.readFunc(count, eq)
case 'm':
eq = &Equation{}
p.readFunc(match, eq)
case 's':
eq = &Equation{}
p.readFunc(search, eq)
case 'N':
p.readEqToken([]byte("Nothing"))
eq = &Equation{result: Nothing}
default:
p.raise("expected a value")
before := p.pos
token := p.readToken()
switch {
case bytes.Equal([]byte("true"), token):
eq = &Equation{result: true}
case bytes.Equal([]byte("false"), token):
eq = &Equation{result: false}
case bytes.Equal([]byte("null"), token):
eq = &Equation{result: nil}
default:
if o := opMap[string(token)]; o != nil {
eq = p.readOpArgs(o)
} else {
p.pos = before
p.raise("'%s' is not a value or function", token)
}
}
}
for p.pos < len(p.buf) {
b := p.nextNonSpace()
Expand All @@ -636,24 +630,36 @@ func (p *parser) readEq() (eq *Equation) {
return
}

func (p *parser) readFunc(o *op, eq *Equation) {
if bytes.HasPrefix(p.buf[p.pos:], []byte(o.name)) && p.buf[p.pos+len(o.name)] == '(' {
eq.o = o
p.pos += len(o.name) + 1
eq.left = p.readEq()
b := p.nextNonSpace()
if b == ',' {
p.pos++
eq.right = p.readEq()
b = p.nextNonSpace()
}
if b != ')' {
p.raise("not terminated")
// reads just lower case alpha characters (a-z)
func (p *parser) readToken() []byte {
start := p.pos
for ; p.pos < len(p.buf); p.pos++ {
b := p.buf[p.pos]
if b < 'a' || 'z' < b {
break
}
}
return p.buf[start:p.pos]
}

func (p *parser) readOpArgs(o *op) (eq *Equation) {
if p.buf[p.pos] != '(' {
p.raise("expected a %s function", o.name)
}
eq = &Equation{o: o}
p.pos++
eq.left = p.readEq()
b := p.nextNonSpace()
if b == ',' {
p.pos++
return
eq.right = p.readEq()
b = p.nextNonSpace()
}
p.raise("expected a %s function", o.name)
if b != ')' {
p.raise("not terminated")
}
p.pos++
return
}

func (p *parser) readEqToken(token []byte) {
Expand Down
18 changes: 12 additions & 6 deletions jp/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ func TestParse(t *testing.T) {
{src: "$[1,'a']", expect: "$[1,'a']"},
{src: "$[1,'a',2,'b']", expect: "$[1,'a',2,'b']"},
{src: "$[ 1, 'a' , 2 ,'b' ]", expect: "$[1,'a',2,'b']"},
{src: "$[?(@.x == true)]", expect: "$[?(@.x == true)]"},
{src: "$[?(@.x == false)]", expect: "$[?(@.x == false)]"},
{src: "$[?(@.x == Nothing)]", expect: "$[?(@.x == Nothing)]"},
{src: "$[?(@.x == null)]", expect: "$[?(@.x == null)]"},
{src: "$[?(@.x == 'abc')]", expect: "$[?(@.x == 'abc')]"},
{src: "$[?(1==1)]", expect: "$[?(1 == 1)]"},
{src: "$[?(@.x)]", expect: "$[?(@.x)]"},
Expand Down Expand Up @@ -105,8 +109,8 @@ func TestParse(t *testing.T) {
{src: "[2,-", err: "expected a number at 5 in [2,-"},
{src: "[2,x", err: "invalid union syntax at 5 in [2,x"},
{src: "[?", err: "not terminated at 3 in [?"},
{src: "[?(", err: "expected a value at 4 in [?("},
{src: "[?x", err: "expected a value at 3 in [?x"},
{src: "[?(", err: "'' is not a value or function at 4 in [?("},
{src: "[?x", err: "'x' is not a value or function at 3 in [?x"},
{src: "[?(@.x == 3)", err: "not terminated at 13 in [?(@.x == 3)"},
{src: "[?(!(@.x == -x)", err: `strconv.ParseInt: parsing "-": invalid syntax at 14 in [?(!(@.x == -x)`},
{src: "[?(!(@.x == 1)]", err: "not terminated at 15 in [?(!(@.x == 1)]"},
Expand All @@ -116,13 +120,15 @@ func TestParse(t *testing.T) {
{src: "[?(2 + 1 ++)]", err: `'++' is not a valid operation at 12 in [?(2 + 1 ++)]`},
{src: "[?(2 + 1 + -)]", err: `strconv.ParseInt: parsing "-": invalid syntax at 13 in [?(2 + 1 + -)]`},
{src: "[?(2 + 1 * -)]", err: `strconv.ParseInt: parsing "-": invalid syntax at 13 in [?(2 + 1 * -)]`},
{src: "[?(@.x == trux)]", err: "expected true at 14 in [?(@.x == trux)]"},
{src: "[?(@.x == fx)]", err: "expected false at 12 in [?(@.x == fx)]"},
{src: "[?(@.x == nulx)]", err: "expected null at 14 in [?(@.x == nulx)]"},
{src: "[?(@.x == x)]", err: "expected a value at 11 in [?(@.x == x)]"},
{src: "[?(@.x == trux)]", err: "'trux' is not a value or function at 11 in [?(@.x == trux)]"},
{src: "[?(@.x == fx)]", err: "'fx' is not a value or function at 11 in [?(@.x == fx)]"},
{src: "[?(@.x == nulx)]", err: "'nulx' is not a value or function at 11 in [?(@.x == nulx)]"},
{src: "[?(@.x == x)]", err: "'x' is not a value or function at 11 in [?(@.x == x)]"},
{src: "[?(@.x -- x)]", err: "'--' is not a valid operation at 9 in [?(@.x -- x)]"},
{src: "[?(@.x =", err: "equation not terminated at 9 in [?(@.x ="},
{src: "[?(@.x in [1 2])]", err: "'' is not a valid operation at 14 in [?(@.x in [1 2])]"},
{src: "$[?(@.x == North)]", err: "expected Nothing at 14 in $[?(@.x == North)]"},
{src: "[?length]", err: "expected a length function at 9 in [?length]"},
} {
if testing.Verbose() {
fmt.Printf("... %s\n", d.src)
Expand Down
88 changes: 88 additions & 0 deletions jp/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
package jp

import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"

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

type nothing int

const userOpCode = 'U'

var (
// Lower precedence is evaluated first.
eq = &op{prec: 3, code: '=', name: "==", cnt: 2}
Expand Down Expand Up @@ -79,6 +83,8 @@ var (

type op struct {
name string
uniFun func(arg any) any
duoFun func(left, right any) any
prec byte
cnt byte
code byte
Expand Down Expand Up @@ -752,6 +758,12 @@ func evalStack(sstack []any) []any {
}
}
}
default:
if o.uniFun != nil {
sstack[i] = o.uniFun(left)
} else if o.duoFun != nil {
sstack[i] = o.duoFun(left, right)
}
}
if i+int(o.cnt)+1 <= len(sstack) {
copy(sstack[i+1:], sstack[i+int(o.cnt)+1:])
Expand Down Expand Up @@ -802,6 +814,15 @@ func (s *Script) appendOp(o *op, left, right any) (pb *precBuf) {
pb.buf = append(pb.buf, ',', ' ')
pb.buf = s.appendValue(pb.buf, right, o.prec)
pb.buf = append(pb.buf, ')')
case userOpCode:
pb.buf = append(pb.buf, o.name...)
pb.buf = append(pb.buf, '(')
pb.buf = s.appendValue(pb.buf, left, o.prec)
if 1 < o.cnt {
pb.buf = append(pb.buf, ',', ' ')
pb.buf = s.appendValue(pb.buf, right, o.prec)
}
pb.buf = append(pb.buf, ')')
default:
pb.buf = s.appendValue(pb.buf, left, o.prec)
pb.buf = append(pb.buf, ' ')
Expand Down Expand Up @@ -854,3 +875,70 @@ func (s *Script) appendValue(buf []byte, v any, prec byte) []byte {
}
return buf
}

var builtInNames = map[string]bool{
"==": true,
"!=": true,
"<": true,
">": true,
"<=": true,
">=": true,
"||": true,
"&&": true,
"!": true,
"+": true,
"-": true,
"*": true,
"/": true,
"get": true,
"in": true,
"empty": true,
"~=": true,
"=~": true,
"has": true,
"exists": true,
"length": true,
"count": true,
"match": true,
"search": true,
"true": true,
"false": true,
"null": true,
}

// RegisterUnaryFunction registers a unary function for scripts. The 'get'
// argument if true indicates a get operation to provide the argument to the
// provided function otherwise the first match is used. Names must be alpha
// characters only.
func RegisterUnaryFunction(name string, get bool, f func(arg any) any) {
name = strings.ToLower(name)
if builtInNames[name] {
panic(fmt.Errorf("operation %s can not be replaced", name))
}
opMap[name] = &op{
name: name,
uniFun: f,
code: userOpCode,
cnt: 1,
getLeft: get,
}
}

// RegisterBinaryFunction registers a function that takes two argument for
// scripts. The 'getLeft' and 'getRight' arguments if true indicates a get
// operation to provide the argument to the provided function otherwise the
// first match is used. Names must be alpha characters only.
func RegisterBinaryFunction(name string, getLeft, getRight bool, f func(left, right any) any) {
name = strings.ToLower(name)
if builtInNames[name] {
panic(fmt.Errorf("operation %s can not be replaced", name))
}
opMap[name] = &op{
name: name,
duoFun: f,
code: userOpCode,
cnt: 2,
getLeft: getLeft,
getRight: getRight,
}
}
Loading

0 comments on commit d291cb2

Please sign in to comment.