Full Code of diegoholiveira/jsonlogic for AI

main c60748e7ebf3 cached
37 files
139.5 KB
43.9k tokens
196 symbols
1 requests
Download .txt
Repository: diegoholiveira/jsonlogic
Branch: main
Commit: c60748e7ebf3
Files: 37
Total size: 139.5 KB

Directory structure:
gitextract_pw5i5pga/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── arrays.go
├── arrays_test.go
├── benchmark/
│   ├── README.md
│   ├── bench
│   └── benchmark_test.go
├── codecov.yml
├── comp.go
├── comp_test.go
├── go.mod
├── go.sum
├── internal/
│   ├── javascript/
│   │   ├── javascript.go
│   │   └── javascript_test.go
│   ├── json_logic_pr_48_tests.json
│   ├── testing.go
│   └── typing/
│       ├── typing.go
│       └── typing_test.go
├── issues_test.go
├── jsonlogic.go
├── jsonlogic_test.go
├── lists.go
├── lists_test.go
├── logic.go
├── math.go
├── math_test.go
├── operation.go
├── operation_test.go
├── readme.md
├── strings.go
├── strings_test.go
├── validator.go
├── validator_test.go
├── vars.go
└── vars_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: Continuous Integration
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go: ['1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24']
    name: Running with Go ${{ matrix.go }}
    steps:
    - name: Install Go
      uses: actions/setup-go@v5
      with:
        go-version: ${{ matrix.go }}
        cache: false
    - name: Checkout code
      uses: actions/checkout@v4
    - name: Run the tests
      run: go test -race -coverprofile=coverage.out -covermode=atomic
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v5


================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
*.prof

.idea/
.vscode/

