From 701cf600a8f77f72468d1ab3f30e06f71f041aea Mon Sep 17 00:00:00 2001 From: "David E. Wheeler" Date: Wed, 11 Sep 2024 21:33:36 -0400 Subject: [PATCH] Add registry package First step in eliminating global variables. Not yet in use, but requires changes to the spec package: * Eliminate `FunctionExpr.AsTypeKind`, as it `FunctionExpr.ResultType` provides the same value and is a more semantically meaningful name. * Merge `logicalFrom` and `newLogicalTypeFrom` into `LogicalFrom` for a single method that can handle converting both `JSONPathValue`s and`bool`s. * Improve the documentation of `FuncType`. * Rename `newNodesTypeFrom` to `NodesFrom` and `newValueTypeFrom` to `ValueFrom`. * Rename `convertsTo` to the public name `ConvertsTo`, so it can be called from custom function validators. * Remove orphan comment lines created by `golangci-lint`. * Rename `filterQuery` to the public `FilterQueryExpr`. * Teach `golangci-lint` to ignore unwrapped errors in `registry/funcs.go` and `FunctionExprArg` return values everywhere. * Add the `FilterQuery()` constructor. --- .golangci.yaml | 4 + registry/funcs.go | 211 +++++++++++++ registry/funcs_test.go | 497 ++++++++++++++++++++++++++++++ registry/registry.go | 112 +++++++ registry/registry_example_test.go | 49 +++ registry/registry_test.go | 109 +++++++ spec/filter.go | 8 +- spec/filter_test.go | 4 +- spec/function.go | 154 ++++----- spec/function_test.go | 62 ++-- spec/op_test.go | 4 +- spec/query.go | 4 +- spec/query_test.go | 2 +- 13 files changed, 1087 insertions(+), 133 deletions(-) create mode 100644 registry/funcs.go create mode 100644 registry/funcs_test.go create mode 100644 registry/registry.go create mode 100644 registry/registry_example_test.go create mode 100644 registry/registry_test.go diff --git a/.golangci.yaml b/.golangci.yaml index b8d91b8..3021d08 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -45,6 +45,9 @@ issues: - err113 - wrapcheck - maintidx + - path: registry/funcs\.go + linters: + - err113 linters: # Enable specific linter @@ -113,6 +116,7 @@ linters-settings: - stdlib - generic - JSONPathValue + - FunctionExprArg # You can specify idiomatic endings for interface # - (or|er)$ # reject-list of interfaces diff --git a/registry/funcs.go b/registry/funcs.go new file mode 100644 index 0000000..9cf0fb5 --- /dev/null +++ b/registry/funcs.go @@ -0,0 +1,211 @@ +package registry + +import ( + "errors" + "fmt" + "regexp" + "regexp/syntax" + "unicode/utf8" + + "github.com/theory/jsonpath/spec" +) + +// checkLengthArgs checks the argument expressions to length() and returns an +// error if there is not exactly one expression that results in a +// [PathValue]-compatible value. +func checkLengthArgs(fea []spec.FunctionExprArg) error { + if len(fea) != 1 { + return fmt.Errorf("expected 1 argument but found %v", len(fea)) + } + + kind := fea[0].ResultType() + if !kind.ConvertsTo(spec.PathValue) { + return errors.New("cannot convert argument to ValueType") + } + + return nil +} + +// lengthFunc extracts the single argument passed in jv and returns its +// length. Panics if jv[0] doesn't exist or is not convertible to [ValueType]. +// +// - if jv[0] is nil, the result is nil +// - If jv[0] is a string, the result is the number of Unicode scalar values +// in the string. +// - If jv[0] is a []any, the result is the number of elements in the slice. +// - If jv[0] is an map[string]any, the result is the number of members in +// the map. +// - For any other value, the result is nil. +func lengthFunc(jv []spec.JSONPathValue) spec.JSONPathValue { + v := spec.ValueFrom(jv[0]) + if v == nil { + return nil + } + switch v := v.Value().(type) { + case string: + // Unicode scalar values + return spec.Value(utf8.RuneCountInString(v)) + case []any: + return spec.Value(len(v)) + case map[string]any: + return spec.Value(len(v)) + default: + return nil + } +} + +// checkCountArgs checks the argument expressions to count() and returns an +// error if there is not exactly one expression that results in a +// [PathNodes]-compatible value. +func checkCountArgs(fea []spec.FunctionExprArg) error { + if len(fea) != 1 { + return fmt.Errorf("expected 1 argument but found %v", len(fea)) + } + + kind := fea[0].ResultType() + if !kind.ConvertsTo(spec.PathNodes) { + return errors.New("cannot convert argument to PathNodes") + } + + return nil +} + +// countFunc implements the [RFC 9535]-standard count function. The result is +// a ValueType containing an unsigned integer for the number of nodes +// in jv[0]. Panics if jv[0] doesn't exist or is not convertible to +// [NodesType]. +func countFunc(jv []spec.JSONPathValue) spec.JSONPathValue { + return spec.Value(len(spec.NodesFrom(jv[0]))) +} + +// checkValueArgs checks the argument expressions to value() and returns an +// error if there is not exactly one expression that results in a +// [PathNodes]-compatible value. +func checkValueArgs(fea []spec.FunctionExprArg) error { + if len(fea) != 1 { + return fmt.Errorf("expected 1 argument but found %v", len(fea)) + } + + kind := fea[0].ResultType() + if !kind.ConvertsTo(spec.PathNodes) { + return errors.New("cannot convert argument to PathNodes") + } + + return nil +} + +// valueFunc implements the [RFC 9535]-standard value function. Panics if +// jv[0] doesn't exist or is not convertible to [NodesType]. Otherwise: +// +// - If jv[0] contains a single node, the result is the value of the node. +// - If jv[0] is empty or contains multiple nodes, the result is nil. +func valueFunc(jv []spec.JSONPathValue) spec.JSONPathValue { + nodes := spec.NodesFrom(jv[0]) + if len(nodes) == 1 { + return spec.Value(nodes[0]) + } + return nil +} + +// checkMatchArgs checks the argument expressions to match() and returns an +// error if there are not exactly two expressions that result in +// [PathValue]-compatible values. +func checkMatchArgs(fea []spec.FunctionExprArg) error { + const matchArgLen = 2 + if len(fea) != matchArgLen { + return fmt.Errorf("expected 2 arguments but found %v", len(fea)) + } + + for i, arg := range fea { + kind := arg.ResultType() + if !kind.ConvertsTo(spec.PathValue) { + return fmt.Errorf("cannot convert argument %v to PathNodes", i+1) + } + } + + return nil +} + +// matchFunc implements the [RFC 9535]-standard match function. If jv[0] and +// jv[1] evaluate to strings, the second is compiled into a regular expression with +// implied \A and \z anchors and used to match the first, returning LogicalTrue for +// a match and LogicalFalse for no match. Returns LogicalFalse if either jv value +// is not a string or if jv[1] fails to compile. +func matchFunc(jv []spec.JSONPathValue) spec.JSONPathValue { + if v, ok := spec.ValueFrom(jv[0]).Value().(string); ok { + if r, ok := spec.ValueFrom(jv[1]).Value().(string); ok { + if rc := compileRegex(`\A` + r + `\z`); rc != nil { + return spec.LogicalFrom(rc.MatchString(v)) + } + } + } + return spec.LogicalFalse +} + +// checkSearchArgs checks the argument expressions to search() and returns an +// error if there are not exactly two expressions that result in +// [PathValue]-compatible values. +func checkSearchArgs(fea []spec.FunctionExprArg) error { + const searchArgLen = 2 + if len(fea) != searchArgLen { + return fmt.Errorf("expected 2 arguments but found %v", len(fea)) + } + + for i, arg := range fea { + kind := arg.ResultType() + if !kind.ConvertsTo(spec.PathValue) { + return fmt.Errorf("cannot convert argument %v to PathNodes", i+1) + } + } + + return nil +} + +// searchFunc implements the [RFC 9535]-standard search function. If both jv[0] +// and jv[1] contain strings, the latter is compiled into a regular expression and used +// to match the former, returning LogicalTrue for a match and LogicalFalse for no +// match. Returns LogicalFalse if either value is not a string, or if jv[1] +// fails to compile. +func searchFunc(jv []spec.JSONPathValue) spec.JSONPathValue { + if val, ok := spec.ValueFrom(jv[0]).Value().(string); ok { + if r, ok := spec.ValueFrom(jv[1]).Value().(string); ok { + if rc := compileRegex(r); rc != nil { + return spec.LogicalFrom(rc.MatchString(val)) + } + } + } + return spec.LogicalFalse +} + +// compileRegex compiles str into a regular expression or returns an error. To +// comply with RFC 9485 regular expression semantics, all instances of "." are +// replaced with "[^\n\r]". This sadly requires compiling the regex twice: +// once to produce an AST to replace "." nodes, and a second time for the +// final regex. +func compileRegex(str string) *regexp.Regexp { + // First compile AST and replace "." with [^\n\r]. + // https://www.rfc-editor.org/rfc/rfc9485.html#name-pcre-re2-and-ruby-regexps + r, err := syntax.Parse(str, syntax.Perl|syntax.DotNL) + if err != nil { + // Could use some way to log these errors rather than failing silently. + return nil + } + + replaceDot(r) + re, _ := regexp.Compile(r.String()) + return re +} + +//nolint:gochecknoglobals +var clrf, _ = syntax.Parse("[^\n\r]", syntax.Perl) + +// replaceDot recurses re to replace all "." nodes with "[^\n\r]" nodes. +func replaceDot(re *syntax.Regexp) { + if re.Op == syntax.OpAnyChar { + *re = *clrf + } else { + for _, re := range re.Sub { + replaceDot(re) + } + } +} diff --git a/registry/funcs_test.go b/registry/funcs_test.go new file mode 100644 index 0000000..dbb43a2 --- /dev/null +++ b/registry/funcs_test.go @@ -0,0 +1,497 @@ +package registry + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/theory/jsonpath/spec" +) + +func TestLengthFunc(t *testing.T) { + t.Parallel() + a := assert.New(t) + + for _, tc := range []struct { + name string + vals []spec.JSONPathValue + exp int + err string + }{ + { + name: "empty_string", + vals: []spec.JSONPathValue{spec.Value("")}, + exp: 0, + }, + { + name: "ascii_string", + vals: []spec.JSONPathValue{spec.Value("abc def")}, + exp: 7, + }, + { + name: "unicode_string", + vals: []spec.JSONPathValue{spec.Value("foö")}, + exp: 3, + }, + { + name: "emoji_string", + vals: []spec.JSONPathValue{spec.Value("Hi 👋🏻")}, + exp: 5, + }, + { + name: "empty_array", + vals: []spec.JSONPathValue{spec.Value([]any{})}, + exp: 0, + }, + { + name: "array", + vals: []spec.JSONPathValue{spec.Value([]any{1, 2, 3, 4, 5})}, + exp: 5, + }, + { + name: "nested_array", + vals: []spec.JSONPathValue{spec.Value([]any{1, 2, 3, "x", []any{456, 67}, true})}, + exp: 6, + }, + { + name: "empty_object", + vals: []spec.JSONPathValue{spec.Value(map[string]any{})}, + exp: 0, + }, + { + name: "object", + vals: []spec.JSONPathValue{spec.Value(map[string]any{"x": 1, "y": 0, "z": 2})}, + exp: 3, + }, + { + name: "nested_object", + vals: []spec.JSONPathValue{spec.Value(map[string]any{ + "x": 1, + "y": 0, + "z": []any{1, 2}, + "a": map[string]any{"b": 9}, + })}, + exp: 4, + }, + { + name: "integer", + vals: []spec.JSONPathValue{spec.Value(42)}, + exp: -1, + }, + { + name: "bool", + vals: []spec.JSONPathValue{spec.Value(true)}, + exp: -1, + }, + { + name: "null", + vals: []spec.JSONPathValue{spec.Value(nil)}, + exp: -1, + }, + { + name: "nil", + vals: []spec.JSONPathValue{nil}, + exp: -1, + }, + { + name: "not_value", + vals: []spec.JSONPathValue{spec.LogicalFalse}, + err: "unexpected argument of type spec.LogicalType", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.err != "" { + a.PanicsWithValue(tc.err, func() { + lengthFunc(tc.vals) + }) + return + } + res := lengthFunc(tc.vals) + if tc.exp < 0 { + a.Nil(res) + } else { + a.Equal(spec.Value(tc.exp), res) + } + }) + } +} + +func TestCheckSingularFuncArgs(t *testing.T) { + t.Parallel() + r := require.New(t) + + for _, tc := range []struct { + name string + expr []spec.FunctionExprArg + err string + lengthErr string + countErr string + valueErr string + }{ + { + name: "no_args", + expr: []spec.FunctionExprArg{}, + err: "expected 1 argument but found 0", + }, + { + name: "two_args", + expr: []spec.FunctionExprArg{spec.Literal(nil), spec.Literal(nil)}, + err: "expected 1 argument but found 2", + }, + { + name: "literal_string", + expr: []spec.FunctionExprArg{spec.Literal(nil)}, + countErr: "cannot convert argument to PathNodes", + valueErr: "cannot convert argument to PathNodes", + }, + { + name: "singular_query", + expr: []spec.FunctionExprArg{spec.SingularQuery(false, nil)}, + }, + { + name: "filter_query", + expr: []spec.FunctionExprArg{spec.FilterQuery( + spec.Query(true, []*spec.Segment{spec.Child(spec.Name("x"))}), + )}, + }, + { + name: "logical_function_expr", + expr: []spec.FunctionExprArg{newFuncExpr( + t, "match", + []spec.FunctionExprArg{ + spec.FilterQuery( + spec.Query(true, []*spec.Segment{spec.Child(spec.Name("x"))}), + ), + spec.Literal("hi"), + }, + )}, + lengthErr: "cannot convert argument to ValueType", + countErr: "cannot convert argument to PathNodes", + valueErr: "cannot convert argument to PathNodes", + }, + { + name: "logical_or", + expr: []spec.FunctionExprArg{spec.LogicalOr{}}, + lengthErr: "cannot convert argument to ValueType", + countErr: "cannot convert argument to PathNodes", + valueErr: "cannot convert argument to PathNodes", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // Test length args + err := checkLengthArgs(tc.expr) + switch { + case tc.err != "": + r.EqualError(err, tc.err) + case tc.lengthErr != "": + r.EqualError(err, tc.lengthErr) + default: + r.NoError(err) + } + + // Test count args + err = checkCountArgs(tc.expr) + switch { + case tc.err != "": + r.EqualError(err, tc.err) + case tc.countErr != "": + r.EqualError(err, tc.countErr) + default: + r.NoError(err) + } + + // Test value args + err = checkValueArgs(tc.expr) + switch { + case tc.err != "": + r.EqualError(err, tc.err) + case tc.valueErr != "": + r.EqualError(err, tc.valueErr) + default: + r.NoError(err) + } + }) + } +} + +func TestCheckRegexFuncArgs(t *testing.T) { + t.Parallel() + r := require.New(t) + + for _, tc := range []struct { + name string + expr []spec.FunctionExprArg + err string + }{ + { + name: "no_args", + expr: []spec.FunctionExprArg{}, + err: "expected 2 arguments but found 0", + }, + { + name: "one_arg", + expr: []spec.FunctionExprArg{spec.Literal("hi")}, + err: "expected 2 arguments but found 1", + }, + { + name: "three_args", + expr: []spec.FunctionExprArg{spec.Literal("hi"), spec.Literal("hi"), spec.Literal("hi")}, + err: "expected 2 arguments but found 3", + }, + { + name: "logical_or_1", + expr: []spec.FunctionExprArg{&spec.LogicalOr{}, spec.Literal("hi")}, + err: "cannot convert argument 1 to PathNodes", + }, + { + name: "logical_or_2", + expr: []spec.FunctionExprArg{spec.Literal("hi"), spec.LogicalOr{}}, + err: "cannot convert argument 2 to PathNodes", + }, + { + name: "singular_query_literal", + expr: []spec.FunctionExprArg{&spec.SingularQueryExpr{}, spec.Literal("hi")}, + }, + { + name: "literal_singular_query", + expr: []spec.FunctionExprArg{spec.Literal("hi"), &spec.SingularQueryExpr{}}, + }, + { + name: "filter_query_1", + expr: []spec.FunctionExprArg{ + spec.FilterQuery(spec.Query(true, []*spec.Segment{spec.Child(spec.Name("x"))})), + spec.Literal("hi"), + }, + }, + { + name: "filter_query_2", + expr: []spec.FunctionExprArg{ + spec.Literal("hi"), + spec.FilterQuery(spec.Query(true, []*spec.Segment{spec.Child(spec.Name("x"))})), + }, + }, + { + name: "function_expr_1", + expr: []spec.FunctionExprArg{ + newFuncExpr( + t, "match", + []spec.FunctionExprArg{ + spec.FilterQuery( + spec.Query(true, []*spec.Segment{spec.Child(spec.Name("x"))}), + ), + spec.Literal("hi"), + }, + ), + spec.Literal("hi"), + }, + err: "cannot convert argument 1 to PathNodes", + }, + { + name: "function_expr_2", + expr: []spec.FunctionExprArg{ + spec.Literal("hi"), + newFuncExpr( + t, "match", + []spec.FunctionExprArg{ + spec.FilterQuery( + spec.Query(true, []*spec.Segment{spec.Child(spec.Name("x"))}), + ), + spec.Literal("hi"), + }, + ), + }, + err: "cannot convert argument 2 to PathNodes", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // Test match args + err := checkMatchArgs(tc.expr) + if tc.err == "" { + r.NoError(err) + } else { + r.EqualError(err, strings.Replace(tc.err, "%v", "match", 1)) + } + + // Test search args + err = checkSearchArgs(tc.expr) + if tc.err == "" { + r.NoError(err) + } else { + r.EqualError(err, strings.Replace(tc.err, "%v", "search", 1)) + } + }) + } +} + +func TestCountFunc(t *testing.T) { + t.Parallel() + a := assert.New(t) + + for _, tc := range []struct { + name string + vals []spec.JSONPathValue + exp int + err string + }{ + {"empty", []spec.JSONPathValue{spec.NodesType([]any{})}, 0, ""}, + {"one", []spec.JSONPathValue{spec.NodesType([]any{1})}, 1, ""}, + {"three", []spec.JSONPathValue{spec.NodesType([]any{1, true, nil})}, 3, ""}, + {"not_nodes", []spec.JSONPathValue{spec.LogicalTrue}, 0, "unexpected argument of type spec.LogicalType"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.err != "" { + a.PanicsWithValue(tc.err, func() { countFunc(tc.vals) }) + return + } + res := countFunc(tc.vals) + if tc.exp < 0 { + a.Nil(res) + } else { + a.Equal(spec.Value(tc.exp), res) + } + }) + } +} + +func TestValueFunc(t *testing.T) { + t.Parallel() + a := assert.New(t) + + for _, tc := range []struct { + name string + vals []spec.JSONPathValue + exp spec.JSONPathValue + err string + }{ + {"empty", []spec.JSONPathValue{spec.NodesType([]any{})}, nil, ""}, + {"one_int", []spec.JSONPathValue{spec.NodesType([]any{1})}, spec.Value(1), ""}, + {"one_null", []spec.JSONPathValue{spec.NodesType([]any{nil})}, spec.Value(nil), ""}, + {"one_string", []spec.JSONPathValue{spec.NodesType([]any{"x"})}, spec.Value("x"), ""}, + {"three", []spec.JSONPathValue{spec.NodesType([]any{1, true, nil})}, nil, ""}, + {"not_nodes", []spec.JSONPathValue{spec.LogicalFalse}, nil, "unexpected argument of type spec.LogicalType"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.err != "" { + a.PanicsWithValue(tc.err, func() { valueFunc(tc.vals) }) + return + } + a.Equal(tc.exp, valueFunc(tc.vals)) + }) + } +} + +func TestRegexFuncs(t *testing.T) { + t.Parallel() + a := assert.New(t) + + for _, tc := range []struct { + name string + input *spec.ValueType + regex *spec.ValueType + match bool + search bool + }{ + { + name: "dot", + input: spec.Value("x"), + regex: spec.Value("."), + match: true, + search: true, + }, + { + name: "two_chars", + input: spec.Value("xx"), + regex: spec.Value("."), + match: false, + search: true, + }, + { + name: "multi_line_newline", + input: spec.Value("xx\nyz"), + regex: spec.Value(".*"), + match: false, + search: true, + }, + { + name: "multi_line_crlf", + input: spec.Value("xx\r\nyz"), + regex: spec.Value(".*"), + match: false, + search: true, + }, + { + name: "not_string_input", + input: spec.Value(1), + regex: spec.Value("."), + match: false, + search: false, + }, + { + name: "not_string_regex", + input: spec.Value("x"), + regex: spec.Value(1), + match: false, + search: false, + }, + { + name: "invalid_regex", + input: spec.Value("x"), + regex: spec.Value(".["), + match: false, + search: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + a.Equal(spec.LogicalFrom(tc.match), matchFunc([]spec.JSONPathValue{tc.input, tc.regex})) + a.Equal(spec.LogicalFrom(tc.search), searchFunc([]spec.JSONPathValue{tc.input, tc.regex})) + }) + } +} + +func TestExecRegexFuncs(t *testing.T) { + t.Parallel() + a := assert.New(t) + + for _, tc := range []struct { + name string + vals []spec.JSONPathValue + match bool + search bool + err string + }{ + { + name: "dot", + vals: []spec.JSONPathValue{spec.Value("x"), spec.Value("x")}, + match: true, + search: true, + }, + { + name: "first_not_value", + vals: []spec.JSONPathValue{spec.NodesType{}, spec.Value("x")}, + err: "unexpected argument of type spec.NodesType", + }, + { + name: "second_not_value", + vals: []spec.JSONPathValue{spec.Value("x"), spec.LogicalFalse}, + err: "unexpected argument of type spec.LogicalType", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if tc.err == "" { + a.Equal(matchFunc(tc.vals), spec.LogicalFrom(tc.match)) + a.Equal(searchFunc(tc.vals), spec.LogicalFrom(tc.search)) + } else { + a.PanicsWithValue(tc.err, func() { matchFunc(tc.vals) }) + a.PanicsWithValue(tc.err, func() { searchFunc(tc.vals) }) + } + }) + } +} diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 0000000..4c90cf0 --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,112 @@ +// Package registry provides a RFC 9535 JSONPath function registry. +package registry + +//go:generate stringer -linecomment -output registry_string.go -type FuncType + +import ( + "sync" + + "github.com/theory/jsonpath/spec" +) + +// Registry maintains a registry of JSONPath functions, including both +// [RFC 9535]-required functions and function extensions. +// +// [RFC 9535]: https://www.rfc-editor.org/rfc/rfc9535.html +type Registry struct { + mu sync.RWMutex + funcs map[string]*Function +} + +// New returns a new [Registry] loaded with the [RFC 9535]-mandated functions: +// +// - [length] +// - [count] +// - [value] +// - [match] +// - [search] +// +// [RFC 9535]: https://www.rfc-editor.org/rfc/rfc9535.html +// [length]: https://www.rfc-editor.org/rfc/rfc9535.html#name-length-function-extension +// [count]: https://www.rfc-editor.org/rfc/rfc9535.html#name-count-function-extension +// [value]: https://www.rfc-editor.org/rfc/rfc9535.html#name-value-function-extension +// [match]: https://www.rfc-editor.org/rfc/rfc9535.html#name-match-function-extension +// [search]: https://www.rfc-editor.org/rfc/rfc9535.html#name-search-function-extension +func New() *Registry { + return &Registry{ + mu: sync.RWMutex{}, + funcs: map[string]*Function{ + "length": { + Name: "length", + ResultType: spec.FuncValue, + Validate: checkLengthArgs, + Evaluate: lengthFunc, + }, + "count": { + Name: "count", + ResultType: spec.FuncValue, + Validate: checkCountArgs, + Evaluate: countFunc, + }, + "value": { + Name: "value", + ResultType: spec.FuncValue, + Validate: checkValueArgs, + Evaluate: valueFunc, + }, + "match": { + Name: "match", + ResultType: spec.FuncLogical, + Validate: checkMatchArgs, + Evaluate: matchFunc, + }, + "search": { + Name: "search", + ResultType: spec.FuncLogical, + Validate: checkSearchArgs, + Evaluate: searchFunc, + }, + }, + } +} + +// Register registers a function extension by its name. Panics if fn is nil or +// Register is called twice with the same fn.name. +func (r *Registry) Register(fn *Function) { + r.mu.Lock() + defer r.mu.Unlock() + if fn == nil { + panic("jsonpath: Register function is nil") + } + if _, dup := r.funcs[fn.Name]; dup { + panic("jsonpath: Register called twice for function " + fn.Name) + } + r.funcs[fn.Name] = fn +} + +// Get returns a reference to the registered function named name. Returns nil +// if no function with that name has been registered. +func (r *Registry) Get(name string) *Function { + r.mu.RLock() + defer r.mu.RUnlock() + function := r.funcs[name] + return function +} + +// Function defines a JSONPath function. Use [Register] to register a new +// function. +type Function struct { + // Name is the name of the function. Must be unique among all functions. + Name string + + // ResultType defines the type of the function return value. + ResultType spec.FuncType + + // Validate executes at parse time to validate that all the args to + // the function are compatible with the function. + Validate func(args []spec.FunctionExprArg) error + + // Evaluate executes the function against args and returns the result of + // type ResultType. + Evaluate func(args []spec.JSONPathValue) spec.JSONPathValue +} diff --git a/registry/registry_example_test.go b/registry/registry_example_test.go new file mode 100644 index 0000000..87b6177 --- /dev/null +++ b/registry/registry_example_test.go @@ -0,0 +1,49 @@ +package registry_test + +import ( + "errors" + "fmt" + + "github.com/theory/jsonpath/registry" + "github.com/theory/jsonpath/spec" +) + +// validateFirstArgs validates that a single argument is passed to the first() +// function, and that it can be converted to [spec.PathNodes], so that first() +// can return the first node. It's called by the parser. +func validateFirstArgs(fea []spec.FunctionExprArg) error { + if len(fea) != 1 { + return fmt.Errorf("expected 1 argument but found %v", len(fea)) + } + + if !fea[0].ResultType().ConvertsTo(spec.PathNodes) { + return errors.New("cannot convert argument to PathNodes") + } + + return nil +} + +// firstFunc defines the custom first() JSONPath function. It converts its +// single argument to a [spec.NodesType] value and returns a [*spec.ValueType] +// that contains the first node. If there are no nodes it returns nil. +func firstFunc(jv []spec.JSONPathValue) spec.JSONPathValue { + nodes := spec.NodesFrom(jv[0]) + if len(nodes) == 0 { + return nil + } + return spec.Value(nodes[0]) +} + +// Create and registry a custom JSONPath expression, first(), that returns the +// first node in a list of nodes passed to it. +func Example() { + reg := registry.New() + reg.Register(®istry.Function{ + Name: "first", + ResultType: spec.FuncValue, + Validate: validateFirstArgs, + Evaluate: firstFunc, + }) + fmt.Printf("%v\n", reg.Get("first").ResultType) + // Output:FuncValue +} diff --git a/registry/registry_test.go b/registry/registry_test.go new file mode 100644 index 0000000..a659f35 --- /dev/null +++ b/registry/registry_test.go @@ -0,0 +1,109 @@ +package registry + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/theory/jsonpath/spec" +) + +func TestRegistry(t *testing.T) { + t.Parallel() + a := assert.New(t) + r := require.New(t) + + for _, tc := range []struct { + name string + rType spec.FuncType + expr []spec.FunctionExprArg + args []spec.JSONPathValue + exp any + }{ + // RFC 9535-defined functions. + { + name: "length", + rType: spec.FuncValue, + expr: []spec.FunctionExprArg{spec.Literal("foo")}, + args: []spec.JSONPathValue{spec.Value("foo")}, + exp: spec.Value(3), + }, + { + name: "count", + rType: spec.FuncValue, + expr: []spec.FunctionExprArg{&spec.SingularQueryExpr{}}, + args: []spec.JSONPathValue{spec.NodesType([]any{1, 2})}, + exp: spec.Value(2), + }, + { + name: "value", + rType: spec.FuncValue, + expr: []spec.FunctionExprArg{&spec.SingularQueryExpr{}}, + args: []spec.JSONPathValue{spec.NodesType([]any{42})}, + exp: spec.Value(42), + }, + { + name: "match", + rType: spec.FuncLogical, + expr: []spec.FunctionExprArg{spec.Literal("foo"), spec.Literal(".*")}, + args: []spec.JSONPathValue{spec.Value("foo"), spec.Value(".*")}, + exp: spec.LogicalTrue, + }, + { + name: "search", + rType: spec.FuncLogical, + expr: []spec.FunctionExprArg{spec.Literal("foo"), spec.Literal(".")}, + args: []spec.JSONPathValue{spec.Value("foo"), spec.Value(".")}, + exp: spec.LogicalTrue, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + reg := New() + a.Len(reg.funcs, 5) + + ft := reg.Get(tc.name) + a.NotNil(ft) + a.Equal(tc.rType, ft.ResultType) + r.NoError(ft.Validate(tc.expr)) + a.Equal(tc.exp, ft.Evaluate(tc.args)) + }) + } +} + +func TestRegisterErr(t *testing.T) { + t.Parallel() + a := assert.New(t) + reg := New() + + for _, tc := range []struct { + name string + fn *Function + err string + }{ + { + name: "nil_func", + fn: nil, + err: "jsonpath: Register function is nil", + }, + { + name: "existing_func", + fn: &Function{Name: "length"}, + err: "jsonpath: Register called twice for function length", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + a.PanicsWithValue(tc.err, func() { reg.Register(tc.fn) }) + }) + } +} + +func newFuncExpr(t *testing.T, name string, args []spec.FunctionExprArg) *spec.FunctionExpr { + t.Helper() + f, err := spec.NewFunctionExpr(name, args) + if err != nil { + t.Fatal(err.Error()) + } + return f +} diff --git a/spec/filter.go b/spec/filter.go index d9c12a4..2219374 100644 --- a/spec/filter.go +++ b/spec/filter.go @@ -63,14 +63,12 @@ func (lo LogicalOr) writeTo(buf *strings.Builder) { // execute evaluates lo and returns LogicalTrue when it returns true and // LogicalFalse when it returns false. -// - func (lo LogicalOr) execute(current, root any) JSONPathValue { - return logicalFrom(lo.testFilter(current, root)) + return LogicalFrom(lo.testFilter(current, root)) } -// asTypeKind returns FuncLogical. Defined by the [FunctionExprArg] interface. -func (lo LogicalOr) asTypeKind() FuncType { +// ResultType returns FuncLogical. Defined by the [FunctionExprArg] interface. +func (lo LogicalOr) ResultType() FuncType { return FuncLogical } diff --git a/spec/filter_test.go b/spec/filter_test.go index d624a34..c537602 100644 --- a/spec/filter_test.go +++ b/spec/filter_test.go @@ -177,9 +177,9 @@ func TestLogicalOrExpr(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() orExpr := LogicalOr(tc.expr) - a.Equal(FuncLogical, orExpr.asTypeKind()) + a.Equal(FuncLogical, orExpr.ResultType()) a.Equal(tc.exp, orExpr.testFilter(tc.current, tc.root)) - a.Equal(logicalFrom(tc.exp), orExpr.execute(tc.current, tc.root)) + a.Equal(LogicalFrom(tc.exp), orExpr.execute(tc.current, tc.root)) a.Equal(tc.str, bufString(orExpr)) // Test ParenExpr. diff --git a/spec/function.go b/spec/function.go index 7008652..ff8dba8 100644 --- a/spec/function.go +++ b/spec/function.go @@ -27,33 +27,36 @@ const ( PathNodes // NodesType ) -// FuncType defines the different types of function arguments and return -// values. Function extensions require arguments that convert to specific -// PathValues and define return values in terms of these types. +// FuncType defines the function argument expressions and return types defined +// by [RFC 9535]. Function extensions check that these types can be converted +// to [spec.PathType] values for evaluation. +// +// [RFC 9535]: https://www.rfc-editor.org/rfc/rfc9535.html type FuncType uint8 const ( - // A literal JSON value. + // FuncLiteral represents a literal JSON value. FuncLiteral FuncType = iota + 1 // FuncLiteral - // A value from a singular query. + // FuncSingularQuery represents a value from a singular query. FuncSingularQuery // FuncSingularQuery - // A JSON value, used to represent functions that return [ValueType]. + // FuncValue represents a JSON value, used to represent functions that + // return [ValueType]. FuncValue // FuncValue - // A node list, either from a filter query argument, or a function that + // FuncNodeList represents a node list, either from a filter query argument, or a function that // returns [NodesType]. FuncNodeList // FuncNodeList - // A logical, either from a logical expression, or from a function that - // returns [LogicalType]. + // FuncLogical represents a logical, either from a logical expression, or + // from a function that returns [LogicalType]. FuncLogical // FuncLogical ) -// convertsTo returns true if a function argument of type ft can be converted +// ConvertsTo returns true if a function argument of type ft can be converted // to pv. -func (ft FuncType) convertsTo(pv PathType) bool { +func (ft FuncType) ConvertsTo(pv PathType) bool { switch ft { case FuncLiteral, FuncValue: return pv == PathValue @@ -87,8 +90,8 @@ func (NodesType) PathType() PathType { return PathNodes } // FuncType returns FuncNodeList. Defined by the JSONPathValue interface. func (NodesType) FuncType() FuncType { return FuncNodeList } -// newNodesTypeFrom attempts to convert value to a NodesType. -func newNodesTypeFrom(value JSONPathValue) NodesType { +// NodesFrom attempts to convert value to a NodesType and panics if it cannot. +func NodesFrom(value JSONPathValue) NodesType { switch v := value.(type) { case NodesType: return v @@ -115,14 +118,6 @@ const ( LogicalTrue // true ) -// logicalFrom converts b to a LogicalType. -func logicalFrom(b bool) LogicalType { - if b { - return LogicalTrue - } - return LogicalFalse -} - // Bool returns the boolean equivalent to lt. func (lt LogicalType) Bool() bool { return lt == LogicalTrue } @@ -132,13 +127,19 @@ func (LogicalType) PathType() PathType { return PathLogical } // FuncType returns FuncLogical. Defined by the JSONPathValue interface. func (LogicalType) FuncType() FuncType { return FuncLogical } -// newNodesTypeFrom attempts to convert value to a NodesType. -func newLogicalTypeFrom(value JSONPathValue) LogicalType { +// LogicalFrom attempts to convert value to a LogicalType and panics if it +// cannot. +func LogicalFrom(value any) LogicalType { switch v := value.(type) { case LogicalType: return v case NodesType: - return logicalFrom(len(v) > 0) + return LogicalFrom(len(v) > 0) + case bool: + if v { + return LogicalTrue + } + return LogicalFalse case nil: return LogicalFalse default: @@ -172,8 +173,8 @@ func (*ValueType) PathType() PathType { return PathValue } // FuncType returns FuncValue. Defined by the JSONPathValue interface. func (*ValueType) FuncType() FuncType { return FuncValue } -// newValueTypeFrom attempts to convert value to a ValueType. -func newValueTypeFrom(value JSONPathValue) *ValueType { +// ValueFrom attempts to convert value to a ValueType and panics if it cannot. +func ValueFrom(value JSONPathValue) *ValueType { switch v := value.(type) { case *ValueType: return v @@ -300,8 +301,8 @@ func checkLengthArgs(fea []FunctionExprArg) error { return fmt.Errorf("expected 1 argument but found %v", len(fea)) } - kind := fea[0].asTypeKind() - if !kind.convertsTo(PathValue) { + kind := fea[0].ResultType() + if !kind.ConvertsTo(PathValue) { return errors.New("cannot convert argument to ValueType") } @@ -318,10 +319,8 @@ func checkLengthArgs(fea []FunctionExprArg) error { // - If jv[0] is an map[string]any, the result is the number of members in // the map. // - For any other value, the result is nil. -// - func lengthFunc(jv []JSONPathValue) JSONPathValue { - v := newValueTypeFrom(jv[0]) + v := ValueFrom(jv[0]) if v == nil { return nil } @@ -348,8 +347,8 @@ func checkCountArgs(fea []FunctionExprArg) error { return fmt.Errorf("expected 1 argument but found %v", len(fea)) } - kind := fea[0].asTypeKind() - if !kind.convertsTo(PathNodes) { + kind := fea[0].ResultType() + if !kind.ConvertsTo(PathNodes) { return errors.New("cannot convert argument to PathNodes") } @@ -360,10 +359,8 @@ func checkCountArgs(fea []FunctionExprArg) error { // a ValueType containing an unsigned integer for the number of nodes // in jv[0]. Panics if jv[0] doesn't exist or is not convertible to // [NodesType]. -// - func countFunc(jv []JSONPathValue) JSONPathValue { - return &ValueType{len(newNodesTypeFrom(jv[0]))} + return &ValueType{len(NodesFrom(jv[0]))} } // checkValueArgs checks the argument expressions to value() and returns an @@ -376,8 +373,8 @@ func checkValueArgs(fea []FunctionExprArg) error { return fmt.Errorf("expected 1 argument but found %v", len(fea)) } - kind := fea[0].asTypeKind() - if !kind.convertsTo(PathNodes) { + kind := fea[0].ResultType() + if !kind.ConvertsTo(PathNodes) { return errors.New("cannot convert argument to PathNodes") } @@ -389,10 +386,8 @@ func checkValueArgs(fea []FunctionExprArg) error { // // - If jv[0] contains a single node, the result is the value of the node. // - If jv[0] is empty or contains multiple nodes, the result is nil. -// - func valueFunc(jv []JSONPathValue) JSONPathValue { - nodes := newNodesTypeFrom(jv[0]) + nodes := NodesFrom(jv[0]) if len(nodes) == 1 { return &ValueType{nodes[0]} } @@ -411,8 +406,8 @@ func checkMatchArgs(fea []FunctionExprArg) error { } for i, arg := range fea { - kind := arg.asTypeKind() - if !kind.convertsTo(PathValue) { + kind := arg.ResultType() + if !kind.ConvertsTo(PathValue) { return fmt.Errorf("cannot convert argument %v to PathNodes", i+1) } } @@ -425,13 +420,11 @@ func checkMatchArgs(fea []FunctionExprArg) error { // implied \A and \z anchors and used to match the first, returning LogicalTrue for // a match and LogicalFalse for no match. Returns LogicalFalse if either jv value // is not a string or if jv[1] fails to compile. -// - func matchFunc(jv []JSONPathValue) JSONPathValue { - if v, ok := newValueTypeFrom(jv[0]).any.(string); ok { - if r, ok := newValueTypeFrom(jv[1]).any.(string); ok { + if v, ok := ValueFrom(jv[0]).any.(string); ok { + if r, ok := ValueFrom(jv[1]).any.(string); ok { if rc := compileRegex(`\A` + r + `\z`); rc != nil { - return logicalFrom(rc.MatchString(v)) + return LogicalFrom(rc.MatchString(v)) } } } @@ -450,8 +443,8 @@ func checkSearchArgs(fea []FunctionExprArg) error { } for i, arg := range fea { - kind := arg.asTypeKind() - if !kind.convertsTo(PathValue) { + kind := arg.ResultType() + if !kind.ConvertsTo(PathValue) { return fmt.Errorf("cannot convert argument %v to PathNodes", i+1) } } @@ -464,13 +457,11 @@ func checkSearchArgs(fea []FunctionExprArg) error { // to match the former, returning LogicalTrue for a match and LogicalFalse for no // match. Returns LogicalFalse if either value is not a string, or if jv[1] // fails to compile. -// - func searchFunc(jv []JSONPathValue) JSONPathValue { - if val, ok := newValueTypeFrom(jv[0]).any.(string); ok { - if r, ok := newValueTypeFrom(jv[1]).any.(string); ok { + if val, ok := ValueFrom(jv[0]).any.(string); ok { + if r, ok := ValueFrom(jv[1]).any.(string); ok { if rc := compileRegex(r); rc != nil { - return logicalFrom(rc.MatchString(val)) + return LogicalFrom(rc.MatchString(val)) } } } @@ -534,9 +525,9 @@ type FunctionExprArg interface { // evaluate evaluates the function expression against current and root and // returns the resulting JSONPathValue. execute(current, root any) JSONPathValue - // asTypeKind returns the FuncType that defines the type of the return + // ResultType returns the FuncType that defines the type of the return // value of JSONPathValue. - asTypeKind() FuncType + ResultType() FuncType } // LiteralArg represents a literal JSON value, excluding objects and arrays. @@ -555,14 +546,12 @@ func (la *LiteralArg) Value() any { return la.literal } // execute returns a [ValueType] containing the literal value. Defined by the // [FunctionExprArg] interface. -// - func (la *LiteralArg) execute(_, _ any) JSONPathValue { return &ValueType{la.literal} } -// asTypeKind returns FuncLiteral. Defined by the [FunctionExprArg] interface. -func (la *LiteralArg) asTypeKind() FuncType { +// ResultType returns FuncLiteral. Defined by the [FunctionExprArg] interface. +func (la *LiteralArg) ResultType() FuncType { return FuncLiteral } @@ -577,8 +566,6 @@ func (la *LiteralArg) writeTo(buf *strings.Builder) { // asValue returns la.literal as a [ValueType]. Defined by the [comparableVal] // interface. -// - func (la *LiteralArg) asValue(_, _ any) JSONPathValue { return &ValueType{la.literal} } @@ -600,8 +587,6 @@ func SingularQuery(root bool, selectors []Selector) *SingularQueryExpr { // execute returns a [ValueType] containing the return value of executing sq. // Defined by the [FunctionExprArg] interface. -// - func (sq *SingularQueryExpr) execute(current, root any) JSONPathValue { target := root if sq.relative { @@ -619,16 +604,14 @@ func (sq *SingularQueryExpr) execute(current, root any) JSONPathValue { return &ValueType{target} } -// asTypeKind returns FuncSingularQuery. Defined by the [FunctionExprArg] +// ResultType returns FuncSingularQuery. Defined by the [FunctionExprArg] // interface. -func (*SingularQueryExpr) asTypeKind() FuncType { +func (*SingularQueryExpr) ResultType() FuncType { return FuncSingularQuery } // asValue returns the result of executing sq.execute against current and root. // Defined by the [comparableVal] interface. -// - func (sq *SingularQueryExpr) asValue(current, root any) JSONPathValue { return sq.execute(current, root) } @@ -648,22 +631,25 @@ func (sq *SingularQueryExpr) writeTo(buf *strings.Builder) { } } -// filterQuery represents a JSONPath Query used in a filter expression. -type filterQuery struct { +// FilterQueryExpr represents a JSONPath Query used in a filter expression. +type FilterQueryExpr struct { *PathQuery } +// FilterQuery creates and returns a new FilterQueryExpr. +func FilterQuery(q *PathQuery) *FilterQueryExpr { + return &FilterQueryExpr{q} +} + // execute returns a [NodesType] containing the result of executing fq. // Defined by the [FunctionExprArg] interface. -// - -func (fq *filterQuery) execute(current, root any) JSONPathValue { +func (fq *FilterQueryExpr) execute(current, root any) JSONPathValue { return NodesType(fq.Select(current, root)) } -// asTypeKind returns FuncSingularQuery if fq is a singular query, and +// ResultType returns FuncSingularQuery if fq is a singular query, and // FuncNodeList if it is not. Defined by the [FunctionExprArg] interface. -func (fq *filterQuery) asTypeKind() FuncType { +func (fq *FilterQueryExpr) ResultType() FuncType { if fq.isSingular() { return FuncSingularQuery } @@ -671,7 +657,7 @@ func (fq *filterQuery) asTypeKind() FuncType { } // writeTo writes a string representation of fq to buf. -func (fq *filterQuery) writeTo(buf *strings.Builder) { +func (fq *FilterQueryExpr) writeTo(buf *strings.Builder) { buf.WriteString(fq.PathQuery.String()) } @@ -707,12 +693,6 @@ func NewFunctionExpr(name string, args []FunctionExprArg) (*FunctionExpr, error) return nil, fmt.Errorf("%w %v()", ErrUnregistered, name) } -// ResultType returns the [FuncType] returned by the execution of the function -// expression. -func (fe *FunctionExpr) ResultType() FuncType { - return fe.fn.ResultType -} - // writeTo writes the string representation of fe to buf. func (fe *FunctionExpr) writeTo(buf *strings.Builder) { buf.WriteString(fe.fn.Name + "(") @@ -727,8 +707,6 @@ func (fe *FunctionExpr) writeTo(buf *strings.Builder) { // execute returns a [NodesType] containing the results of executing each // argument in fe.args. Defined by the [FunctionExprArg] interface. -// - func (fe *FunctionExpr) execute(current, root any) JSONPathValue { res := []JSONPathValue{} for _, a := range fe.args { @@ -738,16 +716,14 @@ func (fe *FunctionExpr) execute(current, root any) JSONPathValue { return fe.fn.Evaluate(res) } -// asTypeKind returns the result type of the registered function named +// ResultType returns the result type of the registered function named // fe.name. Defined by the [FunctionExprArg] interface. -func (fe *FunctionExpr) asTypeKind() FuncType { +func (fe *FunctionExpr) ResultType() FuncType { return fe.fn.ResultType } // asValue returns the result of executing fe.execute against current and root. // Defined by the [comparableVal] interface. -// - func (fe *FunctionExpr) asValue(current, root any) JSONPathValue { return fe.execute(current, root) } diff --git a/spec/function_test.go b/spec/function_test.go index e48c5a0..4e5d8da 100644 --- a/spec/function_test.go +++ b/spec/function_test.go @@ -83,10 +83,10 @@ func TestFuncType(t *testing.T) { t.Parallel() a.Equal(tc.name, tc.fType.String()) for _, pv := range tc.ypv { - a.True(tc.fType.convertsTo(pv)) + a.True(tc.fType.ConvertsTo(pv)) } for _, pv := range tc.npv { - a.False(tc.fType.convertsTo(pv)) + a.False(tc.fType.ConvertsTo(pv)) } }) } @@ -110,10 +110,10 @@ func TestNodesType(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() if tc.err != "" { - a.PanicsWithValue(tc.err, func() { newNodesTypeFrom(tc.from) }) + a.PanicsWithValue(tc.err, func() { NodesFrom(tc.from) }) return } - nt := newNodesTypeFrom(tc.from) + nt := NodesFrom(tc.from) a.Equal(tc.exp, nt) a.Equal(PathNodes, nt.PathType()) a.Equal(FuncNodeList, nt.FuncType()) @@ -145,10 +145,10 @@ func TestLogicalType(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() if tc.err != "" { - a.PanicsWithValue(tc.err, func() { newLogicalTypeFrom(tc.from) }) + a.PanicsWithValue(tc.err, func() { LogicalFrom(tc.from) }) return } - lt := newLogicalTypeFrom(tc.from) + lt := LogicalFrom(tc.from) a.Equal(tc.exp, lt) a.Equal(PathLogical, lt.PathType()) a.Equal(FuncLogical, lt.FuncType()) @@ -156,9 +156,9 @@ func TestLogicalType(t *testing.T) { a.Equal(tc.str, bufString(lt)) a.Equal(tc.boolean, lt.Bool()) if tc.boolean { - a.Equal(LogicalTrue, logicalFrom(tc.boolean)) + a.Equal(LogicalTrue, LogicalFrom(tc.boolean)) } else { - a.Equal(LogicalFalse, logicalFrom(tc.boolean)) + a.Equal(LogicalFalse, LogicalFrom(tc.boolean)) } }) } @@ -234,10 +234,10 @@ func TestValueTypeFrom(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() if tc.err != "" { - a.PanicsWithValue(tc.err, func() { newValueTypeFrom(tc.val) }) + a.PanicsWithValue(tc.err, func() { ValueFrom(tc.val) }) return } - val := newValueTypeFrom(tc.val) + val := ValueFrom(tc.val) a.Equal(tc.exp, val) }) } @@ -543,17 +543,17 @@ func TestCheckSingularFuncArgs(t *testing.T) { }, { name: "filter_query", - expr: []FunctionExprArg{&filterQuery{ + expr: []FunctionExprArg{FilterQuery( Query(true, []*Segment{Child(Name("x"))}), - }}, + )}, }, { name: "logical_function_expr", expr: []FunctionExprArg{&FunctionExpr{ fn: registry["match"], - args: []FunctionExprArg{&filterQuery{ + args: []FunctionExprArg{FilterQuery( Query(true, []*Segment{Child(Name("x"))}), - }}, + )}, }}, lengthErr: "cannot convert argument to ValueType", countErr: "cannot convert argument to PathNodes", @@ -650,7 +650,7 @@ func TestCheckRegexFuncArgs(t *testing.T) { { name: "filter_query_1", expr: []FunctionExprArg{ - &filterQuery{Query(true, []*Segment{Child(Name("x"))})}, + FilterQuery(Query(true, []*Segment{Child(Name("x"))})), Literal("hi"), }, }, @@ -658,16 +658,16 @@ func TestCheckRegexFuncArgs(t *testing.T) { name: "filter_query_2", expr: []FunctionExprArg{ Literal("hi"), - &filterQuery{Query(true, []*Segment{Child(Name("x"))})}, + FilterQuery(Query(true, []*Segment{Child(Name("x"))})), }, }, { name: "function_expr_1", expr: []FunctionExprArg{&FunctionExpr{ fn: registry["match"], - args: []FunctionExprArg{&filterQuery{ + args: []FunctionExprArg{FilterQuery( Query(true, []*Segment{Child(Name("x"))}), - }}, + )}, }, Literal("hi")}, err: "cannot convert argument 1 to PathNodes", }, @@ -675,9 +675,9 @@ func TestCheckRegexFuncArgs(t *testing.T) { name: "function_expr_2", expr: []FunctionExprArg{Literal("hi"), &FunctionExpr{ fn: registry["match"], - args: []FunctionExprArg{&filterQuery{ + args: []FunctionExprArg{FilterQuery( Query(true, []*Segment{Child(Name("x"))}), - }}, + )}, }}, err: "cannot convert argument 2 to PathNodes", }, @@ -825,8 +825,8 @@ func TestRegexFuncs(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Parallel() - a.Equal(logicalFrom(tc.match), matchFunc([]JSONPathValue{tc.input, tc.regex})) - a.Equal(logicalFrom(tc.search), searchFunc([]JSONPathValue{tc.input, tc.regex})) + a.Equal(LogicalFrom(tc.match), matchFunc([]JSONPathValue{tc.input, tc.regex})) + a.Equal(LogicalFrom(tc.search), searchFunc([]JSONPathValue{tc.input, tc.regex})) }) } } @@ -862,8 +862,8 @@ func TestExecRegexFuncs(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() if tc.err == "" { - a.Equal(matchFunc(tc.vals), logicalFrom(tc.match)) - a.Equal(searchFunc(tc.vals), logicalFrom(tc.search)) + a.Equal(matchFunc(tc.vals), LogicalFrom(tc.match)) + a.Equal(searchFunc(tc.vals), LogicalFrom(tc.search)) } else { a.PanicsWithValue(tc.err, func() { matchFunc(tc.vals) }) a.PanicsWithValue(tc.err, func() { searchFunc(tc.vals) }) @@ -907,7 +907,7 @@ func TestJsonFunctionExprArgInterface(t *testing.T) { expr any }{ {"literal", &LiteralArg{}}, - {"filter_query", &filterQuery{}}, + {"filter_query", &FilterQueryExpr{}}, {"singular_query", &SingularQueryExpr{}}, {"logical_or", &LogicalOr{}}, {"function_expr", &FunctionExpr{}}, @@ -959,7 +959,7 @@ func TestLiteralArg(t *testing.T) { a.Equal(Value(tc.literal), lit.execute(nil, nil)) a.Equal(Value(tc.literal), lit.asValue(nil, nil)) a.Equal(tc.literal, lit.Value()) - a.Equal(FuncLiteral, lit.asTypeKind()) + a.Equal(FuncLiteral, lit.ResultType()) a.Equal(tc.str, bufString(lit)) }) } @@ -1021,7 +1021,7 @@ func TestSingularQuery(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() sq := &SingularQueryExpr{selectors: tc.selectors, relative: false} - a.Equal(FuncSingularQuery, sq.asTypeKind()) + a.Equal(FuncSingularQuery, sq.ResultType()) // Start with absolute query. a.False(sq.relative) @@ -1088,8 +1088,8 @@ func TestFilterQuery(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Parallel() - fq := &filterQuery{tc.query} - a.Equal(tc.typeKind, fq.asTypeKind()) + fq := &FilterQueryExpr{tc.query} + a.Equal(tc.typeKind, fq.ResultType()) a.Equal(NodesType(tc.exp), fq.execute(tc.current, tc.root)) a.Equal(tc.query.String(), bufString(fq)) }) @@ -1135,7 +1135,7 @@ func TestFunctionExpr(t *testing.T) { { name: "count", fName: "count", - args: []FunctionExprArg{&filterQuery{rootX}}, + args: []FunctionExprArg{&FilterQueryExpr{rootX}}, root: map[string]any{"x": map[string]any{"x": 1}}, exp: Value(2), logical: true, @@ -1227,7 +1227,7 @@ func TestFunctionExpr(t *testing.T) { a.Nil(fe) return } - a.Equal(registry[tc.fName].ResultType, fe.asTypeKind()) + a.Equal(registry[tc.fName].ResultType, fe.ResultType()) a.Equal(tc.exp, fe.execute(tc.current, tc.root)) a.Equal(tc.exp, fe.asValue(tc.current, tc.root)) a.Equal(tc.logical, fe.testFilter(tc.current, tc.root)) diff --git a/spec/op_test.go b/spec/op_test.go index a42e639..f0531b1 100644 --- a/spec/op_test.go +++ b/spec/op_test.go @@ -368,11 +368,11 @@ func TestComparisonExpr(t *testing.T) { { name: "func_strings_gt", left: &FunctionExpr{ - args: []FunctionExprArg{&filterQuery{Query(false, []*Segment{Child(Name("y"))})}}, + args: []FunctionExprArg{FilterQuery(Query(false, []*Segment{Child(Name("y"))}))}, fn: registry["value"], }, right: &FunctionExpr{ - args: []FunctionExprArg{&filterQuery{Query(false, []*Segment{Child(Name("x"))})}}, + args: []FunctionExprArg{FilterQuery(Query(false, []*Segment{Child(Name("x"))}))}, fn: registry["value"], }, current: map[string]any{"x": "x", "y": "y"}, diff --git a/spec/query.go b/spec/query.go index b48158a..9951e76 100644 --- a/spec/query.go +++ b/spec/query.go @@ -76,14 +76,12 @@ func (q *PathQuery) Singular() *SingularQueryExpr { // Expression returns a singularQuery variant of q if q [isSingular] returns // true, and otherwise returns a filterQuery. -// -//nolint:ireturn func (q *PathQuery) Expression() FunctionExprArg { if q.isSingular() { return singular(q) } - return &filterQuery{q} + return FilterQuery(q) } // singular is a utility function that converts q to a singularQuery. diff --git a/spec/query_test.go b/spec/query_test.go index 482d739..9324fa9 100644 --- a/spec/query_test.go +++ b/spec/query_test.go @@ -955,7 +955,7 @@ func TestSingularExpr(t *testing.T) { if tc.sing == nil { a.False(tc.query.isSingular()) a.Nil(tc.query.Singular()) - a.Equal(&filterQuery{tc.query}, tc.query.Expression()) + a.Equal(FilterQuery(tc.query), tc.query.Expression()) } else { a.True(tc.query.isSingular()) a.Equal(tc.sing, tc.query.Singular())