diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 3206980..ceb735e 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -27,7 +27,7 @@ jobs: go mod download - name: Run Test run: | - go test -v -count=10 ./... + go test -count=10 ./... lint: strategy: matrix: @@ -51,7 +51,7 @@ jobs: release_check: runs-on: ubuntu-latest outputs: - git_diff: ${{ steps.changes.outputs.git_diff }} + git_diff: ${{ steps.output.outputs.git_diff }} steps: - name: Checkout repository uses: actions/checkout@v2 @@ -61,11 +61,12 @@ jobs: uses: technote-space/get-diff-action@v6 with: PATTERNS: | - +**/*.go + **/*.go !**/*_test.go FILES: | go.mod - name: Output Diff + id: output if: env.GIT_DIFF run: | echo "::set-output name=git_diff::${{ env.GIT_DIFF }}" diff --git a/.github/workflows/push_main.yaml b/.github/workflows/push_main.yaml index a437984..9433c40 100644 --- a/.github/workflows/push_main.yaml +++ b/.github/workflows/push_main.yaml @@ -26,7 +26,7 @@ jobs: go mod download - name: Run Test run: | - go test -v -count=10 ./... + go test -count=10 ./... lint: strategy: matrix: @@ -61,7 +61,7 @@ jobs: go mod download - name: Run Test run: | - go test -v -coverprofile=coverage.txt -covermode=atomic -count=10 ./... + go test -coverprofile=coverage.txt -covermode=atomic -count=10 ./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest needs: [test, lint] outputs: - git_diff: ${{ steps.changes.outputs.git_diff }} + git_diff: ${{ steps.output.outputs.git_diff }} steps: - name: Checkout repository uses: actions/checkout@v2 @@ -83,12 +83,13 @@ jobs: uses: technote-space/get-diff-action@v6 with: PATTERNS: | - +**/*.go + **/*.go !**/*_test.go FILES: | go.mod - name: Output Diff if: env.GIT_DIFF + id: output run: | echo "::set-output name=git_diff::${{ env.GIT_DIFF }}" release: diff --git a/script/standard/complex_operators.go b/script/standard/complex_operators.go index 1979417..66d2674 100644 --- a/script/standard/complex_operators.go +++ b/script/standard/complex_operators.go @@ -89,3 +89,45 @@ func (op *selectorOperator) Evaluate(parameters map[string]interface{}) (interfa } return value, nil } + +type inOperator struct { + arg1, arg2 interface{} +} + +func (op *inOperator) Evaluate(parameters map[string]interface{}) (interface{}, error) { + var item interface{} = op.arg1 + if numValue, err := getNumber(op.arg1, parameters); err == nil { + item = numValue + } else if strValue, err := getString(op.arg1, parameters); err == nil { + item = strValue + } else if boolValue, err := getBoolean(op.arg1, parameters); err == nil { + item = boolValue + } + + elements, err := getElements(op.arg2, parameters) + if err != nil { + return nil, err + } + + for _, element := range elements { + if element == item { + return true, nil + } + } + + return false, nil +} + +type notInOperator struct { + arg1, arg2 interface{} +} + +func (op *notInOperator) Evaluate(parameters map[string]interface{}) (interface{}, error) { + inOperator := &inOperator{arg1: op.arg1, arg2: op.arg2} + val, err := inOperator.Evaluate(parameters) + if err != nil { + return nil, err + } + boolVal := val.(bool) + return !boolVal, nil +} diff --git a/script/standard/complex_operators_test.go b/script/standard/complex_operators_test.go index aa90756..2d56f49 100644 --- a/script/standard/complex_operators_test.go +++ b/script/standard/complex_operators_test.go @@ -173,3 +173,163 @@ func Test_selectorOperator(t *testing.T) { } batchOperatorTests(t, tests) } + +func Test_inOperator(t *testing.T) { + currentDSelector, _ := newSelectorOperator("@.d", &ScriptEngine{}, nil) + + tests := []*operatorTest{ + { + input: operatorTestInput{ + operator: &inOperator{arg1: nil, arg2: nil}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + err: "invalid argument. is nil", + }, + }, + { + input: operatorTestInput{ + operator: &inOperator{arg1: nil, arg2: []interface{}{"one"}}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: false, + }, + }, + { + input: operatorTestInput{ + operator: &inOperator{arg1: "one", arg2: []interface{}{"one"}}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: true, + }, + }, + { + input: operatorTestInput{ + operator: &inOperator{arg1: "one", arg2: `["one","two"]`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: true, + }, + }, + { + input: operatorTestInput{ + operator: &inOperator{arg1: "one", arg2: `{"1":"one","2":"two"}`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: true, + }, + }, + { + input: operatorTestInput{ + operator: &inOperator{arg1: "1", arg2: `[1,2,3]`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: true, + }, + }, + { + input: operatorTestInput{ + operator: &inOperator{arg1: "1", arg2: `["1","2","3"]`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: false, + }, + }, + { + input: operatorTestInput{ + operator: &inOperator{ + arg1: "2", + arg2: currentDSelector, + }, + paramters: map[string]interface{}{ + "@": map[string]interface{}{ + "d": []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + }, + }, + expected: operatorTestExpected{ + value: true, + }, + }, + } + batchOperatorTests(t, tests) +} + +func Test_notInOperator(t *testing.T) { + tests := []*operatorTest{ + { + input: operatorTestInput{ + operator: ¬InOperator{arg1: nil, arg2: nil}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + err: "invalid argument. is nil", + }, + }, + { + input: operatorTestInput{ + operator: ¬InOperator{arg1: nil, arg2: []interface{}{"one"}}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: true, + }, + }, + { + input: operatorTestInput{ + operator: ¬InOperator{arg1: "one", arg2: []interface{}{"one"}}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: false, + }, + }, + { + input: operatorTestInput{ + operator: ¬InOperator{arg1: "one", arg2: `["one","two"]`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: false, + }, + }, + { + input: operatorTestInput{ + operator: ¬InOperator{arg1: "one", arg2: `{"1":"one","2":"two"}`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: false, + }, + }, + { + input: operatorTestInput{ + operator: ¬InOperator{arg1: "1", arg2: `[1,2,3]`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: false, + }, + }, + { + input: operatorTestInput{ + operator: ¬InOperator{arg1: "1", arg2: `["1","2","3"]`}, + paramters: map[string]interface{}{}, + }, + expected: operatorTestExpected{ + value: true, + }, + }, + } + batchOperatorTests(t, tests) +} diff --git a/script/standard/engine.go b/script/standard/engine.go index 44be857..bcd6a88 100644 --- a/script/standard/engine.go +++ b/script/standard/engine.go @@ -8,15 +8,13 @@ import ( "github.com/evilmonkeyinc/jsonpath/script" ) -// TODO : add tests for what is in readme -// TODO : update readme to give more details, maybe add readme to this package and link from main // TODO : add support for bitwise operators | &^ ^ & << >> after + and - var defaultTokens []string = []string{ "||", "&&", "==", "!=", "<=", ">=", "<", ">", "=~", "!", "+", "-", "**", "*", "/", "%", - "@", "$", + "not in", "in", "@", "$", } // ScriptEngine standard implementation of the script engine interface @@ -123,6 +121,16 @@ func (engine *ScriptEngine) buildOperators(expression string, tokens []string, o arg1: leftside, arg2: rightside, }, nil + case "not in": + return ¬InOperator{ + arg1: leftside, + arg2: rightside, + }, nil + case "in": + return &inOperator{ + arg1: leftside, + arg2: rightside, + }, nil case "!": if leftside != nil { // There should not be a left side to this operator diff --git a/script/standard/engine_test.go b/script/standard/engine_test.go index 9d8af40..103325d 100644 --- a/script/standard/engine_test.go +++ b/script/standard/engine_test.go @@ -716,6 +716,39 @@ func Test_ScriptEngine_buildOperators(t *testing.T) { err: "", }, }, + { + input: input{ + expression: "1 in [1]", + tokens: defaultTokens, + }, + expected: expected{ + operator: &inOperator{arg1: "1", arg2: []interface{}{float64(1)}}, + err: "", + }, + }, + { + input: input{ + expression: "1 not in [1]", + tokens: defaultTokens, + }, + expected: expected{ + operator: ¬InOperator{arg1: "1", arg2: []interface{}{float64(1)}}, + err: "", + }, + }, + { + input: input{ + expression: "1 in [1] && 1 not in [2]", + tokens: defaultTokens, + }, + expected: expected{ + operator: &andOperator{ + arg1: &inOperator{arg1: "1", arg2: []interface{}{float64(1)}}, + arg2: ¬InOperator{arg1: "1", arg2: []interface{}{float64(2)}}, + }, + err: "", + }, + }, } for idx, test := range tests { diff --git a/script/standard/errors.go b/script/standard/errors.go index a10cfed..ada7b04 100644 --- a/script/standard/errors.go +++ b/script/standard/errors.go @@ -7,13 +7,14 @@ import ( ) var ( - errUnsupportedOperator error = fmt.Errorf("unsupported operator") - errInvalidArgument error = fmt.Errorf("invalid argument") - errInvalidArgumentNil error = fmt.Errorf("%w. is nil", errInvalidArgument) - errInvalidArgumentExpectedInteger error = fmt.Errorf("%w. expected integer", errInvalidArgument) - errInvalidArgumentExpectedNumber error = fmt.Errorf("%w. expected number", errInvalidArgument) - errInvalidArgumentExpectedBoolean error = fmt.Errorf("%w. expected boolean", errInvalidArgument) - errInvalidArgumentExpectedRegex error = fmt.Errorf("%w. expected a valid regexp", errInvalidArgument) + errUnsupportedOperator error = fmt.Errorf("unsupported operator") + errInvalidArgument error = fmt.Errorf("invalid argument") + errInvalidArgumentNil error = fmt.Errorf("%w. is nil", errInvalidArgument) + errInvalidArgumentExpectedInteger error = fmt.Errorf("%w. expected integer", errInvalidArgument) + errInvalidArgumentExpectedNumber error = fmt.Errorf("%w. expected number", errInvalidArgument) + errInvalidArgumentExpectedBoolean error = fmt.Errorf("%w. expected boolean", errInvalidArgument) + errInvalidArgumentExpectedRegex error = fmt.Errorf("%w. expected a valid regexp", errInvalidArgument) + errInvalidArgumentExpectedCollection error = fmt.Errorf("%w. expected array, map, or slice", errInvalidArgument) ) func getInvalidExpressionEmptyError() error { diff --git a/script/standard/operators.go b/script/standard/operators.go index 3e38cc8..c9c7816 100644 --- a/script/standard/operators.go +++ b/script/standard/operators.go @@ -1,7 +1,9 @@ package standard import ( + "encoding/json" "fmt" + "reflect" "strconv" "strings" ) @@ -151,3 +153,74 @@ func getString(argument interface{}, parameters map[string]interface{}) (string, return fmt.Sprintf("%v", argument), nil } + +func getElements(argument interface{}, parameters map[string]interface{}) ([]interface{}, error) { + if argument == nil { + return nil, errInvalidArgumentNil + } + + if sub, ok := argument.(operator); ok { + arg, err := sub.Evaluate(parameters) + if err != nil { + return nil, err + } + argument = arg + } + + if strValue, ok := argument.(string); ok { + if param, ok := parameters[strValue]; ok { + argument = param + } else { + if strings.HasPrefix(strValue, "{") && strings.HasSuffix(strValue, "}") { + // object + root := make(map[string]interface{}) + if err := json.Unmarshal([]byte(strValue), &root); err != nil { + return nil, errInvalidArgument + } + argument = root + } else if strings.HasPrefix(strValue, "[") && strings.HasSuffix(strValue, "]") { + // array + root := make([]interface{}, 0) + if err := json.Unmarshal([]byte(strValue), &root); err != nil { + return nil, errInvalidArgument + } + argument = root + } + } + } + + objType := reflect.TypeOf(argument) + if objType == nil { + return nil, errInvalidArgumentNil + } + objValue := reflect.ValueOf(argument) + + if objType.Kind() == reflect.Ptr { + if objValue.IsNil() { + return nil, errInvalidArgumentNil + } + objType = objType.Elem() + objValue = objValue.Elem() + } + + elements := make([]interface{}, 0) + + switch objType.Kind() { + case reflect.Array, reflect.Slice: + length := objValue.Len() + for i := 0; i < length; i++ { + elements = append(elements, objValue.Index(i).Interface()) + } + break + case reflect.Map: + keys := objValue.MapKeys() + for _, key := range keys { + elements = append(elements, objValue.MapIndex(key).Interface()) + } + break + default: + return nil, errInvalidArgumentExpectedCollection + } + + return elements, nil +} diff --git a/script/standard/operators_test.go b/script/standard/operators_test.go index 186f811..38cff21 100644 --- a/script/standard/operators_test.go +++ b/script/standard/operators_test.go @@ -436,3 +436,164 @@ func Test_getString(t *testing.T) { }) } } + +func Test_getElements(t *testing.T) { + + currentKeySelector, _ := newSelectorOperator("@.key", &ScriptEngine{}, nil) + + getPtr := func(in []interface{}) *[]interface{} { + return &in + } + var nilPtr *string = nil + + type input struct { + argument interface{} + parameters map[string]interface{} + } + + type expected struct { + value []interface{} + err string + } + + tests := []struct { + input input + expected expected + }{ + { + input: input{ + argument: nil, + }, + expected: expected{ + err: "invalid argument. is nil", + }, + }, + { + input: input{ + argument: "[,,]", + }, + expected: expected{ + err: "invalid argument", + }, + }, + { + input: input{ + argument: `["one","two","three"]`, + }, + expected: expected{ + value: []interface{}{ + "one", + "two", + "three", + }, + }, + }, + { + input: input{ + argument: "{{}", + }, + expected: expected{ + err: "invalid argument", + }, + }, + { + input: input{ + argument: `{"one":"one","two":"two"}`, + }, + expected: expected{ + value: []interface{}{"one", "two"}, + }, + }, + { + input: input{ + argument: "@", + parameters: map[string]interface{}{ + "@": []string{"one", "two", "three"}, + }, + }, + expected: expected{ + value: []interface{}{"one", "two", "three"}, + }, + }, + { + input: input{ + argument: "null", + parameters: map[string]interface{}{ + "null": nil, + }, + }, + expected: expected{ + err: "invalid argument. is nil", + }, + }, + { + input: input{ + argument: "ptr", + parameters: map[string]interface{}{ + "ptr": getPtr([]interface{}{"one", "two"}), + }, + }, + expected: expected{ + value: []interface{}{"one", "two"}, + }, + }, + { + input: input{ + argument: "ptr", + parameters: map[string]interface{}{ + "ptr": nilPtr, + }, + }, + expected: expected{ + err: "invalid argument. is nil", + }, + }, + { + input: input{ + argument: "str", + parameters: map[string]interface{}{ + "str": "string", + }, + }, + expected: expected{ + err: "invalid argument. expected array, map, or slice", + }, + }, + { + input: input{ + argument: currentKeySelector, + parameters: map[string]interface{}{ + "@": map[string]interface{}{ + "key": []interface{}{"one"}, + }, + }, + }, + expected: expected{ + value: []interface{}{"one"}, + }, + }, + { + input: input{ + argument: &plusOperator{arg1: "", arg2: ""}, + parameters: map[string]interface{}{}, + }, + expected: expected{ + err: "invalid argument. expected number", + }, + }, + } + + for idx, test := range tests { + t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { + actual, err := getElements(test.input.argument, test.input.parameters) + + if test.expected.err == "" { + assert.Nil(t, err) + } else { + assert.EqualError(t, err, test.expected.err) + } + + assert.ElementsMatch(t, test.expected.value, actual) + }) + } +} diff --git a/test/README.md b/test/README.md index 14846f9..6a729a8 100644 --- a/test/README.md +++ b/test/README.md @@ -216,8 +216,8 @@ This implementation would be closer to the 'Scalar consensus' as it does not alw |:question:|`$.items[?(@.key==$.value)]`|`{"value": 42, "items": [{"key": 10}, {"key": 42}, {"key": 50}]}`|none|`[{"key":42}]`| |:question:|`$[?(@.key>42)]`|`[ {"key": 0}, {"key": 42}, {"key": -1}, {"key": 41}, {"key": 43}, {"key": 42.0001}, {"key": 41.9999}, {"key": 100}, {"key": "43"}, {"key": "42"}, {"key": "41"}, {"key": "value"}, {"some": "value"} ]`|none|`[{"key":43},{"key":42.0001},{"key":100}]`| |:question:|`$[?(@.key>=42)]`|`[ {"key": 0}, {"key": 42}, {"key": -1}, {"key": 41}, {"key": 43}, {"key": 42.0001}, {"key": 41.9999}, {"key": 100}, {"key": "43"}, {"key": "42"}, {"key": "41"}, {"key": "value"}, {"some": "value"} ]`|none|`[{"key":42},{"key":43},{"key":42.0001},{"key":100}]`| -|:no_entry:|`$[?(@.d in [2, 3])]`|`[{"d": 1}, {"d": 2}, {"d": 1}, {"d": 3}, {"d": 4}]`|`nil`|`[]`| -|:no_entry:|`$[?(2 in @.d)]`|`[{"d": [1, 2, 3]}, {"d": [2]}, {"d": [1]}, {"d": [3, 4]}, {"d": [4, 2]}]`|`nil`|`[{"d":[1,2,3]},{"d":[2]},{"d":[1]},{"d":[3,4]},{"d":[4,2]}]`| +|:no_entry:|`$[?(@.d in [2, 3])]`|`[{"d": 1}, {"d": 2}, {"d": 1}, {"d": 3}, {"d": 4}]`|`nil`|`[{"d":2},{"d":3}]`| +|:no_entry:|`$[?(2 in @.d)]`|`[{"d": [1, 2, 3]}, {"d": [2]}, {"d": [1]}, {"d": [3, 4]}, {"d": [4, 2]}]`|`nil`|`[{"d":[1,2,3]},{"d":[2]},{"d":[4,2]}]`| |:question:|`$[?(@.key<42)]`|`[ {"key": 0}, {"key": 42}, {"key": -1}, {"key": 41}, {"key": 43}, {"key": 42.0001}, {"key": 41.9999}, {"key": 100}, {"key": "43"}, {"key": "42"}, {"key": "41"}, {"key": "value"}, {"some": "value"} ]`|none|`[{"key":0},{"key":-1},{"key":41},{"key":41.9999}]`| |:question:|`$[?(@.key<=42)]`|`[ {"key": 0}, {"key": 42}, {"key": -1}, {"key": 41}, {"key": 43}, {"key": 42.0001}, {"key": 41.9999}, {"key": 100}, {"key": "43"}, {"key": "42"}, {"key": "41"}, {"key": "value"}, {"some": "value"} ]`|none|`[{"key":0},{"key":42},{"key":-1},{"key":41},{"key":41.9999}]`| |:question:|`$[?(@.key*2==100)]`|`[{"key": 60}, {"key": 50}, {"key": 10}, {"key": -50}, {"key*2": 100}]`|none|`[{"key":50}]`| diff --git a/test/filter_test.go b/test/filter_test.go index f9e78fe..0b6e42e 100644 --- a/test/filter_test.go +++ b/test/filter_test.go @@ -440,20 +440,21 @@ var filterTests []testData = []testData{ expectedError: "", }, { - selector: `$[?(@.d in [2, 3])]`, // TODO : in keywork will not work - data: `[{"d": 1}, {"d": 2}, {"d": 1}, {"d": 3}, {"d": 4}]`, - expected: []interface{}{}, + selector: `$[?(@.d in [2, 3])]`, + data: `[{"d": 1}, {"d": 2}, {"d": 1}, {"d": 3}, {"d": 4}]`, + expected: []interface{}{ + map[string]interface{}{"d": float64(2)}, + map[string]interface{}{"d": float64(3)}, + }, consensus: nil, expectedError: "", }, { - selector: `$[?(2 in @.d)]`, // TODO : in keywork will not work + selector: `$[?(2 in @.d)]`, data: `[{"d": [1, 2, 3]}, {"d": [2]}, {"d": [1]}, {"d": [3, 4]}, {"d": [4, 2]}]`, expected: []interface{}{ map[string]interface{}{"d": []interface{}{float64(1), float64(2), float64(3)}}, map[string]interface{}{"d": []interface{}{float64(2)}}, - map[string]interface{}{"d": []interface{}{float64(1)}}, - map[string]interface{}{"d": []interface{}{float64(3), float64(4)}}, map[string]interface{}{"d": []interface{}{float64(4), float64(2)}}, }, consensus: nil,