benchmark/*.txt


================================================
FILE: .golangci.yml
================================================
run:
  timeout: 5m
linters-settings:
  goimports:
    local-prefixes: github.com/diegoholiveira/jsonlogic
linters:
  disable-all: true
  enable:
    - bodyclose
    - errcheck
    - goimports
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - typecheck
    - unparam
    - unused
    - whitespace


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2018 Diego Henrique Oliveira

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: arrays.go
================================================
package jsonlogic

import (
	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

// containsAll checks if all elements in the second array exist in the first array.
// Returns true if every element of the required array is found in the search array.
//
// Example:
//
//	{"contains_all": [["a", "b", "c"], ["a", "b"]]} // true
//	{"contains_all": [["a", "b"], ["a", "b", "c"]]} // false
func containsAll(values, data any) any {
	parsed, ok := values.([]any)
	if !ok || len(parsed) != 2 {
		return false
	}

	searchArray := toAnySlice(parsed[0])
	if searchArray == nil {
		return false
	}

	requiredArray := toAnySlice(parsed[1])
	if requiredArray == nil {
		return false
	}

	// Empty required array means all are "contained"
	if len(requiredArray) == 0 {
		return true
	}

	for _, required := range requiredArray {
		if !containsElement(searchArray, required) {
			return false
		}
	}

	return true
}

// containsAny checks if any element in the second array exists in the first array.
// Returns true if at least one element of the check array is found in the search array.
//
// Example:
//
//	{"contains_any": [["a", "b", "c"], ["x", "b"]]} // true
//	{"contains_any": [["a", "b", "c"], ["x", "y"]]} // false
func containsAny(values, data any) any {
	parsed, ok := values.([]any)
	if !ok || len(parsed) != 2 {
		return false
	}

	searchArray := toAnySlice(parsed[0])
	if searchArray == nil {
		return false
	}

	checkArray := toAnySlice(parsed[1])
	if checkArray == nil {
		return false
	}

	for _, check := range checkArray {
		if containsElement(searchArray, check) {
			return true
		}
	}

	return false
}

// containsNone checks if no elements in the second array exist in the first array.
// Returns true if none of the elements of the check array are found in the search array.
//
// Example:
//
//	{"contains_none": [["a", "b", "c"], ["x", "y"]]} // true
//	{"contains_none": [["a", "b", "c"], ["x", "b"]]} // false
func containsNone(values, data any) any {
	parsed, ok := values.([]any)
	if !ok || len(parsed) != 2 {
		return true
	}

	searchArray := toAnySlice(parsed[0])
	if searchArray == nil {
		return true
	}

	checkArray := toAnySlice(parsed[1])
	if checkArray == nil {
		return true
	}

	for _, check := range checkArray {
		if containsElement(searchArray, check) {
			return false
		}
	}

	return true
}

// toAnySlice converts an interface{} to []any if possible.
func toAnySlice(value any) []any {
	if value == nil {
		return nil
	}

	if slice, ok := value.([]any); ok {
		return slice
	}

	return nil
}

// containsElement checks if an element exists in a slice using proper comparison.
func containsElement(slice []any, element any) bool {
	for _, item := range slice {
		if isEqualValue(item, element) {
			return true
		}
	}
	return false
}

// isEqualValue compares two values with type coercion for numbers.
func isEqualValue(a, b any) bool {
	// Direct equality check
	if a == b {
		return true
	}

	// Handle number comparison with type coercion
	if typing.IsNumber(a) && typing.IsNumber(b) {
		return typing.ToNumber(a) == typing.ToNumber(b)
	}

	// Handle string comparison
	if typing.IsString(a) && typing.IsString(b) {
		return a.(string) == b.(string)
	}

	return false
}


================================================
FILE: arrays_test.go
================================================
package jsonlogic

import (
	"bytes"
	"strings"
	"testing"
)

func TestContainsAll(t *testing.T) {
	tests := []struct {
		name     string
		rule     string
		data     string
		expected string
	}{
		{
			name:     "all elements present",
			rule:     `{"contains_all": [["a", "b", "c"], ["a", "b"]]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "all elements present - exact match",
			rule:     `{"contains_all": [["a", "b"], ["a", "b"]]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "some elements missing",
			rule:     `{"contains_all": [["a", "b"], ["a", "b", "c"]]}`,
			data:     `{}`,
			expected: "false",
		},
		{
			name:     "empty required array",
			rule:     `{"contains_all": [["a", "b", "c"], []]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "empty search array",
			rule:     `{"contains_all": [[], ["a"]]}`,
			data:     `{}`,
			expected: "false",
		},
		{
			name:     "with variable",
			rule:     `{"contains_all": [{"var": "selected"}, ["vip", "premium"]]}`,
			data:     `{"selected": ["vip", "premium", "gold"]}`,
			expected: "true",
		},
		{
			name:     "with variable - missing element",
			rule:     `{"contains_all": [{"var": "selected"}, ["vip", "diamond"]]}`,
			data:     `{"selected": ["vip", "premium", "gold"]}`,
			expected: "false",
		},
		{
			name:     "with numbers",
			rule:     `{"contains_all": [[1, 2, 3, 4], [1, 3]]}`,
			data:     `{}`,
			expected: "true",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var result bytes.Buffer
			err := Apply(strings.NewReader(tt.rule), strings.NewReader(tt.data), &result)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if strings.TrimSpace(result.String()) != tt.expected {
				t.Errorf("expected %s, got %s", tt.expected, result.String())
			}
		})
	}
}

func TestContainsAny(t *testing.T) {
	tests := []struct {
		name     string
		rule     string
		data     string
		expected string
	}{
		{
			name:     "one element present",
			rule:     `{"contains_any": [["a", "b", "c"], ["x", "b"]]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "multiple elements present",
			rule:     `{"contains_any": [["a", "b", "c"], ["a", "c"]]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "no elements present",
			rule:     `{"contains_any": [["a", "b", "c"], ["x", "y"]]}`,
			data:     `{}`,
			expected: "false",
		},
		{
			name:     "empty check array",
			rule:     `{"contains_any": [["a", "b", "c"], []]}`,
			data:     `{}`,
			expected: "false",
		},
		{
			name:     "empty search array",
			rule:     `{"contains_any": [[], ["a"]]}`,
			data:     `{}`,
			expected: "false",
		},
		{
			name:     "with variable",
			rule:     `{"contains_any": [{"var": "tags"}, ["urgent", "important"]]}`,
			data:     `{"tags": ["normal", "urgent"]}`,
			expected: "true",
		},
		{
			name:     "with variable - no match",
			rule:     `{"contains_any": [{"var": "tags"}, ["urgent", "important"]]}`,
			data:     `{"tags": ["normal", "low"]}`,
			expected: "false",
		},
		{
			name:     "with numbers",
			rule:     `{"contains_any": [[1, 2, 3], [5, 3, 7]]}`,
			data:     `{}`,
			expected: "true",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var result bytes.Buffer
			err := Apply(strings.NewReader(tt.rule), strings.NewReader(tt.data), &result)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if strings.TrimSpace(result.String()) != tt.expected {
				t.Errorf("expected %s, got %s", tt.expected, result.String())
			}
		})
	}
}

func TestContainsNone(t *testing.T) {
	tests := []struct {
		name     string
		rule     string
		data     string
		expected string
	}{
		{
			name:     "no elements present",
			rule:     `{"contains_none": [["a", "b", "c"], ["x", "y"]]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "one element present",
			rule:     `{"contains_none": [["a", "b", "c"], ["x", "b"]]}`,
			data:     `{}`,
			expected: "false",
		},
		{
			name:     "all elements present",
			rule:     `{"contains_none": [["a", "b", "c"], ["a", "b"]]}`,
			data:     `{}`,
			expected: "false",
		},
		{
			name:     "empty check array",
			rule:     `{"contains_none": [["a", "b", "c"], []]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "empty search array",
			rule:     `{"contains_none": [[], ["a"]]}`,
			data:     `{}`,
			expected: "true",
		},
		{
			name:     "with variable - blocked words not present",
			rule:     `{"contains_none": [{"var": "content"}, ["spam", "blocked"]]}`,
			data:     `{"content": ["hello", "world"]}`,
			expected: "true",
		},
		{
			name:     "with variable - blocked word present",
			rule:     `{"contains_none": [{"var": "content"}, ["spam", "blocked"]]}`,
			data:     `{"content": ["hello", "spam"]}`,
			expected: "false",
		},
		{
			name:     "with numbers",
			rule:     `{"contains_none": [[1, 2, 3], [7, 8, 9]]}`,
			data:     `{}`,
			expected: "true",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var result bytes.Buffer
			err := Apply(strings.NewReader(tt.rule), strings.NewReader(tt.data), &result)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if strings.TrimSpace(result.String()) != tt.expected {
				t.Errorf("expected %s, got %s", tt.expected, result.String())
			}
		})
	}
}


================================================
FILE: benchmark/README.md
================================================
# JSONLogic Benchmark

Benchmark suite to compare performance between different versions of the JSONLogic library.

## Prerequisites

- Go 1.21+
- `benchstat` (install with: `go install golang.org/x/perf/cmd/benchstat@latest`)

## Usage

Compare your current code against a published version:

```bash
./bench v3.7.5
```

The script will:
1. Create an isolated git worktree for the target version
2. **Copy current benchmark code** to target version (ensures fair comparison)
3. Run **comprehensive benchmarks** for both versions (10 iterations each)
4. Display statistical comparison using `benchstat`
5. Clean up automatically

### Benchmark Suites

By default, the script runs the **comprehensive suite** (8 complex benchmarks, ~6-8s total):
```bash
./bench v3.7.5  # Fast, realistic comparison
```

To run the **detailed suite** (all 65 benchmarks, ~60s total):
```bash
./bench v3.7.5 BenchmarkDetailed
```

To run specific benchmark categories:
```bash
./bench v3.7.5 BenchmarkMathOperations
./bench v3.7.5 BenchmarkArrayOperationsScaling
```

## Understanding the Output

`benchstat` shows the performance comparison:

```
name                old time/op    new time/op    delta
JSONLogic/simple-8    900ns ± 2%     850ns ± 3%   -5.56%  (p=0.000 n=10+10)
```

- **old time/op**: Target version performance
- **new time/op**: Current code performance
- **delta**: Percentage change (negative = improvement, positive = regression)
- **±**: Variation/noise in measurements
- **p-value**: Statistical significance (p < 0.05 means the difference is real)

## Benchmark Suites

### Comprehensive Suite (Default)

The comprehensive suite contains 8 complex, realistic benchmarks that exercise multiple operators:

1. **user_validation** - Complex user validation with `and`, `or`, `>=`, `in`, `==`, `+`, `var`
2. **data_pipeline** - Filter + reduce chain for data aggregation
3. **business_rules** - Nested if/else with conditional pricing logic
4. **array_validation** - Combines `all`, `some`, `none` for array validation
5. **string_processing** - String operations with `in`, `substr`, and comparisons
6. **custom_operators** - Tests `contains_all`, `contains_any`, `contains_none`
7. **complex_data_transform** - Chained `filter` + `map` with complex conditions
8. **nested_conditions** - Multi-level conditionals with `missing`, `missing_some`, `cat`

Each benchmark represents real-world usage patterns and exercises 5-7 operators per test.

### Detailed Suite

The detailed suite contains all individual operator benchmarks organized by category:

### Core Operations
- **baseline_noop**: Minimal baseline benchmark (just `true`)
- **simple_equal**: Basic equality check
- **complex_condition**: Nested logical operators with `and`
- **nested_var**: Deep variable path access with defaults
- **complex_logic**: Conditional if/else logic

### Array Operations
- **array_operations**: Array map operations
- **reduce_sum**: Reduce operation with sum accumulation
- **filter_even_numbers**: Filter with modulo operation
- **all_validation**: All operator for validation patterns
- **some_validation**: Some operator for validation patterns
- **merge_arrays**: Merge multiple arrays

### Custom Operators
- **contains_all**: Tests if all elements exist in array
- **contains_any**: Tests if any element exists in array
- **contains_none**: Tests if no elements exist in array

### String Operations
- **string_concatenation**: String concatenation with `cat`
- **substring_extraction**: Substring extraction with `substr`

### Math Operations
- **max_operation**: Find maximum value
- **min_operation**: Find minimum value
- **modulo_operation**: Modulo operator

### Logic Operations
- **or_operation**: Logical OR operator

### Field Validation
- **missing_fields**: Missing field detection

### Complex Scenarios
- **deeply_nested_operations**: Nested filter and map operations

## Benchmark Categories

The benchmark suite is organized into multiple categories for targeted testing:

### Run All Benchmarks
```bash
go test -bench=. ./benchmark/
```

### Run Specific Categories

**Main benchmark suite** (all test cases):
```bash
go test -bench=BenchmarkJSONLogic$ ./benchmark/
```

**Parallel benchmarks** (concurrent performance testing):
```bash
go test -bench=BenchmarkJSONLogicParallel ./benchmark/
```

**Scaling benchmarks** (tests with 10, 100, 1000 element arrays):
```bash
go test -bench=BenchmarkArrayOperationsScaling ./benchmark/
```

**Math operations only**:
```bash
go test -bench=BenchmarkMathOperations ./benchmark/
```

**String operations only**:
```bash
go test -bench=BenchmarkStringOperations ./benchmark/
```

**Logic operations only**:
```bash
go test -bench=BenchmarkLogicOperations ./benchmark/
```

**Custom operators only**:
```bash
go test -bench=BenchmarkCustomOperators ./benchmark/
```

## Benchmark Types

### 1. Standard Benchmarks (`BenchmarkJSONLogic`)
Tests all 22 core operations with realistic data sizes.

### 2. Parallel Benchmarks (`BenchmarkJSONLogicParallel`)
Tests concurrent usage with `RunParallel` for:
- simple_equal
- map
- reduce
- filter

### 3. Scaling Benchmarks (`BenchmarkArrayOperationsScaling`)
Tests performance with different array sizes (10, 100, 1000 elements) for:
- map
- filter
- reduce
- all
- some
- none

### 4. Categorical Benchmarks
Organized by operation type for focused testing:
- Math: +, -, *, /, %, max, min, abs
- String: cat, substr, in
- Logic: and, or, !, if
- Custom: contains_all, contains_any, contains_none

## Advanced Profiling

### Memory profiling:
```bash
cd benchmark
go test -bench=. -benchmem -memprofile=mem.prof
go tool pprof -http=:8080 mem.prof
```

### CPU profiling:
```bash
go test -bench=. -cpuprofile=cpu.prof
go tool pprof -http=:8080 cpu.prof
```

### Compare scaling performance:
```bash
go test -bench=BenchmarkArrayOperationsScaling/map -benchmem
```

This will show how map performance scales from 10 to 1000 elements.


================================================
FILE: benchmark/bench
================================================
#!/usr/bin/env bash
set -e

# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m'

# Check if benchstat is installed
if ! command -v benchstat &>/dev/null; then
	echo -e "${RED}Error: benchstat is not installed${NC}"
	echo "Install it with: go install golang.org/x/perf/cmd/benchstat@latest"
	exit 1
fi

# Validate argument
if [ $# -eq 0 ]; then
	echo -e "${RED}Error: Missing version argument${NC}"
	echo "Usage: $0 <version>"
	echo "Example: $0 v3.7.5"
	exit 1
fi

TARGET_REF="$1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"

# Temporary worktree directory
WORKTREE_DIR=$(mktemp -d)
OLD_BENCH="$SCRIPT_DIR/old.txt"
NEW_BENCH="$SCRIPT_DIR/new.txt"

# Cleanup function
cleanup() {
	echo -e "${BLUE}Cleaning up...${NC}"
	git -C "$PROJECT_ROOT" worktree remove "$WORKTREE_DIR" --force 2>/dev/null || true
	rm -rf "$WORKTREE_DIR"
}
trap cleanup EXIT

echo -e "${GREEN}JSONLogic Benchmark: $TARGET_REF vs current${NC}"
echo

# Benchmark filter (default to comprehensive suite for fast comparisons)
BENCH_FILTER="${2:-BenchmarkComprehensive$}"

# Create worktree for target ref
echo -e "${BLUE}Setting up worktree for $TARGET_REF...${NC}"
git -C "$PROJECT_ROOT" worktree add "$WORKTREE_DIR" "$TARGET_REF" --quiet

# Copy current benchmark code to ensure fair comparison
echo -e "${BLUE}Copying current benchmark code to $TARGET_REF worktree...${NC}"
cp "$PROJECT_ROOT/benchmark/benchmark_test.go" "$WORKTREE_DIR/benchmark/benchmark_test.go"

# Run benchmarks for target ref
echo -e "${BLUE}Running benchmarks for $TARGET_REF...${NC}"
cd "$WORKTREE_DIR/benchmark"
go test -bench="$BENCH_FILTER" -benchmem -count=10 >"$OLD_BENCH" 2>&1

# Run benchmarks for current code
echo -e "${BLUE}Running benchmarks for current code...${NC}"
cd "$PROJECT_ROOT/benchmark"
go test -bench="$BENCH_FILTER" -benchmem -count=10 >"$NEW_BENCH" 2>&1

# Show comparison
echo
echo -e "${GREEN}Results:${NC}"
benchstat "$OLD_BENCH" "$NEW_BENCH"


================================================
FILE: benchmark/benchmark_test.go
================================================
package benchmark

import (
	"bytes"
	"fmt"
	"runtime"
	"strings"
	"testing"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

var TestCases = []struct {
	name  string
	logic string
	data  string
}{
	{
		name:  "baseline_noop",
		logic: `true`,
		data:  `{}`,
	},
	{
		name:  "simple_equal",
		logic: `{"==": [1, 1]}`,
		data:  `{}`,
	},
	{
		name:  "complex_condition",
		logic: `{"and": [{"<": [{"var": "temp"}, 110]}, {"==": [{"var": "pie.filling"}, "apple"]}]}`,
		data:  `{"temp": 100, "pie": {"filling": "apple"}}`,
	},
	{
		name:  "nested_var",
		logic: `{"var": ["deeply.nested.variable", 99]}`,
		data:  `{"deeply": {"nested": {"variable": 42}}}`,
	},
	{
		name:  "array_operations",
		logic: `{"map": [{"var": "integers"}, {"*": [{"var": ""}, 2]}]}`,
		data:  `{"integers": [1, 2, 3, 4, 5]}`,
	},
	{
		name: "complex_logic",
		logic: `{"if": [
            {"<": [{"var": "age"}, 18]},
            "Too young",
            {"and": [
                {"<": [{"var": "age"}, 65]},
                {">=": [{"var": "age"}, 18]}
            ]},
            "Adult",
            "Senior"
        ]}`,
		data: `{"age": 25}`,
	},
	{
		name:  "reduce_sum",
		logic: `{"reduce": [{"var": "numbers"}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}`,
		data:  `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`,
	},
	{
		name:  "filter_even_numbers",
		logic: `{"filter": [{"var": "numbers"}, {"==": [{"%": [{"var": ""}, 2]}, 0]}]}`,
		data:  `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`,
	},
	{
		name:  "contains_all",
		logic: `{"contains_all": [{"var": "tags"}, ["urgent", "reviewed"]]}`,
		data:  `{"tags": ["urgent", "reviewed", "approved", "processed"]}`,
	},
	{
		name:  "contains_any",
		logic: `{"contains_any": [{"var": "permissions"}, ["admin", "superuser"]]}`,
		data:  `{"permissions": ["user", "editor", "admin"]}`,
	},
	{
		name:  "contains_none",
		logic: `{"contains_none": [{"var": "flags"}, ["banned", "suspended"]]}`,
		data:  `{"flags": ["active", "verified", "premium"]}`,
	},
	{
		name:  "all_validation",
		logic: `{"all": [{"var": "users"}, {">": [{"var": ".age"}, 18]}]}`,
		data:  `{"users": [{"age": 25}, {"age": 30}, {"age": 22}, {"age": 19}]}`,
	},
	{
		name:  "some_validation",
		logic: `{"some": [{"var": "items"}, {"<": [{"var": ".price"}, 100]}]}`,
		data:  `{"items": [{"price": 150}, {"price": 75}, {"price": 200}]}`,
	},
	{
		name:  "string_concatenation",
		logic: `{"cat": [{"var": "firstName"}, " ", {"var": "lastName"}]}`,
		data:  `{"firstName": "John", "lastName": "Doe"}`,
	},
	{
		name:  "substring_extraction",
		logic: `{"substr": [{"var": "text"}, 0, 10]}`,
		data:  `{"text": "The quick brown fox jumps over the lazy dog"}`,
	},
	{
		name:  "max_operation",
		logic: `{"max": [85, 92, 78, 95, 88]}`,
		data:  `{}`,
	},
	{
		name:  "min_operation",
		logic: `{"min": [19.99, 15.50, 22.00, 12.99]}`,
		data:  `{}`,
	},
	{
		name:  "modulo_operation",
		logic: `{"%": [{"var": "value"}, 3]}`,
		data:  `{"value": 17}`,
	},
	{
		name:  "or_operation",
		logic: `{"or": [{"<": [{"var": "age"}, 18]}, {">": [{"var": "age"}, 65]}]}`,
		data:  `{"age": 70}`,
	},
	{
		name:  "merge_arrays",
		logic: `{"merge": [{"var": "array1"}, {"var": "array2"}]}`,
		data:  `{"array1": [1, 2, 3], "array2": [4, 5, 6]}`,
	},
	{
		name:  "missing_fields",
		logic: `{"missing": ["name", "email", "phone"]}`,
		data:  `{"name": "John", "email": "john@example.com"}`,
	},
	{
		name:  "deeply_nested_operations",
		logic: `{"and": [{"filter": [{"var": "users"}, {">": [{"var": ".age"}, 18]}]}, {"map": [{"var": "items"}, {"*": [{"var": ".price"}, 1.1]}]}]}`,
		data:  `{"users": [{"age": 25}, {"age": 30}], "items": [{"price": 10}, {"price": 20}]}`,
	},
}

func performWarmupRuns() {
	runtime.GC()

	for _, tc := range TestCases {
		for i := 0; i < 10; i++ {
			logic := strings.NewReader(tc.logic)
			data := strings.NewReader(tc.data)
			var result bytes.Buffer
			_ = jsonlogic.Apply(logic, data, &result)
		}
	}

	runtime.GC()
}

// Helper function to reduce duplication in benchmarks
func runBenchmark(b *testing.B, logic, data string) {
	// Pre-convert to bytes to avoid string overhead in loop
	logicBytes := []byte(logic)
	dataBytes := []byte(data)

	b.ResetTimer()
	b.ReportAllocs()

	for i := 0; i < b.N; i++ {
		logicReader := bytes.NewReader(logicBytes)
		dataReader := bytes.NewReader(dataBytes)
		var result bytes.Buffer
		err := jsonlogic.Apply(logicReader, dataReader, &result)
		if err != nil {
			b.Fatal(err)
		}
	}
}

// BenchmarkComprehensive runs a focused suite of complex, realistic benchmarks
// that exercise multiple operators and represent real-world usage patterns.
// This is the default benchmark suite for version comparisons.
func BenchmarkComprehensive(b *testing.B) {
	cases := []struct {
		name  string
		logic string
		data  string
	}{
		{
			name: "user_validation",
			logic: `{
				"and": [
					{">=": [{"var": "user.age"}, 18]},
					{"in": [{"var": "user.country"}, ["US", "CA", "UK", "AU"]]},
					{"or": [
						{"==": [{"var": "user.subscription"}, "premium"]},
						{"<": [{"+": [{"var": "user.loginCount"}, 1]}, 100]}
					]}
				]
			}`,
			data: `{"user": {"age": 25, "country": "US", "subscription": "premium", "loginCount": 50}}`,
		},
		{
			name: "data_pipeline",
			logic: `{
				"reduce": [
					{"filter": [
						{"var": "orders"},
						{">=": [{"var": ".amount"}, 100]}
					]},
					{"+": [{"var": "accumulator"}, {"var": "current.amount"}]},
					0
				]
			}`,
			data: `{"orders": [{"amount": 50}, {"amount": 150}, {"amount": 200}, {"amount": 75}, {"amount": 120}]}`,
		},
		{
			name: "business_rules",
			logic: `{
				"if": [
					{"and": [
						{">": [{"var": "order.total"}, 1000]},
						{"==": [{"var": "customer.tier"}, "gold"]}
					]},
					{"*": [{"var": "order.total"}, 0.8]},
					{">": [{"var": "order.total"}, 500]},
					{"*": [{"var": "order.total"}, 0.9]},
					{"var": "order.total"}
				]
			}`,
			data: `{"order": {"total": 1200}, "customer": {"tier": "gold"}}`,
		},
		{
			name: "array_validation",
			logic: `{
				"and": [
					{"all": [{"var": "items"}, {">": [{"var": ".quantity"}, 0]}]},
					{"some": [{"var": "items"}, {"<": [{"var": ".price"}, 50]}]},
					{"none": [{"var": "items"}, {"==": [{"var": ".status"}, "cancelled"]}]}
				]
			}`,
			data: `{"items": [{"quantity": 2, "price": 30, "status": "active"}, {"quantity": 1, "price": 75, "status": "active"}]}`,
		},
		{
			name: "string_processing",
			logic: `{
				"and": [
					{"in": ["error", {"var": "message"}]},
					{">": [{"var": "severity"}, 5]},
					{"==": [{"substr": [{"var": "code"}, 0, 3]}, "ERR"]}
				]
			}`,
			data: `{"message": "System error detected", "severity": 8, "code": "ERR-500"}`,
		},
		{
			name: "custom_operators",
			logic: `{
				"and": [
					{"contains_all": [{"var": "required_permissions"}, ["read", "write"]]},
					{"contains_any": [{"var": "user_roles"}, ["admin", "moderator"]]},
					{"contains_none": [{"var": "flags"}, ["banned", "suspended"]]}
				]
			}`,
			data: `{"required_permissions": ["read", "write", "execute"], "user_roles": ["admin", "user"], "flags": ["active", "verified"]}`,
		},
		{
			name: "complex_data_transform",
			logic: `{
				"map": [
					{"filter": [
						{"var": "products"},
						{"and": [
							{"in": [{"var": ".category"}, ["electronics", "accessories"]]},
							{">": [{"var": ".stock"}, 0]}
						]}
					]},
					{"*": [{"var": ".price"}, 1.1]}
				]
			}`,
			data: `{"products": [{"category": "electronics", "price": 100, "stock": 5}, {"category": "clothing", "price": 50, "stock": 10}, {"category": "accessories", "price": 25, "stock": 0}]}`,
		},
		{
			name: "nested_conditions",
			logic: `{
				"if": [
					{"and": [
						{"missing": ["name", "email"]},
						{">": [{"var": "age"}, 0]}
					]},
					{"cat": ["Missing required fields for user ", {"var": "id"}]},
					{"or": [
						{"<": [{"var": "age"}, 13]},
						{"missing_some": [1, ["parent_email", "guardian_name"]]}
					]},
					"Parental consent required",
					"Valid user"
				]
			}`,
			data: `{"name": "John", "email": "john@example.com", "age": 25, "id": "12345"}`,
		},
	}

	for _, tc := range cases {
		b.Run(tc.name, func(b *testing.B) {
			runBenchmark(b, tc.logic, tc.data)
		})
	}
}

// BenchmarkDetailed runs all detailed benchmarks (65 total).
// Use this for comprehensive testing of individual operators.
// For version comparisons, use BenchmarkComprehensive instead.
func BenchmarkDetailed(b *testing.B) {
	performWarmupRuns()

	for _, tc := range TestCases {
		b.Run(tc.name, func(b *testing.B) {
			runBenchmark(b, tc.logic, tc.data)
		})
	}
}

// Parallel benchmarks for testing concurrent performance
func BenchmarkJSONLogicParallel(b *testing.B) {
	parallelCases := []struct {
		name  string
		logic string
		data  string
	}{
		{
			name:  "simple_equal",
			logic: `{"==": [1, 1]}`,
			data:  `{}`,
		},
		{
			name:  "map",
			logic: `{"map": [{"var": "integers"}, {"*": [{"var": ""}, 2]}]}`,
			data:  `{"integers": [1, 2, 3, 4, 5]}`,
		},
		{
			name:  "reduce",
			logic: `{"reduce": [{"var": "numbers"}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}`,
			data:  `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`,
		},
		{
			name:  "filter",
			logic: `{"filter": [{"var": "numbers"}, {"==": [{"%": [{"var": ""}, 2]}, 0]}]}`,
			data:  `{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}`,
		},
	}

	for _, tc := range parallelCases {
		b.Run(tc.name, func(b *testing.B) {
			logicBytes := []byte(tc.logic)
			dataBytes := []byte(tc.data)

			b.ResetTimer()
			b.ReportAllocs()
			b.RunParallel(func(pb *testing.PB) {
				for pb.Next() {
					logic := bytes.NewReader(logicBytes)
					data := bytes.NewReader(dataBytes)
					var result bytes.Buffer
					err := jsonlogic.Apply(logic, data, &result)
					if err != nil {
						b.Fatal(err)
					}
				}
			})
		})
	}
}

// Size variation benchmarks to test scalability
func BenchmarkArrayOperationsScaling(b *testing.B) {
	sizes := []struct {
		name string
		size int
	}{
		{"small_10", 10},
		{"medium_100", 100},
		{"large_1000", 1000},
	}

	generateIntArray := func(size int) string {
		result := "["
		for i := 0; i < size; i++ {
			if i > 0 {
				result += ","
			}
			result += fmt.Sprintf("%d", i+1)
		}
		result += "]"
		return result
	}

	// Map operation scaling
	b.Run("map", func(b *testing.B) {
		for _, s := range sizes {
			b.Run(s.name, func(b *testing.B) {
				logic := `{"map": [{"var": "integers"}, {"*": [{"var": ""}, 2]}]}`
				data := fmt.Sprintf(`{"integers": %s}`, generateIntArray(s.size))
				runBenchmark(b, logic, data)
			})
		}
	})

	// Filter operation scaling
	b.Run("filter", func(b *testing.B) {
		for _, s := range sizes {
			b.Run(s.name, func(b *testing.B) {
				logic := `{"filter": [{"var": "numbers"}, {"==": [{"%": [{"var": ""}, 2]}, 0]}]}`
				data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size))
				runBenchmark(b, logic, data)
			})
		}
	})

	// Reduce operation scaling
	b.Run("reduce", func(b *testing.B) {
		for _, s := range sizes {
			b.Run(s.name, func(b *testing.B) {
				logic := `{"reduce": [{"var": "numbers"}, {"+": [{"var": "accumulator"}, {"var": "current"}]}, 0]}`
				data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size))
				runBenchmark(b, logic, data)
			})
		}
	})

	// All operation scaling
	b.Run("all", func(b *testing.B) {
		for _, s := range sizes {
			b.Run(s.name, func(b *testing.B) {
				logic := `{"all": [{"var": "numbers"}, {">": [{"var": ""}, 0]}]}`
				data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size))
				runBenchmark(b, logic, data)
			})
		}
	})

	// Some operation scaling
	b.Run("some", func(b *testing.B) {
		for _, s := range sizes {
			b.Run(s.name, func(b *testing.B) {
				logic := `{"some": [{"var": "numbers"}, {">": [{"var": ""}, 500]}]}`
				data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size))
				runBenchmark(b, logic, data)
			})
		}
	})

	// None operation scaling
	b.Run("none", func(b *testing.B) {
		for _, s := range sizes {
			b.Run(s.name, func(b *testing.B) {
				logic := `{"none": [{"var": "numbers"}, {"<": [{"var": ""}, 0]}]}`
				data := fmt.Sprintf(`{"numbers": %s}`, generateIntArray(s.size))
				runBenchmark(b, logic, data)
			})
		}
	})
}

// Categorical benchmarks for easier filtering
func BenchmarkMathOperations(b *testing.B) {
	cases := []struct {
		name  string
		logic string
		data  string
	}{
		{"add", `{"+": [5, 3]}`, `{}`},
		{"subtract", `{"-": [10, 3]}`, `{}`},
		{"multiply", `{"*": [4, 5]}`, `{}`},
		{"divide", `{"/": [20, 4]}`, `{}`},
		{"modulo", `{"%": [17, 3]}`, `{}`},
		{"max", `{"max": [85, 92, 78, 95, 88]}`, `{}`},
		{"min", `{"min": [19.99, 15.50, 22.00, 12.99]}`, `{}`},
		{"abs", `{"abs": [-42]}`, `{}`},
	}

	for _, tc := range cases {
		b.Run(tc.name, func(b *testing.B) {
			runBenchmark(b, tc.logic, tc.data)
		})
	}
}

func BenchmarkStringOperations(b *testing.B) {
	cases := []struct {
		name  string
		logic string
		data  string
	}{
		{
			"concat",
			`{"cat": [{"var": "firstName"}, " ", {"var": "lastName"}]}`,
			`{"firstName": "John", "lastName": "Doe"}`,
		},
		{
			"substr",
			`{"substr": [{"var": "text"}, 0, 10]}`,
			`{"text": "The quick brown fox jumps over the lazy dog"}`,
		},
		{
			"in_string",
			`{"in": ["quick", {"var": "text"}]}`,
			`{"text": "The quick brown fox"}`,
		},
	}

	for _, tc := range cases {
		b.Run(tc.name, func(b *testing.B) {
			runBenchmark(b, tc.logic, tc.data)
		})
	}
}

func BenchmarkLogicOperations(b *testing.B) {
	cases := []struct {
		name  string
		logic string
		data  string
	}{
		{
			"and",
			`{"and": [{"<": [{"var": "temp"}, 110]}, {"==": [{"var": "status"}, "ok"]}]}`,
			`{"temp": 100, "status": "ok"}`,
		},
		{
			"or",
			`{"or": [{"<": [{"var": "age"}, 18]}, {">": [{"var": "age"}, 65]}]}`,
			`{"age": 70}`,
		},
		{
			"not",
			`{"!": [false]}`,
			`{}`,
		},
		{
			"if",
			`{"if": [{"<": [{"var": "age"}, 18]}, "minor", "adult"]}`,
			`{"age": 25}`,
		},
	}

	for _, tc := range cases {
		b.Run(tc.name, func(b *testing.B) {
			runBenchmark(b, tc.logic, tc.data)
		})
	}
}

func BenchmarkCustomOperators(b *testing.B) {
	cases := []struct {
		name  string
		logic string
		data  string
	}{
		{
			"contains_all",
			`{"contains_all": [{"var": "tags"}, ["urgent", "reviewed"]]}`,
			`{"tags": ["urgent", "reviewed", "approved", "processed"]}`,
		},
		{
			"contains_any",
			`{"contains_any": [{"var": "permissions"}, ["admin", "superuser"]]}`,
			`{"permissions": ["user", "editor", "admin"]}`,
		},
		{
			"contains_none",
			`{"contains_none": [{"var": "flags"}, ["banned", "suspended"]]}`,
			`{"flags": ["active", "verified", "premium"]}`,
		},
	}

	for _, tc := range cases {
		b.Run(tc.name, func(b *testing.B) {
			runBenchmark(b, tc.logic, tc.data)
		})
	}
}


================================================
FILE: codecov.yml
================================================
ignore:
  - internal/testing.go


================================================
FILE: comp.go
================================================
package jsonlogic

import (
	"reflect"

	"github.com/diegoholiveira/jsonlogic/v3/internal/javascript"
	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

func hardEquals(values, data any) any {
	values = parseValues(values, data)
	if !typing.IsSlice(values) {
		return false
	}

	parsed := values.([]any)
	if len(parsed) < 2 {
		return false
	}

	a, b := parsed[0], parsed[1]

	if a == nil || b == nil {
		return a == b
	}

	ra := reflect.ValueOf(a).Kind()
	rb := reflect.ValueOf(b).Kind()

	if ra != rb {
		return false
	}

	return equals(a, b)
}

func isLessThan(values, data any) any {
	parsed := parseValues(values, data).([]any)
	if len(parsed) < 2 {
		return false
	}

	a := parsed[0]
	b := parsed[1]

	if len(parsed) == 3 {
		c := parsed[2]

		return less(a, b) && less(b, c)
	}

	return less(a, b)
}

func isLessOrEqualThan(values, data any) any {
	parsed := parseValues(values, data).([]any)
	if len(parsed) < 2 {
		return false
	}

	a := parsed[0]
	b := parsed[1]

	if len(parsed) == 3 {
		c := parsed[2]

		return (less(a, b) || equals(a, b)) && (less(b, c) || equals(b, c))
	}

	return less(a, b) || equals(a, b)
}

func isGreaterThan(values, data any) any {
	parsed := parseValues(values, data).([]any)
	if len(parsed) < 2 {
		return false
	}
	a := parsed[0]
	b := parsed[1]

	if len(parsed) == 3 {
		c := parsed[2]

		return less(c, b) && less(b, a)
	}

	return less(b, a)
}

func isGreaterOrEqualThan(values, data any) any {
	parsed := parseValues(values, data).([]any)
	if len(parsed) < 2 {
		return false
	}

	a := parsed[0]
	b := parsed[1]

	if len(parsed) == 3 {
		c := parsed[2]

		return (less(c, b) || equals(c, b)) && (less(b, a) || equals(b, a))
	}

	return less(b, a) || equals(b, a)
}

func isEqual(values, data any) any {
	parsed := parseValues(values, data).([]any)
	if len(parsed) < 2 {
		return false
	}

	a := parsed[0]
	b := parsed[1]

	return equals(a, b)
}

// less reference javascript implementation
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Less_than#description
func less(a, b any) bool {
	// If both values are strings, they are compared as strings,
	// based on the values of the Unicode code points they contain.
	if typing.IsString(a) && typing.IsString(b) {
		return typing.ToString(b) > typing.ToString(a)
	}

	// Otherwise the values are compared as numeric values.
	return javascript.ToNumber(b) > javascript.ToNumber(a)
}

func equals(a, b any) bool {
	// comparison to a nil value is falsy
	if a == nil || b == nil {
		// if a and b is nil, return true, else return falsy
		return a == b
	}

	if typing.IsString(a) && typing.IsString(b) {
		return a == b
	}

	return javascript.ToNumber(a) == javascript.ToNumber(b)
}


================================================
FILE: comp_test.go
================================================
package jsonlogic_test

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

func TestHardEqualsWithNonSliceValues(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"===": 42
	}`)

	var expected json.RawMessage = json.RawMessage("false")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestHardEqualsWithSingleValueInSlice(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"===": [42]
	}`)

	var expected json.RawMessage = json.RawMessage("false")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestHardEqualsWithNilInParams(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"===": [null, 42]
	}`)

	var expected json.RawMessage = json.RawMessage("false")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))

	rule = json.RawMessage(`{
		"===": [null, null]
	}`)

	expected = json.RawMessage("true")

	output, err = jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestLessThanWithSingleArgument(t *testing.T) {
	rule := json.RawMessage(`{"<": [1]}`)
	output, err := jsonlogic.ApplyRaw(rule, nil)
	assert.NoError(t, err)
	assert.JSONEq(t, `false`, string(output))
}

func TestLessOrEqualThanWithSingleArgument(t *testing.T) {
	rule := json.RawMessage(`{"<=": [1]}`)
	output, err := jsonlogic.ApplyRaw(rule, nil)
	assert.NoError(t, err)
	assert.JSONEq(t, `false`, string(output))
}

func TestGreaterThanWithSingleArgument(t *testing.T) {
	rule := json.RawMessage(`{">": [1]}`)
	output, err := jsonlogic.ApplyRaw(rule, nil)
	assert.NoError(t, err)
	assert.JSONEq(t, `false`, string(output))
}

func TestGreaterOrEqualThanWithSingleArgument(t *testing.T) {
	rule := json.RawMessage(`{">=": [1]}`)
	output, err := jsonlogic.ApplyRaw(rule, nil)
	assert.NoError(t, err)
	assert.JSONEq(t, `false`, string(output))
}

func TestEqualWithSingleArgument(t *testing.T) {
	rule := json.RawMessage(`{"==": [1]}`)
	output, err := jsonlogic.ApplyRaw(rule, nil)
	assert.NoError(t, err)
	assert.JSONEq(t, `false`, string(output))
}

func TestHardEqualsWithDifferentTypes(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"===": ["42", 42]
	}`)

	var expected json.RawMessage = json.RawMessage("false")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))

	rule = json.RawMessage(`{
		"===": ["42", "43"]
	}`)

	expected = json.RawMessage("false")

	output, err = jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))

	rule = json.RawMessage(`{
		"===": ["42", "42"]
	}`)

	expected = json.RawMessage("true")

	output, err = jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}


================================================
FILE: go.mod
================================================
module github.com/diegoholiveira/jsonlogic/v3

go 1.18

require github.com/stretchr/testify v1.10.0

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: internal/javascript/javascript.go
================================================
// Package javascript provides utilities for working with JavaScript code and runtime integration.
package javascript

import (
	"math"
	"reflect"
	"strconv"
	"strings"
)

type UndefinedType struct{}

// At returns the element at the specified index in the slice.
// If index is negative, it counts from the end of the slice.
// If index is out of bounds, it returns nil.
//
// Example:
//
//	At([]any{1,2,3}, 1)  // Returns: 2
//	At([]any{1,2,3}, -1) // Returns: 3
func At(values []any, index int) any {
	if index >= 0 && index < len(values) {
		return values[index]
	}
	return UndefinedType{}
}

// ToNumber converts various input types to float64.
//
// Examples:
//
//	ToNumber(42)                             // Returns: 42.0
//	ToNumber("3.14")                         // Returns: 3.14
//	ToNumber(true)                           // Returns: 1.0
//	ToNumber(false)                          // Returns: 0.0
//	ToNumber([]int{1, 2, 3})                 // Returns: 3.0 (length of slice)
//	ToNumber(map[string]int{"a": 1, "b": 2}) // Returns: 2.0 (length of map)
//	ToNumber(nil)                            // Returns: 0.0
//
// Note: For unsupported types, it returns 0.0
func ToNumber(v any) float64 {
	switch value := v.(type) {
	case nil:
		return 0
	case UndefinedType:
		return math.NaN()
	case float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
		return reflect.ValueOf(value).Convert(reflect.TypeOf(float64(0))).Float()
	case bool: // Boolean values true and false are converted to 1 and 0 respectively.
		if value {
			return 1
		} else {
			return 0
		}
	case string:
		if strings.TrimSpace(value) == "" {
			return 0
		}

		n, err := strconv.ParseFloat(value, 64)
		switch err {
		case strconv.ErrRange, nil:
			return n
		default:
			return math.NaN()
		}
	default:
		return math.NaN()
	}
}


================================================
FILE: internal/javascript/javascript_test.go
================================================
package javascript

import (
	"math"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestAt(t *testing.T) {
	tests := []struct {
		name     string
		values   []any
		index    int
		expected any
	}{
		{
			name:     "valid index",
			values:   []any{1, "test", true},
			index:    1,
			expected: "test",
		},
		{
			name:     "index out of bounds (positive)",
			values:   []any{1, 2, 3},
			index:    5,
			expected: UndefinedType{},
		},
		{
			name:     "index out of bounds (negative)",
			values:   []any{1, 2, 3},
			index:    -1,
			expected: UndefinedType{},
		},
		{
			name:     "empty array",
			values:   []any{},
			index:    0,
			expected: UndefinedType{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := At(tt.values, tt.index)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestToNumber(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected float64
		isNaN    bool
	}{
		{
			name:     "nil input",
			input:    nil,
			expected: 0,
		},
		{
			name:  "undefined input",
			input: UndefinedType{},
			isNaN: true,
		},
		{
			name:     "int input",
			input:    42,
			expected: 42,
		},
		{
			name:     "float64 input",
			input:    3.14,
			expected: 3.14,
		},
		{
			name:     "true boolean input",
			input:    true,
			expected: 1,
		},
		{
			name:     "false boolean input",
			input:    false,
			expected: 0,
		},
		{
			name:     "valid numeric string",
			input:    "123.45",
			expected: 123.45,
		},
		{
			name:     "empty string",
			input:    "",
			expected: 0,
		},
		{
			name:     "whitespace string",
			input:    "   ",
			expected: 0,
		},
		{
			name:  "invalid numeric string",
			input: "not a number",
			isNaN: true,
		},
		{
			name:  "complex type (map)",
			input: map[string]int{"a": 1, "b": 2},
			isNaN: true,
		},
		{
			name:  "complex type (struct)",
			input: struct{ Name string }{"test"},
			isNaN: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := ToNumber(tt.input)

			if tt.isNaN {
				assert.True(t, math.IsNaN(result), "Expected NaN result for %v", tt.input)
			} else {
				assert.Equal(t, tt.expected, result)
			}
		})
	}
}


================================================
FILE: internal/json_logic_pr_48_tests.json
================================================
[
    "# Non-rules get passed through",
    [ true, {}, true ],
    [ false, {}, false ],
    [ 17, {}, 17 ],
    [ 3.14, {}, 3.14 ],
    [ "apple", {}, "apple" ],
    [ null, {}, null ],
    [ ["a","b"], {}, ["a","b"] ],

    "# Single operator tests",
    [ {"==":[false,false]}, {}, true ],
    [ {"==":[0,false]}, {}, true ],
    [ {"==":[false,0]}, {}, true ],
    [ {"==":[false,"0"]}, {}, true ],
    [ {"==":[1,true]}, {}, true ],
    [ {"==":["1",true]}, {}, true ],
    [ {"==":["1.000",true]}, {}, true ],
    [ {"==":["0",0]}, {}, true ],
    [ {"==":["0.0000",0]}, {}, true ],
    [ {"==":["0.0000",false]}, {}, true ],
    [ {"==":["0.0000","0"]}, {}, false ],
    [ {"==":["",0]}, {}, true ],
    [ {"==":[" ",0]}, {}, true ],
    [ {"==":["  ",0]}, {}, true ],
    [ {"==":["  ",false]}, {}, true ],
    [ {"==":[0,""]}, {}, true ],
    [ {"==":[1,1]}, {}, true ],
    [ {"==":[1,"1"]}, {}, true ],
    [ {"==":["1",1]}, {}, true ],
    [ {"==":["42.0",42]}, {}, true ],
    [ {"==":[42.0000,"42"]}, {}, true ],
    [ {"==":["42.0000",42]}, {}, true ],
    [ {"==":[1,2]}, {}, false ],
    [ {"==":[true,"true"]}, {}, false ],
    [ {"==":["true",true]}, {}, false ],
    [ {"==":["a ","a"]}, {}, false ],
    [ {"===":[1,1]}, {}, true ],
    [ {"===":[1,"1"]}, {}, false ],
    [ {"===":[1,2]}, {}, false ],
    [ {"!=":[1,2]}, {}, true ],
    [ {"!=":[1,1]}, {}, false ],
    [ {"!=":[1,"1"]}, {}, false ],
    [ {"!=":["1",1]}, {}, false ],
    [ {"!==":[1,2]}, {}, true ],
    [ {"!==":[1,1]}, {}, false ],
    [ {"!==":[1,"1"]}, {}, true ],
    [ {">":[2,1]}, {}, true ],
    [ {">":[1,1]}, {}, false ],
    [ {">":[1,2]}, {}, false ],
    [ {">":["2",1]}, {}, true ],
    [ {">=":[2,1]}, {}, true ],
    [ {">=":[1,1]}, {}, true ],
    [ {">=":[1,2]}, {}, false ],
    [ {">=":["2",1]}, {}, true ],
    [ {"<":["",1]}, {}, true ],
    [ {"<":["",-1]}, {}, false ],
    [ {"<":[""," "]}, {}, true ],
    [ {"<":[2,1]}, {}, false ],
    [ {"<":[1,1]}, {}, false ],
    [ {"<":[1,2]}, {}, true ],
    [ {"<":["1",2]}, {}, true ],
    [ {"<":[1,2,3]}, {}, true ],
    [ {"<":[1,1,3]}, {}, false ],
    [ {"<":[1,4,3]}, {}, false ],
    [ {"<=":[2,1]}, {}, false ],
    [ {"<=":[1,1]}, {}, true ],
    [ {"<=":[1,2]}, {}, true ],
    [ {"<=":["1",2]}, {}, true ],
    [ {"<=":[1,2,3]}, {}, true ],
    [ {"<=":[1,4,3]}, {}, false ],
    [ {"!":[false]}, {}, true ],
    [ {"!":false}, {}, true ],
    [ {"!":[true]}, {}, false ],
    [ {"!":true}, {}, false ],
    [ {"!":0}, {}, true ],
    [ {"!":1}, {}, false ],
    [ {"or":[true,true]}, {}, true ],
    [ {"or":[false,true]}, {}, true ],
    [ {"or":[true,false]}, {}, true ],
    [ {"or":[false,false]}, {}, false ],
    [ {"or":[false,false,true]}, {}, true ],
    [ {"or":[false,false,false]}, {}, false ],
    [ {"or":[false]}, {}, false ],
    [ {"or":[true]}, {}, true ],
    [ {"or":[1,3]}, {}, 1 ],
    [ {"or":[3,false]}, {}, 3 ],
    [ {"or":[false,3]}, {}, 3 ],
    [ {"and":[true,true]}, {}, true ],
    [ {"and":[false,true]}, {}, false ],
    [ {"and":[true,false]}, {}, false ],
    [ {"and":[false,false]}, {}, false ],
    [ {"and":[true,true,true]}, {}, true ],
    [ {"and":[true,true,false]}, {}, false ],
    [ {"and":[false]}, {}, false ],
    [ {"and":[true]}, {}, true ],
    [ {"and":[1,3]}, {}, 3 ],
    [ {"and":[3,false]}, {}, false ],
    [ {"and":[false,3]}, {}, false ],
    [ {"?:":[true,1,2]}, {}, 1 ],
    [ {"?:":[false,1,2]}, {}, 2 ],
    [ {"in":["Bart",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, true ],
    [ {"in":["Milhouse",["Bart","Homer","Lisa","Marge","Maggie"]]}, {}, false ],
    [ {"in":["Spring","Springfield"]}, {}, true ],
    [ {"in":["i","team"]}, {}, false ],
    [ {"cat":"ice"}, {}, "ice" ],
    [ {"cat":["ice"]}, {}, "ice" ],
    [ {"cat":["ice","cream"]}, {}, "icecream" ],
    [ {"cat":[1,2]}, {}, "12" ],
    [ {"cat":["Robocop",2]}, {}, "Robocop2" ],
    [ {"cat":["we all scream for ","ice","cream"]}, {}, "we all scream for icecream" ],
    [ {"%":[1,2]}, {}, 1 ],
    [ {"%":[2,2]}, {}, 0 ],
    [ {"%":[3,2]}, {}, 1 ],
    [ {"max":[1,2,3]}, {}, 3 ],
    [ {"max":[1,3,3]}, {}, 3 ],
    [ {"max":[3,2,1]}, {}, 3 ],
    [ {"max":[1]}, {}, 1 ],
    [ {"min":[1,2,3]}, {}, 1 ],
    [ {"min":[1,1,3]}, {}, 1 ],
    [ {"min":[3,2,1]}, {}, 1 ],
    [ {"min":[1]}, {}, 1 ],

    [ {"+":[1,2]}, {}, 3 ],
    [ {"+":[2,2,2]}, {}, 6 ],
    [ {"+":[1]}, {}, 1 ],
    [ {"+":["1",1]}, {}, 2 ],
    [ {"*":[3,2]}, {}, 6 ],
    [ {"*":[2,2,2]}, {}, 8 ],
    [ {"*":[1]}, {}, 1 ],
    [ {"*":["1",1]}, {}, 1 ],
    [ {"-":[2,3]}, {}, -1 ],
    [ {"-":[3,2]}, {}, 1 ],
    [ {"-":[3]}, {}, -3 ],
    [ {"-":["1",1]}, {}, 0 ],
    [ {"/":[4,2]}, {}, 2 ],
    [ {"/":[2,4]}, {}, 0.5 ],
    [ {"/":["1",1]}, {}, 1 ],

    "Substring",
    [{"substr":["jsonlogic", 4]}, null, "logic"],
    [{"substr":["jsonlogic", -5]}, null, "logic"],
    [{"substr":["jsonlogic", 0, 1]}, null, "j"],
    [{"substr":["jsonlogic", -1, 1]}, null, "c"],
    [{"substr":["jsonlogic", 4, 5]}, null, "logic"],
    [{"substr":["jsonlogic", -5, 5]}, null, "logic"],
    [{"substr":["jsonlogic", -5, -2]}, null, "log"],
    [{"substr":["jsonlogic", 1, -5]}, null, "son"],

    "Merge arrays",
    [{"merge":[]}, null, []],
    [{"merge":[[1]]}, null, [1]],
    [{"merge":[[1],[]]}, null, [1]],
    [{"merge":[[1], [2]]}, null, [1,2]],
    [{"merge":[[1], [2], [3]]}, null, [1,2,3]],
    [{"merge":[[1, 2], [3]]}, null, [1,2,3]],
    [{"merge":[[1], [2, 3]]}, null, [1,2,3]],
    "Given non-array arguments, merge converts them to arrays",
    [{"merge":1}, null, [1]],
    [{"merge":[1,2]}, null, [1,2]],
    [{"merge":[1,[2]]}, null, [1,2]],

    "Too few args",
    [{"if":[]}, null, null],
    [{"if":[true]}, null, true],
    [{"if":[false]}, null, false],
    [{"if":["apple"]}, null, "apple"],

    "Simple if/then/else cases",
    [{"if":[true, "apple"]}, null, "apple"],
    [{"if":[false, "apple"]}, null, null],
    [{"if":[true, "apple", "banana"]}, null, "apple"],
    [{"if":[false, "apple", "banana"]}, null, "banana"],

    "Empty arrays are falsey",
    [{"if":[ [], "apple", "banana"]}, null, "banana"],
    [{"if":[ [1], "apple", "banana"]}, null, "apple"],
    [{"if":[ [1,2,3,4], "apple", "banana"]}, null, "apple"],

    "Empty strings are falsey, all other strings are truthy",
    [{"if":[ "", "apple", "banana"]}, null, "banana"],
    [{"if":[ "zucchini", "apple", "banana"]}, null, "apple"],
    [{"if":[ "0", "apple", "banana"]}, null, "apple"],

    "You can cast a string to numeric with a unary + ",
    [{"===":[0,"0"]}, null, false],
    [{"===":[0,{"+":"0"}]}, null, true],
    [{"if":[ {"+":"0"}, "apple", "banana"]}, null, "banana"],
    [{"if":[ {"+":"1"}, "apple", "banana"]}, null, "apple"],

    "Zero is falsy, all other numbers are truthy",
    [{"if":[ 0, "apple", "banana"]}, null, "banana"],
    [{"if":[ 1, "apple", "banana"]}, null, "apple"],
    [{"if":[ 3.1416, "apple", "banana"]}, null, "apple"],
    [{"if":[ -1, "apple", "banana"]}, null, "apple"],

    "Truthy and falsy definitions matter in Boolean operations",
    [{"and" : [ { "!": [ ] }, { "!": [ [] ] }, { "!": { "missing": "foo" } } ]}, {"foo": "bar"}, true],
    [{"!" : []}, {}, true],
    [{"!" : [ [] ]}, {}, true],
    [{"!!" : []}, {}, false],
    [{"!!" : [ [] ]}, {}, false],
    [{"and" : [ [], true ]}, {}, [] ],
    [{"or" : [ [], true ]}, {}, true ],

    [{"!" : [ 0 ]}, {}, true],
    [{"!!" : [ 0 ]}, {}, false],
    [{"and" : [ 0, true ]}, {}, 0 ],
    [{"or" : [ 0, true ]}, {}, true ],

    [{"!" : [ "" ]}, {}, true],
    [{"!!" : [ "" ]}, {}, false],
    [{"and" : [ "", true ]}, {}, "" ],
    [{"or" : [ "", true ]}, {}, true ],

    [{"!" : [ "0" ]}, {}, false],
    [{"!!" : [ "0" ]}, {}, true],
    [{"and" : [ "0", true ]}, {}, true ],
    [{"or" : [ "0", true ]}, {}, "0" ],

    "If the conditional is logic, it gets evaluated",
    [{"if":[ {">":[2,1]}, "apple", "banana"]}, null, "apple"],
    [{"if":[ {">":[1,2]}, "apple", "banana"]}, null, "banana"],

    "If the consequents are logic, they get evaluated",
    [{"if":[ true, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "apple"],
    [{"if":[ false, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "banana"],

    "If/then/elseif/then cases",
    [{"if":[true, "apple", true, "banana"]}, null, "apple"],
    [{"if":[true, "apple", false, "banana"]}, null, "apple"],
    [{"if":[false, "apple", true, "banana"]}, null, "banana"],
    [{"if":[false, "apple", false, "banana"]}, null, null],

    [{"if":[true, "apple", true, "banana", "carrot"]}, null, "apple"],
    [{"if":[true, "apple", false, "banana", "carrot"]}, null, "apple"],
    [{"if":[false, "apple", true, "banana", "carrot"]}, null, "banana"],
    [{"if":[false, "apple", false, "banana", "carrot"]}, null, "carrot"],

    [{"if":[false, "apple", false, "banana", false, "carrot"]}, null, null],
    [{"if":[false, "apple", false, "banana", false, "carrot", "date"]}, null, "date"],
    [{"if":[false, "apple", false, "banana", true, "carrot", "date"]}, null, "carrot"],
    [{"if":[false, "apple", true, "banana", false, "carrot", "date"]}, null, "banana"],
    [{"if":[false, "apple", true, "banana", true, "carrot", "date"]}, null, "banana"],
    [{"if":[true, "apple", false, "banana", false, "carrot", "date"]}, null, "apple"],
    [{"if":[true, "apple", false, "banana", true, "carrot", "date"]}, null, "apple"],
    [{"if":[true, "apple", true, "banana", false, "carrot", "date"]}, null, "apple"],
    [{"if":[true, "apple", true, "banana", true, "carrot", "date"]}, null, "apple"],

    "Arrays with logic",
    [[1, {"var": "x"}, 3], {"x": 2}, [1, 2, 3]],
    [{"if": [{"var": "x"}, [{"var": "y"}], 99]}, {"x": true, "y": 42}, [42]],

    "# Compound Tests",
    [ {"and":[{">":[3,1]},true]}, {}, true ],
    [ {"and":[{">":[3,1]},false]}, {}, false ],
    [ {"and":[{">":[3,1]},{"!":true}]}, {}, false ],
    [ {"and":[{">":[3,1]},{"<":[1,3]}]}, {}, true ],
    [ {"?:":[{">":[3,1]},"visible","hidden"]}, {}, "visible" ],

    "# Data-Driven",
    [ {"var":["a"]},{"a":1},1 ],
    [ {"var":["b"]},{"a":1},null ],
    [ {"var":["a"]},null,null ],
    [ {"var":"a"},{"a":1},1 ],
    [ {"var":"b"},{"a":1},null ],
    [ {"var":"a"},null,null ],
    [ {"var":["a", 1]},null,1 ],
    [ {"var":["b", 2]},{"a":1},2 ],
    [ {"var":"a.b"},{"a":{"b":"c"}},"c" ],
    [ {"var":"a.q"},{"a":{"b":"c"}},null ],
    [ {"var":["a.q", 9]},{"a":{"b":"c"}},9 ],
    [ {"var":1}, ["apple","banana"], "banana" ],
    [ {"var":"1"}, ["apple","banana"], "banana" ],
    [ {"var":"1.1"}, ["apple",["banana","beer"]], "beer" ],
    [ {"and":[{"<":[{"var":"temp"},110]},{"==":[{"var":"pie.filling"},"apple"]}]},{"temp":100,"pie":{"filling":"apple"}},true ],
    [ {"var":[{"?:":[{"<":[{"var":"temp"},110]},"pie.filling","pie.eta"]}]},{"temp":100,"pie":{"filling":"apple","eta":"60s"}},"apple" ],
    [ {"in":[{"var":"filling"},["apple","cherry"]]},{"filling":"apple"},true ],
    [ {"var":"a.b.c"}, null, null ],
    [ {"var":"a.b.c"}, {"a":null}, null ],
    [ {"var":"a.b.c"}, {"a":{"b":null}}, null ],
    [ {"var":""}, 1, 1 ],
    [ {"var":null}, 1, 1 ],
    [ {"var":[]}, 1, 1 ],

    "Missing",
    [{"missing":[]}, null, []],
    [{"missing":["a"]}, null, ["a"]],
    [{"missing":"a"}, null, ["a"]],
    [{"missing":"a"}, {"a":"apple"}, []],
    [{"missing":["a"]}, {"a":"apple"}, []],
    [{"missing":["a","b"]}, {"a":"apple"}, ["b"]],
    [{"missing":["a","b"]}, {"b":"banana"}, ["a"]],
    [{"missing":["a","b"]}, {"a":"apple", "b":"banana"}, []],
    [{"missing":["a","b"]}, {}, ["a","b"]],
    [{"missing":["a","b"]}, null, ["a","b"]],

    [{"missing":["a.b"]}, null, ["a.b"]],
    [{"missing":["a.b"]}, {"a":"apple"}, ["a.b"]],
    [{"missing":["a.b"]}, {"a":{"c":"apple cake"}}, ["a.b"]],
    [{"missing":["a.b"]}, {"a":{"b":"apple brownie"}}, []],
    [{"missing":["a.b", "a.c"]}, {"a":{"b":"apple brownie"}}, ["a.c"]],


    "Missing some",
    [{"missing_some":[1, ["a", "b"]]}, {"a":"apple"}, [] ],
    [{"missing_some":[1, ["a", "b"]]}, {"b":"banana"}, [] ],
    [{"missing_some":[1, ["a", "b"]]}, {"a":"apple", "b":"banana"}, [] ],
    [{"missing_some":[1, ["a", "b"]]}, {"c":"carrot"}, ["a", "b"]],

    [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana"}, [] ],
    [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "c":"carrot"}, [] ],
    [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "b":"banana", "c":"carrot"}, [] ],
    [{"missing_some":[2, ["a", "b", "c"]]}, {"a":"apple", "d":"durian"}, ["b", "c"] ],
    [{"missing_some":[2, ["a", "b", "c"]]}, {"d":"durian", "e":"eggplant"}, ["a", "b", "c"] ],


    "Missing and If are friends, because empty arrays are falsey in JsonLogic",
    [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"a":"apple"}, "found it"],
    [{"if":[ {"missing":"a"}, "missed it", "found it" ]}, {"b":"banana"}, "missed it"],

    "Missing, Merge, and If are friends. VIN is always required, APR is only required if financing is true.",
    [
        {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} },
        {"financing":true},
        ["vin","apr"]
    ],

    [
        {"missing":{"merge":[ "vin", {"if": [{"var":"financing"}, ["apr"], [] ]} ]} },
        {"financing":false},
        ["vin"]
    ],

    "Filter, map, all, none, and some",
    [
        {"filter":[{"var":"integers"}, true]},
        {"integers":[1,2,3]},
        [1,2,3]
    ],
    [
        {"filter":[{"var":"integers"}, false]},
        {"integers":[1,2,3]},
        []
    ],
    [
        {"filter":[{"var":"integers"}, {">=":[{"var":""},2]}]},
        {"integers":[1,2,3]},
        [2,3]
    ],
    [
        {"filter":[{"var":"integers"}, {"%":[{"var":""},2]}]},
        {"integers":[1,2,3]},
        [1,3]
    ],

    [
        {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]},
        {"integers":[1,2,3]},
        [2,4,6]
    ],
    [
        {"map":[{"var":"integers"}, {"*":[{"var":""},2]}]},
        null,
        []
    ],
    [
        {"map":[{"var":"desserts"}, {"var":"qty"}]},
        {"desserts":[
            {"name":"apple","qty":1},
            {"name":"brownie","qty":2},
            {"name":"cupcake","qty":3}
        ]},
        [1,2,3]
    ],

    [
        {"reduce":[
            {"var":"integers"},
            {"+":[{"var":"current"}, {"var":"accumulator"}]},
            0
        ]},
        {"integers":[1,2,3,4]},
        10
    ],
    [
        {"reduce":[
            {"var":"integers"},
            {"+":[{"var":"current"}, {"var":"accumulator"}]},
            {"var": "start_with"}
        ]},
        {"integers":[1,2,3,4], "start_with": 59},
        69
    ],
    [
        {"reduce":[
            {"var":"integers"},
            {"+":[{"var":"current"}, {"var":"accumulator"}]},
            0
        ]},
        null,
        0
    ],
    [
        {"reduce":[
            {"var":"integers"},
            {"*":[{"var":"current"}, {"var":"accumulator"}]},
            1
        ]},
        {"integers":[1,2,3,4]},
        24
    ],
    [
        {"reduce":[
            {"var":"integers"},
            {"*":[{"var":"current"}, {"var":"accumulator"}]},
            0
        ]},
        {"integers":[1,2,3,4]},
        0
    ],
    [
        {"reduce": [
            {"var":"desserts"},
            {"+":[ {"var":"accumulator"}, {"var":"current.qty"}]},
            0
        ]},
        {"desserts":[
            {"name":"apple","qty":1},
            {"name":"brownie","qty":2},
            {"name":"cupcake","qty":3}
        ]},
        6
    ],


    [
        {"all":[{"var":"integers"}, {">=":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        true
    ],
    [
        {"all":[{"var":"integers"}, {"==":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        false
    ],
    [
        {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        false
    ],
    [
        {"all":[{"var":"integers"}, {"<":[{"var":""}, 1]}]},
        {"integers":[]},
        false
    ],
    [
        {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        true
    ],
    [
        {"all":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        false
    ],
    [
        {"all":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        false
    ],
    [
        {"all":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]},
        {"items":[]},
        false
    ],


    [
        {"none":[{"var":"integers"}, {">=":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        false
    ],
    [
        {"none":[{"var":"integers"}, {"==":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        false
    ],
    [
        {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        true
    ],
    [
        {"none":[{"var":"integers"}, {"<":[{"var":""}, 1]}]},
        {"integers":[]},
        true
    ],
    [
        {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        false
    ],
    [
        {"none":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        false
    ],
    [
        {"none":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        true
    ],
    [
        {"none":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]},
        {"items":[]},
        true
    ],

    [
        {"some":[{"var":"integers"}, {">=":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        true
    ],
    [
        {"some":[{"var":"integers"}, {"==":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        true
    ],
    [
        {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]},
        {"integers":[1,2,3]},
        false
    ],
    [
        {"some":[{"var":"integers"}, {"<":[{"var":""}, 1]}]},
        {"integers":[]},
        false
    ],
    [
        {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        true
    ],
    [
        {"some":[ {"var":"items"}, {">":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        true
    ],
    [
        {"some":[ {"var":"items"}, {"<":[{"var":"qty"}, 1]}]},
        {"items":[{"qty":1,"sku":"apple"},{"qty":2,"sku":"banana"}]},
        false
    ],
    [
        {"some":[ {"var":"items"}, {">=":[{"var":"qty"}, 1]}]},
        {"items":[]},
        false
    ],

    "EOF"
]


================================================
FILE: internal/testing.go
================================================
package internal

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"reflect"
)

type (
	Test struct {
		Rule     any
		Data     any
		Expected any
		Scenario string
		Index    int
	}

	Tests []Test
)

// GetScenariosFromProposedOfficialTestSuite reads the tests.json file that we've proposed become the new official one in
// https://github.com/jwadhams/json-logic/pull/48 but that hasn't merged yet.
func GetScenariosFromProposedOfficialTestSuite() Tests {
	buffer, err := os.ReadFile("internal/json_logic_pr_48_tests.json")
	if err != nil {
		log.Fatal(err)
	}

	return getScenariosFromFile(buffer)
}

// GetScenariosFromOfficialTestSuite fetches test scenarios from the official JSON Logic test suite.
// It makes an HTTP request to jsonlogic.com to retrieve the latest test cases.
func GetScenariosFromOfficialTestSuite() Tests {
	req, err := http.NewRequest("GET", "http://jsonlogic.com/tests.json", nil)
	if err != nil {
		log.Fatal(err)
	}

	response, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()

	buffer, err := io.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}

	return getScenariosFromFile(buffer)
}

func getScenariosFromFile(buffer []byte) Tests {
	var (
		tests     Tests
		scenarios []any
		err       = json.Unmarshal(buffer, &scenarios)
	)
	if err != nil {
		log.Fatal(err)
	}

	// add missing but relevant scenarios
	var rule []any

	scenarios = append(scenarios,
		append(rule,
			make(map[string]any),
			make(map[string]any),
			make(map[string]any)))

	scenarioName := ""
	testIndex := 0
	for _, scenario := range scenarios {
		if reflect.ValueOf(scenario).Kind() == reflect.String {
			scenarioName = scenario.(string)
			testIndex = 0
			continue
		}

		tests = append(tests, Test{
			Rule:     scenario.([]any)[0],
			Data:     scenario.([]any)[1],
			Expected: scenario.([]any)[2],
			Scenario: scenarioName,
			Index:    testIndex,
		})
		testIndex++
	}

	return tests
}


================================================
FILE: internal/typing/typing.go
================================================
// Package typing provides type checking and conversion utilities for JSON data types.
package typing

import (
	"reflect"
	"strconv"
)

func is(obj any, kind reflect.Kind) bool {
	return obj != nil && reflect.TypeOf(obj).Kind() == kind
}

// IsBool checks if the provided value is a boolean type.
// Returns false if the value is nil.
//
// Example:
//
//	IsBool(true)   // Returns: true
//	IsBool(false)  // Returns: true
//	IsBool("true") // Returns: false
//	IsBool(nil)    // Returns: false
func IsBool(obj any) bool {
	return is(obj, reflect.Bool)
}

// IsString checks if the provided value is a string type.
// Returns false if the value is nil.
//
// Example:
//
//	IsString("test")  // Returns: true
//	IsString("")      // Returns: true
//	IsString(42)      // Returns: false
//	IsString(nil)     // Returns: false
func IsString(obj any) bool {
	return is(obj, reflect.String)
}

// IsNumber checks if the provided value is a numeric type (int or float64).
// Returns false for any other type including nil.
//
// Example:
//
//	IsNumber(42)       // Returns: true
//	IsNumber(3.14)     // Returns: true
//	IsNumber("42")     // Returns: false
//	IsNumber(nil)      // Returns: false
func IsNumber(obj any) bool {
	switch obj.(type) {
	case int, float64:
		return true
	default:
		return false
	}
}

// IsPrimitive checks if the provided value is a primitive type (boolean, string, or number).
// Returns false if the value is nil or any other type.
//
// Example:
//
//	IsPrimitive(42)      // Returns: true
//	IsPrimitive("test")  // Returns: true
//	IsPrimitive(true)    // Returns: true
//	IsPrimitive([])      // Returns: false
//	IsPrimitive(nil)     // Returns: false
func IsPrimitive(obj any) bool {
	return IsBool(obj) || IsString(obj) || IsNumber(obj)
}

// IsMap checks if the provided value is a map type.
// Returns false if the value is nil.
//
// Example:
//
//	IsMap(map[string]int{"a": 1})  // Returns: true
//	IsMap(map[string]any{})        // Returns: true
//	IsMap([]int{1, 2, 3})          // Returns: false
//	IsMap(nil)                     // Returns: false
func IsMap(obj any) bool {
	return is(obj, reflect.Map)
}

// IsSlice checks if the provided value is a slice type.
// Returns false if the value is nil.
//
// Example:
//
//	IsSlice([]int{1, 2, 3})  // Returns: true
//	IsSlice([]any{})         // Returns: true
//	IsSlice("test")          // Returns: false
//	IsSlice(nil)             // Returns: false
func IsSlice(obj any) bool {
	return is(obj, reflect.Slice)
}

// IsEmptySlice checks if the provided value is a slice and all its elements are falsy.
// Returns false if the value is not a slice or if all elements in the slice are falsy.
// A falsy value is: false, 0, "", empty array, or empty map.
//
// Example:
//
//	IsEmptySlice([]any{})             // Returns: true
//	IsEmptySlice([]any{0, "", false}) // Returns: true
//	IsEmptySlice([]any{1, 2, 3})      // Returns: false
//	IsEmptySlice("test")              // Returns: false
func IsEmptySlice(obj any) bool {
	if !IsSlice(obj) {
		return false
	}

	for _, v := range obj.([]any) {
		if IsTrue(v) {
			return false
		}
	}

	return true
}

// IsTrue checks if the provided value is considered truthy in JavaScript logic.
// For booleans: true is truthy
// For numbers: non-zero is truthy
// For strings: non-empty string is truthy
// For slices/maps: non-empty slice/map is truthy
// Returns false for nil or any other type.
//
// Example:
//
//	IsTrue(true)                      // Returns: true
//	IsTrue(42)                        // Returns: true
//	IsTrue("test")                    // Returns: true
//	IsTrue([]any{1, 2, 3})            // Returns: true
//	IsTrue(false)                     // Returns: false
//	IsTrue(0)                         // Returns: false
//	IsTrue("")                        // Returns: false
//	IsTrue([]any{})                   // Returns: false
//	IsTrue(nil)                       // Returns: false
func IsTrue(obj any) bool {
	if IsBool(obj) {
		return obj.(bool)
	}

	if IsNumber(obj) {
		return ToNumber(obj) != 0
	}

	if IsString(obj) || IsSlice(obj) || IsMap(obj) {
		return reflect.ValueOf(obj).Len() > 0
	}

	return false
}

// ToNumber converts the provided value to a float64.
// If the value is a string, it attempts to parse it as a float64.
// If the value is an int, it converts it to float64.
// If the value is already a float64, it returns it as is.
// For all other types, it attempts a type assertion to float64.
//
// Example:
//
//	ToNumber(42)                 // Returns: 42.0
//	ToNumber(3.14)               // Returns: 3.14
//	ToNumber("42")               // Returns: 42.0
//	ToNumber("3.14")             // Returns: 3.14
//	ToNumber("invalid")          // Returns: 0.0
func ToNumber(value any) float64 {
	if IsString(value) {
		w, _ := strconv.ParseFloat(value.(string), 64)

		return w
	}

	switch value := value.(type) {
	case int:
		return float64(value)
	default:
		return value.(float64)
	}
}

// ToString converts the provided value to a string.
// For numbers: converts to string representation
// For nil: returns an empty string
// For other types: performs a direct type assertion to string
//
// Example:
//
//	ToString(42)        // Returns: "42"
//	ToString(3.14)      // Returns: "3.14"
//	ToString("test")    // Returns: "test"
//	ToString(nil)       // Returns: ""
func ToString(value any) string {
	if IsNumber(value) {
		switch value := value.(type) {
		case int:
			return strconv.FormatInt(int64(value), 10)
		default:
			return strconv.FormatFloat(value.(float64), 'f', -1, 64)
		}
	}

	if value == nil {
		return ""
	}

	return value.(string)
}


================================================
FILE: internal/typing/typing_test.go
================================================
package typing

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestIsBool(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"true value", true, true},
		{"false value", false, true},
		{"nil value", nil, false},
		{"string value", "true", false},
		{"int value", 1, false},
		{"float value", 1.5, false},
		{"slice value", []any{}, false},
		{"map value", map[string]any{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsBool(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsString(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"empty string", "", true},
		{"non-empty string", "hello", true},
		{"nil value", nil, false},
		{"boolean value", true, false},
		{"int value", 1, false},
		{"float value", 1.5, false},
		{"slice value", []any{}, false},
		{"map value", map[string]any{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsString(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsNumber(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"int zero", 0, true},
		{"int positive", 42, true},
		{"int negative", -10, true},
		{"float zero", 0.0, true},
		{"float positive", 3.14, true},
		{"float negative", -2.5, true},
		{"nil value", nil, false},
		{"boolean value", true, false},
		{"string value", "123", false},
		{"slice value", []any{}, false},
		{"map value", map[string]any{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsNumber(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsPrimitive(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"boolean", true, true},
		{"string", "hello", true},
		{"int", 42, true},
		{"float", 3.14, true},
		{"nil value", nil, false},
		{"slice value", []any{}, false},
		{"map value", map[string]any{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsPrimitive(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsMap(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"empty map", map[string]any{}, true},
		{"non-empty map", map[string]any{"key": "value"}, true},
		{"nil value", nil, false},
		{"boolean value", true, false},
		{"int value", 1, false},
		{"float value", 1.5, false},
		{"string value", "hello", false},
		{"slice value", []any{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsMap(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsSlice(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"empty slice", []any{}, true},
		{"non-empty slice", []any{1, 2, 3}, true},
		{"nil value", nil, false},
		{"boolean value", true, false},
		{"int value", 1, false},
		{"float value", 1.5, false},
		{"string value", "hello", false},
		{"map value", map[string]any{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsSlice(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsEmptySlice(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"empty slice", []any{}, true},
		{"slice with zeros", []any{0, 0, 0}, true},
		{"slice with empty strings", []any{"", ""}, true},
		{"slice with false values", []any{false, false}, true},
		{"slice with mixed falsy values", []any{0, "", false, []any{}}, true},
		{"non-empty slice with truthy value", []any{0, 1, 0}, false},
		{"non-empty slice with true", []any{false, true}, false},
		{"nil value", nil, false},
		{"boolean value", true, false},
		{"int value", 1, false},
		{"float value", 1.5, false},
		{"string value", "hello", false},
		{"map value", map[string]any{}, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsEmptySlice(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestIsTrue(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected bool
	}{
		{"true boolean", true, true},
		{"false boolean", false, false},
		{"positive number", 42, true},
		{"negative number", -10, true},
		{"zero number", 0, false},
		{"non-empty string", "hello", true},
		{"empty string", "", false},
		{"non-empty slice", []any{1, 2, 3}, true},
		{"empty slice", []any{}, false},
		{"non-empty map", map[string]any{"key": "value"}, true},
		{"empty map", map[string]any{}, false},
		{"nil value", nil, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsTrue(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestToNumber(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected float64
	}{
		{"int zero", 0, 0.0},
		{"int positive", 42, 42.0},
		{"int negative", -10, -10.0},
		{"float zero", 0.0, 0.0},
		{"float positive", 3.14, 3.14},
		{"float negative", -2.5, -2.5},
		{"string number integer", "42", 42.0},
		{"string number float", "3.14", 3.14},
		{"string number negative", "-10", -10.0},
		{"string empty", "", 0.0},
		{"string non-number", "hello", 0.0},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := ToNumber(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestToString(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		expected string
	}{
		{"int zero", 0, "0"},
		{"int positive", 42, "42"},
		{"int negative", -10, "-10"},
		{"float zero", 0.0, "0"},
		{"float positive", 3.14, "3.14"},
		{"float negative", -2.5, "-2.5"},
		{"string", "hello", "hello"},
		{"empty string", "", ""},
		{"nil value", nil, ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := ToString(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}


================================================
FILE: issues_test.go
================================================
package jsonlogic_test

import (
	"bytes"
	"encoding/json"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

func TestIssue50(t *testing.T) {
	logic := strings.NewReader(`{"<": ["abc", 3]}`)
	data := strings.NewReader(`{}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `false`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue51_example1(t *testing.T) {
	logic := strings.NewReader(`{"==":[{"var":"test"},true]}`)
	data := strings.NewReader(`{}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `false`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue51_example2(t *testing.T) {
	logic := strings.NewReader(`{"==":[{"var":"test"},"true"]}`)
	data := strings.NewReader(`{"test": true}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `false`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue52_example1(t *testing.T) {
	data := strings.NewReader(`{}`)
	logic := strings.NewReader(`{"substr": ["jsonlogic", -10]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `"jsonlogic"`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue52_example2(t *testing.T) {
	data := strings.NewReader(`{}`)
	logic := strings.NewReader(`{"substr": ["jsonlogic", 10]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `"jsonlogic"`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue58_example(t *testing.T) {
	data := strings.NewReader(`{"foo": "bar"}`)
	logic := strings.NewReader(`{"if":[
		{"==":[{"var":"foo"},"bar"]},{"foo":"is_bar","path":"foo_is_bar"},
		{"foo":"not_bar","path":"default_object"}
	]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"foo":"is_bar","path":"foo_is_bar"}`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue70(t *testing.T) {
	data := strings.NewReader(`{"people": [
		{"age":18, "name":"John"},
		{"age":20, "name":"Luke"},
		{"age":18, "name":"Mark"}
]}`)
	logic := strings.NewReader(`{"filter": [
	{"var": ["people"]},
	{"==": [{"var": ["age"]}, 18]}
]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[
    {"age": 18, "name": "John"},
    {"age": 18, "name": "Mark"}
]`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue71_example_empty_min(t *testing.T) {
	data := strings.NewReader(`{}`)
	logic := strings.NewReader(`{"min":[]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `null`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue71_example_empty_max(t *testing.T) {
	data := strings.NewReader(`{}`)
	logic := strings.NewReader(`{"max":[]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `null`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue71_example_max(t *testing.T) {
	data := strings.NewReader(`{}`)
	logic := strings.NewReader(`{"max":[-3, -2]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `-2`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue74(t *testing.T) {
	logic := strings.NewReader(`{"if":[ false, {"var":"values.0.categories"}, "else" ]}`)
	data := strings.NewReader(`{ "values": [] }`)

	var result bytes.Buffer
	_ = jsonlogic.Apply(logic, data, &result)
	expected := `"else"`
	assert.JSONEq(t, expected, result.String())
}

func TestJsonLogicWithSolvedVars(t *testing.T) {
	rule := json.RawMessage(`{
		"or":[
		{
			"and":[
				{"==": [{ "var":"is_foo" }, true ]},
				{"==": [{ "var":"is_bar" }, true ]},
				{">=": [{ "var":"foo" }, 17179869184 ]},
				{"==": [{ "var":"bar" }, 0 ]}
			]
      	},
      	{
			"and":[
				{"==": [{ "var":"is_bar" }, true ]},
				{"==": [{ "var":"is_foo" }, false ]},
				{"==": [{ "var":"foo" }, 34359738368 ]},
				{"==": [{ "var":"bar" }, 0 ]}
			]
      	}]
    }`)

	data := json.RawMessage(`{"foo": 34359738368, "bar": 10, "is_foo": false, "is_bar": true}`)

	output, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{
		"or":[
		{
			"and":[
				{ "==":[ false, true ] },
				{ "==":[ true, true ] },
				{ ">=":[ 34359738368, 17179869184 ] },
				{ "==":[ 10, 0 ] }
			]
		},
		{
			"and":[
				{ "==":[ true, true ] },
				{ "==":[ false, false ] },
				{ "==":[ 34359738368, 34359738368 ] },
				{ "==":[ 10, 0 ] }
			]
		}]
	}`

	assert.JSONEq(t, expected, string(output))
}

func TestIssue79(t *testing.T) {
	rule := strings.NewReader(
		`{"and": [
        {"in": [
          {"var": "flow"},
          ["BRAND"]
        ]},
        {"or": [
          {"if": [
            {"missing": ["gender"]},
            true,
            false
          ]},
          {"some": [
            {"var": "gender"},
            {"==": [
              {"var": null},
              "men"
            ]}
          ]}
        ]}
      ]}`,
	)

	data := strings.NewReader(`{"category":["sneakers"],"flow":"BRAND","gender":["men"],"market":"US"}`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `true`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue83(t *testing.T) {
	rule := `{
	  "map": [
	    {"var": "listOfLists"},
	    {"in": ["item_a", {"var": ""}]}
	  ]
	}`

	data := `{
	  "listOfLists": [
	    ["item_a", "item_b", "item_c"],
	    ["item_b", "item_c"],
	    ["item_a", "item_c"]
	  ]
	}`

	var result bytes.Buffer

	err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result)

	if assert.Nil(t, err) {
		expected := `[true,false,true]`
		assert.JSONEq(t, expected, result.String())
	}
}

func TestIssue81(t *testing.T) {
	rule := `{
      "some": [
        {"var": "A"},
        {"!=": [
          {"var": ".B"},
          {"var": "B"}
        ]}
      ]}
         `

	data := `{"A":[{"B":1}], "B":2}`

	var result bytes.Buffer

	err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `true`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue96(t *testing.T) {
	rule := `{"map":[
      {"var":"integers"},
	  {"*":[{"var":[""]},2]}
    ]}`

	data := `{"integers": [1,2,3]}`

	var result bytes.Buffer

	err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[2, 4, 6]`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue98(t *testing.T) {
	rule := `{"or": [{"and": [true]}]}`
	data := `{}`

	var result bytes.Buffer

	err := jsonlogic.Apply(strings.NewReader(rule), strings.NewReader(data), &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `true`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue110(t *testing.T) {
	logic := strings.NewReader(`{ "map":[{"var": "arr"},{"var":["xxx", "default"]}]}`)
	data := strings.NewReader(`{"arr": [{"xxx": "111","yyy": "222"},{"xxx": "333","yyy": "444"}]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `["111","333"]`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue125_InOperatorWithVarsInSlice(t *testing.T) {
	// This test demonstrates the issue: vars within slices are not resolved
	rule := strings.NewReader(`{"in": [{"var": "needle"}, [{"var": "item1"}, {"var": "item2"}]]}`)
	data := strings.NewReader(`{"needle":"foo", "item1":"bar", "item2":"foo"}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	// Should be true because "foo" should be found in the resolved array ["bar", "foo"]
	// Currently fails because it compares "foo" against unresolved [{"var": "item1"}, {"var": "item2"}]
	expected := `true`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue125_CustomOperatorWithVarsInSlice(t *testing.T) {
	// Add a custom operator that processes slice elements
	jsonlogic.AddOperator("contains_any", func(values, data any) any {
		parsed := values.([]any)
		needle := parsed[0]
		haystack := parsed[1].([]any)

		for _, item := range haystack {
			if item == needle {
				return true
			}
		}
		return false
	})

	rule := strings.NewReader(`{"contains_any": [{"var": "needle"}, [{"var": "item1"}, {"var": "item2"}]]}`)
	data := strings.NewReader(`{"needle":"foo", "item1":"bar", "item2":"foo"}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	// Should be true because "foo" should be found in the resolved array ["bar", "foo"]
	// Currently fails because the custom operator receives unresolved [{"var": "item1"}, {"var": "item2"}]
	expected := `true`
	assert.JSONEq(t, expected, result.String())
}

func TestIssue135(t *testing.T) {
	cases := []struct {
		name     string
		rule     string
		expected string
	}{
		{
			name:     "or returns last operand when all are falsy",
			rule:     `{"or":[null,0]}`,
			expected: `0`,
		},
		{
			name:     "and returns last operand when all are truthy",
			rule:     `{"and":[1,"result"]}`,
			expected: `"result"`,
		},
		{
			name:     "and returns last truthy operand, not max",
			rule:     `{"and":[3,1]}`,
			expected: `1`,
		},
		{
			name:     "and example from jsonlogic.com docs",
			rule:     `{"and":[true,"a",3]}`,
			expected: `3`,
		},
		{
			name:     "and returns first falsy operand (empty string)",
			rule:     `{"and":[true,"",3]}`,
			expected: `""`,
		},
		{
			name:     "or short-circuits on first truthy operand",
			rule:     `{"or":[1,0]}`,
			expected: `1`,
		},
		{
			name:     "and with non-empty slice continues past it",
			rule:     `{"and":[[1,2,3],true]}`,
			expected: `true`,
		},
		{
			name:     "and with empty slice returns it",
			rule:     `{"and":[[],true]}`,
			expected: `[]`,
		},
		{
			name:     "or returns null when all operands are null",
			rule:     `{"or":[null,null]}`,
			expected: `null`,
		},
		{
			name:     "or returns empty array as last falsy operand",
			rule:     `{"or":[false,[]]}`,
			expected: `[]`,
		},
		{
			name:     "and with empty operand list returns null",
			rule:     `{"and":[]}`,
			expected: `null`,
		},
		{
			name:     "or with empty operand list returns null",
			rule:     `{"or":[]}`,
			expected: `null`,
		},
		{
			name:     "or with single falsy operand returns it",
			rule:     `{"or":[0]}`,
			expected: `0`,
		},
		{
			name:     "and with single falsy operand returns it",
			rule:     `{"and":[0]}`,
			expected: `0`,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var result bytes.Buffer
			err := jsonlogic.Apply(strings.NewReader(tc.rule), strings.NewReader(`{}`), &result)
			if err != nil {
				t.Fatal(err)
			}
			// `or` should return the first truthy operand or the last operand;
			// `and` should return the first falsy operand or the last operand.
			assert.JSONEq(t, tc.expected, result.String())
		})
	}
}


================================================
FILE: jsonlogic.go
================================================
// Package jsonlogic provides a Go implementation of JSONLogic rules engine.
// JSONLogic is a way to write rules that involve logic (boolean and mathematical operations),
// consistently in JSON. It's designed to be a lightweight, portable way to share logic
// between front-end and back-end systems.
//
// The package supports all standard JSONLogic operators and allows for custom operator registration.
// Rules can be applied to data using various input/output formats including io.Reader/Writer,
// json.RawMessage, and native Go interfaces.
//
// Basic usage:
//
//	rule := strings.NewReader(`{"==":[{"var":"name"}, "John"]}`)
//	data := strings.NewReader(`{"name":"John"}`)
//	var result strings.Builder
//
//	err := jsonlogic.Apply(rule, data, &result)
//	if err != nil {
//		log.Fatal(err)
//	}
//	// result.String() will be "true"
//
// For more examples and documentation, see: https://jsonlogic.com
package jsonlogic

import (
	"encoding/json"
	"io"
	"strings"

	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

// Apply reads a rule and data from `io.Reader`, applies the rule to the data
// and writes the result to the provided writer. It returns an error if rule
// processing or data handling fails.
//
// Parameters:
//   - rule: io.Reader representing the transformation rule to be applied
//   - data: io.Reader containing the input data to transform
//   - result: io.Writer containing the transformed data
//
// Returns:
//   - err: error if the transformation fails or if type assertions are invalid
func Apply(rule, data io.Reader, result io.Writer) error {
	if data == nil {
		data = strings.NewReader("{}")
	}

	var _rule any
	var _data any

	decoder := json.NewDecoder(rule)
	err := decoder.Decode(&_rule)
	if err != nil {
		return err
	}

	decoder = json.NewDecoder(data)
	err = decoder.Decode(&_data)
	if err != nil {
		return err
	}

	output, err := ApplyInterface(_rule, _data)
	if err != nil {
		return err
	}

	return json.NewEncoder(result).Encode(output)
}

// ApplyRaw applies a validation rule to a JSON data input, both provided as raw JSON messages.
// It processes the input data according to the provided rule and returns the transformed result.
//
// Parameters:
//   - rule: json.RawMessage representing the transformation rule to be applied
//   - data: json.RawMessage containing the input data to transform
//
// Returns:
//   - output: json.RawMessage containing the transformed data
//   - err: error if the transformation fails or if type assertions are invalid
func ApplyRaw(rule, data json.RawMessage) (json.RawMessage, error) {
	if data == nil {
		data = json.RawMessage("{}")
	}

	var _rule any
	var _data any

	err := json.Unmarshal(rule, &_rule)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(data, &_data)
	if err != nil {
		return nil, err
	}

	result, err := ApplyInterface(_rule, _data)
	if err != nil {
		return nil, err
	}

	return json.Marshal(&result)
}

// ApplyInterface applies a transformation rule to input data using interface type assertions.
// It processes the input data according to the provided rule and returns the transformed result.
//
// Parameters:
//   - rule: interface{} representing the transformation rule to be applied
//   - data: interface{} containing the input data to transform
//
// Returns:
//   - output: interface{} containing the transformed data
//   - err: error if the transformation fails or if type assertions are invalid
func ApplyInterface(rule, data any) (output any, err error) {
	defer func() {
		if e := recover(); e != nil {
			// fmt.Println("stacktrace from panic: \n" + string(debug.Stack()))
			err = e.(error)
		}
	}()

	if typing.IsMap(rule) {
		return apply(rule, data), err
	}

	if typing.IsSlice(rule) {
		inputSlice := rule.([]any)
		parsed := make([]any, 0, len(inputSlice))

		for _, value := range inputSlice {
			parsed = append(parsed, parseValues(value, data))
		}

		return any(parsed), nil
	}

	return rule, err
}

// GetJsonLogicWithSolvedVars processes a JSON Logic rule by resolving variables with actual data values.
// It returns the rule with variables substituted but maintains the JSON Logic structure.
//
// Parameters:
//   - rule: json.RawMessage containing the JSON Logic rule
//   - data: json.RawMessage containing the data context for variable resolution
//
// Returns:
//   - []byte: the processed rule with resolved variables as JSON bytes
//   - error: error if unmarshaling or processing fails
//
// This is useful for debugging or when you need to see the rule with variables resolved.
func GetJsonLogicWithSolvedVars(rule, data json.RawMessage) ([]byte, error) {
	if data == nil {
		data = json.RawMessage("{}")
	}

	// parse rule and data from json.RawMessage to interface
	var _rule any
	var _data any

	err := json.Unmarshal(rule, &_rule)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(data, &_data)
	if err != nil {
		return nil, err
	}

	return solveVarsBackToJsonLogic(_rule, _data)
}

func parseValues(values, data any) any {
	if values == nil || typing.IsPrimitive(values) {
		return values
	}

	if typing.IsMap(values) {
		return apply(values, data)
	}

	inputSlice := values.([]any)
	length := len(inputSlice)
	if length == 0 {
		return inputSlice
	}

	parsed := make([]any, 0, length)

	for _, value := range inputSlice {
		if typing.IsMap(value) {
			parsed = append(parsed, apply(value, data))
		} else {
			parsed = append(parsed, parseValues(value, data))
		}
	}

	return parsed
}

func apply(rules, data any) any {
	ruleMap := rules.(map[string]any)

	// A map with more than 1 key counts as a primitive so it's time to end recursion
	if len(ruleMap) > 1 {
		return ruleMap
	}

	for operator, values := range ruleMap {
		return operation(operator, values, data)
	}

	return make(map[string]any)
}


================================================
FILE: jsonlogic_test.go
================================================
package jsonlogic_test

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
	"github.com/diegoholiveira/jsonlogic/v3/internal"
)

func TestRulesFromJsonLogic(t *testing.T) {
	suites := map[string][]internal.Test{
		"Official": internal.GetScenariosFromOfficialTestSuite(),
		"Proposed in https://github.com/jwadhams/json-logic/pull/48": internal.GetScenariosFromProposedOfficialTestSuite(),
	}

	for suiteName, tests := range suites {
		t.Run(suiteName, func(t *testing.T) {
			for _, test := range tests {
				t.Run(fmt.Sprintf("%s_%d", test.Scenario, test.Index), func(t *testing.T) {
					result, err := jsonlogic.ApplyInterface(test.Rule, test.Data)
					if err != nil {
						t.Fatal(err)
					}

					assert.Equal(t, test.Expected, result, "Applying rule %v to data %v", toJSON(test.Rule), toJSON(test.Data))
				})
			}
		})
	}
}

func toJSON(val any) string {
	res, err := json.Marshal(val)
	if err != nil {
		panic(err)
	}
	return string(res)
}

func TestDivWithOnlyOneValue(t *testing.T) {
	rule := strings.NewReader(`{"/":[4]}`)
	data := strings.NewReader(`null`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, `4`, result.String())
}

func TestSetAValue(t *testing.T) {
	rule := strings.NewReader(`{
		"map": [
			{"var": "objects"},
			{"set": [
				{"var": ""},
				"age",
				{"+": [{"var": ".age"}, 2]}
			]}
		]
	}`)

	data := strings.NewReader(`{
		"objects": [
			{"age": 100, "location": "north"},
			{"age": 500, "location": "south"}
		]
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[
		{"age": 102, "location": "north"},
		{"age": 502, "location": "south"}
	]`

	assert.JSONEq(t, expected, result.String())
}

func TestLocalContext(t *testing.T) {
	rule := strings.NewReader(`{
		"filter": [
			{"var": "people"},
			{"==": [
				{"var": ".age"},
				{"min": {"map": [
					{"var": "people"},
					{"var": ".age"}
				]}}
			]}
		]
	}`)

	data := strings.NewReader(`{
		"people": [
			{"age":18, "name":"John"},
			{"age":20, "name":"Luke"},
			{"age":18, "name":"Mark"}
		]
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[
		{"age": 18, "name": "John"},
		{"age": 18, "name": "Mark"}
	]`

	assert.JSONEq(t, expected, result.String())
}

func TestMapWithZeroValue(t *testing.T) {
	rule := strings.NewReader(`{
		"filter": [
			{"var": "people"},
			{"==": [
				{"var": ".age"},
				{"min": {"map": [
					{"var": "people"},
					{"var": ".age"}
				]}}
			]}
		]
	}`)

	data := strings.NewReader(`{
		"people": [
			{"age":0, "name":"John"}
		]
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[
		{"age": 0, "name": "John"}
	]`

	assert.JSONEq(t, expected, result.String())
}

func TestListOfRanges(t *testing.T) {
	rule := strings.NewReader(`{
		"filter": [
			{"var": "people"},
			{"in": [
				{"var": ".age"},
				[
					[12, 18],
					[22, 28],
					[32, 38]
				]
			]}
		]
	}`)

	data := strings.NewReader(`{
		"people": [
			{"age":18, "name":"John"},
			{"age":20, "name":"Luke"},
			{"age":18, "name":"Mark"}
		]
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[
		{"age": 18, "name": "John"},
		{"age": 18, "name": "Mark"}
	]`

	assert.JSONEq(t, expected, result.String())
}

func TestSomeWithLists(t *testing.T) {
	rule := strings.NewReader(`{
		"some": [
			[511, 521, 811],
			{"in":[
				{"var":""},
				[1, 2, 3, 511]
			]}
		]
	}`)

	data := strings.NewReader(`{}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, "true", result.String())
}

func TestAllWithLists(t *testing.T) {
	rule := strings.NewReader(`{
		"all": [
			[511, 521, 811],
			{"in":[
				{"var":""},
				[511, 521, 811, 3]
			]}
		]
	}`)

	data := strings.NewReader("{}")

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, "true", result.String())
}

func TestAllWithArrayOfMapData(t *testing.T) {
	data := strings.NewReader(`[
		{
		  "P1": "A",
		  "P2":"a"
		},

		{
		  "P1": "B",
		  "P2":"b"
		}
	  ]`)
	rule := strings.NewReader(`
	  {
		"all": [
		  { "var": "" },
		  { "in": [ {"var": "P1"} , ["A","B"]] }
		]
	  }
	`)
	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}
	assert.JSONEq(t, "true", result.String())
}

func TestNoneWithLists(t *testing.T) {
	rule := strings.NewReader(`{
		"none": [
			[511, 521, 811],
			{"in":[
				{"var":""},
				[1, 2]
			]}
		]
	}`)

	data := strings.NewReader("{}")

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, "true", result.String())
}

func TestInOperatorWorksWithMaps(t *testing.T) {
	rule := strings.NewReader(`{
		"some": [
			[511,521,811],
			{"in": [
				{"var": ""},
				{"map": [
					{"var": "my_list"},
					{"var": ".service_id"}
				]}
			]}
		]
	}`)

	data := strings.NewReader(`{
		"my_list": [
			{"service_id": 511},
			{"service_id": 771},
			{"service_id": 521},
			{"service_id": 181}
		]
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, "true", result.String())
}

func TestAbsoluteValue(t *testing.T) {
	rule := strings.NewReader(`{
		"abs": { "var": "test.number" }
	}`)

	data := strings.NewReader(`{
		"test": {
			"number": -2
		}
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, "2", result.String())
}

func TestMergeArrayOfArrays(t *testing.T) {
	rule := strings.NewReader(`{
		"merge": [
			[
				[
					"18800000",
					"18800969"
				]
			],
			[
				[
					"19840000",
					"19840969"
				]
			]
		]
	}`)
	data := strings.NewReader(`{}`)

	expectedResult := "[[\"18800000\",\"18800969\"],[\"19840000\",\"19840969\"]]"

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, expectedResult, result.String())
}

func TestDataWithDefaultValueWithApplyRaw(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"+": [
			1,
			2
		]
	}`)

	var expected json.RawMessage = json.RawMessage("3")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestDataWithDefaultValueWithApplyInterface(t *testing.T) {
	rule := map[string]any{
		"+": []any{
			float64(1),
			float64(2),
		},
	}

	expected := float64(3)
	output, err := jsonlogic.ApplyInterface(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.Equal(t, expected, output.(float64))
}

func TestMissingOperators(t *testing.T) {
	rule := map[string]any{
		"sum": []any{
			float64(1),
			float64(2),
		},
	}

	_, err := jsonlogic.ApplyInterface(rule, nil)

	assert.EqualError(t, err, "The operator \"sum\" is not supported")
}

func TestZeroDivision(t *testing.T) {
	logic := strings.NewReader(`{"/":[0,10]}`)
	data := strings.NewReader(`{}`)
	var result bytes.Buffer

	jsonlogic.Apply(logic, data, &result) // nolint:errcheck

	assert.JSONEq(t, `0`, result.String())
}

func TestSliceWithOnlyWithNumbersAsKey(t *testing.T) {
	rule := strings.NewReader(`{"var": "people.0"}`)

	data := strings.NewReader(`{
		"people": [
			{"age":18, "name":"John"},
			{"age":20, "name":"Luke"},
			{"age":18, "name":"Mark"}
		]
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"age": 18, "name": "John"}`

	assert.JSONEq(t, expected, result.String())
}

func TestMapWithOnlyWithNumbersAsKey(t *testing.T) {
	rule := strings.NewReader(`{"var": "people.103"}`)

	data := strings.NewReader(`{
		"people": {
			"100": {"age":18, "name":"John"},
			"101": {"age":20, "name":"Luke"},
			"103": {"age":18, "name":"Mark"}
		}
	}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"age": 18, "name": "Mark"}`

	assert.JSONEq(t, expected, result.String())
}

func TestBetweenIsBiggerEq(t *testing.T) {
	rule := strings.NewReader(`{
		"filter": [
			[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
			{">=": [8, {"var": ""}, 3]}
		]
	}`)

	data := strings.NewReader(`{}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[3, 4, 5, 6, 7, 8]`

	assert.JSONEq(t, expected, result.String())
}

func TestBetweenIsBigger(t *testing.T) {
	rule := strings.NewReader(`{
		"filter": [
			[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
			{">": [8, {"var": ""}, 3]}
		]
	}`)

	data := strings.NewReader(`{}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[4, 5, 6, 7]`

	assert.JSONEq(t, expected, result.String())
}

func TestUnaryOperation(t *testing.T) {
	logic := strings.NewReader(`{"and":[{"!":{"var":"var_not_in_data"}}]}`)
	data := strings.NewReader(`{"some_key": "value"}`)

	var result bytes.Buffer
	assert.Nil(t, jsonlogic.Apply(logic, data, &result))

	assert.JSONEq(t, `true`, result.String())
}

func TestInOperatorAgainstNil(t *testing.T) {
	rule := strings.NewReader(`{"filter":[{"var": "accounts"},{"and":[{"in":["abc",{"var":"tags.tag-1"}]}]}]}`)
	data := strings.NewReader(`{"accounts":[{"name":"account-1","tags":{"tag-1":"abc"}}, {"name":"account-2","tags":{"tag-2":"xyz"}}]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `[
		{
			"name": "account-1",
			"tags": {
				"tag-1": "abc"
			}
		}
	]`

	assert.JSONEq(t, expected, result.String())
}

func TestReduceFilterAndContains(t *testing.T) {
	rule := strings.NewReader(`{"reduce":[{"filter":[{"var":"data.level1.level2"},{"==":[{"var":"access"},true]}]},{"or":[{"var":"current.access"},{"var":"accumulator"}]},false]}`)
	data := strings.NewReader(`{"data":{"level1":{"level2":[{"access":true }]}}}}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `true`

	assert.JSONEq(t, expected, result.String())
}

func TestReduceFilterAndNotContains(t *testing.T) {
	rule := strings.NewReader(`{"reduce":[{"filter":[{"var":"data.level1.level2"},{"==":[{"var":"access"},true]}]},{"or":[{"var":"current.access"},{"var":"accumulator"}]},false]}`)
	data := strings.NewReader(`{"data":{"level1":{"level2":[{"access":false }]}}}}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `false`

	assert.JSONEq(t, expected, result.String())
}

func TestReduceWithUnsupportedValue(t *testing.T) {
	b := []byte(`{"reduce":[{"filter":[{"var":"data"},{"==":[{"var":""},""]}]},{"cat":[{"var":"current"},{"var":"accumulator"}]},null]}`)

	rule := map[string]any{}
	_ = json.Unmarshal(b, &rule)
	data := map[string]any{
		"data": []any{"str"},
	}

	_, err := jsonlogic.ApplyInterface(rule, data)
	assert.EqualError(t, err, "The type \"<nil>\" is not supported")
}

func TestAddOperator(t *testing.T) {
	jsonlogic.AddOperator("strlen", func(values, data any) any {
		v, ok := values.(string)

		if ok {
			return len(v)
		}
		return 0
	})
	logic := strings.NewReader(`{ "strlen": { "var": "foo" } }`)
	data := strings.NewReader(`{"foo": "bar"}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	expected := `3`

	assert.JSONEq(t, expected, result.String())
}

func TestInWithOneParam(t *testing.T) {
	rule := strings.NewReader(`{"in": [ "Ringo" ]}`)
	data := strings.NewReader(`null`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, `false`, result.String())
}

func TestEqualWithList(t *testing.T) {
	rule := strings.NewReader(`{"==": [ 2, [3, 2, 1] ]}`)
	data := strings.NewReader(`null`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, `false`, result.String())
}

func TestMinusWithEmptyList(t *testing.T) {
	rule := strings.NewReader(`{"-": []}`)
	data := strings.NewReader(`null`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, `0`, result.String())
}

func TestDivWithEmptyList(t *testing.T) {
	rule := strings.NewReader(`{"/": []}`)
	data := strings.NewReader(`null`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, data, &result)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, `0`, result.String())
}


================================================
FILE: lists.go
================================================
package jsonlogic

import (
	"fmt"
	"strings"

	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

// ErrReduceDataType represents an error when an unsupported data type is used in reduce operations.
// It contains the data type name that caused the error.
type ErrReduceDataType struct {
	dataType string
}

func (e ErrReduceDataType) Error() string {
	return fmt.Sprintf("The type \"%s\" is not supported", e.dataType)
}

func extractSubject(parsed []any, data any) any {
	var subject any

	if typing.IsSlice(parsed[0]) {
		subject = parsed[0]
	}

	if typing.IsMap(parsed[0]) {
		subject = apply(parsed[0], data)
	}

	return subject
}

func filter(values, data any) any {
	parsed := values.([]any)
	if len(parsed) < 2 {
		return []any{}
	}

	subject := extractSubject(parsed, data)
	if subject == nil {
		return []any{}
	}

	subjectSlice := subject.([]any)
	subjectLen := len(subjectSlice)

	// Pre-allocate result with capacity that's reasonable for filtering
	// Assuming at least half might pass the filter (heuristic)
	result := make([]any, 0, subjectLen/2)

	logic := solveVars(parsed[1], data)

	for _, value := range subjectSlice {
		v := parseValues(logic, value)

		if typing.IsTrue(v) {
			result = append(result, value)
		}
	}

	return result
}

func _map(values, data any) any {
	parsed := values.([]any)
	if len(parsed) < 2 {
		return []any{}
	}

	subject := extractSubject(parsed, data)
	if subject == nil {
		return []any{}
	}

	subjectSlice := subject.([]any)
	subjectLen := len(subjectSlice)

	result := make([]any, 0, subjectLen)

	logic := parsed[1]

	for _, value := range subjectSlice {
		v := parseValues(logic, value)
		result = append(result, v)
	}

	return result
}

func reduce(values, data any) any {
	parsed := values.([]any)
	if len(parsed) < 3 {
		return float64(0)
	}

	var (
		accumulator any
		valueType   string
	)

	{
		initialValue := parsed[2]
		if typing.IsMap(initialValue) {
			initialValue = apply(initialValue, data)
		}

		if typing.IsBool(initialValue) {
			accumulator = typing.IsTrue(initialValue)
			valueType = "bool"
		} else if typing.IsNumber(initialValue) {
			accumulator = typing.ToNumber(initialValue)
			valueType = "number"
		} else if typing.IsString(initialValue) {
			accumulator = typing.ToString(initialValue)
			valueType = "string"
		} else {
			panic(ErrReduceDataType{
				dataType: fmt.Sprintf("%T", parsed[2]),
			})
		}
	}

	context := map[string]any{
		"current":     float64(0),
		"accumulator": accumulator,
		"valueType":   valueType,
	}

	subject := extractSubject(parsed, data)
	if subject == nil {
		return float64(0)
	}

	for _, value := range subject.([]any) {
		if value == nil {
			continue
		}

		context["current"] = value

		v := apply(parsed[1], context)

		switch context["valueType"] {
		case "bool":
			context["accumulator"] = typing.IsTrue(v)
		case "number":
			context["accumulator"] = typing.ToNumber(v)
		case "string":
			context["accumulator"] = typing.ToString(v)
		}
	}

	return context["accumulator"]
}

func _in(values, data any) any {
	values = parseValues(values, data)

	parsed := values.([]any)

	a := parsed[0]
	var b any
	if len(parsed) > 1 {
		b = parsed[1]
	}

	if typing.IsString(b) {
		return strings.Contains(b.(string), a.(string))
	}

	if !typing.IsSlice(b) {
		return false
	}

	for _, element := range b.([]any) {
		if typing.IsSlice(element) {
			if _inRange(a, element.([]any)) {
				return true
			}

			continue
		}

		if typing.IsNumber(a) {
			if typing.ToNumber(element) == a {
				return true
			}

			continue
		}

		if element == a {
			return true
		}
	}

	return false
}

func merge(values, data any) any {
	values = parseValues(values, data)
	if typing.IsPrimitive(values) {
		return []any{values}
	}

	inputSlice := values.([]any)
	sliceLen := len(inputSlice)
	if sliceLen == 0 {
		return inputSlice
	}

	totalCapacity := 0
	for _, value := range inputSlice {
		if typing.IsSlice(value) {
			totalCapacity += len(value.([]any))
		} else {
			totalCapacity++
		}
	}

	result := make([]any, 0, totalCapacity)

	for _, value := range inputSlice {
		if !typing.IsSlice(value) {
			result = append(result, value)
			continue
		}

		result = append(result, value.([]any)...)
	}

	return result
}

func missing(values, data any) any {
	values = parseValues(values, data)
	if typing.IsString(values) {
		values = []any{values}
	}

	missing := make([]any, 0)

	for _, _var := range values.([]any) {
		_value := getVar(_var, data)

		if _value == nil {
			missing = append(missing, _var)
		}
	}

	return missing
}

func missingSome(values, data any) any {
	values = parseValues(values, data)
	parsed := values.([]any)
	number := int(typing.ToNumber(parsed[0]))
	vars := parsed[1]

	missing := make([]any, 0)
	found := make([]any, 0)

	for _, _var := range vars.([]any) {
		_value := getVar(_var, data)

		if _value == nil {
			missing = append(missing, _var)
		} else {
			found = append(found, _var)
		}
	}

	if number > len(found) {
		return missing
	}

	return make([]any, 0)
}

func all(values, data any) any {
	parsed := values.([]any)

	subject := extractSubject(parsed, data)
	if !typing.IsTrue(subject) {
		return false
	}

	for _, value := range subject.([]any) {
		conditions := solveVars(parsed[1], value)
		v := apply(conditions, value)

		if !typing.IsTrue(v) {
			return false
		}
	}

	return true
}

func none(values, data any) any {
	parsed := values.([]any)

	subject := extractSubject(parsed, data)

	if !typing.IsTrue(subject) {
		return true
	}

	conditions := solveVars(parsed[1], data)

	for _, value := range subject.([]any) {
		v := apply(conditions, value)

		if typing.IsTrue(v) {
			return false
		}
	}

	return true
}

func some(values, data any) any {
	parsed := values.([]any)
	subject := extractSubject(parsed, data)

	if !typing.IsTrue(subject) {
		return false
	}

	logic := solveVars(parsed[1], data)
	for _, value := range subject.([]any) {
		conditions := solveVars(logic, value)
		v := apply(conditions, value)

		if typing.IsTrue(v) {
			return true
		}
	}

	return false
}

func _inRange(value any, values []any) bool {
	i := values[0]
	j := values[1]

	return typing.ToNumber(value) >= typing.ToNumber(i) && typing.ToNumber(j) >= typing.ToNumber(value)
}


================================================
FILE: lists_test.go
================================================
package jsonlogic_test

import (
	"bytes"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

func TestFilterParseTheSubjectFromFirstPosition(t *testing.T) {
	rule := strings.NewReader(`{"filter": [
		[1,2,3,4,5],
		{"%":[{"var":""},2]}
	]}`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, nil, &result)
	assert.Nil(t, err)
	assert.JSONEq(t, `[1,3,5]`, result.String())
}

func TestFilterParseTheSubjectFromNullValue(t *testing.T) {
	rule := strings.NewReader(`{"filter": [
		null,
		{"%":[{"var":""},2]}
	]}`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, nil, &result)
	assert.Nil(t, err)
	assert.JSONEq(t, `[]`, result.String())
}

func TestReduceSkipNullValues(t *testing.T) {
	rule := strings.NewReader(`{"reduce": [
		[1,2,null,4,5],
		{"+":[{"var":"current"}, {"var":"accumulator"}]},
		0
	]}`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, nil, &result)
	assert.Nil(t, err)
	assert.JSONEq(t, `12`, result.String())
}

func TestReduceBoolValues(t *testing.T) {
	rule := strings.NewReader(`{"reduce": [
		[true,false,true,null],
		{"or":[{"var":"current"}, {"var":"accumulator"}]},
		false
	]}`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, nil, &result)
	assert.Nil(t, err)
	assert.JSONEq(t, `true`, result.String())
}

func TestReduceStringValues(t *testing.T) {
	rule := strings.NewReader(`{"reduce": [
		["a",null,"b"],
		{"cat":[{"var":"current"}, {"var":"accumulator"}]},
		""
	]}`)

	var result bytes.Buffer

	err := jsonlogic.Apply(rule, nil, &result)
	assert.Nil(t, err)
	assert.JSONEq(t, `"ba"`, result.String())
}

func TestFilterWithMissingLogicArgument(t *testing.T) {
	// filter needs [array, logic]; omitting the logic argument must not panic.
	rule := strings.NewReader(`{"filter": [[1,2,3]]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, nil, &result)
	assert.NoError(t, err)
	assert.JSONEq(t, `[]`, result.String())
}

func TestMapWithMissingLogicArgument(t *testing.T) {
	// map needs [array, logic]; omitting the logic argument must not panic.
	rule := strings.NewReader(`{"map": [[1,2,3]]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, nil, &result)
	assert.NoError(t, err)
	assert.JSONEq(t, `[]`, result.String())
}

func TestReduceWithMissingInitialValue(t *testing.T) {
	// reduce needs [array, logic, initial]; omitting initial value must not panic.
	rule := strings.NewReader(`{"reduce": [
		[1,2,3],
		{"+":[{"var":"current"},{"var":"accumulator"}]}
	]}`)

	var result bytes.Buffer
	err := jsonlogic.Apply(rule, nil, &result)
	assert.NoError(t, err)
	assert.JSONEq(t, `0`, result.String())
}


================================================
FILE: logic.go
================================================
package jsonlogic

import (
	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

func _and(values, data any) any {
	values = values.([]any)

	var last any
	for _, value := range values.([]any) {
		last = parseValues(value, data)
		if !typing.IsTrue(last) {
			return last
		}
	}

	return last
}

func _or(values, data any) any {
	values = values.([]any)

	var last any
	for _, value := range values.([]any) {
		last = parseValues(value, data)
		if typing.IsTrue(last) {
			return last
		}
	}

	return last
}

func evaluateClause(clause any, data any) any {
	parsed := parseValues(clause, data)

	if typing.IsMap(parsed) {
		return apply(parsed, data)
	}

	return parsed
}

func conditional(values, data any) any {
	values = values.([]any)

	clauses := values.([]any)

	length := len(clauses)

	if length == 0 {
		return nil
	}

	// Evaluate each if/then pair
	for i := 0; i < length-1; i = i + 2 {
		condition := parseValues(clauses[i], data)

		// If the condition is true, evaluate and return the then clause
		if typing.IsTrue(condition) {
			return evaluateClause(clauses[i+1], data)
		}
	}

	// If no matches and there is an odd number of clauses, evaluate and return the else clause
	if length%2 == 1 {
		return evaluateClause(clauses[length-1], data)
	}

	return nil
}

func negative(values, data any) any {
	values = parseValues(values, data)
	// If the slice is not empty, there is an argument to negate
	if typing.IsSlice(values) && len(values.([]any)) > 0 {
		return !typing.IsTrue(values.([]any)[0])
	}
	return !typing.IsTrue(values)
}


================================================
FILE: math.go
================================================
package jsonlogic

import (
	"math"

	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

func mod(values, data any) any {
	_values := parseValues(values, data).([]any)

	a, b := _values[0], _values[1]

	_a := typing.ToNumber(a)
	_b := typing.ToNumber(b)

	return math.Mod(_a, _b)
}

func abs(values, data any) any {
	values = parseValues(values, data)
	if typing.IsSlice(values) {
		return math.Abs(typing.ToNumber(values.([]any)[0]))
	}

	return math.Abs(typing.ToNumber(values))
}

func sum(values, data any) any {
	values = parseValues(values, data)
	if !typing.IsSlice(values) {
		return typing.ToNumber(values)
	}

	inputSlice := values.([]any)
	sliceLen := len(inputSlice)

	if sliceLen == 0 {
		return float64(0)
	}

	if sliceLen == 1 {
		return typing.ToNumber(inputSlice[0])
	}

	sum := float64(0)
	for _, n := range inputSlice {
		sum += typing.ToNumber(n)
	}

	return sum
}

func minus(values, data any) any {
	_values := parseValues(values, data).([]any)

	if len(_values) == 0 {
		return 0
	}

	if len(_values) == 1 {
		return -1 * typing.ToNumber(_values[0])
	}

	sum := typing.ToNumber(_values[0])
	for i := 1; len(_values) > i; i++ {
		sum -= typing.ToNumber(_values[i])
	}

	return sum
}

func mult(values, data any) any {
	values = parseValues(values, data)

	sum := float64(1)

	for _, n := range values.([]any) {
		sum *= typing.ToNumber(n)
	}

	return sum
}

func div(values, data any) any {
	_values := parseValues(values, data).([]any)

	if len(_values) == 0 {
		return 0
	}

	sum := typing.ToNumber(_values[0])
	for i := 1; len(_values) > i; i++ {
		sum = sum / typing.ToNumber(_values[i])
	}

	return sum
}

func max(values, data any) any {
	values = parseValues(values, data)
	parsed := values.([]any)
	size := len(parsed)
	if size == 0 {
		return nil
	}

	bigger := typing.ToNumber(parsed[0])

	for i := 1; i < size; i++ {
		_n := typing.ToNumber(parsed[i])
		if _n > bigger {
			bigger = _n
		}
	}

	return bigger
}

func min(values, data any) any {
	values = parseValues(values, data)
	parsed := values.([]any)
	size := len(parsed)
	if size == 0 {
		return nil
	}

	smallest := typing.ToNumber(parsed[0])

	for i := 1; i < size; i++ {
		_n := typing.ToNumber(parsed[i])
		if smallest > _n {
			smallest = _n
		}
	}

	return smallest
}


================================================
FILE: math_test.go
================================================
package jsonlogic_test

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

func TestSubOperation(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"-": [
			0,
			10
		]
	}`)

	var expected json.RawMessage = json.RawMessage("-10")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestAbsOperationWithScalar(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"abs": -42
	}`)

	var expected json.RawMessage = json.RawMessage("42")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestAbsOperationWithArray(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"abs": [-42]
	}`)

	var expected json.RawMessage = json.RawMessage("42")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestSumOperationWithEmptyArray(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"+": []
	}`)

	var expected json.RawMessage = json.RawMessage("0")

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}


================================================
FILE: operation.go
================================================
package jsonlogic

import (
	"fmt"
	"sync"

	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

// OperatorFn defines the signature for custom operator functions.
// It takes values and data as input and returns a result.
type OperatorFn func(values, data any) (result any)

// ErrInvalidOperator represents an error when an unsupported operator is used.
// It contains the operator name that caused the error.
type ErrInvalidOperator struct {
	operator string
}

func (e ErrInvalidOperator) Error() string {
	return fmt.Sprintf("The operator \"%s\" is not supported", e.operator)
}

// operators holds custom operators
var operators = make(map[string]OperatorFn)

var operatorsLock = &sync.RWMutex{}

// AddOperator registers a custom operator with the given key and function.
// The operator function will be called with parsed values and the original data context.
//
// Parameters:
//   - key: the operator name to register (e.g., "custom_op")
//   - cb: the function to execute when the operator is encountered
//
// Concurrency: This function is safe for concurrent use as it properly locks the operators map.
func AddOperator(key string, cb OperatorFn) {
	operatorsLock.Lock()
	defer operatorsLock.Unlock()

	operators[key] = func(values, data any) any {
		return cb(parseValues(values, data), data)
	}
}

func operation(operator string, values, data any) any {
	operatorsLock.RLock()
	opFn, found := operators[operator]
	operatorsLock.RUnlock()
	if found {
		return opFn(values, data)
	}

	panic(ErrInvalidOperator{
		operator: operator,
	})
}

func init() {
	operatorsLock.Lock()
	defer operatorsLock.Unlock()

	operators["and"] = _and
	operators["or"] = _or
	operators["filter"] = filter
	operators["map"] = _map
	operators["reduce"] = reduce
	operators["all"] = all
	operators["none"] = none
	operators["some"] = some
	operators["in"] = _in
	operators["missing"] = missing
	operators["missing_some"] = missingSome
	operators["var"] = getVar
	operators["set"] = setProperty
	operators["cat"] = concat
	operators["substr"] = substr
	operators["merge"] = merge
	operators["if"] = conditional
	operators["?:"] = conditional
	operators["max"] = max
	operators["min"] = min
	operators["+"] = sum
	operators["-"] = minus
	operators["*"] = mult
	operators["/"] = div
	operators["%"] = mod
	operators["abs"] = abs
	operators["!"] = negative
	operators["!!"] = func(v, d any) any { return !typing.IsTrue(negative(v, d)) }
	operators["==="] = hardEquals
	operators["!=="] = func(v, d any) any { return !hardEquals(v, d).(bool) }
	operators["<"] = isLessThan
	operators["<="] = isLessOrEqualThan
	operators[">"] = isGreaterThan
	operators[">="] = isGreaterOrEqualThan
	operators["=="] = isEqual
	operators["!="] = func(v, d any) any { return !isEqual(v, d).(bool) }

	/* CUSTOM OPERATORS */
	operators["contains_all"] = func(v, d any) any { return containsAll(parseValues(v, d), d) }
	operators["contains_any"] = func(v, d any) any { return containsAny(parseValues(v, d), d) }
	operators["contains_none"] = func(v, d any) any { return containsNone(parseValues(v, d), d) }
}


