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