Skip to content

Add jp functions #178

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

Merged
merged 3 commits into from
Jul 7, 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
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
Loading