================================================
FILE: operation_test.go
================================================
package jsonlogic

import (
	"io"
	"strings"
	"sync"
	"testing"
)

// TestConcurrentApplyAndAddOperator validates that validating rules and adding operators concurrently
// doesn't cause fatal errors or deadlocks.
func TestConcurrentValidationAndAddOperator(t *testing.T) {
	var wg sync.WaitGroup
	numRoutines := 10
	numIterations := 100

	// Start multiple goroutines to validate rules concurrently
	for i := 0; i < numRoutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < numIterations; j++ {
				rule := `{"==": [1, 1]}`
				_ = IsValid(strings.NewReader(rule))
			}
		}()
	}

	// Start a goroutine to add a new operator concurrently
	wg.Add(1)
	go func() {
		defer wg.Done()
		for j := 0; j < numIterations; j++ {
			AddOperator("test_op", func(values, data any) any {
				return "test"
			})
		}
	}()

	wg.Wait()
}

// TestConcurrentApplyAndAddOperator validates that applying rules and adding operators concurrently
// doesn't cause fatal errors or deadlocks.
func TestConcurrentApplyAndAddOperator(t *testing.T) {
	var wg sync.WaitGroup
	numRoutines := 10
	numIterations := 100

	// Start multiple goroutines to apply rules concurrently
	for i := 0; i < numRoutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < numIterations; j++ {
				rule := `{"==": [1, 1]}`
				data := `{}`
				_ = Apply(strings.NewReader(rule), strings.NewReader(data), io.Discard)
			}
		}()
	}

	// Start a goroutine to add a new operator concurrently
	wg.Add(1)
	go func() {
		defer wg.Done()
		for j := 0; j < numIterations; j++ {
			AddOperator("test_op", func(values, data any) any {
				return "test"
			})
		}
	}()

	wg.Wait()
}


