Repository: diegoholiveira/jsonlogic Branch: main Commit: c60748e7ebf3 Files: 37 Total size: 139.5 KB Directory structure: gitextract_pw5i5pga/ ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── arrays.go ├── arrays_test.go ├── benchmark/ │ ├── README.md │ ├── bench │ └── benchmark_test.go ├── codecov.yml ├── comp.go ├── comp_test.go ├── go.mod ├── go.sum ├── internal/ │ ├── javascript/ │ │ ├── javascript.go │ │ └── javascript_test.go │ ├── json_logic_pr_48_tests.json │ ├── testing.go │ └── typing/ │ ├── typing.go │ └── typing_test.go ├── issues_test.go ├── jsonlogic.go ├── jsonlogic_test.go ├── lists.go ├── lists_test.go ├── logic.go ├── math.go ├── math_test.go ├── operation.go ├── operation_test.go ├── readme.md ├── strings.go ├── strings_test.go ├── validator.go ├── validator_test.go ├── vars.go └── vars_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/test.yml ================================================ name: Continuous Integration on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: go: ['1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24'] name: Running with Go ${{ matrix.go }} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} cache: false - name: Checkout code uses: actions/checkout@v4 - name: Run the tests run: go test -race -coverprofile=coverage.out -covermode=atomic - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out *.prof .idea/ .vscode/ benchmark/*.txt ================================================ FILE: .golangci.yml ================================================ run: timeout: 5m linters-settings: goimports: local-prefixes: github.com/diegoholiveira/jsonlogic linters: disable-all: true enable: - bodyclose - errcheck - goimports - gosimple - govet - ineffassign - staticcheck - typecheck - unparam - unused - whitespace ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Diego Henrique Oliveira Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: arrays.go ================================================ package jsonlogic import ( "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) // containsAll checks if all elements in the second array exist in the first array. // Returns true if every element of the required array is found in the search array. // // Example: // // {"contains_all": [["a", "b", "c"], ["a", "b"]]} // true // {"contains_all": [["a", "b"], ["a", "b", "c"]]} // false func containsAll(values, data any) any { parsed, ok := values.([]any) if !ok || len(parsed) != 2 { return false } searchArray := toAnySlice(parsed[0]) if searchArray == nil { return false } requiredArray := toAnySlice(parsed[1]) if requiredArray == nil { return false } // Empty required array means all are "contained" if len(requiredArray) == 0 { return true } for _, required := range requiredArray { if !containsElement(searchArray, required) { return false } } return true } // containsAny checks if any element in the second array exists in the first array. // Returns true if at least one element of the check array is found in the search array. // // Example: // // {"contains_any": [["a", "b", "c"], ["x", "b"]]} // true // {"contains_any": [["a", "b", "c"], ["x", "y"]]} // false func containsAny(values, data any) any { parsed, ok := values.([]any) if !ok || len(parsed) != 2 { return false } searchArray := toAnySlice(parsed[0]) if searchArray == nil { return false } checkArray := toAnySlice(parsed[1]) if checkArray == nil { return false } for _, check := range checkArray { if containsElement(searchArray, check) { return true } } return false } // containsNone checks if no elements in the second array exist in the first array. // Returns true if none of the elements of the check array are found in the search array. // // Example: // // {"contains_none": [["a", "b", "c"], ["x", "y"]]} // true // {"contains_none": [["a", "b", "c"], ["x", "b"]]} // false func containsNone(values, data any) any { parsed, ok := values.([]any) if !ok || len(parsed) != 2 { return true } searchArray := toAnySlice(parsed[0]) if searchArray == nil { return true } checkArray := toAnySlice(parsed[1]) if checkArray == nil { return true } for _, check := range checkArray { if containsElement(searchArray, check) { return false } } return true } // toAnySlice converts an interface{} to []any if possible. func toAnySlice(value any) []any { if value == nil { return nil } if slice, ok := value.([]any); ok { return slice } return nil } // containsElement checks if an element exists in a slice using proper comparison. func containsElement(slice []any, element any) bool { for _, item := range slice { if isEqualValue(item, element) { return true } } return false } // isEqualValue compares two values with type coercion for numbers. func isEqualValue(a, b any) bool { // Direct equality check if a == b { return true } // Handle number comparison with type coercion if typing.IsNumber(a) && typing.IsNumber(b) { return typing.ToNumber(a) == typing.ToNumber(b) } // Handle string comparison if typing.IsString(a) && typing.IsString(b) { return a.(string) == b.(string) } return false } ================================================ FILE: arrays_test.go ================================================ package jsonlogic import ( "bytes" "strings" "testing" ) func TestContainsAll(t *testing.T) { tests := []struct { name string rule string data string expected string }{ { name: "all elements present", rule: `{"contains_all": [["a", "b", "c"], ["a", "b"]]}`, data: `{}`, expected: "true", }, { name: "all elements present - exact match", rule: `{"contains_all": [["a", "b"], ["a", "b"]]}`, data: `{}`, expected: "true", }, { name: "some elements missing", rule: `{"contains_all": [["a", "b"], ["a", "b", "c"]]}`, data: `{}`, expected: "false", }, { name: "empty required array", rule: `{"contains_all": [["a", "b", "c"], []]}`, data: `{}`, expected: "true", }, { name: "empty search array", rule: `{"contains_all": [[], ["a"]]}`, data: `{}`, expected: "false", }, { name: "with variable", rule: `{"contains_all": [{"var": "selected"}, ["vip", "premium"]]}`, data: `{"selected": ["vip", "premium", "gold"]}`, expected: "true", }, { name: "with variable - missing element", rule: `{"contains_all": [{"var": "selected"}, ["vip", "diamond"]]}`, data: `{"selected": ["vip", "premium", "gold"]}`, expected: "false", }, { name: "with numbers", rule: `{"contains_all": [[1, 2, 3, 4], [1, 3]]}`, data: `{}`, expected: "true", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var result bytes.Buffer err := Apply(strings.NewReader(tt.rule), strings.NewReader(tt.data), &result) if err != nil { t.Fatalf("unexpected error: %v", err) } if strings.TrimSpace(result.String()) != tt.expected { t.Errorf("expected %s, got %s", tt.expected, result.String()) } }) } } func TestContainsAny(t *testing.T) { tests := []struct { name string rule string data string expected string }{ { name: "one element present", rule: `{"contains_any": [["a", "b", "c"], ["x", "b"]]}`, data: `{}`, expected: "true", }, { name: "multiple elements present", rule: `{"contains_any": [["a", "b", "c"], ["a", "c"]]}`, data: `{}`, expected: "true", }, { name: "no elements present", rule: `{"contains_any": [["a", "b", "c"], ["x", "y"]]}`, data: `{}`, expected: "false", }, { name: "empty check array", rule: `{"contains_any": [["a", "b", "c"], []]}`, data: `{}`, expected: "false", }, { name: "empty search array", rule: `{"contains_any": [[], ["a"]]}`, data: `{}`, expected: "false", }, { name: "with variable", rule: `{"contains_any": [{"var": "tags"}, ["urgent", "important"]]}`, data: `{"tags": ["normal", "urgent"]}`, expected: "true", }, { name: "with variable - no match", rule: `{"contains_any": [{"var": "tags"}, ["urgent", "important"]]}`, data: `{"tags": ["normal", "low"]}`, expected: "false", }, { name: "with numbers", rule: `{"contains_any": [[1, 2, 3], [5, 3, 7]]}`, data: `{}`, expected: "true", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var result bytes.Buffer err := Apply(strings.NewReader(tt.rule), strings.NewReader(tt.data), &result) if err != nil { t.Fatalf("unexpected error: %v", err) } if strings.TrimSpace(result.String()) != tt.expected { t.Errorf("expected %s, got %s", tt.expected, result.String()) } }) } } func TestContainsNone(t *testing.T) { tests := []struct { name string rule string data string expected string }{ { name: "no elements present", rule: `{"contains_none": [["a", "b", "c"], ["x", "y"]]}`, data: `{}`, expected: "true", }, { name: "one element present", rule: `{"contains_none": [["a", "b", "c"], ["x", "b"]]}`, data: `{}`, expected: "false", }, { name: "all elements present", rule: `{"contains_none": [["a", "b", "c"], ["a", "b"]]}`, data: `{}`, expected: "false", }, { name: "empty check array", rule: `{"contains_none": [["a", "b", "c"], []]}`, data: `{}`, expected: "true", }, { name: "empty search array", rule: `{"contains_none": [[], ["a"]]}`, data: `{}`, expected: "true", }, { name: "with variable - blocked words not present", rule: `{"contains_none": [{"var": "content"}, ["spam", "blocked"]]}`, data: `{"content": ["hello", "world"]}`, expected: "true", }, { name: "with variable - blocked word present", rule: `{"contains_none": [{"var": "content"}, ["spam", "blocked"]]}`, data: `{"content": ["hello", "spam"]}`, expected: "false", }, { name: "with numbers", rule: `{"contains_none": [[1, 2, 3], [7, 8, 9]]}`, data: `{}`, expected: "true", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var result bytes.Buffer err := Apply(strings.NewReader(tt.rule), strings.NewReader(tt.data), &result) if err != nil { t.Fatalf("unexpected error: %v", err) } if strings.TrimSpace(result.String()) != tt.expected { t.Errorf("expected %s, got %s", tt.expected, result.String()) } }) } } ================================================ FILE: benchmark/README.md ================================================ # JSONLogic Benchmark Benchmark suite to compare performance between different versions of the JSONLogic library. ## Prerequisites - Go 1.21+ - `benchstat` (install with: `go install golang.org/x/perf/cmd/benchstat@latest`) ## Usage Compare your current code against a published version: ```bash ./bench v3.7.5 ``` The script will: 1. Create an isolated git worktree for the target version 2. **Copy current benchmark code** to target version (ensures fair comparison) 3. Run **comprehensive benchmarks** for both versions (10 iterations each) 4. Display statistical comparison using `benchstat` 5. Clean up automatically ### Benchmark Suites By default, the script runs the **comprehensive suite** (8 complex benchmarks, ~6-8s total): ```bash ./bench v3.7.5 # Fast, realistic comparison ``` To run the **detailed suite** (all 65 benchmarks, ~60s total): ```bash ./bench v3.7.5 BenchmarkDetailed ``` To run specific benchmark categories: ```bash ./bench v3.7.5 BenchmarkMathOperations ./bench v3.7.5 BenchmarkArrayOperationsScaling ``` ## Understanding the Output `benchstat` shows the performance comparison: ``` name old time/op new time/op delta JSONLogic/simple-8 900ns ± 2% 850ns ± 3% -5.56% (p=0.000 n=10+10) ``` - **old time/op**: Target version performance - **new time/op**: Current code performance - **delta**: Percentage change (negative = improvement, positive = regression) - **±**: Variation/noise in measurements - **p-value**: Statistical significance (p < 0.05 means the difference is real) ## Benchmark Suites ### Comprehensive Suite (Default) The comprehensive suite contains 8 complex, realistic benchmarks that exercise multiple operators: 1. **user_validation** - Complex user validation with `and`, `or`, `>=`, `in`, `==`, `+`, `var` 2. **data_pipeline** - Filter + reduce chain for data aggregation 3. **business_rules** - Nested if/else with conditional pricing logic 4. **array_validation** - Combines `all`, `some`, `none` for array validation 5. **string_processing** - String operations with `in`, `substr`, and comparisons 6. **custom_operators** - Tests `contains_all`, `contains_any`, `contains_none` 7. **complex_data_transform** - Chained `filter` + `map` with complex conditions 8. **nested_conditions** - Multi-level conditionals with `missing`, `missing_some`, `cat` Each benchmark represents real-world usage patterns and exercises 5-7 operators per test. ### Detailed Suite The detailed suite contains all individual operator benchmarks organized by category: ### Core Operations - **baseline_noop**: Minimal baseline benchmark (just `true`) - **simple_equal**: Basic equality check - **complex_condition**: Nested logical operators with `and` - **nested_var**: Deep variable path access with defaults - **complex_logic**: Conditional if/else logic ### Array Operations - **array_operations**: Array map operations - **reduce_sum**: Reduce operation with sum accumulation - **filter_even_numbers**: Filter with modulo operation - **all_validation**: All operator for validation patterns - **some_validation**: Some operator for validation patterns - **merge_arrays**: Merge multiple arrays ### Custom Operators - **contains_all**: Tests if all elements exist in array - **contains_any**: Tests if any element exists in array - **contains_none**: Tests if no elements exist in array ### String Operations - **string_concatenation**: String concatenation with `cat` - **substring_extraction**: Substring extraction with `substr` ### Math Operations - **max_operation**: Find maximum value - **min_operation**: Find minimum value - **modulo_operation**: Modulo operator ### Logic Operations - **or_operation**: Logical OR operator ### Field Validation - **missing_fields**: Missing field detection ### Complex Scenarios - **deeply_nested_operations**: Nested filter and map operations ## Benchmark Categories The benchmark suite is organized into multiple categories for targeted testing: ### Run All Benchmarks ```bash go test -bench=. ./benchmark/ ``` ### Run Specific Categories **Main benchmark suite** (all test cases): ```bash go test -bench=BenchmarkJSONLogic$ ./benchmark/ ``` **Parallel benchmarks** (concurrent performance testing): ```bash go test -bench=BenchmarkJSONLogicParallel ./benchmark/ ``` **Scaling benchmarks** (tests with 10, 100, 1000 element arrays): ```bash go test -bench=BenchmarkArrayOperationsScaling ./benchmark/ ``` **Math operations only**: ```bash go test -bench=BenchmarkMathOperations ./benchmark/ ``` **String operations only**: ```bash go test -bench=BenchmarkStringOperations ./benchmark/ ``` **Logic operations only**: ```bash go test -bench=BenchmarkLogicOperations ./benchmark/ ``` **Custom operators only**: ```bash go test -bench=BenchmarkCustomOperators ./benchmark/ ``` ## Benchmark Types ### 1. Standard Benchmarks (`BenchmarkJSONLogic`) Tests all 22 core operations with realistic data sizes. ### 2. Parallel Benchmarks (`BenchmarkJSONLogicParallel`) Tests concurrent usage with `RunParallel` for: - simple_equal - map - reduce - filter ### 3. Scaling Benchmarks (`BenchmarkArrayOperationsScaling`) Tests performance with different array sizes (10, 100, 1000 elements) for: - map - filter - reduce - all - some - none ### 4. Categorical Benchmarks Organized by operation type for focused testing: - Math: +, -, *, /, %, max, min, abs - String: cat, substr, in - Logic: and, or, !, if - Custom: contains_all, contains_any, contains_none ## Advanced Profiling ### Memory profiling: ```bash cd benchmark go test -bench=. -benchmem -memprofile=mem.prof go tool pprof -http=:8080 mem.prof ``` ### CPU profiling: ```bash go test -bench=. -cpuprofile=cpu.prof go tool pprof -http=:8080 cpu.prof ``` ### Compare scaling performance: ```bash go test -bench=BenchmarkArrayOperationsScaling/map -benchmem ``` This will show how map performance scales from 10 to 1000 elements. ================================================ FILE: benchmark/bench ================================================ #!/usr/bin/env bash set -e # Colors for output GREEN='\033[0;32m' BLUE='\033[0;34m' RED='\033[0;31m' NC='\033[0m' # Check if benchstat is installed if ! command -v benchstat &>/dev/null; then echo -e "${RED}Error: benchstat is not installed${NC}" echo "Install it with: go install golang.org/x/perf/cmd/benchstat@latest" exit 1 fi # Validate argument if [ $# -eq 0 ]; then echo -e "${RED}Error: Missing version argument${NC}" echo "Usage: $0 " echo "Example: $0 v3.7.5" exit 1 fi TARGET_REF="$1" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Temporary worktree directory WORKTREE_DIR=$(mktemp -d) OLD_BENCH="$SCRIPT_DIR/old.txt" NEW_BENCH="$SCRIPT_DIR/new.txt" # Cleanup function cleanup() { echo -e "${BLUE}Cleaning up...${NC}" git -C "$PROJECT_ROOT" worktree remove "$WORKTREE_DIR" --force 2>/dev/null || true rm -rf "$WORKTREE_DIR" } trap cleanup EXIT echo -e "${GREEN}JSONLogic Benchmark: $TARGET_REF vs current${NC}" echo # Benchmark filter (default to comprehensive suite for fast comparisons) BENCH_FILTER="${2:-BenchmarkComprehensive$}" # Create worktree for target ref echo -e "${BLUE}Setting up worktree for $TARGET_REF...${NC}" git -C "$PROJECT_ROOT" worktree add "$WORKTREE_DIR" "$TARGET_REF" --quiet # Copy current benchmark code to ensure fair comparison echo -e "${BLUE}Copying current benchmark code to $TARGET_REF worktree...${NC}" cp "$PROJECT_ROOT/benchmark/benchmark_test.go" "$WORKTREE_DIR/benchmark/benchmark_test.go" # Run benchmarks for target ref echo -e "${BLUE}Running benchmarks for $TARGET_REF...${NC}" cd "$WORKTREE_DIR/benchmark" go test -bench="$BENCH_FILTER" -benchmem -count=10 >"$OLD_BENCH" 2>&1 # Run benchmarks for current code echo -e "${BLUE}Running benchmarks for current code...${NC}" cd "$PROJECT_ROOT/benchmark" go test -bench="$BENCH_FILTER" -benchmem -count=10 >"$NEW_BENCH" 2>&1 # Show comparison echo echo -e "${GREEN}Results:${NC}" benchstat "$OLD_BENCH" "$NEW_BENCH" ================================================ FILE: benchmark/benchmark_test.go ================================================ package benchmark import ( "bytes" "fmt" "runtime" "strings" "testing" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) var TestCases = []struct { name string logic string data string }{ { name: "baseline_noop", logic: `true`, data: `{}`, }, { name: "simple_equal", logic: `{"==": [1, 1]}`, data: `{}`, }, { name: "complex_condition", logic: `{"and": [{"<": [{"var": "temp"}, 110]}, {"==": [{"var": "pie.filling"}, "apple"]}]}`, data: `{"temp": 100, "pie": {"filling": "apple"}}`, }, { name: "nested_var", logic: `{"var": ["deeply.nested.variable", 99]}`, data: `{"deeply": {"nested": {"variable": 42}}}`, }, { name: "array_operations", logic: `{"map": [{"var": "integers"}, {"*": [{"var": ""}, 2]}]}`, data: `{"integers": [1, 2, 3, 4, 5]}`, }, { name: "complex_logic", logic: `{"if": [ {"<": [{"var": "age"}, 18]}, "Too young", {"and": [ {"<": [{"var": "age"}, 65]}, {">=": [{"var": "age"}, 18]} ]}, "Adult", "Senior" ]}`, data: `{"age": 25}`, }, { name: "reduce_sum", logic: `{"reduce": [{"var": "numbers"}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}`, data: `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`, }, { name: "filter_even_numbers", logic: `{"filter": [{"var": "numbers"}, {"==": [{"%": [{"var": ""}, 2]}, 0]}]}`, data: `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`, }, { name: "contains_all", logic: `{"contains_all": [{"var": "tags"}, ["urgent", "reviewed"]]}`, data: `{"tags": ["urgent", "reviewed", "approved", "processed"]}`, }, { name: "contains_any", logic: `{"contains_any": [{"var": "permissions"}, ["admin", "superuser"]]}`, data: `{"permissions": ["user", "editor", "admin"]}`, }, { name: "contains_none", logic: `{"contains_none": [{"var": "flags"}, ["banned", "suspended"]]}`, data: `{"flags": ["active", "verified", "premium"]}`, }, { name: "all_validation", logic: `{"all": [{"var": "users"}, {">": [{"var": ".age"}, 18]}]}`, data: `{"users": [{"age": 25}, {"age": 30}, {"age": 22}, {"age": 19}]}`, }, { name: "some_validation", logic: `{"some": [{"var": "items"}, {"<": [{"var": ".price"}, 100]}]}`, data: `{"items": [{"price": 150}, {"price": 75}, {"price": 200}]}`, }, { name: "string_concatenation", logic: `{"cat": [{"var": "firstName"}, " ", {"var": "lastName"}]}`, data: `{"firstName": "John", "lastName": "Doe"}`, }, { name: "substring_extraction", logic: `{"substr": [{"var": "text"}, 0, 10]}`, data: `{"text": "The quick brown fox jumps over the lazy dog"}`, }, { name: "max_operation", logic: `{"max": [85, 92, 78, 95, 88]}`, data: `{}`, }, { name: "min_operation", logic: `{"min": [19.99, 15.50, 22.00, 12.99]}`, data: `{}`, }, { name: "modulo_operation", logic: `{"%": [{"var": "value"}, 3]}`, data: `{"value": 17}`, }, { name: "or_operation", logic: `{"or": [{"<": [{"var": "age"}, 18]}, {">": [{"var": "age"}, 65]}]}`, data: `{"age": 70}`, }, { name: "merge_arrays", logic: `{"merge": [{"var": "array1"}, {"var": "array2"}]}`, data: `{"array1": [1, 2, 3], "array2": [4, 5, 6]}`, }, { name: "missing_fields", logic: `{"missing": ["name", "email", "phone"]}`, data: `{"name": "John", "email": "john@example.com"}`, }, { name: "deeply_nested_operations", logic: `{"and": [{"filter": [{"var": "users"}, {">": [{"var": ".age"}, 18]}]}, {"map": [{"var": "items"}, {"*": [{"var": ".price"}, 1.1]}]}]}`, data: `{"users": [{"age": 25}, {"age": 30}], "items": [{"price": 10}, {"price": 20}]}`, }, } func performWarmupRuns() { runtime.GC() for _, tc := range TestCases { for i := 0; i < 10; i++ { logic := strings.NewReader(tc.logic) data := strings.NewReader(tc.data) var result bytes.Buffer _ = jsonlogic.Apply(logic, data, &result) } } runtime.GC() } // Helper function to reduce duplication in benchmarks func runBenchmark(b *testing.B, logic, data string) { // Pre-convert to bytes to avoid string overhead in loop logicBytes := []byte(logic) dataBytes := []byte(data) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { logicReader := bytes.NewReader(logicBytes) dataReader := bytes.NewReader(dataBytes) var result bytes.Buffer err := jsonlogic.Apply(logicReader, dataReader, &result) if err != nil { b.Fatal(err) } } } // BenchmarkComprehensive runs a focused suite of complex, realistic benchmarks // that exercise multiple operators and represent real-world usage patterns. // This is the default benchmark suite for version comparisons. func BenchmarkComprehensive(b *testing.B) { cases := []struct { name string logic string data string }{ { name: "user_validation", logic: `{ "and": [ {">=": [{"var": "user.age"}, 18]}, {"in": [{"var": "user.country"}, ["US", "CA", "UK", "AU"]]}, {"or": [ {"==": [{"var": "user.subscription"}, "premium"]}, {"<": [{"+": [{"var": "user.loginCount"}, 1]}, 100]} ]} ] }`, data: `{"user": {"age": 25, "country": "US", "subscription": "premium", "loginCount": 50}}`, }, { name: "data_pipeline", logic: `{ "reduce": [ {"filter": [ {"var": "orders"}, {">=": [{"var": ".amount"}, 100]} ]}, {"+": [{"var": "accumulator"}, {"var": "current.amount"}]}, 0 ] }`, data: `{"orders": [{"amount": 50}, {"amount": 150}, {"amount": 200}, {"amount": 75}, {"amount": 120}]}`, }, { name: "business_rules", logic: `{ "if": [ {"and": [ {">": [{"var": "order.total"}, 1000]}, {"==": [{"var": "customer.tier"}, "gold"]} ]}, {"*": [{"var": "order.total"}, 0.8]}, {">": [{"var": "order.total"}, 500]}, {"*": [{"var": "order.total"}, 0.9]}, {"var": "order.total"} ] }`, data: `{"order": {"total": 1200}, "customer": {"tier": "gold"}}`, }, { name: "array_validation", logic: `{ "and": [ {"all": [{"var": "items"}, {">": [{"var": ".quantity"}, 0]}]}, {"some": [{"var": "items"}, {"<": [{"var": ".price"}, 50]}]}, {"none": [{"var": "items"}, {"==": [{"var": ".status"}, "cancelled"]}]} ] }`, data: `{"items": [{"quantity": 2, "price": 30, "status": "active"}, {"quantity": 1, "price": 75, "status": "active"}]}`, }, { name: "string_processing", logic: `{ "and": [ {"in": ["error", {"var": "message"}]}, {">": [{"var": "severity"}, 5]}, {"==": [{"substr": [{"var": "code"}, 0, 3]}, "ERR"]} ] }`, data: `{"message": "System error detected", "severity": 8, "code": "ERR-500"}`, }, { name: "custom_operators", logic: `{ "and": [ {"contains_all": [{"var": "required_permissions"}, ["read", "write"]]}, {"contains_any": [{"var": "user_roles"}, ["admin", "moderator"]]}, {"contains_none": [{"var": "flags"}, ["banned", "suspended"]]} ] }`, data: `{"required_permissions": ["read", "write", "execute"], "user_roles": ["admin", "user"], "flags": ["active", "verified"]}`, }, { name: "complex_data_transform", logic: `{ "map": [ {"filter": [ {"var": "products"}, {"and": [ {"in": [{"var": ".category"}, ["electronics", "accessories"]]}, {">": [{"var": ".stock"}, 0]} ]} ]}, {"*": [{"var": ".price"}, 1.1]} ] }`, data: `{"products": [{"category": "electronics", "price": 100, "stock": 5}, {"category": "clothing", "price": 50, "stock": 10}, {"category": "accessories", "price": 25, "stock": 0}]}`, }, { name: "nested_conditions", logic: `{ "if": [ {"and": [ {"missing": ["name", "email"]}, {">": [{"var": "age"}, 0]} ]}, {"cat": ["Missing required fields for user ", {"var": "id"}]}, {"or": [ {"<": [{"var": "age"}, 13]}, {"missing_some": [1, ["parent_email", "guardian_name"]]} ]}, "Parental consent required", "Valid user" ] }`, data: `{"name": "John", "email": "john@example.com", "age": 25, "id": "12345"}`, }, } for _, tc := range cases { b.Run(tc.name, func(b *testing.B) { runBenchmark(b, tc.logic, tc.data) }) } } // BenchmarkDetailed runs all detailed benchmarks (65 total). // Use this for comprehensive testing of individual operators. // For version comparisons, use BenchmarkComprehensive instead. func BenchmarkDetailed(b *testing.B) { performWarmupRuns() for _, tc := range TestCases { b.Run(tc.name, func(b *testing.B) { runBenchmark(b, tc.logic, tc.data) }) } } // Parallel benchmarks for testing concurrent performance func BenchmarkJSONLogicParallel(b *testing.B) { parallelCases := []struct { name string logic string data string }{ { name: "simple_equal", logic: `{"==": [1, 1]}`, data: `{}`, }, { name: "map", logic: `{"map": [{"var": "integers"}, {"*": [{"var": ""}, 2]}]}`, data: `{"integers": [1, 2, 3, 4, 5]}`, }, { name: "reduce", logic: `{"reduce": [{"var": "numbers"}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}`, data: `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`, }, { name: "filter", logic: `{"filter": [{"var": "numbers"}, {"==": [{"%": [{"var": ""}, 2]}, 0]}]}`, data: `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`, }, } for _, tc := range parallelCases { b.Run(tc.name, func(b *testing.B) { logicBytes := []byte(tc.logic) dataBytes := []byte(tc.data) b.ResetTimer() b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logic := bytes.NewReader(logicBytes) data := bytes.NewReader(dataBytes) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { b.Fatal(err) } } }) }) } } // Size variation benchmarks to test scalability func BenchmarkArrayOperationsScaling(b *testing.B) { sizes := []struct { name string size int }{ {"small_10", 10}, {"medium_100", 100}, {"large_1000", 1000}, } generateIntArray := func(size int) string { result := "[" for i := 0; i < size; i++ { if i > 0 { result += "," } result += fmt.Sprintf("%d", i+1) } result += "]" return result } // Map operation scaling b.Run("map", func(b *testing.B) { for _, s := range sizes { b.Run(s.name, func(b *testing.B) { logic := `{"map": [{"var": "integers"}, {"*": [{"var": ""}, 2]}]}` data := fmt.Sprintf(`{"integers": %s}`, generateIntArray(s.size)) runBenchmark(b, logic, data) }) } }) // Filter operation scaling b.Run("filter", func(b *testing.B) { for _, s := range sizes { b.Run(s.name, func(b *testing.B) { logic := `{"filter": [{"var": "numbers"}, {"==": [{"%": [{"var": ""}, 2]}, 0]}]}` data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size)) runBenchmark(b, logic, data) }) } }) // Reduce operation scaling b.Run("reduce", func(b *testing.B) { for _, s := range sizes { b.Run(s.name, func(b *testing.B) { logic := `{"reduce": [{"var": "numbers"}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}` data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size)) runBenchmark(b, logic, data) }) } }) // All operation scaling b.Run("all", func(b *testing.B) { for _, s := range sizes { b.Run(s.name, func(b *testing.B) { logic := `{"all": [{"var": "numbers"}, {">": [{"var": ""}, 0]}]}` data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size)) runBenchmark(b, logic, data) }) } }) // Some operation scaling b.Run("some", func(b *testing.B) { for _, s := range sizes { b.Run(s.name, func(b *testing.B) { logic := `{"some": [{"var": "numbers"}, {">": [{"var": ""}, 500]}]}` data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size)) runBenchmark(b, logic, data) }) } }) // None operation scaling b.Run("none", func(b *testing.B) { for _, s := range sizes { b.Run(s.name, func(b *testing.B) { logic := `{"none": [{"var": "numbers"}, {"<": [{"var": ""}, 0]}]}` data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size)) runBenchmark(b, logic, data) }) } }) } // Categorical benchmarks for easier filtering func BenchmarkMathOperations(b *testing.B) { cases := []struct { name string logic string data string }{ {"add", `{"+": [5, 3]}`, `{}`}, {"subtract", `{"-": [10, 3]}`, `{}`}, {"multiply", `{"*": [4, 5]}`, `{}`}, {"divide", `{"/": [20, 4]}`, `{}`}, {"modulo", `{"%": [17, 3]}`, `{}`}, {"max", `{"max": [85, 92, 78, 95, 88]}`, `{}`}, {"min", `{"min": [19.99, 15.50, 22.00, 12.99]}`, `{}`}, {"abs", `{"abs": [-42]}`, `{}`}, } for _, tc := range cases { b.Run(tc.name, func(b *testing.B) { runBenchmark(b, tc.logic, tc.data) }) } } func BenchmarkStringOperations(b *testing.B) { cases := []struct { name string logic string data string }{ { "concat", `{"cat": [{"var": "firstName"}, " ", {"var": "lastName"}]}`, `{"firstName": "John", "lastName": "Doe"}`, }, { "substr", `{"substr": [{"var": "text"}, 0, 10]}`, `{"text": "The quick brown fox jumps over the lazy dog"}`, }, { "in_string", `{"in": ["quick", {"var": "text"}]}`, `{"text": "The quick brown fox"}`, }, } for _, tc := range cases { b.Run(tc.name, func(b *testing.B) { runBenchmark(b, tc.logic, tc.data) }) } } func BenchmarkLogicOperations(b *testing.B) { cases := []struct { name string logic string data string }{ { "and", `{"and": [{"<": [{"var": "temp"}, 110]}, {"==": [{"var": "status"}, "ok"]}]}`, `{"temp": 100, "status": "ok"}`, }, { "or", `{"or": [{"<": [{"var": "age"}, 18]}, {">": [{"var": "age"}, 65]}]}`, `{"age": 70}`, }, { "not", `{"!": [false]}`, `{}`, }, { "if", `{"if": [{"<": [{"var": "age"}, 18]}, "minor", "adult"]}`, `{"age": 25}`, }, } for _, tc := range cases { b.Run(tc.name, func(b *testing.B) { runBenchmark(b, tc.logic, tc.data) }) } } func BenchmarkCustomOperators(b *testing.B) { cases := []struct { name string logic string data string }{ { "contains_all", `{"contains_all": [{"var": "tags"}, ["urgent", "reviewed"]]}`, `{"tags": ["urgent", "reviewed", "approved", "processed"]}`, }, { "contains_any", `{"contains_any": [{"var": "permissions"}, ["admin", "superuser"]]}`, `{"permissions": ["user", "editor", "admin"]}`, }, { "contains_none", `{"contains_none": [{"var": "flags"}, ["banned", "suspended"]]}`, `{"flags": ["active", "verified", "premium"]}`, }, } for _, tc := range cases { b.Run(tc.name, func(b *testing.B) { runBenchmark(b, tc.logic, tc.data) }) } } ================================================ FILE: codecov.yml ================================================ ignore: - internal/testing.go ================================================ FILE: comp.go ================================================ package jsonlogic import ( "reflect" "github.com/diegoholiveira/jsonlogic/v3/internal/javascript" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) func hardEquals(values, data any) any { values = parseValues(values, data) if !typing.IsSlice(values) { return false } parsed := values.([]any) if len(parsed) < 2 { return false } a, b := parsed[0], parsed[1] if a == nil || b == nil { return a == b } ra := reflect.ValueOf(a).Kind() rb := reflect.ValueOf(b).Kind() if ra != rb { return false } return equals(a, b) } func isLessThan(values, data any) any { parsed := parseValues(values, data).([]any) if len(parsed) < 2 { return false } a := parsed[0] b := parsed[1] if len(parsed) == 3 { c := parsed[2] return less(a, b) && less(b, c) } return less(a, b) } func isLessOrEqualThan(values, data any) any { parsed := parseValues(values, data).([]any) if len(parsed) < 2 { return false } a := parsed[0] b := parsed[1] if len(parsed) == 3 { c := parsed[2] return (less(a, b) || equals(a, b)) && (less(b, c) || equals(b, c)) } return less(a, b) || equals(a, b) } func isGreaterThan(values, data any) any { parsed := parseValues(values, data).([]any) if len(parsed) < 2 { return false } a := parsed[0] b := parsed[1] if len(parsed) == 3 { c := parsed[2] return less(c, b) && less(b, a) } return less(b, a) } func isGreaterOrEqualThan(values, data any) any { parsed := parseValues(values, data).([]any) if len(parsed) < 2 { return false } a := parsed[0] b := parsed[1] if len(parsed) == 3 { c := parsed[2] return (less(c, b) || equals(c, b)) && (less(b, a) || equals(b, a)) } return less(b, a) || equals(b, a) } func isEqual(values, data any) any { parsed := parseValues(values, data).([]any) if len(parsed) < 2 { return false } a := parsed[0] b := parsed[1] return equals(a, b) } // less reference javascript implementation // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than#description func less(a, b any) bool { // If both values are strings, they are compared as strings, // based on the values of the Unicode code points they contain. if typing.IsString(a) && typing.IsString(b) { return typing.ToString(b) > typing.ToString(a) } // Otherwise the values are compared as numeric values. return javascript.ToNumber(b) > javascript.ToNumber(a) } func equals(a, b any) bool { // comparison to a nil value is falsy if a == nil || b == nil { // if a and b is nil, return true, else return falsy return a == b } if typing.IsString(a) && typing.IsString(b) { return a == b } return javascript.ToNumber(a) == javascript.ToNumber(b) } ================================================ FILE: comp_test.go ================================================ package jsonlogic_test import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) func TestHardEqualsWithNonSliceValues(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "===": 42 }`) var expected json.RawMessage = json.RawMessage("false") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestHardEqualsWithSingleValueInSlice(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "===": [42] }`) var expected json.RawMessage = json.RawMessage("false") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestHardEqualsWithNilInParams(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "===": [null, 42] }`) var expected json.RawMessage = json.RawMessage("false") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) rule = json.RawMessage(`{ "===": [null, null] }`) expected = json.RawMessage("true") output, err = jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestLessThanWithSingleArgument(t *testing.T) { rule := json.RawMessage(`{"<": [1]}`) output, err := jsonlogic.ApplyRaw(rule, nil) assert.NoError(t, err) assert.JSONEq(t, `false`, string(output)) } func TestLessOrEqualThanWithSingleArgument(t *testing.T) { rule := json.RawMessage(`{"<=": [1]}`) output, err := jsonlogic.ApplyRaw(rule, nil) assert.NoError(t, err) assert.JSONEq(t, `false`, string(output)) } func TestGreaterThanWithSingleArgument(t *testing.T) { rule := json.RawMessage(`{">": [1]}`) output, err := jsonlogic.ApplyRaw(rule, nil) assert.NoError(t, err) assert.JSONEq(t, `false`, string(output)) } func TestGreaterOrEqualThanWithSingleArgument(t *testing.T) { rule := json.RawMessage(`{">=": [1]}`) output, err := jsonlogic.ApplyRaw(rule, nil) assert.NoError(t, err) assert.JSONEq(t, `false`, string(output)) } func TestEqualWithSingleArgument(t *testing.T) { rule := json.RawMessage(`{"==": [1]}`) output, err := jsonlogic.ApplyRaw(rule, nil) assert.NoError(t, err) assert.JSONEq(t, `false`, string(output)) } func TestHardEqualsWithDifferentTypes(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "===": ["42", 42] }`) var expected json.RawMessage = json.RawMessage("false") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) rule = json.RawMessage(`{ "===": ["42", "43"] }`) expected = json.RawMessage("false") output, err = jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) rule = json.RawMessage(`{ "===": ["42", "42"] }`) expected = json.RawMessage("true") output, err = jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } ================================================ FILE: go.mod ================================================ module github.com/diegoholiveira/jsonlogic/v3 go 1.18 require github.com/stretchr/testify v1.10.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/javascript/javascript.go ================================================ // Package javascript provides utilities for working with JavaScript code and runtime integration. package javascript import ( "math" "reflect" "strconv" "strings" ) type UndefinedType struct{} // At returns the element at the specified index in the slice. // If index is negative, it counts from the end of the slice. // If index is out of bounds, it returns nil. // // Example: // // At([]any{1,2,3}, 1) // Returns: 2 // At([]any{1,2,3}, -1) // Returns: 3 func At(values []any, index int) any { if index >= 0 && index < len(values) { return values[index] } return UndefinedType{} } // ToNumber converts various input types to float64. // // Examples: // // ToNumber(42) // Returns: 42.0 // ToNumber("3.14") // Returns: 3.14 // ToNumber(true) // Returns: 1.0 // ToNumber(false) // Returns: 0.0 // ToNumber([]int{1, 2, 3}) // Returns: 3.0 (length of slice) // ToNumber(map[string]int{"a": 1, "b": 2}) // Returns: 2.0 (length of map) // ToNumber(nil) // Returns: 0.0 // // Note: For unsupported types, it returns 0.0 func ToNumber(v any) float64 { switch value := v.(type) { case nil: return 0 case UndefinedType: return math.NaN() case float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return reflect.ValueOf(value).Convert(reflect.TypeOf(float64(0))).Float() case bool: // Boolean values true and false are converted to 1 and 0 respectively. if value { return 1 } else { return 0 } case string: if strings.TrimSpace(value) == "" { return 0 } n, err := strconv.ParseFloat(value, 64) switch err { case strconv.ErrRange, nil: return n default: return math.NaN() } default: return math.NaN() } } ================================================ FILE: internal/javascript/javascript_test.go ================================================ package javascript import ( "math" "testing" "github.com/stretchr/testify/assert" ) func TestAt(t *testing.T) { tests := []struct { name string values []any index int expected any }{ { name: "valid index", values: []any{1, "test", true}, index: 1, expected: "test", }, { name: "index out of bounds (positive)", values: []any{1, 2, 3}, index: 5, expected: UndefinedType{}, }, { name: "index out of bounds (negative)", values: []any{1, 2, 3}, index: -1, expected: UndefinedType{}, }, { name: "empty array", values: []any{}, index: 0, expected: UndefinedType{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := At(tt.values, tt.index) assert.Equal(t, tt.expected, result) }) } } func TestToNumber(t *testing.T) { tests := []struct { name string input any expected float64 isNaN bool }{ { name: "nil input", input: nil, expected: 0, }, { name: "undefined input", input: UndefinedType{}, isNaN: true, }, { name: "int input", input: 42, expected: 42, }, { name: "float64 input", input: 3.14, expected: 3.14, }, { name: "true boolean input", input: true, expected: 1, }, { name: "false boolean input", input: false, expected: 0, }, { name: "valid numeric string", input: "123.45", expected: 123.45, }, { name: "empty string", input: "", expected: 0, }, { name: "whitespace string", input: " ", expected: 0, }, { name: "invalid numeric string", input: "not a number", isNaN: true, }, { name: "complex type (map)", input: map[string]int{"a": 1, "b": 2}, isNaN: true, }, { name: "complex type (struct)", input: struct{ Name string }{"test"}, isNaN: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ToNumber(tt.input) if tt.isNaN { assert.True(t, math.IsNaN(result), "Expected NaN result for %v", tt.input) } else { assert.Equal(t, tt.expected, result) } }) } } ================================================ FILE: internal/json_logic_pr_48_tests.json ================================================ [ "# Non-rules get passed through", [ true, {}, true ], [ false, {}, false ], [ 17, {}, 17 ], [ 3.14, {}, 3.14 ], [ "apple", {}, "apple" ], [ null, {}, null ], [ ["a","b"], {}, ["a","b"] ], "# Single operator tests", [ {"==":[false,false]}, {}, true ], [ {"==":[0,false]}, {}, true ], [ {"==":[false,0]}, {}, true ], [ {"==":[false,"0"]}, {}, true ], [ {"==":[1,true]}, {}, true ], [ {"==":["1",true]}, {}, true ], [ {"==":["1.000",true]}, {}, true ], [ {"==":["0",0]}, {}, true ], [ {"==":["0.0000",0]}, {}, true ], [ {"==":["0.0000",false]}, {}, true ], [ {"==":["0.0000","0"]}, {}, false ], [ {"==":["",0]}, {}, true ], [ {"==":[" ",0]}, {}, true ], [ {"==":[" ",0]}, {}, true ], [ {"==":[" ",false]}, {}, true ], [ {"==":[0,""]}, {}, true ], [ {"==":[1,1]}, {}, true ], [ {"==":[1,"1"]}, {}, true ], [ {"==":["1",1]}, {}, true ], [ {"==":["42.0",42]}, {}, true ], [ {"==":[42.0000,"42"]}, {}, true ], [ {"==":["42.0000",42]}, {}, true ], [ {"==":[1,2]}, {}, false ], [ {"==":[true,"true"]}, {}, false ], [ {"==":["true",true]}, {}, false ], [ {"==":["a ","a"]}, {}, false ], [ {"===":[1,1]}, {}, true ], [ {"===":[1,"1"]}, {}, false ], [ {"===":[1,2]}, {}, false ], [ {"!=":[1,2]}, {}, true ], [ {"!=":[1,1]}, {}, false ], [ {"!=":[1,"1"]}, {}, false ], [ {"!=":["1",1]}, {}, false ], [ {"!==":[1,2]}, {}, true ], [ {"!==":[1,1]}, {}, false ], [ {"!==":[1,"1"]}, {}, true ], [ {">":[2,1]}, {}, true ], [ {">":[1,1]}, {}, false ], [ {">":[1,2]}, {}, false ], [ {">":["2",1]}, {}, true ], [ {">=":[2,1]}, {}, true ], [ {">=":[1,1]}, {}, true ], [ {">=":[1,2]}, {}, false ], [ {">=":["2",1]}, {}, true ], [ {"<":["",1]}, {}, true ], [ {"<":["",-1]}, {}, false ], [ {"<":[""," "]}, {}, true ], [ {"<":[2,1]}, {}, false ], [ {"<":[1,1]}, {}, false ], [ {"<":[1,2]}, {}, true ], [ {"<":["1",2]}, {}, true ], [ {"<":[1,2,3]}, {}, true ], [ {"<":[1,1,3]}, {}, false ], [ {"<":[1,4,3]}, {}, false ], [ {"<=":[2,1]}, {}, false ], [ {"<=":[1,1]}, {}, true ], [ {"<=":[1,2]}, {}, true ], [ {"<=":["1",2]}, {}, true ], [ {"<=":[1,2,3]}, {}, true ], [ {"<=":[1,4,3]}, {}, false ], [ {"!":[false]}, {}, true ], [ {"!":false}, {}, true ], [ {"!":[true]}, {}, false ], [ {"!":true}, {}, false ], [ {"!":0}, {}, true ], [ {"!":1}, {}, false ], [ {"or":[true,true]}, {}, true ], [ {"or":[false,true]}, {}, true ], [ {"or":[true,false]}, {}, true ], [ {"or":[false,false]}, {}, false ], [ {"or":[false,false,true]}, {}, true ], [ {"or":[false,false,false]}, {}, false ], [ {"or":[false]}, {}, false ], [ {"or":[true]}, {}, true ], [ {"or":[1,3]}, {}, 1 ], [ {"or":[3,false]}, {}, 3 ], [ {"or":[false,3]}, {}, 3 ], [ {"and":[true,true]}, {}, true ], [ {"and":[false,true]}, {}, false ], [ {"and":[true,false]}, {}, false ], [ {"and":[false,false]}, {}, false ], [ {"and":[true,true,true]}, {}, true ], [ {"and":[true,true,false]}, {}, false ], [ {"and":[false]}, {}, false ], [ {"and":[true]}, {}, true ], [ {"and":[1,3]}, {}, 3 ], [ {"and":[3,false]}, {}, false ], [ {"and":[false,3]}, {}, false ], [ {"?:":[true,1,2]}, {}, 1 ], [ {"?:":[false,1,2]}, {}, 2 ], [ {"in":["Bart",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, true ], [ {"in":["Milhouse",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, false ], [ {"in":["Spring","Springfield"]}, {}, true ], [ {"in":["i","team"]}, {}, false ], [ {"cat":"ice"}, {}, "ice" ], [ {"cat":["ice"]}, {}, "ice" ], [ {"cat":["ice","cream"]}, {}, "icecream" ], [ {"cat":[1,2]}, {}, "12" ], [ {"cat":["Robocop",2]}, {}, "Robocop2" ], [ {"cat":["we all scream for ","ice","cream"]}, {}, "we all scream for icecream" ], [ {"%":[1,2]}, {}, 1 ], [ {"%":[2,2]}, {}, 0 ], [ {"%":[3,2]}, {}, 1 ], [ {"max":[1,2,3]}, {}, 3 ], [ {"max":[1,3,3]}, {}, 3 ], [ {"max":[3,2,1]}, {}, 3 ], [ {"max":[1]}, {}, 1 ], [ {"min":[1,2,3]}, {}, 1 ], [ {"min":[1,1,3]}, {}, 1 ], [ {"min":[3,2,1]}, {}, 1 ], [ {"min":[1]}, {}, 1 ], [ {"+":[1,2]}, {}, 3 ], [ {"+":[2,2,2]}, {}, 6 ], [ {"+":[1]}, {}, 1 ], [ {"+":["1",1]}, {}, 2 ], [ {"*":[3,2]}, {}, 6 ], [ {"*":[2,2,2]}, {}, 8 ], [ {"*":[1]}, {}, 1 ], [ {"*":["1",1]}, {}, 1 ], [ {"-":[2,3]}, {}, -1 ], [ {"-":[3,2]}, {}, 1 ], [ {"-":[3]}, {}, -3 ], [ {"-":["1",1]}, {}, 0 ], [ {"/":[4,2]}, {}, 2 ], [ {"/":[2,4]}, {}, 0.5 ], [ {"/":["1",1]}, {}, 1 ], "Substring", [{"substr":["jsonlogic", 4]}, null, "logic"], [{"substr":["jsonlogic", -5]}, null, "logic"], [{"substr":["jsonlogic", 0, 1]}, null, "j"], [{"substr":["jsonlogic", -1, 1]}, null, "c"], [{"substr":["jsonlogic", 4, 5]}, null, "logic"], [{"substr":["jsonlogic", -5, 5]}, null, "logic"], [{"substr":["jsonlogic", -5, -2]}, null, "log"], [{"substr":["jsonlogic", 1, -5]}, null, "son"], "Merge arrays", [{"merge":[]}, null, []], [{"merge":[[1]]}, null, [1]], [{"merge":[[1],[]]}, null, [1]], [{"merge":[[1], [2]]}, null, [1,2]], [{"merge":[[1], [2], [3]]}, null, [1,2,3]], [{"merge":[[1, 2], [3]]}, null, [1,2,3]], [{"merge":[[1], [2, 3]]}, null, [1,2,3]], "Given non-array arguments, merge converts them to arrays", [{"merge":1}, null, [1]], [{"merge":[1,2]}, null, [1,2]], [{"merge":[1,[2]]}, null, [1,2]], "Too few args", [{"if":[]}, null, null], [{"if":[true]}, null, true], [{"if":[false]}, null, false], [{"if":["apple"]}, null, "apple"], "Simple if/then/else cases", [{"if":[true, "apple"]}, null, "apple"], [{"if":[false, "apple"]}, null, null], [{"if":[true, "apple", "banana"]}, null, "apple"], [{"if":[false, "apple", "banana"]}, null, "banana"], "Empty arrays are falsey", [{"if":[ [], "apple", "banana"]}, null, "banana"], [{"if":[ [1], "apple", "banana"]}, null, "apple"], [{"if":[ [1,2,3,4], "apple", "banana"]}, null, "apple"], "Empty strings are falsey, all other strings are truthy", [{"if":[ "", "apple", "banana"]}, null, "banana"], [{"if":[ "zucchini", "apple", "banana"]}, null, "apple"], [{"if":[ "0", "apple", "banana"]}, null, "apple"], "You can cast a string to numeric with a unary + ", [{"===":[0,"0"]}, null, false], [{"===":[0,{"+":"0"}]}, null, true], [{"if":[ {"+":"0"}, "apple", "banana"]}, null, "banana"], [{"if":[ {"+":"1"}, "apple", "banana"]}, null, "apple"], "Zero is falsy, all other numbers are truthy", [{"if":[ 0, "apple", "banana"]}, null, "banana"], [{"if":[ 1, "apple", "banana"]}, null, "apple"], [{"if":[ 3.1416, "apple", "banana"]}, null, "apple"], [{"if":[ -1, "apple", "banana"]}, null, "apple"], "Truthy and falsy definitions matter in Boolean operations", [{"and" : [ { "!": [ ] }, { "!": [ [] ] }, { "!": { "missing": "foo" } } ]}, {"foo": "bar"}, true], [{"!" : []}, {}, true], [{"!" : [ [] ]}, {}, true], [{"!!" : []}, {}, false], [{"!!" : [ [] ]}, {}, false], [{"and" : [ [], true ]}, {}, [] ], [{"or" : [ [], true ]}, {}, true ], [{"!" : [ 0 ]}, {}, true], [{"!!" : [ 0 ]}, {}, false], [{"and" : [ 0, true ]}, {}, 0 ], [{"or" : [ 0, true ]}, {}, true ], [{"!" : [ "" ]}, {}, true], [{"!!" : [ "" ]}, {}, false], [{"and" : [ "", true ]}, {}, "" ], [{"or" : [ "", true ]}, {}, true ], [{"!" : [ "0" ]}, {}, false], [{"!!" : [ "0" ]}, {}, true], [{"and" : [ "0", true ]}, {}, true ], [{"or" : [ "0", true ]}, {}, "0" ], "If the conditional is logic, it gets evaluated", [{"if":[ {">":[2,1]}, "apple", "banana"]}, null, "apple"], [{"if":[ {">":[1,2]}, "apple", "banana"]}, null, "banana"], "If the consequents are logic, they get evaluated", [{"if":[ true, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "apple"], [{"if":[ false, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "banana"], "If/then/elseif/then cases", [{"if":[true, "apple", true, "banana"]}, null, "apple"], [{"if":[true, "apple", false, "banana"]}, null, "apple"], [{"if":[false, "apple", true, "banana"]}, null, "banana"], [{"if":[false, "apple", false, "banana"]}, null, null], [{"if":[true, "apple", true, "banana", "carrot"]}, null, "apple"], [{"if":[true, "apple", false, "banana", "carrot"]}, null, "apple"], [{"if":[false, "apple", true, "banana", "carrot"]}, null, "banana"], [{"if":[false, "apple", false, "banana", "carrot"]}, null, "carrot"], [{"if":[false, "apple", false, "banana", false, "carrot"]}, null, null], [{"if":[false, "apple", false, "banana", false, "carrot", "date"]}, null, "date"], [{"if":[false, "apple", false, "banana", true, "carrot", "date"]}, null, "carrot"], [{"if":[false, "apple", true, "banana", false, "carrot", "date"]}, null, "banana"], [{"if":[false, "apple", true, "banana", true, "carrot", "date"]}, null, "banana"], [{"if":[true, "apple", false, "banana", false, "carrot", "date"]}, null, "apple"], [{"if":[true, "apple", false, "banana", true, "carrot", "date"]}, null, "apple"], [{"if":[true, "apple", true, "banana", false, "carrot", "date"]}, null, "apple"], [{"if":[true, "apple", true, "banana", true, "carrot", "date"]}, null, "apple"], "Arrays with logic", [[1, {"var": "x"}, 3], {"x": 2}, [1, 2, 3]], [{"if": [{"var": "x"}, [{"var": "y"}], 99]}, {"x": true, "y": 42}, [42]], "# Compound Tests", [ {"and":[{">":[3,1]},true]}, {}, true ], [ {"and":[{">":[3,1]},false]}, {}, false ], [ {"and":[{">":[3,1]},{"!":true}]}, {}, false ], [ {"and":[{">":[3,1]},{"<":[1,3]}]}, {}, true ], [ {"?:":[{">":[3,1]},"visible","hidden"]}, {}, "visible" ], "# Data-Driven", [ {"var":["a"]},{"a":1},1 ], [ {"var":["b"]},{"a":1},null ], [ {"var":["a"]},null,null ], [ {"var":"a"},{"a":1},1 ], [ {"var":"b"},{"a":1},null ], [ {"var":"a"},null,null ], [ {"var":["a", 1]},null,1 ], [ {"var":["b", 2]},{"a":1},2 ], [ {"var":"a.b"},{"a":{"b":"c"}},"c" ], [ {"var":"a.q"},{"a":{"b":"c"}},null ], [ {"var":["a.q", 9]},{"a":{"b":"c"}},9 ], [ {"var":1}, ["apple","banana"], "banana" ], [ {"var":"1"}, ["apple","banana"], "banana" ], [ {"var":"1.1"}, ["apple",["banana","beer"]], "beer" ], [ {"and":[{"<":[{"var":"temp"},110]},{"==":[{"var":"pie.filling"},"apple"]}]},{"temp":100,"pie":{"filling":"apple"}},true ], [ {"var":[{"?:":[{"<":[{"var":"temp"},110]},"pie.filling","pie.eta"]}]},{"temp":100,"pie":{"filling":"apple","eta":"60s"}},"apple" ], [ {"in":[{"var":"filling"},["apple","cherry"]]},{"filling":"apple"},true ], [ {"var":"a.b.c"}, null, null ], [ {"var":"a.b.c"}, {"a":null}, null ], [ {"var":"a.b.c"}, {"a":{"b":null}}, null ], [ {"var":""}, 1, 1 ], [ {"var":null}, 1, 1 ], [ {"var":[]}, 1, 1 ], "Missing", [{"missing":[]}, null, []], [{"missing":["a"]}, null, ["a"]], [{"missing":"a"}, null, ["a"]], [{"missing":"a"}, {"a":"apple"}, []], [{"missing":["a"]}, {"a":"apple"}, []], [{"missing":["a","b"]}, {"a":"apple"}, ["b"]], [{"missing":["a","b"]}, {"b":"banana"}, ["a"]], [{"missing":["a","b"]}, {"a":"apple", "b":"banana"}, []], [{"missing":["a","b"]}, {}, ["a","b"]], [{"missing":["a","b"]}, null, ["a","b"]], [{"missing":["a.b"]}, null, ["a.b"]], [{"missing":["a.b"]}, {"a":"apple"}, ["a.b"]], [{"missing":["a.b"]}, {"a":{"c":"apple cake"}}, ["a.b"]], [{"missing":["a.b"]}, {"a":{"b":"apple brownie"}}, []], [{"missing":["a.b", "a.c"]}, {"a":{"b":"apple brownie"}}, ["a.c"]], "Missing some", [{"missing_some":[1, ["a", "b"]]}, {"a":"apple"}, [] ], [{"missing_some":[1, ["a", "b"]]}, {"b":"banana"}, [] ], [{"missing_some":[1, ["a", "b"]]}, {"a":"apple", "b":"banana"}, [] ], [{"missing_some":[1, ["a", "b"]]}, {"c":"carrot"}, ["a", "b"]], [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana"}, [] ], [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "c":"carrot"}, [] ], [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana", "c":"carrot"}, [] ], [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "d":"durian"}, ["b", "c"] ], [{"missing_some":[2, ["a", "b", "c"]]}, {"d":"durian", "e":"eggplant"}, ["a", "b", "c"] ], "Missing and If are friends, because empty arrays are falsey in JsonLogic", [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"a":"apple"}, "found it"], [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"b":"banana"}, "missed it"], "Missing, Merge, and If are friends. VIN is always required, APR is only required if financing is true.", [ {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} }, {"financing":true}, ["vin","apr"] ], [ {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} }, {"financing":false}, ["vin"] ], "Filter, map, all, none, and some", [ {"filter":[{"var":"integers"}, true]}, {"integers":[1,2,3]}, [1,2,3] ], [ {"filter":[{"var":"integers"}, false]}, {"integers":[1,2,3]}, [] ], [ {"filter":[{"var":"integers"}, {">=":[{"var":""},2]}]}, {"integers":[1,2,3]}, [2,3] ], [ {"filter":[{"var":"integers"}, {"%":[{"var":""},2]}]}, {"integers":[1,2,3]}, [1,3] ], [ {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]}, {"integers":[1,2,3]}, [2,4,6] ], [ {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]}, null, [] ], [ {"map":[{"var":"desserts"}, {"var":"qty"}]}, {"desserts":[ {"name":"apple","qty":1}, {"name":"brownie","qty":2}, {"name":"cupcake","qty":3} ]}, [1,2,3] ], [ {"reduce":[ {"var":"integers"}, {"+":[{"var":"current"}, {"var":"accumulator"}]}, 0 ]}, {"integers":[1,2,3,4]}, 10 ], [ {"reduce":[ {"var":"integers"}, {"+":[{"var":"current"}, {"var":"accumulator"}]}, {"var": "start_with"} ]}, {"integers":[1,2,3,4], "start_with": 59}, 69 ], [ {"reduce":[ {"var":"integers"}, {"+":[{"var":"current"}, {"var":"accumulator"}]}, 0 ]}, null, 0 ], [ {"reduce":[ {"var":"integers"}, {"*":[{"var":"current"}, {"var":"accumulator"}]}, 1 ]}, {"integers":[1,2,3,4]}, 24 ], [ {"reduce":[ {"var":"integers"}, {"*":[{"var":"current"}, {"var":"accumulator"}]}, 0 ]}, {"integers":[1,2,3,4]}, 0 ], [ {"reduce": [ {"var":"desserts"}, {"+":[ {"var":"accumulator"}, {"var":"current.qty"}]}, 0 ]}, {"desserts":[ {"name":"apple","qty":1}, {"name":"brownie","qty":2}, {"name":"cupcake","qty":3} ]}, 6 ], [ {"all":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, true ], [ {"all":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, false ], [ {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, false ], [ {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, {"integers":[]}, false ], [ {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, true ], [ {"all":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, false ], [ {"all":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, false ], [ {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, {"items":[]}, false ], [ {"none":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, false ], [ {"none":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, false ], [ {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, true ], [ {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, {"integers":[]}, true ], [ {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, false ], [ {"none":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, false ], [ {"none":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, true ], [ {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, {"items":[]}, true ], [ {"some":[{"var":"integers"}, {">=":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, true ], [ {"some":[{"var":"integers"}, {"==":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, true ], [ {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, {"integers":[1,2,3]}, false ], [ {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]}, {"integers":[]}, false ], [ {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, true ], [ {"some":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, true ], [ {"some":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]}, {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]}, false ], [ {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]}, {"items":[]}, false ], "EOF" ] ================================================ FILE: internal/testing.go ================================================ package internal import ( "encoding/json" "io" "log" "net/http" "os" "reflect" ) type ( Test struct { Rule any Data any Expected any Scenario string Index int } Tests []Test ) // GetScenariosFromProposedOfficialTestSuite reads the tests.json file that we've proposed become the new official one in // https://github.com/jwadhams/json-logic/pull/48 but that hasn't merged yet. func GetScenariosFromProposedOfficialTestSuite() Tests { buffer, err := os.ReadFile("internal/json_logic_pr_48_tests.json") if err != nil { log.Fatal(err) } return getScenariosFromFile(buffer) } // GetScenariosFromOfficialTestSuite fetches test scenarios from the official JSON Logic test suite. // It makes an HTTP request to jsonlogic.com to retrieve the latest test cases. func GetScenariosFromOfficialTestSuite() Tests { req, err := http.NewRequest("GET", "http://jsonlogic.com/tests.json", nil) if err != nil { log.Fatal(err) } response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close() buffer, err := io.ReadAll(response.Body) if err != nil { log.Fatal(err) } return getScenariosFromFile(buffer) } func getScenariosFromFile(buffer []byte) Tests { var ( tests Tests scenarios []any err = json.Unmarshal(buffer, &scenarios) ) if err != nil { log.Fatal(err) } // add missing but relevant scenarios var rule []any scenarios = append(scenarios, append(rule, make(map[string]any), make(map[string]any), make(map[string]any))) scenarioName := "" testIndex := 0 for _, scenario := range scenarios { if reflect.ValueOf(scenario).Kind() == reflect.String { scenarioName = scenario.(string) testIndex = 0 continue } tests = append(tests, Test{ Rule: scenario.([]any)[0], Data: scenario.([]any)[1], Expected: scenario.([]any)[2], Scenario: scenarioName, Index: testIndex, }) testIndex++ } return tests } ================================================ FILE: internal/typing/typing.go ================================================ // Package typing provides type checking and conversion utilities for JSON data types. package typing import ( "reflect" "strconv" ) func is(obj any, kind reflect.Kind) bool { return obj != nil && reflect.TypeOf(obj).Kind() == kind } // IsBool checks if the provided value is a boolean type. // Returns false if the value is nil. // // Example: // // IsBool(true) // Returns: true // IsBool(false) // Returns: true // IsBool("true") // Returns: false // IsBool(nil) // Returns: false func IsBool(obj any) bool { return is(obj, reflect.Bool) } // IsString checks if the provided value is a string type. // Returns false if the value is nil. // // Example: // // IsString("test") // Returns: true // IsString("") // Returns: true // IsString(42) // Returns: false // IsString(nil) // Returns: false func IsString(obj any) bool { return is(obj, reflect.String) } // IsNumber checks if the provided value is a numeric type (int or float64). // Returns false for any other type including nil. // // Example: // // IsNumber(42) // Returns: true // IsNumber(3.14) // Returns: true // IsNumber("42") // Returns: false // IsNumber(nil) // Returns: false func IsNumber(obj any) bool { switch obj.(type) { case int, float64: return true default: return false } } // IsPrimitive checks if the provided value is a primitive type (boolean, string, or number). // Returns false if the value is nil or any other type. // // Example: // // IsPrimitive(42) // Returns: true // IsPrimitive("test") // Returns: true // IsPrimitive(true) // Returns: true // IsPrimitive([]) // Returns: false // IsPrimitive(nil) // Returns: false func IsPrimitive(obj any) bool { return IsBool(obj) || IsString(obj) || IsNumber(obj) } // IsMap checks if the provided value is a map type. // Returns false if the value is nil. // // Example: // // IsMap(map[string]int{"a": 1}) // Returns: true // IsMap(map[string]any{}) // Returns: true // IsMap([]int{1, 2, 3}) // Returns: false // IsMap(nil) // Returns: false func IsMap(obj any) bool { return is(obj, reflect.Map) } // IsSlice checks if the provided value is a slice type. // Returns false if the value is nil. // // Example: // // IsSlice([]int{1, 2, 3}) // Returns: true // IsSlice([]any{}) // Returns: true // IsSlice("test") // Returns: false // IsSlice(nil) // Returns: false func IsSlice(obj any) bool { return is(obj, reflect.Slice) } // IsEmptySlice checks if the provided value is a slice and all its elements are falsy. // Returns false if the value is not a slice or if all elements in the slice are falsy. // A falsy value is: false, 0, "", empty array, or empty map. // // Example: // // IsEmptySlice([]any{}) // Returns: true // IsEmptySlice([]any{0, "", false}) // Returns: true // IsEmptySlice([]any{1, 2, 3}) // Returns: false // IsEmptySlice("test") // Returns: false func IsEmptySlice(obj any) bool { if !IsSlice(obj) { return false } for _, v := range obj.([]any) { if IsTrue(v) { return false } } return true } // IsTrue checks if the provided value is considered truthy in JavaScript logic. // For booleans: true is truthy // For numbers: non-zero is truthy // For strings: non-empty string is truthy // For slices/maps: non-empty slice/map is truthy // Returns false for nil or any other type. // // Example: // // IsTrue(true) // Returns: true // IsTrue(42) // Returns: true // IsTrue("test") // Returns: true // IsTrue([]any{1, 2, 3}) // Returns: true // IsTrue(false) // Returns: false // IsTrue(0) // Returns: false // IsTrue("") // Returns: false // IsTrue([]any{}) // Returns: false // IsTrue(nil) // Returns: false func IsTrue(obj any) bool { if IsBool(obj) { return obj.(bool) } if IsNumber(obj) { return ToNumber(obj) != 0 } if IsString(obj) || IsSlice(obj) || IsMap(obj) { return reflect.ValueOf(obj).Len() > 0 } return false } // ToNumber converts the provided value to a float64. // If the value is a string, it attempts to parse it as a float64. // If the value is an int, it converts it to float64. // If the value is already a float64, it returns it as is. // For all other types, it attempts a type assertion to float64. // // Example: // // ToNumber(42) // Returns: 42.0 // ToNumber(3.14) // Returns: 3.14 // ToNumber("42") // Returns: 42.0 // ToNumber("3.14") // Returns: 3.14 // ToNumber("invalid") // Returns: 0.0 func ToNumber(value any) float64 { if IsString(value) { w, _ := strconv.ParseFloat(value.(string), 64) return w } switch value := value.(type) { case int: return float64(value) default: return value.(float64) } } // ToString converts the provided value to a string. // For numbers: converts to string representation // For nil: returns an empty string // For other types: performs a direct type assertion to string // // Example: // // ToString(42) // Returns: "42" // ToString(3.14) // Returns: "3.14" // ToString("test") // Returns: "test" // ToString(nil) // Returns: "" func ToString(value any) string { if IsNumber(value) { switch value := value.(type) { case int: return strconv.FormatInt(int64(value), 10) default: return strconv.FormatFloat(value.(float64), 'f', -1, 64) } } if value == nil { return "" } return value.(string) } ================================================ FILE: internal/typing/typing_test.go ================================================ package typing import ( "testing" "github.com/stretchr/testify/assert" ) func TestIsBool(t *testing.T) { tests := []struct { name string input any expected bool }{ {"true value", true, true}, {"false value", false, true}, {"nil value", nil, false}, {"string value", "true", false}, {"int value", 1, false}, {"float value", 1.5, false}, {"slice value", []any{}, false}, {"map value", map[string]any{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsBool(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsString(t *testing.T) { tests := []struct { name string input any expected bool }{ {"empty string", "", true}, {"non-empty string", "hello", true}, {"nil value", nil, false}, {"boolean value", true, false}, {"int value", 1, false}, {"float value", 1.5, false}, {"slice value", []any{}, false}, {"map value", map[string]any{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsString(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsNumber(t *testing.T) { tests := []struct { name string input any expected bool }{ {"int zero", 0, true}, {"int positive", 42, true}, {"int negative", -10, true}, {"float zero", 0.0, true}, {"float positive", 3.14, true}, {"float negative", -2.5, true}, {"nil value", nil, false}, {"boolean value", true, false}, {"string value", "123", false}, {"slice value", []any{}, false}, {"map value", map[string]any{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsNumber(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsPrimitive(t *testing.T) { tests := []struct { name string input any expected bool }{ {"boolean", true, true}, {"string", "hello", true}, {"int", 42, true}, {"float", 3.14, true}, {"nil value", nil, false}, {"slice value", []any{}, false}, {"map value", map[string]any{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsPrimitive(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsMap(t *testing.T) { tests := []struct { name string input any expected bool }{ {"empty map", map[string]any{}, true}, {"non-empty map", map[string]any{"key": "value"}, true}, {"nil value", nil, false}, {"boolean value", true, false}, {"int value", 1, false}, {"float value", 1.5, false}, {"string value", "hello", false}, {"slice value", []any{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsMap(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsSlice(t *testing.T) { tests := []struct { name string input any expected bool }{ {"empty slice", []any{}, true}, {"non-empty slice", []any{1, 2, 3}, true}, {"nil value", nil, false}, {"boolean value", true, false}, {"int value", 1, false}, {"float value", 1.5, false}, {"string value", "hello", false}, {"map value", map[string]any{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsSlice(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsEmptySlice(t *testing.T) { tests := []struct { name string input any expected bool }{ {"empty slice", []any{}, true}, {"slice with zeros", []any{0, 0, 0}, true}, {"slice with empty strings", []any{"", ""}, true}, {"slice with false values", []any{false, false}, true}, {"slice with mixed falsy values", []any{0, "", false, []any{}}, true}, {"non-empty slice with truthy value", []any{0, 1, 0}, false}, {"non-empty slice with true", []any{false, true}, false}, {"nil value", nil, false}, {"boolean value", true, false}, {"int value", 1, false}, {"float value", 1.5, false}, {"string value", "hello", false}, {"map value", map[string]any{}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsEmptySlice(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsTrue(t *testing.T) { tests := []struct { name string input any expected bool }{ {"true boolean", true, true}, {"false boolean", false, false}, {"positive number", 42, true}, {"negative number", -10, true}, {"zero number", 0, false}, {"non-empty string", "hello", true}, {"empty string", "", false}, {"non-empty slice", []any{1, 2, 3}, true}, {"empty slice", []any{}, false}, {"non-empty map", map[string]any{"key": "value"}, true}, {"empty map", map[string]any{}, false}, {"nil value", nil, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsTrue(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestToNumber(t *testing.T) { tests := []struct { name string input any expected float64 }{ {"int zero", 0, 0.0}, {"int positive", 42, 42.0}, {"int negative", -10, -10.0}, {"float zero", 0.0, 0.0}, {"float positive", 3.14, 3.14}, {"float negative", -2.5, -2.5}, {"string number integer", "42", 42.0}, {"string number float", "3.14", 3.14}, {"string number negative", "-10", -10.0}, {"string empty", "", 0.0}, {"string non-number", "hello", 0.0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ToNumber(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestToString(t *testing.T) { tests := []struct { name string input any expected string }{ {"int zero", 0, "0"}, {"int positive", 42, "42"}, {"int negative", -10, "-10"}, {"float zero", 0.0, "0"}, {"float positive", 3.14, "3.14"}, {"float negative", -2.5, "-2.5"}, {"string", "hello", "hello"}, {"empty string", "", ""}, {"nil value", nil, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ToString(tt.input) assert.Equal(t, tt.expected, result) }) } } ================================================ FILE: issues_test.go ================================================ package jsonlogic_test import ( "bytes" "encoding/json" "strings" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) func TestIssue50(t *testing.T) { logic := strings.NewReader(`{"<": ["abc", 3]}`) data := strings.NewReader(`{}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `false` assert.JSONEq(t, expected, result.String()) } func TestIssue51_example1(t *testing.T) { logic := strings.NewReader(`{"==":[{"var":"test"},true]}`) data := strings.NewReader(`{}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `false` assert.JSONEq(t, expected, result.String()) } func TestIssue51_example2(t *testing.T) { logic := strings.NewReader(`{"==":[{"var":"test"},"true"]}`) data := strings.NewReader(`{"test": true}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `false` assert.JSONEq(t, expected, result.String()) } func TestIssue52_example1(t *testing.T) { data := strings.NewReader(`{}`) logic := strings.NewReader(`{"substr": ["jsonlogic", -10]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `"jsonlogic"` assert.JSONEq(t, expected, result.String()) } func TestIssue52_example2(t *testing.T) { data := strings.NewReader(`{}`) logic := strings.NewReader(`{"substr": ["jsonlogic", 10]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `"jsonlogic"` assert.JSONEq(t, expected, result.String()) } func TestIssue58_example(t *testing.T) { data := strings.NewReader(`{"foo": "bar"}`) logic := strings.NewReader(`{"if":[ {"==":[{"var":"foo"},"bar"]},{"foo":"is_bar","path":"foo_is_bar"}, {"foo":"not_bar","path":"default_object"} ]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `{"foo":"is_bar","path":"foo_is_bar"}` assert.JSONEq(t, expected, result.String()) } func TestIssue70(t *testing.T) { data := strings.NewReader(`{"people": [ {"age":18, "name":"John"}, {"age":20, "name":"Luke"}, {"age":18, "name":"Mark"} ]}`) logic := strings.NewReader(`{"filter": [ {"var": ["people"]}, {"==": [{"var": ["age"]}, 18]} ]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `[ {"age": 18, "name": "John"}, {"age": 18, "name": "Mark"} ]` assert.JSONEq(t, expected, result.String()) } func TestIssue71_example_empty_min(t *testing.T) { data := strings.NewReader(`{}`) logic := strings.NewReader(`{"min":[]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `null` assert.JSONEq(t, expected, result.String()) } func TestIssue71_example_empty_max(t *testing.T) { data := strings.NewReader(`{}`) logic := strings.NewReader(`{"max":[]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `null` assert.JSONEq(t, expected, result.String()) } func TestIssue71_example_max(t *testing.T) { data := strings.NewReader(`{}`) logic := strings.NewReader(`{"max":[-3, -2]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `-2` assert.JSONEq(t, expected, result.String()) } func TestIssue74(t *testing.T) { logic := strings.NewReader(`{"if":[ false, {"var":"values.0.categories"}, "else" ]}`) data := strings.NewReader(`{ "values": [] }`) var result bytes.Buffer _ = jsonlogic.Apply(logic, data, &result) expected := `"else"` assert.JSONEq(t, expected, result.String()) } func TestJsonLogicWithSolvedVars(t *testing.T) { rule := json.RawMessage(`{ "or":[ { "and":[ {"==": [{ "var":"is_foo" }, true ]}, {"==": [{ "var":"is_bar" }, true ]}, {">=": [{ "var":"foo" }, 17179869184 ]}, {"==": [{ "var":"bar" }, 0 ]} ] }, { "and":[ {"==": [{ "var":"is_bar" }, true ]}, {"==": [{ "var":"is_foo" }, false ]}, {"==": [{ "var":"foo" }, 34359738368 ]}, {"==": [{ "var":"bar" }, 0 ]} ] }] }`) data := json.RawMessage(`{"foo": 34359738368, "bar": 10, "is_foo": false, "is_bar": true}`) output, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data) if err != nil { t.Fatal(err) } expected := `{ "or":[ { "and":[ { "==":[ false, true ] }, { "==":[ true, true ] }, { ">=":[ 34359738368, 17179869184 ] }, { "==":[ 10, 0 ] } ] }, { "and":[ { "==":[ true, true ] }, { "==":[ false, false ] }, { "==":[ 34359738368, 34359738368 ] }, { "==":[ 10, 0 ] } ] }] }` assert.JSONEq(t, expected, string(output)) } func TestIssue79(t *testing.T) { rule := strings.NewReader( `{"and": [ {"in": [ {"var": "flow"}, ["BRAND"] ]}, {"or": [ {"if": [ {"missing": ["gender"]}, true, false ]}, {"some": [ {"var": "gender"}, {"==": [ {"var": null}, "men" ]} ]} ]} ]}`, ) data := strings.NewReader(`{"category":["sneakers"],"flow":"BRAND","gender":["men"],"market":"US"}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `true` assert.JSONEq(t, expected, result.String()) } func TestIssue83(t *testing.T) { rule := `{ "map": [ {"var": "listOfLists"}, {"in": ["item_a", {"var": ""}]} ] }` data := `{ "listOfLists": [ ["item_a", "item_b", "item_c"], ["item_b", "item_c"], ["item_a", "item_c"] ] }` var result bytes.Buffer err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result) if assert.Nil(t, err) { expected := `[true,false,true]` assert.JSONEq(t, expected, result.String()) } } func TestIssue81(t *testing.T) { rule := `{ "some": [ {"var": "A"}, {"!=": [ {"var": ".B"}, {"var": "B"} ]} ]} ` data := `{"A":[{"B":1}], "B":2}` var result bytes.Buffer err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result) if err != nil { t.Fatal(err) } expected := `true` assert.JSONEq(t, expected, result.String()) } func TestIssue96(t *testing.T) { rule := `{"map":[ {"var":"integers"}, {"*":[{"var":[""]},2]} ]}` data := `{"integers": [1,2,3]}` var result bytes.Buffer err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result) if err != nil { t.Fatal(err) } expected := `[2, 4, 6]` assert.JSONEq(t, expected, result.String()) } func TestIssue98(t *testing.T) { rule := `{"or": [{"and": [true]}]}` data := `{}` var result bytes.Buffer err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result) if err != nil { t.Fatal(err) } expected := `true` assert.JSONEq(t, expected, result.String()) } func TestIssue110(t *testing.T) { logic := strings.NewReader(`{ "map":[{"var": "arr"},{"var":["xxx", "default"]}]}`) data := strings.NewReader(`{"arr": [{"xxx": "111","yyy": "222"},{"xxx": "333","yyy": "444"}]}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `["111","333"]` assert.JSONEq(t, expected, result.String()) } func TestIssue125_InOperatorWithVarsInSlice(t *testing.T) { // This test demonstrates the issue: vars within slices are not resolved rule := strings.NewReader(`{"in": [{"var": "needle"}, [{"var": "item1"}, {"var": "item2"}]]}`) data := strings.NewReader(`{"needle":"foo", "item1":"bar", "item2":"foo"}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } // Should be true because "foo" should be found in the resolved array ["bar", "foo"] // Currently fails because it compares "foo" against unresolved [{"var": "item1"}, {"var": "item2"}] expected := `true` assert.JSONEq(t, expected, result.String()) } func TestIssue125_CustomOperatorWithVarsInSlice(t *testing.T) { // Add a custom operator that processes slice elements jsonlogic.AddOperator("contains_any", func(values, data any) any { parsed := values.([]any) needle := parsed[0] haystack := parsed[1].([]any) for _, item := range haystack { if item == needle { return true } } return false }) rule := strings.NewReader(`{"contains_any": [{"var": "needle"}, [{"var": "item1"}, {"var": "item2"}]]}`) data := strings.NewReader(`{"needle":"foo", "item1":"bar", "item2":"foo"}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } // Should be true because "foo" should be found in the resolved array ["bar", "foo"] // Currently fails because the custom operator receives unresolved [{"var": "item1"}, {"var": "item2"}] expected := `true` assert.JSONEq(t, expected, result.String()) } func TestIssue135(t *testing.T) { cases := []struct { name string rule string expected string }{ { name: "or returns last operand when all are falsy", rule: `{"or":[null,0]}`, expected: `0`, }, { name: "and returns last operand when all are truthy", rule: `{"and":[1,"result"]}`, expected: `"result"`, }, { name: "and returns last truthy operand, not max", rule: `{"and":[3,1]}`, expected: `1`, }, { name: "and example from jsonlogic.com docs", rule: `{"and":[true,"a",3]}`, expected: `3`, }, { name: "and returns first falsy operand (empty string)", rule: `{"and":[true,"",3]}`, expected: `""`, }, { name: "or short-circuits on first truthy operand", rule: `{"or":[1,0]}`, expected: `1`, }, { name: "and with non-empty slice continues past it", rule: `{"and":[[1,2,3],true]}`, expected: `true`, }, { name: "and with empty slice returns it", rule: `{"and":[[],true]}`, expected: `[]`, }, { name: "or returns null when all operands are null", rule: `{"or":[null,null]}`, expected: `null`, }, { name: "or returns empty array as last falsy operand", rule: `{"or":[false,[]]}`, expected: `[]`, }, { name: "and with empty operand list returns null", rule: `{"and":[]}`, expected: `null`, }, { name: "or with empty operand list returns null", rule: `{"or":[]}`, expected: `null`, }, { name: "or with single falsy operand returns it", rule: `{"or":[0]}`, expected: `0`, }, { name: "and with single falsy operand returns it", rule: `{"and":[0]}`, expected: `0`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var result bytes.Buffer err := jsonlogic.Apply(strings.NewReader(tc.rule), strings.NewReader(`{}`), &result) if err != nil { t.Fatal(err) } // `or` should return the first truthy operand or the last operand; // `and` should return the first falsy operand or the last operand. assert.JSONEq(t, tc.expected, result.String()) }) } } ================================================ FILE: jsonlogic.go ================================================ // Package jsonlogic provides a Go implementation of JSONLogic rules engine. // JSONLogic is a way to write rules that involve logic (boolean and mathematical operations), // consistently in JSON. It's designed to be a lightweight, portable way to share logic // between front-end and back-end systems. // // The package supports all standard JSONLogic operators and allows for custom operator registration. // Rules can be applied to data using various input/output formats including io.Reader/Writer, // json.RawMessage, and native Go interfaces. // // Basic usage: // // rule := strings.NewReader(`{"==":[{"var":"name"}, "John"]}`) // data := strings.NewReader(`{"name":"John"}`) // var result strings.Builder // // err := jsonlogic.Apply(rule, data, &result) // if err != nil { // log.Fatal(err) // } // // result.String() will be "true" // // For more examples and documentation, see: https://jsonlogic.com package jsonlogic import ( "encoding/json" "io" "strings" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) // Apply reads a rule and data from `io.Reader`, applies the rule to the data // and writes the result to the provided writer. It returns an error if rule // processing or data handling fails. // // Parameters: // - rule: io.Reader representing the transformation rule to be applied // - data: io.Reader containing the input data to transform // - result: io.Writer containing the transformed data // // Returns: // - err: error if the transformation fails or if type assertions are invalid func Apply(rule, data io.Reader, result io.Writer) error { if data == nil { data = strings.NewReader("{}") } var _rule any var _data any decoder := json.NewDecoder(rule) err := decoder.Decode(&_rule) if err != nil { return err } decoder = json.NewDecoder(data) err = decoder.Decode(&_data) if err != nil { return err } output, err := ApplyInterface(_rule, _data) if err != nil { return err } return json.NewEncoder(result).Encode(output) } // ApplyRaw applies a validation rule to a JSON data input, both provided as raw JSON messages. // It processes the input data according to the provided rule and returns the transformed result. // // Parameters: // - rule: json.RawMessage representing the transformation rule to be applied // - data: json.RawMessage containing the input data to transform // // Returns: // - output: json.RawMessage containing the transformed data // - err: error if the transformation fails or if type assertions are invalid func ApplyRaw(rule, data json.RawMessage) (json.RawMessage, error) { if data == nil { data = json.RawMessage("{}") } var _rule any var _data any err := json.Unmarshal(rule, &_rule) if err != nil { return nil, err } err = json.Unmarshal(data, &_data) if err != nil { return nil, err } result, err := ApplyInterface(_rule, _data) if err != nil { return nil, err } return json.Marshal(&result) } // ApplyInterface applies a transformation rule to input data using interface type assertions. // It processes the input data according to the provided rule and returns the transformed result. // // Parameters: // - rule: interface{} representing the transformation rule to be applied // - data: interface{} containing the input data to transform // // Returns: // - output: interface{} containing the transformed data // - err: error if the transformation fails or if type assertions are invalid func ApplyInterface(rule, data any) (output any, err error) { defer func() { if e := recover(); e != nil { // fmt.Println("stacktrace from panic: \n" + string(debug.Stack())) err = e.(error) } }() if typing.IsMap(rule) { return apply(rule, data), err } if typing.IsSlice(rule) { inputSlice := rule.([]any) parsed := make([]any, 0, len(inputSlice)) for _, value := range inputSlice { parsed = append(parsed, parseValues(value, data)) } return any(parsed), nil } return rule, err } // GetJsonLogicWithSolvedVars processes a JSON Logic rule by resolving variables with actual data values. // It returns the rule with variables substituted but maintains the JSON Logic structure. // // Parameters: // - rule: json.RawMessage containing the JSON Logic rule // - data: json.RawMessage containing the data context for variable resolution // // Returns: // - []byte: the processed rule with resolved variables as JSON bytes // - error: error if unmarshaling or processing fails // // This is useful for debugging or when you need to see the rule with variables resolved. func GetJsonLogicWithSolvedVars(rule, data json.RawMessage) ([]byte, error) { if data == nil { data = json.RawMessage("{}") } // parse rule and data from json.RawMessage to interface var _rule any var _data any err := json.Unmarshal(rule, &_rule) if err != nil { return nil, err } err = json.Unmarshal(data, &_data) if err != nil { return nil, err } return solveVarsBackToJsonLogic(_rule, _data) } func parseValues(values, data any) any { if values == nil || typing.IsPrimitive(values) { return values } if typing.IsMap(values) { return apply(values, data) } inputSlice := values.([]any) length := len(inputSlice) if length == 0 { return inputSlice } parsed := make([]any, 0, length) for _, value := range inputSlice { if typing.IsMap(value) { parsed = append(parsed, apply(value, data)) } else { parsed = append(parsed, parseValues(value, data)) } } return parsed } func apply(rules, data any) any { ruleMap := rules.(map[string]any) // A map with more than 1 key counts as a primitive so it's time to end recursion if len(ruleMap) > 1 { return ruleMap } for operator, values := range ruleMap { return operation(operator, values, data) } return make(map[string]any) } ================================================ FILE: jsonlogic_test.go ================================================ package jsonlogic_test import ( "bytes" "encoding/json" "fmt" "strings" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" "github.com/diegoholiveira/jsonlogic/v3/internal" ) func TestRulesFromJsonLogic(t *testing.T) { suites := map[string][]internal.Test{ "Official": internal.GetScenariosFromOfficialTestSuite(), "Proposed in https://github.com/jwadhams/json-logic/pull/48": internal.GetScenariosFromProposedOfficialTestSuite(), } for suiteName, tests := range suites { t.Run(suiteName, func(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("%s_%d", test.Scenario, test.Index), func(t *testing.T) { result, err := jsonlogic.ApplyInterface(test.Rule, test.Data) if err != nil { t.Fatal(err) } assert.Equal(t, test.Expected, result, "Applying rule %v to data %v", toJSON(test.Rule), toJSON(test.Data)) }) } }) } } func toJSON(val any) string { res, err := json.Marshal(val) if err != nil { panic(err) } return string(res) } func TestDivWithOnlyOneValue(t *testing.T) { rule := strings.NewReader(`{"/":[4]}`) data := strings.NewReader(`null`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, `4`, result.String()) } func TestSetAValue(t *testing.T) { rule := strings.NewReader(`{ "map": [ {"var": "objects"}, {"set": [ {"var": ""}, "age", {"+": [{"var": ".age"}, 2]} ]} ] }`) data := strings.NewReader(`{ "objects": [ {"age": 100, "location": "north"}, {"age": 500, "location": "south"} ] }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `[ {"age": 102, "location": "north"}, {"age": 502, "location": "south"} ]` assert.JSONEq(t, expected, result.String()) } func TestLocalContext(t *testing.T) { rule := strings.NewReader(`{ "filter": [ {"var": "people"}, {"==": [ {"var": ".age"}, {"min": {"map": [ {"var": "people"}, {"var": ".age"} ]}} ]} ] }`) data := strings.NewReader(`{ "people": [ {"age":18, "name":"John"}, {"age":20, "name":"Luke"}, {"age":18, "name":"Mark"} ] }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `[ {"age": 18, "name": "John"}, {"age": 18, "name": "Mark"} ]` assert.JSONEq(t, expected, result.String()) } func TestMapWithZeroValue(t *testing.T) { rule := strings.NewReader(`{ "filter": [ {"var": "people"}, {"==": [ {"var": ".age"}, {"min": {"map": [ {"var": "people"}, {"var": ".age"} ]}} ]} ] }`) data := strings.NewReader(`{ "people": [ {"age":0, "name":"John"} ] }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `[ {"age": 0, "name": "John"} ]` assert.JSONEq(t, expected, result.String()) } func TestListOfRanges(t *testing.T) { rule := strings.NewReader(`{ "filter": [ {"var": "people"}, {"in": [ {"var": ".age"}, [ [12, 18], [22, 28], [32, 38] ] ]} ] }`) data := strings.NewReader(`{ "people": [ {"age":18, "name":"John"}, {"age":20, "name":"Luke"}, {"age":18, "name":"Mark"} ] }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `[ {"age": 18, "name": "John"}, {"age": 18, "name": "Mark"} ]` assert.JSONEq(t, expected, result.String()) } func TestSomeWithLists(t *testing.T) { rule := strings.NewReader(`{ "some": [ [511, 521, 811], {"in":[ {"var":""}, [1, 2, 3, 511] ]} ] }`) data := strings.NewReader(`{}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, "true", result.String()) } func TestAllWithLists(t *testing.T) { rule := strings.NewReader(`{ "all": [ [511, 521, 811], {"in":[ {"var":""}, [511, 521, 811, 3] ]} ] }`) data := strings.NewReader("{}") var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, "true", result.String()) } func TestAllWithArrayOfMapData(t *testing.T) { data := strings.NewReader(`[ { "P1": "A", "P2":"a" }, { "P1": "B", "P2":"b" } ]`) rule := strings.NewReader(` { "all": [ { "var": "" }, { "in": [ {"var": "P1"} , ["A","B"]] } ] } `) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, "true", result.String()) } func TestNoneWithLists(t *testing.T) { rule := strings.NewReader(`{ "none": [ [511, 521, 811], {"in":[ {"var":""}, [1, 2] ]} ] }`) data := strings.NewReader("{}") var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, "true", result.String()) } func TestInOperatorWorksWithMaps(t *testing.T) { rule := strings.NewReader(`{ "some": [ [511,521,811], {"in": [ {"var": ""}, {"map": [ {"var": "my_list"}, {"var": ".service_id"} ]} ]} ] }`) data := strings.NewReader(`{ "my_list": [ {"service_id": 511}, {"service_id": 771}, {"service_id": 521}, {"service_id": 181} ] }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, "true", result.String()) } func TestAbsoluteValue(t *testing.T) { rule := strings.NewReader(`{ "abs": { "var": "test.number" } }`) data := strings.NewReader(`{ "test": { "number": -2 } }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, "2", result.String()) } func TestMergeArrayOfArrays(t *testing.T) { rule := strings.NewReader(`{ "merge": [ [ [ "18800000", "18800969" ] ], [ [ "19840000", "19840969" ] ] ] }`) data := strings.NewReader(`{}`) expectedResult := "[[\"18800000\",\"18800969\"],[\"19840000\",\"19840969\"]]" var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, expectedResult, result.String()) } func TestDataWithDefaultValueWithApplyRaw(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "+": [ 1, 2 ] }`) var expected json.RawMessage = json.RawMessage("3") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestDataWithDefaultValueWithApplyInterface(t *testing.T) { rule := map[string]any{ "+": []any{ float64(1), float64(2), }, } expected := float64(3) output, err := jsonlogic.ApplyInterface(rule, nil) if err != nil { t.Fatal(err) } assert.Equal(t, expected, output.(float64)) } func TestMissingOperators(t *testing.T) { rule := map[string]any{ "sum": []any{ float64(1), float64(2), }, } _, err := jsonlogic.ApplyInterface(rule, nil) assert.EqualError(t, err, "The operator \"sum\" is not supported") } func TestZeroDivision(t *testing.T) { logic := strings.NewReader(`{"/":[0,10]}`) data := strings.NewReader(`{}`) var result bytes.Buffer jsonlogic.Apply(logic, data, &result) // nolint:errcheck assert.JSONEq(t, `0`, result.String()) } func TestSliceWithOnlyWithNumbersAsKey(t *testing.T) { rule := strings.NewReader(`{"var": "people.0"}`) data := strings.NewReader(`{ "people": [ {"age":18, "name":"John"}, {"age":20, "name":"Luke"}, {"age":18, "name":"Mark"} ] }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `{"age": 18, "name": "John"}` assert.JSONEq(t, expected, result.String()) } func TestMapWithOnlyWithNumbersAsKey(t *testing.T) { rule := strings.NewReader(`{"var": "people.103"}`) data := strings.NewReader(`{ "people": { "100": {"age":18, "name":"John"}, "101": {"age":20, "name":"Luke"}, "103": {"age":18, "name":"Mark"} } }`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `{"age": 18, "name": "Mark"}` assert.JSONEq(t, expected, result.String()) } func TestBetweenIsBiggerEq(t *testing.T) { rule := strings.NewReader(`{ "filter": [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {">=": [8, {"var": ""}, 3]} ] }`) data := strings.NewReader(`{}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `[3, 4, 5, 6, 7, 8]` assert.JSONEq(t, expected, result.String()) } func TestBetweenIsBigger(t *testing.T) { rule := strings.NewReader(`{ "filter": [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], {">": [8, {"var": ""}, 3]} ] }`) data := strings.NewReader(`{}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `[4, 5, 6, 7]` assert.JSONEq(t, expected, result.String()) } func TestUnaryOperation(t *testing.T) { logic := strings.NewReader(`{"and":[{"!":{"var":"var_not_in_data"}}]}`) data := strings.NewReader(`{"some_key": "value"}`) var result bytes.Buffer assert.Nil(t, jsonlogic.Apply(logic, data, &result)) assert.JSONEq(t, `true`, result.String()) } func TestInOperatorAgainstNil(t *testing.T) { rule := strings.NewReader(`{"filter":[{"var": "accounts"},{"and":[{"in":["abc",{"var":"tags.tag-1"}]}]}]}`) data := strings.NewReader(`{"accounts":[{"name":"account-1","tags":{"tag-1":"abc"}}, {"name":"account-2","tags":{"tag-2":"xyz"}}]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `[ { "name": "account-1", "tags": { "tag-1": "abc" } } ]` assert.JSONEq(t, expected, result.String()) } func TestReduceFilterAndContains(t *testing.T) { rule := strings.NewReader(`{"reduce":[{"filter":[{"var":"data.level1.level2"},{"==":[{"var":"access"},true]}]},{"or":[{"var":"current.access"},{"var":"accumulator"}]},false]}`) data := strings.NewReader(`{"data":{"level1":{"level2":[{"access":true }]}}}}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `true` assert.JSONEq(t, expected, result.String()) } func TestReduceFilterAndNotContains(t *testing.T) { rule := strings.NewReader(`{"reduce":[{"filter":[{"var":"data.level1.level2"},{"==":[{"var":"access"},true]}]},{"or":[{"var":"current.access"},{"var":"accumulator"}]},false]}`) data := strings.NewReader(`{"data":{"level1":{"level2":[{"access":false }]}}}}`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } expected := `false` assert.JSONEq(t, expected, result.String()) } func TestReduceWithUnsupportedValue(t *testing.T) { b := []byte(`{"reduce":[{"filter":[{"var":"data"},{"==":[{"var":""},""]}]},{"cat":[{"var":"current"},{"var":"accumulator"}]},null]}`) rule := map[string]any{} _ = json.Unmarshal(b, &rule) data := map[string]any{ "data": []any{"str"}, } _, err := jsonlogic.ApplyInterface(rule, data) assert.EqualError(t, err, "The type \"\" is not supported") } func TestAddOperator(t *testing.T) { jsonlogic.AddOperator("strlen", func(values, data any) any { v, ok := values.(string) if ok { return len(v) } return 0 }) logic := strings.NewReader(`{ "strlen": { "var": "foo" } }`) data := strings.NewReader(`{"foo": "bar"}`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { t.Fatal(err) } expected := `3` assert.JSONEq(t, expected, result.String()) } func TestInWithOneParam(t *testing.T) { rule := strings.NewReader(`{"in": [ "Ringo" ]}`) data := strings.NewReader(`null`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, `false`, result.String()) } func TestEqualWithList(t *testing.T) { rule := strings.NewReader(`{"==": [ 2, [3, 2, 1] ]}`) data := strings.NewReader(`null`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, `false`, result.String()) } func TestMinusWithEmptyList(t *testing.T) { rule := strings.NewReader(`{"-": []}`) data := strings.NewReader(`null`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, `0`, result.String()) } func TestDivWithEmptyList(t *testing.T) { rule := strings.NewReader(`{"/": []}`) data := strings.NewReader(`null`) var result bytes.Buffer err := jsonlogic.Apply(rule, data, &result) if err != nil { t.Fatal(err) } assert.JSONEq(t, `0`, result.String()) } ================================================ FILE: lists.go ================================================ package jsonlogic import ( "fmt" "strings" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) // ErrReduceDataType represents an error when an unsupported data type is used in reduce operations. // It contains the data type name that caused the error. type ErrReduceDataType struct { dataType string } func (e ErrReduceDataType) Error() string { return fmt.Sprintf("The type \"%s\" is not supported", e.dataType) } func extractSubject(parsed []any, data any) any { var subject any if typing.IsSlice(parsed[0]) { subject = parsed[0] } if typing.IsMap(parsed[0]) { subject = apply(parsed[0], data) } return subject } func filter(values, data any) any { parsed := values.([]any) if len(parsed) < 2 { return []any{} } subject := extractSubject(parsed, data) if subject == nil { return []any{} } subjectSlice := subject.([]any) subjectLen := len(subjectSlice) // Pre-allocate result with capacity that's reasonable for filtering // Assuming at least half might pass the filter (heuristic) result := make([]any, 0, subjectLen/2) logic := solveVars(parsed[1], data) for _, value := range subjectSlice { v := parseValues(logic, value) if typing.IsTrue(v) { result = append(result, value) } } return result } func _map(values, data any) any { parsed := values.([]any) if len(parsed) < 2 { return []any{} } subject := extractSubject(parsed, data) if subject == nil { return []any{} } subjectSlice := subject.([]any) subjectLen := len(subjectSlice) result := make([]any, 0, subjectLen) logic := parsed[1] for _, value := range subjectSlice { v := parseValues(logic, value) result = append(result, v) } return result } func reduce(values, data any) any { parsed := values.([]any) if len(parsed) < 3 { return float64(0) } var ( accumulator any valueType string ) { initialValue := parsed[2] if typing.IsMap(initialValue) { initialValue = apply(initialValue, data) } if typing.IsBool(initialValue) { accumulator = typing.IsTrue(initialValue) valueType = "bool" } else if typing.IsNumber(initialValue) { accumulator = typing.ToNumber(initialValue) valueType = "number" } else if typing.IsString(initialValue) { accumulator = typing.ToString(initialValue) valueType = "string" } else { panic(ErrReduceDataType{ dataType: fmt.Sprintf("%T", parsed[2]), }) } } context := map[string]any{ "current": float64(0), "accumulator": accumulator, "valueType": valueType, } subject := extractSubject(parsed, data) if subject == nil { return float64(0) } for _, value := range subject.([]any) { if value == nil { continue } context["current"] = value v := apply(parsed[1], context) switch context["valueType"] { case "bool": context["accumulator"] = typing.IsTrue(v) case "number": context["accumulator"] = typing.ToNumber(v) case "string": context["accumulator"] = typing.ToString(v) } } return context["accumulator"] } func _in(values, data any) any { values = parseValues(values, data) parsed := values.([]any) a := parsed[0] var b any if len(parsed) > 1 { b = parsed[1] } if typing.IsString(b) { return strings.Contains(b.(string), a.(string)) } if !typing.IsSlice(b) { return false } for _, element := range b.([]any) { if typing.IsSlice(element) { if _inRange(a, element.([]any)) { return true } continue } if typing.IsNumber(a) { if typing.ToNumber(element) == a { return true } continue } if element == a { return true } } return false } func merge(values, data any) any { values = parseValues(values, data) if typing.IsPrimitive(values) { return []any{values} } inputSlice := values.([]any) sliceLen := len(inputSlice) if sliceLen == 0 { return inputSlice } totalCapacity := 0 for _, value := range inputSlice { if typing.IsSlice(value) { totalCapacity += len(value.([]any)) } else { totalCapacity++ } } result := make([]any, 0, totalCapacity) for _, value := range inputSlice { if !typing.IsSlice(value) { result = append(result, value) continue } result = append(result, value.([]any)...) } return result } func missing(values, data any) any { values = parseValues(values, data) if typing.IsString(values) { values = []any{values} } missing := make([]any, 0) for _, _var := range values.([]any) { _value := getVar(_var, data) if _value == nil { missing = append(missing, _var) } } return missing } func missingSome(values, data any) any { values = parseValues(values, data) parsed := values.([]any) number := int(typing.ToNumber(parsed[0])) vars := parsed[1] missing := make([]any, 0) found := make([]any, 0) for _, _var := range vars.([]any) { _value := getVar(_var, data) if _value == nil { missing = append(missing, _var) } else { found = append(found, _var) } } if number > len(found) { return missing } return make([]any, 0) } func all(values, data any) any { parsed := values.([]any) subject := extractSubject(parsed, data) if !typing.IsTrue(subject) { return false } for _, value := range subject.([]any) { conditions := solveVars(parsed[1], value) v := apply(conditions, value) if !typing.IsTrue(v) { return false } } return true } func none(values, data any) any { parsed := values.([]any) subject := extractSubject(parsed, data) if !typing.IsTrue(subject) { return true } conditions := solveVars(parsed[1], data) for _, value := range subject.([]any) { v := apply(conditions, value) if typing.IsTrue(v) { return false } } return true } func some(values, data any) any { parsed := values.([]any) subject := extractSubject(parsed, data) if !typing.IsTrue(subject) { return false } logic := solveVars(parsed[1], data) for _, value := range subject.([]any) { conditions := solveVars(logic, value) v := apply(conditions, value) if typing.IsTrue(v) { return true } } return false } func _inRange(value any, values []any) bool { i := values[0] j := values[1] return typing.ToNumber(value) >= typing.ToNumber(i) && typing.ToNumber(j) >= typing.ToNumber(value) } ================================================ FILE: lists_test.go ================================================ package jsonlogic_test import ( "bytes" "strings" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) func TestFilterParseTheSubjectFromFirstPosition(t *testing.T) { rule := strings.NewReader(`{"filter": [ [1,2,3,4,5], {"%":[{"var":""},2]} ]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.Nil(t, err) assert.JSONEq(t, `[1,3,5]`, result.String()) } func TestFilterParseTheSubjectFromNullValue(t *testing.T) { rule := strings.NewReader(`{"filter": [ null, {"%":[{"var":""},2]} ]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.Nil(t, err) assert.JSONEq(t, `[]`, result.String()) } func TestReduceSkipNullValues(t *testing.T) { rule := strings.NewReader(`{"reduce": [ [1,2,null,4,5], {"+":[{"var":"current"}, {"var":"accumulator"}]}, 0 ]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.Nil(t, err) assert.JSONEq(t, `12`, result.String()) } func TestReduceBoolValues(t *testing.T) { rule := strings.NewReader(`{"reduce": [ [true,false,true,null], {"or":[{"var":"current"}, {"var":"accumulator"}]}, false ]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.Nil(t, err) assert.JSONEq(t, `true`, result.String()) } func TestReduceStringValues(t *testing.T) { rule := strings.NewReader(`{"reduce": [ ["a",null,"b"], {"cat":[{"var":"current"}, {"var":"accumulator"}]}, "" ]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.Nil(t, err) assert.JSONEq(t, `"ba"`, result.String()) } func TestFilterWithMissingLogicArgument(t *testing.T) { // filter needs [array, logic]; omitting the logic argument must not panic. rule := strings.NewReader(`{"filter": [[1,2,3]]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.NoError(t, err) assert.JSONEq(t, `[]`, result.String()) } func TestMapWithMissingLogicArgument(t *testing.T) { // map needs [array, logic]; omitting the logic argument must not panic. rule := strings.NewReader(`{"map": [[1,2,3]]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.NoError(t, err) assert.JSONEq(t, `[]`, result.String()) } func TestReduceWithMissingInitialValue(t *testing.T) { // reduce needs [array, logic, initial]; omitting initial value must not panic. rule := strings.NewReader(`{"reduce": [ [1,2,3], {"+":[{"var":"current"},{"var":"accumulator"}]} ]}`) var result bytes.Buffer err := jsonlogic.Apply(rule, nil, &result) assert.NoError(t, err) assert.JSONEq(t, `0`, result.String()) } ================================================ FILE: logic.go ================================================ package jsonlogic import ( "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) func _and(values, data any) any { values = values.([]any) var last any for _, value := range values.([]any) { last = parseValues(value, data) if !typing.IsTrue(last) { return last } } return last } func _or(values, data any) any { values = values.([]any) var last any for _, value := range values.([]any) { last = parseValues(value, data) if typing.IsTrue(last) { return last } } return last } func evaluateClause(clause any, data any) any { parsed := parseValues(clause, data) if typing.IsMap(parsed) { return apply(parsed, data) } return parsed } func conditional(values, data any) any { values = values.([]any) clauses := values.([]any) length := len(clauses) if length == 0 { return nil } // Evaluate each if/then pair for i := 0; i < length-1; i = i + 2 { condition := parseValues(clauses[i], data) // If the condition is true, evaluate and return the then clause if typing.IsTrue(condition) { return evaluateClause(clauses[i+1], data) } } // If no matches and there is an odd number of clauses, evaluate and return the else clause if length%2 == 1 { return evaluateClause(clauses[length-1], data) } return nil } func negative(values, data any) any { values = parseValues(values, data) // If the slice is not empty, there is an argument to negate if typing.IsSlice(values) && len(values.([]any)) > 0 { return !typing.IsTrue(values.([]any)[0]) } return !typing.IsTrue(values) } ================================================ FILE: math.go ================================================ package jsonlogic import ( "math" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) func mod(values, data any) any { _values := parseValues(values, data).([]any) a, b := _values[0], _values[1] _a := typing.ToNumber(a) _b := typing.ToNumber(b) return math.Mod(_a, _b) } func abs(values, data any) any { values = parseValues(values, data) if typing.IsSlice(values) { return math.Abs(typing.ToNumber(values.([]any)[0])) } return math.Abs(typing.ToNumber(values)) } func sum(values, data any) any { values = parseValues(values, data) if !typing.IsSlice(values) { return typing.ToNumber(values) } inputSlice := values.([]any) sliceLen := len(inputSlice) if sliceLen == 0 { return float64(0) } if sliceLen == 1 { return typing.ToNumber(inputSlice[0]) } sum := float64(0) for _, n := range inputSlice { sum += typing.ToNumber(n) } return sum } func minus(values, data any) any { _values := parseValues(values, data).([]any) if len(_values) == 0 { return 0 } if len(_values) == 1 { return -1 * typing.ToNumber(_values[0]) } sum := typing.ToNumber(_values[0]) for i := 1; len(_values) > i; i++ { sum -= typing.ToNumber(_values[i]) } return sum } func mult(values, data any) any { values = parseValues(values, data) sum := float64(1) for _, n := range values.([]any) { sum *= typing.ToNumber(n) } return sum } func div(values, data any) any { _values := parseValues(values, data).([]any) if len(_values) == 0 { return 0 } sum := typing.ToNumber(_values[0]) for i := 1; len(_values) > i; i++ { sum = sum / typing.ToNumber(_values[i]) } return sum } func max(values, data any) any { values = parseValues(values, data) parsed := values.([]any) size := len(parsed) if size == 0 { return nil } bigger := typing.ToNumber(parsed[0]) for i := 1; i < size; i++ { _n := typing.ToNumber(parsed[i]) if _n > bigger { bigger = _n } } return bigger } func min(values, data any) any { values = parseValues(values, data) parsed := values.([]any) size := len(parsed) if size == 0 { return nil } smallest := typing.ToNumber(parsed[0]) for i := 1; i < size; i++ { _n := typing.ToNumber(parsed[i]) if smallest > _n { smallest = _n } } return smallest } ================================================ FILE: math_test.go ================================================ package jsonlogic_test import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) func TestSubOperation(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "-": [ 0, 10 ] }`) var expected json.RawMessage = json.RawMessage("-10") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestAbsOperationWithScalar(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "abs": -42 }`) var expected json.RawMessage = json.RawMessage("42") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestAbsOperationWithArray(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "abs": [-42] }`) var expected json.RawMessage = json.RawMessage("42") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestSumOperationWithEmptyArray(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "+": [] }`) var expected json.RawMessage = json.RawMessage("0") output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } ================================================ FILE: operation.go ================================================ package jsonlogic import ( "fmt" "sync" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) // OperatorFn defines the signature for custom operator functions. // It takes values and data as input and returns a result. type OperatorFn func(values, data any) (result any) // ErrInvalidOperator represents an error when an unsupported operator is used. // It contains the operator name that caused the error. type ErrInvalidOperator struct { operator string } func (e ErrInvalidOperator) Error() string { return fmt.Sprintf("The operator \"%s\" is not supported", e.operator) } // operators holds custom operators var operators = make(map[string]OperatorFn) var operatorsLock = &sync.RWMutex{} // AddOperator registers a custom operator with the given key and function. // The operator function will be called with parsed values and the original data context. // // Parameters: // - key: the operator name to register (e.g., "custom_op") // - cb: the function to execute when the operator is encountered // // Concurrency: This function is safe for concurrent use as it properly locks the operators map. func AddOperator(key string, cb OperatorFn) { operatorsLock.Lock() defer operatorsLock.Unlock() operators[key] = func(values, data any) any { return cb(parseValues(values, data), data) } } func operation(operator string, values, data any) any { operatorsLock.RLock() opFn, found := operators[operator] operatorsLock.RUnlock() if found { return opFn(values, data) } panic(ErrInvalidOperator{ operator: operator, }) } func init() { operatorsLock.Lock() defer operatorsLock.Unlock() operators["and"] = _and operators["or"] = _or operators["filter"] = filter operators["map"] = _map operators["reduce"] = reduce operators["all"] = all operators["none"] = none operators["some"] = some operators["in"] = _in operators["missing"] = missing operators["missing_some"] = missingSome operators["var"] = getVar operators["set"] = setProperty operators["cat"] = concat operators["substr"] = substr operators["merge"] = merge operators["if"] = conditional operators["?:"] = conditional operators["max"] = max operators["min"] = min operators["+"] = sum operators["-"] = minus operators["*"] = mult operators["/"] = div operators["%"] = mod operators["abs"] = abs operators["!"] = negative operators["!!"] = func(v, d any) any { return !typing.IsTrue(negative(v, d)) } operators["==="] = hardEquals operators["!=="] = func(v, d any) any { return !hardEquals(v, d).(bool) } operators["<"] = isLessThan operators["<="] = isLessOrEqualThan operators[">"] = isGreaterThan operators[">="] = isGreaterOrEqualThan operators["=="] = isEqual operators["!="] = func(v, d any) any { return !isEqual(v, d).(bool) } /* CUSTOM OPERATORS */ operators["contains_all"] = func(v, d any) any { return containsAll(parseValues(v, d), d) } operators["contains_any"] = func(v, d any) any { return containsAny(parseValues(v, d), d) } operators["contains_none"] = func(v, d any) any { return containsNone(parseValues(v, d), d) } } ================================================ FILE: operation_test.go ================================================ package jsonlogic import ( "io" "strings" "sync" "testing" ) // TestConcurrentApplyAndAddOperator validates that validating rules and adding operators concurrently // doesn't cause fatal errors or deadlocks. func TestConcurrentValidationAndAddOperator(t *testing.T) { var wg sync.WaitGroup numRoutines := 10 numIterations := 100 // Start multiple goroutines to validate rules concurrently for i := 0; i < numRoutines; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < numIterations; j++ { rule := `{"==": [1, 1]}` _ = IsValid(strings.NewReader(rule)) } }() } // Start a goroutine to add a new operator concurrently wg.Add(1) go func() { defer wg.Done() for j := 0; j < numIterations; j++ { AddOperator("test_op", func(values, data any) any { return "test" }) } }() wg.Wait() } // TestConcurrentApplyAndAddOperator validates that applying rules and adding operators concurrently // doesn't cause fatal errors or deadlocks. func TestConcurrentApplyAndAddOperator(t *testing.T) { var wg sync.WaitGroup numRoutines := 10 numIterations := 100 // Start multiple goroutines to apply rules concurrently for i := 0; i < numRoutines; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < numIterations; j++ { rule := `{"==": [1, 1]}` data := `{}` _ = Apply(strings.NewReader(rule), strings.NewReader(data), io.Discard) } }() } // Start a goroutine to add a new operator concurrently wg.Add(1) go func() { defer wg.Done() for j := 0; j < numIterations; j++ { AddOperator("test_op", func(values, data any) any { return "test" }) } }() wg.Wait() } ================================================ FILE: readme.md ================================================ # Go JsonLogic ![test workflow](https://github.com/diegoholiveira/jsonlogic/actions/workflows/test.yml/badge.svg) [![codecov](https://codecov.io/gh/diegoholiveira/jsonlogic/branch/master/graph/badge.svg)](https://codecov.io/gh/diegoholiveira/jsonlogic) [![Go Report Card](https://goreportcard.com/badge/github.com/diegoholiveira/jsonlogic)](https://goreportcard.com/report/github.com/diegoholiveira/jsonlogic) Implementation of [JsonLogic](http://jsonlogic.com) in Go Lang. ## What's JsonLogic? JsonLogic is a DSL to write logic decisions in JSON. It's has a great specification and is very simple to learn. The [official website](http://jsonlogic.com) has great documentation with examples. ## How to use it The use of this library is very straightforward. Here's a simple example: ```go package main import ( "bytes" "fmt" "strings" "github.com/diegoholiveira/jsonlogic/v3" ) func main() { logic := strings.NewReader(`{"==": [1, 1]}`) data := strings.NewReader(`{}`) var result bytes.Buffer jsonlogic.Apply(logic, data, &result) fmt.Println(result.String()) } ``` This will output `true` in your console. Here's another example, but this time using variables passed in the `data` parameter: ```go package main import ( "bytes" "encoding/json" "fmt" "strings" "github.com/diegoholiveira/jsonlogic/v3" ) type ( User struct { Name string `json:"name"` Age int `json:"age"` Location string `json:"location"` } Users []User ) func main() { logic := strings.NewReader(`{ "filter": [ {"var": "users"}, {">=": [ {"var": ".age"}, 18 ]} ] }`) data := strings.NewReader(`{ "users": [ {"name": "Diego", "age": 33, "location": "Florianópolis"}, {"name": "Jack", "age": 12, "location": "London"}, {"name": "Pedro", "age": 19, "location": "Lisbon"}, {"name": "Leopoldina", "age": 30, "location": "Rio de Janeiro"} ] }`) var result bytes.Buffer err := jsonlogic.Apply(logic, data, &result) if err != nil { fmt.Println(err.Error()) return } var users Users decoder := json.NewDecoder(&result) decoder.Decode(&users) for _, user := range users { fmt.Printf(" - %s\n", user.Name) } } ``` If you have a function you want to expose as a JsonLogic operation, you can use: ```go package main import ( "bytes" "fmt" "strings" "github.com/diegoholiveira/jsonlogic/v3" ) func main() { // add a new operator "strlen" for get string length jsonlogic.AddOperator("strlen", func(values, data any) any { v, ok := values.(string) if ok { return len(v) } return 0 }) logic := strings.NewReader(`{ "strlen": { "var": "foo" } }`) data := strings.NewReader(`{"foo": "bar"}`) var result bytes.Buffer jsonlogic.Apply(logic, data, &result) fmt.Println(result.String()) // the string length of "bar" is 3 } ``` If you want to get the JsonLogic used, with the variables replaced by their values: ```go package main import ( "fmt" "encoding/json" "github.com/diegoholiveira/jsonlogic/v3" ) func main() { logic := json.RawMessage(`{ "==":[{ "var":"foo" }, true] }`) data := json.RawMessage(`{"foo": "false"}`) result, err := jsonlogic.GetJsonLogicWithSolvedVars(logic, data) if err != nil { fmt.Println(err) } fmt.Println(string(result)) // will output { "==":[false, true] } } ``` ## Custom Operators (Non-standard) > ⚠️ **Warning**: These operators are not part of the official JsonLogic specification and may be deprecated in future versions. | Operator | Description | Example | |----------|-------------|---------| | `contains_all` | Returns `true` if **all** elements in the second array exist in the first array | `{"contains_all": [["a","b","c"], ["a","b"]]}` → `true` | | `contains_any` | Returns `true` if **any** element in the second array exists in the first array | `{"contains_any": [["a","b"], ["x","a"]]}` → `true` | | `contains_none` | Returns `true` if **no** elements in the second array exist in the first array | `{"contains_none": [["a","b"], ["x","y"]]}` → `true` | # License This project is licensed under the MIT License - see [LICENSE](./LICENSE) for details. ================================================ FILE: strings.go ================================================ package jsonlogic import ( "strings" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) func substr(values, data any) any { values = parseValues(values, data) parsed := values.([]any) runes := []rune(typing.ToString(parsed[0])) from := int(typing.ToNumber(parsed[1])) length := len(runes) if from < 0 { from = length + from } if from < 0 || from > length { // case from is still negative, we must stop right now and return the original string return string(runes) } if len(parsed) == 3 { length = int(typing.ToNumber(parsed[2])) } var to int if length < 0 { length = len(runes) + length to = length } else { to = from + length } if to > len(runes) { to = len(runes) } return string(runes[from:to]) } func concat(values, data any) any { values = parseValues(values, data) if typing.IsString(values) { return values } inputSlice := values.([]any) if len(inputSlice) == 0 { return "" } if len(inputSlice) == 1 { return typing.ToString(inputSlice[0]) } var s strings.Builder for _, text := range inputSlice { s.WriteString(typing.ToString(text)) } return strings.TrimSpace(s.String()) } ================================================ FILE: strings_test.go ================================================ package jsonlogic_test import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) func TestCat(t *testing.T) { testCases := []struct { name string rule string data string expected string }{ { name: "Empty string", rule: `{"cat": ""}`, data: `{}`, expected: `""`, }, { name: "Empty array", rule: `{"cat": []}`, data: `{}`, expected: `""`, }, { name: "Single string", rule: `{"cat": "hello"}`, data: `{}`, expected: `"hello"`, }, { name: "Multiple strings", rule: `{"cat": ["hello", " ", "world"]}`, data: `{}`, expected: `"hello world"`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { rule := json.RawMessage(tc.rule) data := json.RawMessage(tc.data) expected := json.RawMessage(tc.expected) output, err := jsonlogic.ApplyRaw(rule, data) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) }) } } ================================================ FILE: validator.go ================================================ package jsonlogic import ( "encoding/json" "io" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) // IsValid reads a JSON Logic rule from io.Reader and validates its syntax. // It checks if the rule conforms to valid JSON Logic format and uses supported operators. // // Parameters: // - rule: io.Reader containing the JSON Logic rule to validate // // Returns: // - bool: true if the rule is valid, false otherwise // // The function returns false if the JSON cannot be parsed or if the rule contains invalid operators. func IsValid(rule io.Reader) bool { var _rule any decoderRule := json.NewDecoder(rule) err := decoderRule.Decode(&_rule) if err != nil { return false } return ValidateJsonLogic(_rule) } // ValidateJsonLogic validates if the given rules conform to JSON Logic format. // It recursively checks the structure and ensures all operators are supported. // // Parameters: // - rules: any value representing the JSON Logic rule to validate // // Returns: // - bool: true if the rules are valid JSON Logic, false otherwise // // The function handles primitives, maps (operators), slices (arrays), and variable references. func ValidateJsonLogic(rules any) bool { if isVar(rules) { return true } if typing.IsMap(rules) { rulesMap := rules.(map[string]any) // A map with more than 1 key counts as a primitive so it's time to end recursion if len(rulesMap) > 1 { return true } for operator, value := range rulesMap { if !isOperator(operator) { return false } return ValidateJsonLogic(value) } } if typing.IsSlice(rules) { for _, value := range rules.([]any) { if typing.IsSlice(value) || typing.IsMap(value) { if ValidateJsonLogic(value) { continue } return false } if isVar(value) || typing.IsPrimitive(value) { continue } } return true } return typing.IsPrimitive(rules) } func isOperator(op string) bool { operatorsLock.RLock() _, isOperator := operators[op] operatorsLock.RUnlock() return isOperator } func isVar(value any) bool { if !typing.IsMap(value) { return false } _var, ok := value.(map[string]any)["var"] if !ok { return false } return typing.IsString(_var) || typing.IsNumber(_var) || _var == nil } ================================================ FILE: validator_test.go ================================================ package jsonlogic_test import ( "fmt" "io" "strings" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) func TestJSONLogicValidator(t *testing.T) { jsonlogic.AddOperator("customOperator", func(values, data any) any { return values }) scenarios := map[string]struct { IsValid bool Rule io.Reader }{ "invalid rule": { IsValid: false, Rule: strings.NewReader(`{"a", "b"}`), }, "invalid operator": { IsValid: false, Rule: strings.NewReader(`{"filt":[[10, 1, 100], {">=":[{"var":""},2]}]}`), }, "invalid condition inside a filter": { IsValid: false, Rule: strings.NewReader(`{"filter":[{"var":"integers"}, {"=": [{"var":""}, [10]]}]}`), }, "primitive is a valid rule": { IsValid: true, Rule: strings.NewReader(`10`), }, "primitive map is a valid rule": { IsValid: true, Rule: strings.NewReader(`{"if": [{">=": [{ "var": "amount" }, 10] }, { "var": "amount" }, { "output": true, "result": "too low" } ]}`), }, "set must be valid": { IsValid: true, Rule: strings.NewReader(`{ "map": [ {"var": "objects"}, {"set": [ {"var": ""}, "age", {"+": [{"var": ".age"}, 2]} ]}, {"customOperator": [1, 2, 3]} ] }`), }, } for name, scenario := range scenarios { t.Run(fmt.Sprintf("SCENARIO:%s", name), func(t *testing.T) { assert.Equal(t, scenario.IsValid, jsonlogic.IsValid(scenario.Rule)) }) } } ================================================ FILE: vars.go ================================================ package jsonlogic import ( "encoding/json" "strconv" "strings" "github.com/diegoholiveira/jsonlogic/v3/internal/typing" ) func solveVars(values, data any) any { if typing.IsMap(values) { logic := map[string]any{} for key, value := range values.(map[string]any) { if key == "var" { if typing.IsString(value) && (value == "" || strings.HasPrefix(value.(string), ".")) { logic["var"] = value continue } val := getVar(value, data) if val != nil { return val } logic["var"] = value } else { logic[key] = solveVars(value, data) } } return any(logic) } if typing.IsSlice(values) { inputSlice := values.([]any) logic := make([]any, 0, len(inputSlice)) for _, value := range inputSlice { logic = append(logic, solveVars(value, data)) } return logic } return values } func getVar(values, data any) any { values = parseValues(values, data) if values == nil { if !typing.IsPrimitive(data) { return nil } return data } if typing.IsString(values) && typing.ToString(values) == "" { return data } if typing.IsNumber(values) { values = typing.ToString(values) } var _default any if typing.IsSlice(values) { // syntax sugar v := values.([]any) if len(v) == 0 { return data } if len(v) == 2 { _default = v[1] } values = v[0].(string) } if data == nil { return _default } parts := strings.Split(values.(string), ".") var _value any = data for _, part := range parts { if part == "" { continue } if typing.IsMap(_value) { _value = _value.(map[string]any)[part] } else if typing.IsSlice(_value) { pos := int(typing.ToNumber(part)) container := _value.([]any) if pos < 0 || pos >= len(container) { return _default } _value = container[pos] } else { return _default } if _value == nil { return _default } } return _value } func solveVarsBackToJsonLogic(rule, data any) (json.RawMessage, error) { ruleMap := rule.(map[string]any) result := make(map[string]any) for operator, values := range ruleMap { result[operator] = solveVars(values, data) } resultJson, err := json.Marshal(result) if err != nil { return nil, err } // we need to use Unquote due to unicode characters (example \u003e= need to be >=) // used for prettier json.RawMessage resultEscaped, err := strconv.Unquote(strings.Replace(strconv.Quote(string(resultJson)), `\\u`, `\u`, -1)) if err != nil { return nil, err } return []byte(resultEscaped), nil } // deepCopyMap returns a deep copy of a value produced by encoding/json: // map[string]any, []any, or a primitive. It only handles the types that // json.Unmarshal produces, which is all we need here. func deepCopyMap(v any) any { switch val := v.(type) { case map[string]any: out := make(map[string]any, len(val)) for k, v2 := range val { out[k] = deepCopyMap(v2) } return out case []any: out := make([]any, len(val)) for i, v2 := range val { out[i] = deepCopyMap(v2) } return out default: return val } } func setProperty(values, data any) any { values = parseValues(values, data).([]any) _value := values.([]any) if len(_value) < 3 { if len(_value) == 0 { return nil } return _value[0] } object := _value[0] if !typing.IsMap(object) { return object } property := _value[1].(string) _modified := deepCopyMap(object).(map[string]any) _modified[property] = parseValues(_value[2], data) return any(_modified) } ================================================ FILE: vars_test.go ================================================ package jsonlogic_test import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" jsonlogic "github.com/diegoholiveira/jsonlogic/v3" ) func TestSetProperty(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "set": [ {"a": 1, "b": 2}, "c", 3 ] }`) var expected json.RawMessage = json.RawMessage(`{"a":1,"b":2,"c":3}`) output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestSetPropertyWithNonMapInput(t *testing.T) { var rule json.RawMessage = json.RawMessage(`{ "set": [ "not_a_map", "property", "value" ] }`) var expected json.RawMessage = json.RawMessage(`"not_a_map"`) output, err := jsonlogic.ApplyRaw(rule, nil) if err != nil { t.Fatal(err) } assert.JSONEq(t, string(expected), string(output)) } func TestGetJsonLogicWithSolvedVarsInvalidRule(t *testing.T) { rule := json.RawMessage(`invalid_json`) data := json.RawMessage(`{}`) _, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data) assert.Error(t, err) } func TestGetJsonLogicWithSolvedVarsInvalidData(t *testing.T) { rule := json.RawMessage(`{}`) data := json.RawMessage(`invalid_json`) _, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data) assert.Error(t, err) } func TestGetJsonLogicWithSolvedVarsNoData(t *testing.T) { rule := json.RawMessage(`{"var": "foo"}`) var data json.RawMessage = nil output, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data) if err != nil { t.Fatal(err) } expected := `{"var":"foo"}` assert.JSONEq(t, expected, string(output)) } func TestSolveVarsBackToJsonLogicWithUnicodeChars(t *testing.T) { rule := json.RawMessage(`{">=":[{"var":"value"},10]}`) data := json.RawMessage(`{"value":20}`) output, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data) if err != nil { t.Fatal(err) } expected := `{">=":[20,10]}` assert.JSONEq(t, expected, string(output)) } func TestGetVarWithOutOfBoundsArrayIndex(t *testing.T) { rule := json.RawMessage(`{"var": "items.999"}`) data := json.RawMessage(`{"items": [1, 2, 3]}`) output, err := jsonlogic.ApplyRaw(rule, data) assert.NoError(t, err) assert.JSONEq(t, `null`, string(output)) } func TestGetVarWithOutOfBoundsArrayIndexReturnsDefault(t *testing.T) { rule := json.RawMessage(`{"var": ["items.999", "fallback"]}`) data := json.RawMessage(`{"items": [1, 2, 3]}`) output, err := jsonlogic.ApplyRaw(rule, data) assert.NoError(t, err) assert.JSONEq(t, `"fallback"`, string(output)) } func TestSetPropertyWithMissingValueArgument(t *testing.T) { rule := json.RawMessage(`{"set": [{"a": 1, "b": 2}, "c"]}`) output, err := jsonlogic.ApplyRaw(rule, nil) assert.NoError(t, err) assert.JSONEq(t, `{"a":1,"b":2}`, string(output)) } func TestSetPropertyWithOnlyObjectArgument(t *testing.T) { rule := json.RawMessage(`{"set": [{"a": 1, "b": 2}]}`) output, err := jsonlogic.ApplyRaw(rule, nil) assert.NoError(t, err) assert.JSONEq(t, `{"a":1,"b":2}`, string(output)) }