================================================
FILE: readme.md
================================================
# Go JsonLogic

![test workflow](https://github.com/diegoholiveira/jsonlogic/actions/workflows/test.yml/badge.svg)
[![codecov](https://codecov.io/gh/diegoholiveira/jsonlogic/branch/master/graph/badge.svg)](https://codecov.io/gh/diegoholiveira/jsonlogic)
[![Go Report Card](https://goreportcard.com/badge/github.com/diegoholiveira/jsonlogic)](https://goreportcard.com/report/github.com/diegoholiveira/jsonlogic)

Implementation of [JsonLogic](http://jsonlogic.com) in Go Lang.

## What's JsonLogic?

JsonLogic is a DSL to write logic decisions in JSON. It's has a great specification and is very simple to learn.
The [official website](http://jsonlogic.com) has great documentation with examples.

## How to use it

The use of this library is very straightforward. Here's a simple example:

```go
package main

import (
	"bytes"
	"fmt"
	"strings"

	"github.com/diegoholiveira/jsonlogic/v3"
)

func main() {
	logic := strings.NewReader(`{"==": [1, 1]}`)
	data := strings.NewReader(`{}`)

	var result bytes.Buffer

	jsonlogic.Apply(logic, data, &result)

	fmt.Println(result.String())
}
```

This will output `true` in your console.

Here's another example, but this time using variables passed in the `data` parameter:

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/diegoholiveira/jsonlogic/v3"
)

type (
	User struct {
		Name     string `json:"name"`
		Age      int    `json:"age"`
		Location string `json:"location"`
	}

	Users []User
)

func main() {
	logic := strings.NewReader(`{
        "filter": [
            {"var": "users"},
            {">=": [
                {"var": ".age"},
                18
            ]}
        ]
    }`)

	data := strings.NewReader(`{
        "users": [
            {"name": "Diego", "age": 33, "location": "Florianópolis"},
            {"name": "Jack", "age": 12, "location": "London"},
            {"name": "Pedro", "age": 19, "location": "Lisbon"},
            {"name": "Leopoldina", "age": 30, "location": "Rio de Janeiro"}
        ]
    }`)

	var result bytes.Buffer

	err := jsonlogic.Apply(logic, data, &result)
	if err != nil {
		fmt.Println(err.Error())

		return
	}

	var users Users

	decoder := json.NewDecoder(&result)
	decoder.Decode(&users)

	for _, user := range users {
		fmt.Printf("    - %s\n", user.Name)
	}
}
```

If you have a function you want to expose as a JsonLogic operation, you can use:

```go
package main

import (
	"bytes"
	"fmt"
	"strings"

	"github.com/diegoholiveira/jsonlogic/v3"
)

func main() {
	// add a new operator "strlen" for get string length
	jsonlogic.AddOperator("strlen", func(values, data any) any {
		v, ok := values.(string)
		if ok {
			return len(v)
		}
		return 0
	})

	logic := strings.NewReader(`{ "strlen": { "var": "foo" } }`)
	data := strings.NewReader(`{"foo": "bar"}`)

	var result bytes.Buffer

	jsonlogic.Apply(logic, data, &result)

	fmt.Println(result.String()) // the string length of "bar" is 3
}
```

If you want to get the JsonLogic used, with the variables replaced by their values:

```go
package main

import (
	"fmt"
	"encoding/json"

	"github.com/diegoholiveira/jsonlogic/v3"
)

func main() {
	logic := json.RawMessage(`{ "==":[{ "var":"foo" }, true] }`)
	data := json.RawMessage(`{"foo": "false"}`)

	result, err := jsonlogic.GetJsonLogicWithSolvedVars(logic, data)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(result)) // will output { "==":[false, true] }
}
```

## Custom Operators (Non-standard)

> ⚠️ **Warning**: These operators are not part of the official JsonLogic specification and may be deprecated in future versions.

| Operator | Description | Example |
|----------|-------------|---------|
| `contains_all` | Returns `true` if **all** elements in the second array exist in the first array | `{"contains_all": [["a","b","c"], ["a","b"]]}` → `true` |
| `contains_any` | Returns `true` if **any** element in the second array exists in the first array | `{"contains_any": [["a","b"], ["x","a"]]}` → `true` |
| `contains_none` | Returns `true` if **no** elements in the second array exist in the first array | `{"contains_none": [["a","b"], ["x","y"]]}` → `true` |

# License

This project is licensed under the MIT License - see [LICENSE](./LICENSE) for details.


================================================
FILE: strings.go
================================================
package jsonlogic

import (
	"strings"

	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

func substr(values, data any) any {
	values = parseValues(values, data)
	parsed := values.([]any)

	runes := []rune(typing.ToString(parsed[0]))

	from := int(typing.ToNumber(parsed[1]))
	length := len(runes)

	if from < 0 {
		from = length + from
	}

	if from < 0 || from > length {
		// case from is still negative, we must stop right now and return the original string
		return string(runes)
	}

	if len(parsed) == 3 {
		length = int(typing.ToNumber(parsed[2]))
	}

	var to int
	if length < 0 {
		length = len(runes) + length
		to = length
	} else {
		to = from + length
	}

	if to > len(runes) {
		to = len(runes)
	}

	return string(runes[from:to])
}

func concat(values, data any) any {
	values = parseValues(values, data)
	if typing.IsString(values) {
		return values
	}

	inputSlice := values.([]any)

	if len(inputSlice) == 0 {
		return ""
	}

	if len(inputSlice) == 1 {
		return typing.ToString(inputSlice[0])
	}

	var s strings.Builder

	for _, text := range inputSlice {
		s.WriteString(typing.ToString(text))
	}

	return strings.TrimSpace(s.String())
}


================================================
FILE: strings_test.go
================================================
package jsonlogic_test

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

func TestCat(t *testing.T) {
	testCases := []struct {
		name     string
		rule     string
		data     string
		expected string
	}{
		{
			name:     "Empty string",
			rule:     `{"cat": ""}`,
			data:     `{}`,
			expected: `""`,
		},
		{
			name:     "Empty array",
			rule:     `{"cat": []}`,
			data:     `{}`,
			expected: `""`,
		},
		{
			name:     "Single string",
			rule:     `{"cat": "hello"}`,
			data:     `{}`,
			expected: `"hello"`,
		},
		{
			name:     "Multiple strings",
			rule:     `{"cat": ["hello", " ", "world"]}`,
			data:     `{}`,
			expected: `"hello world"`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			rule := json.RawMessage(tc.rule)
			data := json.RawMessage(tc.data)
			expected := json.RawMessage(tc.expected)

			output, err := jsonlogic.ApplyRaw(rule, data)
			if err != nil {
				t.Fatal(err)
			}

			assert.JSONEq(t, string(expected), string(output))
		})
	}
}


================================================
FILE: validator.go
================================================
package jsonlogic

import (
	"encoding/json"
	"io"

	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

// IsValid reads a JSON Logic rule from io.Reader and validates its syntax.
// It checks if the rule conforms to valid JSON Logic format and uses supported operators.
//
// Parameters:
//   - rule: io.Reader containing the JSON Logic rule to validate
//
// Returns:
//   - bool: true if the rule is valid, false otherwise
//
// The function returns false if the JSON cannot be parsed or if the rule contains invalid operators.
func IsValid(rule io.Reader) bool {
	var _rule any

	decoderRule := json.NewDecoder(rule)
	err := decoderRule.Decode(&_rule)
	if err != nil {
		return false
	}

	return ValidateJsonLogic(_rule)
}

// ValidateJsonLogic validates if the given rules conform to JSON Logic format.
// It recursively checks the structure and ensures all operators are supported.
//
// Parameters:
//   - rules: any value representing the JSON Logic rule to validate
//
// Returns:
//   - bool: true if the rules are valid JSON Logic, false otherwise
//
// The function handles primitives, maps (operators), slices (arrays), and variable references.
func ValidateJsonLogic(rules any) bool {
	if isVar(rules) {
		return true
	}

	if typing.IsMap(rules) {
		rulesMap := rules.(map[string]any)

		// A map with more than 1 key counts as a primitive so it's time to end recursion
		if len(rulesMap) > 1 {
			return true
		}

		for operator, value := range rulesMap {
			if !isOperator(operator) {
				return false
			}

			return ValidateJsonLogic(value)
		}
	}

	if typing.IsSlice(rules) {
		for _, value := range rules.([]any) {
			if typing.IsSlice(value) || typing.IsMap(value) {
				if ValidateJsonLogic(value) {
					continue
				}

				return false
			}

			if isVar(value) || typing.IsPrimitive(value) {
				continue
			}
		}

		return true
	}

	return typing.IsPrimitive(rules)
}

func isOperator(op string) bool {
	operatorsLock.RLock()
	_, isOperator := operators[op]
	operatorsLock.RUnlock()
	return isOperator
}

func isVar(value any) bool {
	if !typing.IsMap(value) {
		return false
	}

	_var, ok := value.(map[string]any)["var"]
	if !ok {
		return false
	}

	return typing.IsString(_var) || typing.IsNumber(_var) || _var == nil
}


================================================
FILE: validator_test.go
================================================
package jsonlogic_test

import (
	"fmt"
	"io"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

func TestJSONLogicValidator(t *testing.T) {
	jsonlogic.AddOperator("customOperator", func(values, data any) any {
		return values
	})

	scenarios := map[string]struct {
		IsValid bool
		Rule    io.Reader
	}{
		"invalid rule": {
			IsValid: false,
			Rule:    strings.NewReader(`{"a", "b"}`),
		},
		"invalid operator": {
			IsValid: false,
			Rule:    strings.NewReader(`{"filt":[[10, 1, 100], {">=":[{"var":""},2]}]}`),
		},
		"invalid condition inside a filter": {
			IsValid: false,
			Rule:    strings.NewReader(`{"filter":[{"var":"integers"}, {"=": [{"var":""}, [10]]}]}`),
		},
		"primitive is a valid rule": {
			IsValid: true,
			Rule:    strings.NewReader(`10`),
		},
		"primitive map is a valid rule": {
			IsValid: true,
			Rule:    strings.NewReader(`{"if": [{">=": [{ "var": "amount" }, 10] }, { "var": "amount" }, { "output": true, "result": "too low" } ]}`),
		},
		"set must be valid": {
			IsValid: true,
			Rule: strings.NewReader(`{
				"map": [
					{"var": "objects"},
					{"set": [
						{"var": ""},
						"age",
						{"+": [{"var": ".age"}, 2]}
					]},
					{"customOperator": [1, 2, 3]}
				]
			}`),
		},
	}

	for name, scenario := range scenarios {
		t.Run(fmt.Sprintf("SCENARIO:%s", name), func(t *testing.T) {
			assert.Equal(t, scenario.IsValid, jsonlogic.IsValid(scenario.Rule))
		})
	}
}


================================================
FILE: vars.go
================================================
package jsonlogic

import (
	"encoding/json"
	"strconv"
	"strings"

	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
)

func solveVars(values, data any) any {
	if typing.IsMap(values) {
		logic := map[string]any{}

		for key, value := range values.(map[string]any) {
			if key == "var" {
				if typing.IsString(value) && (value == "" || strings.HasPrefix(value.(string), ".")) {
					logic["var"] = value
					continue
				}

				val := getVar(value, data)
				if val != nil {
					return val
				}

				logic["var"] = value
			} else {
				logic[key] = solveVars(value, data)
			}
		}

		return any(logic)
	}

	if typing.IsSlice(values) {
		inputSlice := values.([]any)
		logic := make([]any, 0, len(inputSlice))

		for _, value := range inputSlice {
			logic = append(logic, solveVars(value, data))
		}

		return logic
	}

	return values
}

func getVar(values, data any) any {
	values = parseValues(values, data)
	if values == nil {
		if !typing.IsPrimitive(data) {
			return nil
		}
		return data
	}

	if typing.IsString(values) && typing.ToString(values) == "" {
		return data
	}

	if typing.IsNumber(values) {
		values = typing.ToString(values)
	}

	var _default any

	if typing.IsSlice(values) { // syntax sugar
		v := values.([]any)

		if len(v) == 0 {
			return data
		}

		if len(v) == 2 {
			_default = v[1]
		}

		values = v[0].(string)
	}

	if data == nil {
		return _default
	}

	parts := strings.Split(values.(string), ".")

	var _value any = data

	for _, part := range parts {
		if part == "" {
			continue
		}

		if typing.IsMap(_value) {
			_value = _value.(map[string]any)[part]
		} else if typing.IsSlice(_value) {
			pos := int(typing.ToNumber(part))
			container := _value.([]any)
			if pos < 0 || pos >= len(container) {
				return _default
			}
			_value = container[pos]
		} else {
			return _default
		}

		if _value == nil {
			return _default
		}
	}

	return _value
}

func solveVarsBackToJsonLogic(rule, data any) (json.RawMessage, error) {
	ruleMap := rule.(map[string]any)
	result := make(map[string]any)

	for operator, values := range ruleMap {
		result[operator] = solveVars(values, data)
	}

	resultJson, err := json.Marshal(result)
	if err != nil {
		return nil, err
	}

	// we need to use Unquote due to unicode characters (example \u003e= need to be >=)
	// used for prettier json.RawMessage
	resultEscaped, err := strconv.Unquote(strings.Replace(strconv.Quote(string(resultJson)), `\\u`, `\u`, -1))
	if err != nil {
		return nil, err
	}

	return []byte(resultEscaped), nil
}

// deepCopyMap returns a deep copy of a value produced by encoding/json:
// map[string]any, []any, or a primitive. It only handles the types that
// json.Unmarshal produces, which is all we need here.
func deepCopyMap(v any) any {
	switch val := v.(type) {
	case map[string]any:
		out := make(map[string]any, len(val))
		for k, v2 := range val {
			out[k] = deepCopyMap(v2)
		}
		return out
	case []any:
		out := make([]any, len(val))
		for i, v2 := range val {
			out[i] = deepCopyMap(v2)
		}
		return out
	default:
		return val
	}
}

func setProperty(values, data any) any {
	values = parseValues(values, data).([]any)

	_value := values.([]any)

	if len(_value) < 3 {
		if len(_value) == 0 {
			return nil
		}
		return _value[0]
	}

	object := _value[0]

	if !typing.IsMap(object) {
		return object
	}

	property := _value[1].(string)
	_modified := deepCopyMap(object).(map[string]any)
	_modified[property] = parseValues(_value[2], data)

	return any(_modified)
}


================================================
FILE: vars_test.go
================================================
package jsonlogic_test

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/assert"

	jsonlogic "github.com/diegoholiveira/jsonlogic/v3"
)

func TestSetProperty(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"set": [
			{"a": 1, "b": 2},
			"c",
			3
		]
	}`)

	var expected json.RawMessage = json.RawMessage(`{"a":1,"b":2,"c":3}`)

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestSetPropertyWithNonMapInput(t *testing.T) {
	var rule json.RawMessage = json.RawMessage(`{
		"set": [
			"not_a_map",
			"property",
			"value"
		]
	}`)

	var expected json.RawMessage = json.RawMessage(`"not_a_map"`)

	output, err := jsonlogic.ApplyRaw(rule, nil)
	if err != nil {
		t.Fatal(err)
	}

	assert.JSONEq(t, string(expected), string(output))
}

func TestGetJsonLogicWithSolvedVarsInvalidRule(t *testing.T) {
	rule := json.RawMessage(`invalid_json`)
	data := json.RawMessage(`{}`)

	_, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data)
	assert.Error(t, err)
}

func TestGetJsonLogicWithSolvedVarsInvalidData(t *testing.T) {
	rule := json.RawMessage(`{}`)
	data := json.RawMessage(`invalid_json`)

	_, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data)
	assert.Error(t, err)
}

func TestGetJsonLogicWithSolvedVarsNoData(t *testing.T) {
	rule := json.RawMessage(`{"var": "foo"}`)
	var data json.RawMessage = nil

	output, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"var":"foo"}`
	assert.JSONEq(t, expected, string(output))
}

func TestSolveVarsBackToJsonLogicWithUnicodeChars(t *testing.T) {
	rule := json.RawMessage(`{">=":[{"var":"value"},10]}`)
	data := json.RawMessage(`{"value":20}`)

	output, err := jsonlogic.GetJsonLogicWithSolvedVars(rule, data)
	if err != nil {
		t.Fatal(err)
	}

	expected := `{">=":[20,10]}`
	assert.JSONEq(t, expected, string(output))
}

func TestGetVarWithOutOfBoundsArrayIndex(t *testing.T) {
	rule := json.RawMessage(`{"var": "items.999"}`)
	data := json.RawMessage(`{"items": [1, 2, 3]}`)

	output, err := jsonlogic.ApplyRaw(rule, data)
	assert.NoError(t, err)
	assert.JSONEq(t, `null`, string(output))
}

func TestGetVarWithOutOfBoundsArrayIndexReturnsDefault(t *testing.T) {
	rule := json.RawMessage(`{"var": ["items.999", "fallback"]}`)
	data := json.RawMessage(`{"items": [1, 2, 3]}`)

	output, err := jsonlogic.ApplyRaw(rule, data)
	assert.NoError(t, err)
	assert.JSONEq(t, `"fallback"`, string(output))
}

func TestSetPropertyWithMissingValueArgument(t *testing.T) {
	rule := json.RawMessage(`{"set": [{"a": 1, "b": 2}, "c"]}`)

	output, err := jsonlogic.ApplyRaw(rule, nil)
	assert.NoError(t, err)
	assert.JSONEq(t, `{"a":1,"b":2}`, string(output))
}

func TestSetPropertyWithOnlyObjectArgument(t *testing.T) {
	rule := json.RawMessage(`{"set": [{"a": 1, "b": 2}]}`)

	output, err := jsonlogic.ApplyRaw(rule, nil)
	assert.NoError(t, err)
	assert.JSONEq(t, `{"a":1,"b":2}`, string(output))
}
Download .txt
gitextract_pw5i5pga/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── arrays.go
├── arrays_test.go
├── benchmark/
│   ├── README.md
│   ├── bench
│   └── benchmark_test.go
├── codecov.yml
├── comp.go
├── comp_test.go
├── go.mod
├── go.sum
├── internal/
│   ├── javascript/
│   │   ├── javascript.go
│   │   └── javascript_test.go
│   ├── json_logic_pr_48_tests.json
│   ├── testing.go
│   └── typing/
│       ├── typing.go
│       └── typing_test.go
├── issues_test.go
├── jsonlogic.go
├── jsonlogic_test.go
├── lists.go
├── lists_test.go
├── logic.go
├── math.go
├── math_test.go
├── operation.go
├── operation_test.go
├── readme.md
├── strings.go
├── strings_test.go
├── validator.go
├── validator_test.go
├── vars.go
└── vars_test.go
Download .txt
SYMBOL INDEX (196 symbols across 26 files)

FILE: arrays.go
  function containsAll (line 14) | func containsAll(values, data any) any {
  function containsAny (line 51) | func containsAny(values, data any) any {
  function containsNone (line 83) | func containsNone(values, data any) any {
  function toAnySlice (line 109) | func toAnySlice(value any) []any {
  function containsElement (line 122) | func containsElement(slice []any, element any) bool {
  function isEqualValue (line 132) | func isEqualValue(a, b any) bool {

FILE: arrays_test.go
  function TestContainsAll (line 9) | func TestContainsAll(t *testing.T) {
  function TestContainsAny (line 80) | func TestContainsAny(t *testing.T) {
  function TestContainsNone (line 151) | func TestContainsNone(t *testing.T) {

FILE: benchmark/benchmark_test.go
  function performWarmupRuns (line 139) | func performWarmupRuns() {
  function runBenchmark (line 155) | func runBenchmark(b *testing.B, logic, data string) {
  function BenchmarkComprehensive (line 177) | func BenchmarkComprehensive(b *testing.B) {
  function BenchmarkDetailed (line 307) | func BenchmarkDetailed(b *testing.B) {
  function BenchmarkJSONLogicParallel (line 318) | func BenchmarkJSONLogicParallel(b *testing.B) {
  function BenchmarkArrayOperationsScaling (line 369) | func BenchmarkArrayOperationsScaling(b *testing.B) {
  function BenchmarkMathOperations (line 459) | func BenchmarkMathOperations(b *testing.B) {
  function BenchmarkStringOperations (line 482) | func BenchmarkStringOperations(b *testing.B) {
  function BenchmarkLogicOperations (line 512) | func BenchmarkLogicOperations(b *testing.B) {
  function BenchmarkCustomOperators (line 547) | func BenchmarkCustomOperators(b *testing.B) {

FILE: comp.go
  function hardEquals (line 10) | func hardEquals(values, data any) any {
  function isLessThan (line 37) | func isLessThan(values, data any) any {
  function isLessOrEqualThan (line 55) | func isLessOrEqualThan(values, data any) any {
  function isGreaterThan (line 73) | func isGreaterThan(values, data any) any {
  function isGreaterOrEqualThan (line 90) | func isGreaterOrEqualThan(values, data any) any {
  function isEqual (line 108) | func isEqual(values, data any) any {
  function less (line 122) | func less(a, b any) bool {
  function equals (line 133) | func equals(a, b any) bool {

FILE: comp_test.go
  function TestHardEqualsWithNonSliceValues (line 12) | func TestHardEqualsWithNonSliceValues(t *testing.T) {
  function TestHardEqualsWithSingleValueInSlice (line 27) | func TestHardEqualsWithSingleValueInSlice(t *testing.T) {
  function TestHardEqualsWithNilInParams (line 42) | func TestHardEqualsWithNilInParams(t *testing.T) {
  function TestLessThanWithSingleArgument (line 70) | func TestLessThanWithSingleArgument(t *testing.T) {
  function TestLessOrEqualThanWithSingleArgument (line 77) | func TestLessOrEqualThanWithSingleArgument(t *testing.T) {
  function TestGreaterThanWithSingleArgument (line 84) | func TestGreaterThanWithSingleArgument(t *testing.T) {
  function TestGreaterOrEqualThanWithSingleArgument (line 91) | func TestGreaterOrEqualThanWithSingleArgument(t *testing.T) {
  function TestEqualWithSingleArgument (line 98) | func TestEqualWithSingleArgument(t *testing.T) {
  function TestHardEqualsWithDifferentTypes (line 105) | func TestHardEqualsWithDifferentTypes(t *testing.T) {

FILE: internal/javascript/javascript.go
  type UndefinedType (line 11) | type UndefinedType struct
  function At (line 21) | func At(values []any, index int) any {
  function ToNumber (line 41) | func ToNumber(v any) float64 {

FILE: internal/javascript/javascript_test.go
  function TestAt (line 10) | func TestAt(t *testing.T) {
  function TestToNumber (line 51) | func TestToNumber(t *testing.T) {

FILE: internal/testing.go
  type Test (line 13) | type Test struct
  type Tests (line 21) | type Tests
  function GetScenariosFromProposedOfficialTestSuite (line 26) | func GetScenariosFromProposedOfficialTestSuite() Tests {
  function GetScenariosFromOfficialTestSuite (line 37) | func GetScenariosFromOfficialTestSuite() Tests {
  function getScenariosFromFile (line 57) | func getScenariosFromFile(buffer []byte) Tests {

FILE: internal/typing/typing.go
  function is (line 9) | func is(obj any, kind reflect.Kind) bool {
  function IsBool (line 22) | func IsBool(obj any) bool {
  function IsString (line 35) | func IsString(obj any) bool {
  function IsNumber (line 48) | func IsNumber(obj any) bool {
  function IsPrimitive (line 67) | func IsPrimitive(obj any) bool {
  function IsMap (line 80) | func IsMap(obj any) bool {
  function IsSlice (line 93) | func IsSlice(obj any) bool {
  function IsEmptySlice (line 107) | func IsEmptySlice(obj any) bool {
  function IsTrue (line 139) | func IsTrue(obj any) bool {
  function ToNumber (line 168) | func ToNumber(value any) float64 {
  function ToString (line 194) | func ToString(value any) string {

FILE: internal/typing/typing_test.go
  function TestIsBool (line 9) | func TestIsBool(t *testing.T) {
  function TestIsString (line 33) | func TestIsString(t *testing.T) {
  function TestIsNumber (line 57) | func TestIsNumber(t *testing.T) {
  function TestIsPrimitive (line 84) | func TestIsPrimitive(t *testing.T) {
  function TestIsMap (line 107) | func TestIsMap(t *testing.T) {
  function TestIsSlice (line 131) | func TestIsSlice(t *testing.T) {
  function TestIsEmptySlice (line 155) | func TestIsEmptySlice(t *testing.T) {
  function TestIsTrue (line 184) | func TestIsTrue(t *testing.T) {
  function TestToNumber (line 212) | func TestToNumber(t *testing.T) {
  function TestToString (line 239) | func TestToString(t *testing.T) {

FILE: issues_test.go
  function TestIssue50 (line 14) | func TestIssue50(t *testing.T) {
  function TestIssue51_example1 (line 28) | func TestIssue51_example1(t *testing.T) {
  function TestIssue51_example2 (line 42) | func TestIssue51_example2(t *testing.T) {
  function TestIssue52_example1 (line 56) | func TestIssue52_example1(t *testing.T) {
  function TestIssue52_example2 (line 70) | func TestIssue52_example2(t *testing.T) {
  function TestIssue58_example (line 84) | func TestIssue58_example(t *testing.T) {
  function TestIssue70 (line 101) | func TestIssue70(t *testing.T) {
  function TestIssue71_example_empty_min (line 125) | func TestIssue71_example_empty_min(t *testing.T) {
  function TestIssue71_example_empty_max (line 139) | func TestIssue71_example_empty_max(t *testing.T) {
  function TestIssue71_example_max (line 153) | func TestIssue71_example_max(t *testing.T) {
  function TestIssue74 (line 167) | func TestIssue74(t *testing.T) {
  function TestJsonLogicWithSolvedVars (line 177) | func TestJsonLogicWithSolvedVars(t *testing.T) {
  function TestIssue79 (line 228) | func TestIssue79(t *testing.T) {
  function TestIssue83 (line 265) | func TestIssue83(t *testing.T) {
  function TestIssue81 (line 291) | func TestIssue81(t *testing.T) {
  function TestIssue96 (line 315) | func TestIssue96(t *testing.T) {
  function TestIssue98 (line 334) | func TestIssue98(t *testing.T) {
  function TestIssue110 (line 349) | func TestIssue110(t *testing.T) {
  function TestIssue125_InOperatorWithVarsInSlice (line 363) | func TestIssue125_InOperatorWithVarsInSlice(t *testing.T) {
  function TestIssue125_CustomOperatorWithVarsInSlice (line 380) | func TestIssue125_CustomOperatorWithVarsInSlice(t *testing.T) {
  function TestIssue135 (line 410) | func TestIssue135(t *testing.T) {

FILE: jsonlogic.go
  function Apply (line 44) | func Apply(rule, data io.Reader, result io.Writer) error {
  function ApplyRaw (line 82) | func ApplyRaw(rule, data json.RawMessage) (json.RawMessage, error) {
  function ApplyInterface (line 118) | func ApplyInterface(rule, data any) (output any, err error) {
  function GetJsonLogicWithSolvedVars (line 156) | func GetJsonLogicWithSolvedVars(rule, data json.RawMessage) ([]byte, err...
  function parseValues (line 178) | func parseValues(values, data any) any {
  function apply (line 206) | func apply(rules, data any) any {

FILE: jsonlogic_test.go
  function TestRulesFromJsonLogic (line 16) | func TestRulesFromJsonLogic(t *testing.T) {
  function toJSON (line 38) | func toJSON(val any) string {
  function TestDivWithOnlyOneValue (line 46) | func TestDivWithOnlyOneValue(t *testing.T) {
  function TestSetAValue (line 60) | func TestSetAValue(t *testing.T) {
  function TestLocalContext (line 93) | func TestLocalContext(t *testing.T) {
  function TestMapWithZeroValue (line 129) | func TestMapWithZeroValue(t *testing.T) {
  function TestListOfRanges (line 162) | func TestListOfRanges(t *testing.T) {
  function TestSomeWithLists (line 199) | func TestSomeWithLists(t *testing.T) {
  function TestAllWithLists (line 221) | func TestAllWithLists(t *testing.T) {
  function TestAllWithArrayOfMapData (line 243) | func TestAllWithArrayOfMapData(t *testing.T) {
  function TestNoneWithLists (line 271) | func TestNoneWithLists(t *testing.T) {
  function TestInOperatorWorksWithMaps (line 293) | func TestInOperatorWorksWithMaps(t *testing.T) {
  function TestAbsoluteValue (line 325) | func TestAbsoluteValue(t *testing.T) {
  function TestMergeArrayOfArrays (line 345) | func TestMergeArrayOfArrays(t *testing.T) {
  function TestDataWithDefaultValueWithApplyRaw (line 375) | func TestDataWithDefaultValueWithApplyRaw(t *testing.T) {
  function TestDataWithDefaultValueWithApplyInterface (line 393) | func TestDataWithDefaultValueWithApplyInterface(t *testing.T) {
  function TestMissingOperators (line 410) | func TestMissingOperators(t *testing.T) {
  function TestZeroDivision (line 423) | func TestZeroDivision(t *testing.T) {
  function TestSliceWithOnlyWithNumbersAsKey (line 433) | func TestSliceWithOnlyWithNumbersAsKey(t *testing.T) {
  function TestMapWithOnlyWithNumbersAsKey (line 455) | func TestMapWithOnlyWithNumbersAsKey(t *testing.T) {
  function TestBetweenIsBiggerEq (line 477) | func TestBetweenIsBiggerEq(t *testing.T) {
  function TestBetweenIsBigger (line 498) | func TestBetweenIsBigger(t *testing.T) {
  function TestUnaryOperation (line 519) | func TestUnaryOperation(t *testing.T) {
  function TestInOperatorAgainstNil (line 529) | func TestInOperatorAgainstNil(t *testing.T) {
  function TestReduceFilterAndContains (line 551) | func TestReduceFilterAndContains(t *testing.T) {
  function TestReduceFilterAndNotContains (line 566) | func TestReduceFilterAndNotContains(t *testing.T) {
  function TestReduceWithUnsupportedValue (line 581) | func TestReduceWithUnsupportedValue(t *testing.T) {
  function TestAddOperator (line 594) | func TestAddOperator(t *testing.T) {
  function TestInWithOneParam (line 617) | func TestInWithOneParam(t *testing.T) {
  function TestEqualWithList (line 631) | func TestEqualWithList(t *testing.T) {
  function TestMinusWithEmptyList (line 645) | func TestMinusWithEmptyList(t *testing.T) {
  function TestDivWithEmptyList (line 659) | func TestDivWithEmptyList(t *testing.T) {

FILE: lists.go
  type ErrReduceDataType (line 12) | type ErrReduceDataType struct
    method Error (line 16) | func (e ErrReduceDataType) Error() string {
  function extractSubject (line 20) | func extractSubject(parsed []any, data any) any {
  function filter (line 34) | func filter(values, data any) any {
  function _map (line 65) | func _map(values, data any) any {
  function reduce (line 91) | func reduce(values, data any) any {
  function _in (line 157) | func _in(values, data any) any {
  function merge (line 201) | func merge(values, data any) any {
  function missing (line 236) | func missing(values, data any) any {
  function missingSome (line 255) | func missingSome(values, data any) any {
  function all (line 281) | func all(values, data any) any {
  function none (line 301) | func none(values, data any) any {
  function some (line 323) | func some(values, data any) any {
  function _inRange (line 344) | func _inRange(value any, values []any) bool {

FILE: lists_test.go
  function TestFilterParseTheSubjectFromFirstPosition (line 13) | func TestFilterParseTheSubjectFromFirstPosition(t *testing.T) {
  function TestFilterParseTheSubjectFromNullValue (line 26) | func TestFilterParseTheSubjectFromNullValue(t *testing.T) {
  function TestReduceSkipNullValues (line 39) | func TestReduceSkipNullValues(t *testing.T) {
  function TestReduceBoolValues (line 53) | func TestReduceBoolValues(t *testing.T) {
  function TestReduceStringValues (line 67) | func TestReduceStringValues(t *testing.T) {
  function TestFilterWithMissingLogicArgument (line 81) | func TestFilterWithMissingLogicArgument(t *testing.T) {
  function TestMapWithMissingLogicArgument (line 91) | func TestMapWithMissingLogicArgument(t *testing.T) {
  function TestReduceWithMissingInitialValue (line 101) | func TestReduceWithMissingInitialValue(t *testing.T) {

FILE: logic.go
  function _and (line 7) | func _and(values, data any) any {
  function _or (line 21) | func _or(values, data any) any {
  function evaluateClause (line 35) | func evaluateClause(clause any, data any) any {
  function conditional (line 45) | func conditional(values, data any) any {
  function negative (line 74) | func negative(values, data any) any {

FILE: math.go
  function mod (line 9) | func mod(values, data any) any {
  function abs (line 20) | func abs(values, data any) any {
  function sum (line 29) | func sum(values, data any) any {
  function minus (line 54) | func minus(values, data any) any {
  function mult (line 73) | func mult(values, data any) any {
  function div (line 85) | func div(values, data any) any {
  function max (line 100) | func max(values, data any) any {
  function min (line 120) | func min(values, data any) any {

FILE: math_test.go
  function TestSubOperation (line 12) | func TestSubOperation(t *testing.T) {
  function TestAbsOperationWithScalar (line 30) | func TestAbsOperationWithScalar(t *testing.T) {
  function TestAbsOperationWithArray (line 45) | func TestAbsOperationWithArray(t *testing.T) {
  function TestSumOperationWithEmptyArray (line 60) | func TestSumOperationWithEmptyArray(t *testing.T) {

FILE: operation.go
  type OperatorFn (line 12) | type OperatorFn
  type ErrInvalidOperator (line 16) | type ErrInvalidOperator struct
    method Error (line 20) | func (e ErrInvalidOperator) Error() string {
  function AddOperator (line 37) | func AddOperator(key string, cb OperatorFn) {
  function operation (line 46) | func operation(operator string, values, data any) any {
  function init (line 59) | func init() {

FILE: operation_test.go
  function TestConcurrentValidationAndAddOperator (line 12) | func TestConcurrentValidationAndAddOperator(t *testing.T) {
  function TestConcurrentApplyAndAddOperator (line 45) | func TestConcurrentApplyAndAddOperator(t *testing.T) {

FILE: strings.go
  function substr (line 9) | func substr(values, data any) any {
  function concat (line 46) | func concat(values, data any) any {

FILE: strings_test.go
  function TestCat (line 12) | func TestCat(t *testing.T) {

FILE: validator.go
  function IsValid (line 20) | func IsValid(rule io.Reader) bool {
  function ValidateJsonLogic (line 42) | func ValidateJsonLogic(rules any) bool {
  function isOperator (line 85) | func isOperator(op string) bool {
  function isVar (line 92) | func isVar(value any) bool {

FILE: validator_test.go
  function TestJSONLogicValidator (line 14) | func TestJSONLogicValidator(t *testing.T) {

FILE: vars.go
  function solveVars (line 11) | func solveVars(values, data any) any {
  function getVar (line 50) | func getVar(values, data any) any {
  function solveVarsBackToJsonLogic (line 117) | func solveVarsBackToJsonLogic(rule, data any) (json.RawMessage, error) {
  function deepCopyMap (line 143) | func deepCopyMap(v any) any {
  function setProperty (line 162) | func setProperty(values, data any) any {

FILE: vars_test.go
  function TestSetProperty (line 12) | func TestSetProperty(t *testing.T) {
  function TestSetPropertyWithNonMapInput (line 31) | func TestSetPropertyWithNonMapInput(t *testing.T) {
  function TestGetJsonLogicWithSolvedVarsInvalidRule (line 50) | func TestGetJsonLogicWithSolvedVarsInvalidRule(t *testing.T) {
  function TestGetJsonLogicWithSolvedVarsInvalidData (line 58) | func TestGetJsonLogicWithSolvedVarsInvalidData(t *testing.T) {
  function TestGetJsonLogicWithSolvedVarsNoData (line 66) | func TestGetJsonLogicWithSolvedVarsNoData(t *testing.T) {
  function TestSolveVarsBackToJsonLogicWithUnicodeChars (line 79) | func TestSolveVarsBackToJsonLogicWithUnicodeChars(t *testing.T) {
  function TestGetVarWithOutOfBoundsArrayIndex (line 92) | func TestGetVarWithOutOfBoundsArrayIndex(t *testing.T) {
  function TestGetVarWithOutOfBoundsArrayIndexReturnsDefault (line 101) | func TestGetVarWithOutOfBoundsArrayIndexReturnsDefault(t *testing.T) {
  function TestSetPropertyWithMissingValueArgument (line 110) | func TestSetPropertyWithMissingValueArgument(t *testing.T) {
  function TestSetPropertyWithOnlyObjectArgument (line 118) | func TestSetPropertyWithOnlyObjectArgument(t *testing.T) {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (163K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 602,
    "preview": "name: Continuous Integration\non: [push, pull_request]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matri"
  },
  {
    "path": ".gitignore",
    "chars": 233,
    "preview": "# 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# Ou"
  },
  {
    "path": ".golangci.yml",
    "chars": 315,
    "preview": "run:\n  timeout: 5m\nlinters-settings:\n  goimports:\n    local-prefixes: github.com/diegoholiveira/jsonlogic\nlinters:\n  dis"
  },
  {
    "path": "LICENSE",
    "chars": 1090,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2018 Diego Henrique Oliveira\n\nPermission is hereby granted, free of charge, to any "
  },
  {
    "path": "arrays.go",
    "chars": 3216,
    "preview": "package jsonlogic\n\nimport (\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/typing\"\n)\n\n// containsAll checks if all el"
  },
  {
    "path": "arrays_test.go",
    "chars": 5408,
    "preview": "package jsonlogic\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestContainsAll(t *testing.T) {\n\ttests := []struct {\n"
  },
  {
    "path": "benchmark/README.md",
    "chars": 5934,
    "preview": "# JSONLogic Benchmark\n\nBenchmark suite to compare performance between different versions of the JSONLogic library.\n\n## P"
  },
  {
    "path": "benchmark/bench",
    "chars": 2008,
    "preview": "#!/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# Ch"
  },
  {
    "path": "benchmark/benchmark_test.go",
    "chars": 14767,
    "preview": "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/json"
  },
  {
    "path": "codecov.yml",
    "chars": 32,
    "preview": "ignore:\n  - internal/testing.go\n"
  },
  {
    "path": "comp.go",
    "chars": 2712,
    "preview": "package jsonlogic\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/javascript\"\n\t\"github.com/diego"
  },
  {
    "path": "comp_test.go",
    "chars": 3164,
    "preview": "package jsonlogic_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tjsonlogic \"github."
  },
  {
    "path": "go.mod",
    "chars": 247,
    "preview": "module github.com/diegoholiveira/jsonlogic/v3\n\ngo 1.18\n\nrequire github.com/stretchr/testify v1.10.0\n\nrequire (\n\tgithub.c"
  },
  {
    "path": "go.sum",
    "chars": 883,
    "preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.m"
  },
  {
    "path": "internal/javascript/javascript.go",
    "chars": 1842,
    "preview": "// Package javascript provides utilities for working with JavaScript code and runtime integration.\npackage javascript\n\ni"
  },
  {
    "path": "internal/javascript/javascript_test.go",
    "chars": 2222,
    "preview": "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\tt"
  },
  {
    "path": "internal/json_logic_pr_48_tests.json",
    "chars": 18740,
    "preview": "[\n    \"# Non-rules get passed through\",\n    [ true, {}, true ],\n    [ false, {}, false ],\n    [ 17, {}, 17 ],\n    [ 3.14"
  },
  {
    "path": "internal/testing.go",
    "chars": 1974,
    "preview": "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   "
  },
  {
    "path": "internal/typing/typing.go",
    "chars": 5631,
    "preview": "// Package typing provides type checking and conversion utilities for JSON data types.\npackage typing\n\nimport (\n\t\"reflec"
  },
  {
    "path": "internal/typing/typing_test.go",
    "chars": 6031,
    "preview": "package typing\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsBool(t *testing.T) {\n\ttests := "
  },
  {
    "path": "issues_test.go",
    "chars": 11457,
    "preview": "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"
  },
  {
    "path": "jsonlogic.go",
    "chars": 5801,
    "preview": "// Package jsonlogic provides a Go implementation of JSONLogic rules engine.\n// JSONLogic is a way to write rules that i"
  },
  {
    "path": "jsonlogic_test.go",
    "chars": 12976,
    "preview": "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/a"
  },
  {
    "path": "lists.go",
    "chars": 6224,
    "preview": "package jsonlogic\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/typing\"\n)\n\n// ErrReduce"
  },
  {
    "path": "lists_test.go",
    "chars": 2658,
    "preview": "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 \"gith"
  },
  {
    "path": "logic.go",
    "chars": 1556,
    "preview": "package jsonlogic\n\nimport (\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/typing\"\n)\n\nfunc _and(values, data any) any"
  },
  {
    "path": "math.go",
    "chars": 2273,
    "preview": "package jsonlogic\n\nimport (\n\t\"math\"\n\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/typing\"\n)\n\nfunc mod(values, data "
  },
  {
    "path": "math_test.go",
    "chars": 1383,
    "preview": "package jsonlogic_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tjsonlogic \"github."
  },
  {
    "path": "operation.go",
    "chars": 3082,
    "preview": "package jsonlogic\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/typing\"\n)\n\n// OperatorFn d"
  },
  {
    "path": "operation_test.go",
    "chars": 1668,
    "preview": "package jsonlogic\n\nimport (\n\t\"io\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n)\n\n// TestConcurrentApplyAndAddOperator validates that v"
  },
  {
    "path": "readme.md",
    "chars": 4241,
    "preview": "# Go JsonLogic\n\n![test workflow](https://github.com/diegoholiveira/jsonlogic/actions/workflows/test.yml/badge.svg)\n[![co"
  },
  {
    "path": "strings.go",
    "chars": 1165,
    "preview": "package jsonlogic\n\nimport (\n\t\"strings\"\n\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/typing\"\n)\n\nfunc substr(values,"
  },
  {
    "path": "strings_test.go",
    "chars": 1094,
    "preview": "package jsonlogic_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tjsonlogic \"github."
  },
  {
    "path": "validator.go",
    "chars": 2258,
    "preview": "package jsonlogic\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/typing\"\n)\n\n// IsVa"
  },
  {
    "path": "validator_test.go",
    "chars": 1486,
    "preview": "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 \""
  },
  {
    "path": "vars.go",
    "chars": 3491,
    "preview": "package jsonlogic\n\nimport (\n\t\"encoding/json\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/diegoholiveira/jsonlogic/v3/internal/ty"
  },
  {
    "path": "vars_test.go",
    "chars": 3026,
    "preview": "package jsonlogic_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tjsonlogic \"github."
  }
]

About this extraction

This page contains the full source code of the diegoholiveira/jsonlogic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (139.5 KB), approximately 43.9k tokens, and a symbol index with 196 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!