Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 19 additions & 35 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,40 @@

# Contribution Guide

**Contributions are very welcome!** Whether it's a small typo fix, a new operator, a better example, or a larger refactor — your help makes this gem better. If you're unsure where to start, open an issue and we can figure it out together.
# Contribution Guide

## Quick links (README)
- **Install & Quick start:** see [README → Install](./README.md#install) and [Quick start](./README.md#quick-start)
- **How the engine works (value vs lazy/enumerable):** see [README → How](./README.md#how)
- **Supported operations:** see [README → Supported Operations (Built‑in)](./README.md#supported-operations-built-in)
- **Adding operations:** see [README → Adding Operations](./README.md#adding-operations)
- **JsonLogic semantics (comparisons & truthiness):** see [README → JsonLogic Semantic](./README.md#jsonlogic-semantic)
- **Compliance & tests:** see [README → Compliance and tests](./README.md#compliance-and-tests)
**Contributions of every size are very welcome!** Whether it's a small typo fix, a new operator, a better example, or a larger refactor — your help makes this gem better. If you're unsure where to start, open an issue and we can figure it out together.

## Running tests & compliance
We keep this gem small and sharp. If you can make it simpler – do it. If you can make it clearer – do it.

Use the **same commands** as in the README’s [Compliance and tests](./README.md#compliance-and-tests) section.
## Quick links

See **[README](./README.md)** — everything you need to understand the JsonLogic rule tree specifics in Ruby.
## How to contribute

1. Fork the repo and create a branch from `main`.
2. Make your change (code, docs, or tests).
3. Include examples for new operators or pretty mappings.
4. Update `README.md` and docs if public behavior changes.
5. Run your tests.
6. Open a Pull Request and describe:
- What changed and why
- Any breaking impacts
- Before/after output if applicable
Fork. Branch. Change. Test. PR.


### Adding an operator

Operator creation and registration are described in the README’s [Adding Operations](./README.md#adding-operations) section.
Read **[§ Adding Operations](./README.md#adding-operations)**. Prefer the class‑based API. The Proc & Lambda DSL is fine for a quick spike; promote to a class before merge.

Auto‑registration works for classes under [lib/json_logic/operations/](./lib/json_logic/operations/).

Auto-registration is enabled for classes under `lib/json_logic/operations/`.

## Coding style

- **Follow [README → Security](./README.md#security)**
- Prefer simple, composable code.
- Match the semantics from the official docs:
- Operations: <https://jsonlogic.com/operations.html>
- Truthiness: <https://jsonlogic.com/truthy.html>
- When relevant, use `using JsonLogic::Semantics` to align comparisons/truthiness with JsonLogic.
- **Follow [§ Security](./README.md#security)**.
- **Follow [§ JsonLogic Semantic](./README.md#jsonlogic-semantic)**.
- Prefer small, composable code with real examples.

## PR checklist

- [ ] Tests or examples included (when applicable)
- [ ] Compliance suite passes (see [README → Compliance and tests](./README.md#compliance-and-tests))
- [ ] README/docs updated if user‑facing behavior changed
- [ ] Tests or examples included (when applicable).
- [ ] Compliance suite passes (see **[§ Compliance and tests](./README.md#compliance-and-tests)**).
- [ ] **[README](./README.md)** updated if user‑facing behavior changed.
- [ ] Version bumped in [Gem Version File](./lib/json_logic/version.rb).
- [ ] **[CHANGELOG](./CHANGELOG.md)** updated.

## Versioning

We use **Semantic Versioning** (MAJOR.MINOR.PATCH).

- Bump `lib/json_logic/version.rb` using SemVer.
- Update `CHANGELOG.md`.
We use **[Semantic Versioning](https://semver.org/)**.
147 changes: 80 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@




# json-logic-rb

Ruby implementation of [JsonLogic](https://jsonlogic.com/) — simple and extensible. Ships with a compliance runner for the official test suite.
Expand Down Expand Up @@ -27,9 +30,20 @@ JsonLogic rules are JSON trees. The engine walks that tree and returns a Ruby va

## Install

Download the gem locally
```bash
gem install json-logic-rb
```
If needed – add to your Gemfile

```ruby
gem "json-logic-rb"
```

Then install
```shell
bundle install
```

## Quick start

Expand All @@ -51,11 +65,11 @@ JsonLogic.apply({ "var" => "user.age" }, { "user" => { "age" => 42 } })

## How

There are **two types of operations** in this implementation: Default Operations and Lazy Operations.
There are two types of operations: [Default Operations](#1-default-operations) and [Lazy Operations](#2-lazy-operations)

### 1. Default Operations

For **Default Operations**, the engine **evaluates all arguments first** and then calls the operator with the **resulting Ruby values**.
For **Default Operations**, the it evaluates all arguments first and then calls the operator with the resulting Ruby values.
This matches the reference behavior for arithmetic, comparisons, string operations, and other pure operations that do not control evaluation order.

**Groups and references:**
Expand All @@ -66,20 +80,20 @@ This matches the reference behavior for arithmetic, comparisons, string operatio

### 2. Lazy Operations

Some operations must control **whether** and **when** their arguments are evaluated. They implement branching, short-circuiting, or “apply a rule per item” semantics. For these **Lazy Operations**, the engine passes **raw sub-rules** and current data. The operator then evaluates only the sub-rules it actually needs.
Some operations must control whether and when their arguments are evaluated. They implement branching, short-circuiting, or “apply a rule per item” semantics. For these **Lazy Operations**, the engine passes raw sub-rules and data. The operator then evaluates only the sub-rules it actually needs.

**Groups and references:**

- **Branching / boolean control** — `if`, `?:`, `and`, `or`, `var`
[Logic & boolean operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) • [Truthiness](https://jsonlogic.com/truthy.html)
[Logic & boolean operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations)

- **Enumerable operators** — `map`, `filter`, `reduce`, `all`, `none`, `some`
[Array operations](https://jsonlogic.com/operations.html#array-operations)

**How enumerable per-item evaluation works:**

1. The first argument is a rule that returns the list of items — evaluated **once** to a Ruby array.
2. The second argument is the per-item rule — evaluated **for each item** with that item as the **current root**.
1. The first argument is a rule that returns the list of items — evaluated once to a Ruby array.
2. The second argument is the per-item rule — evaluated for each item with that item as the current root.
3. For `reduce`, the current item is also available as `"current"`, and the running total as `"accumulator"`.


Expand Down Expand Up @@ -110,21 +124,20 @@ JsonLogic.apply(

### Why laziness matters?

Lazy operations **prevent evaluation** of branches you do not need. If division by zero raised an error (hypothetically), lazy control would avoid it:
Lazy operations prevent evaluation of branches you do not need.

If hypothetically division by zero raises an error, lazy control would avoid it.
```ruby
# "or" short-circuits: 1 is truthy, so the right side is NOT evaluated.
# If the right side were evaluated eagerly, it would attempt 1/0 (error).
JsonLogic.apply({ "or" => [1, { "/" => [1, 0] }] })
# => 1
```

> In this gem `/` returns `nil` on divide‑by‑zero, but these examples show **why** lazy evaluation is required by the spec: branching and boolean operators must **not** evaluate unused branches.
> In this gem division returns nil on divide‑by‑zero, but this example show why lazy evaluation is required by the spec: branching and boolean operators must not evaluate unused branches.

## Supported Operations (Built‑in)


Below is a list that mirrors the sections on [jsonlogic.com/operations.html](https://jsonlogic.com/operations.html) and shows what this gem (library) implements. From the reference page’s list, everything except `log` is implemented.
Below is a list that mirrors the sections on [Json Logic Website Opeations](https://jsonlogic.com/operations.html) and shows what this gem implements.

| Operator | Supported |
|---|---:|
Expand Down Expand Up @@ -169,93 +182,91 @@ Below is a list that mirrors the sections on [jsonlogic.com/operations.html](htt

## Adding Operations

Need a custom operation? It’s straightforward.
Need a custom Operation? It’s straightforward. Start small with a Proc or Lambda. If needed – promote it to a Class.

### Quick — register a Proc or Lambda

Register little anonymous functions, by passing a Proc or Lambda.

```ruby
JsonLogic.add_operation("times2") { |(value), _| value.to_i * 2 }
```
### Enable JsonLogic Semantics (optional)
Enable semantics to mirror JsonLogic’s comparison and truthiness in Ruby.

Once the function added, you can use it in your logic.
See [§JsonLogic Semantic](#jsonlogic-semantic) for details.

```ruby
JsonLogic.apply({ "times2" => [21] })
# => 42
```

Is useful for rapid prototyping with minimal boilerplate;
Later you can “promote” it into a full class or use additional features.
### Parameters

Operator function use a consistent call shape:

### 1) Pick the Operation type
Choose one of:
- **Default**
```ruby
class JsonLogic::Operations::StartsWith < JsonLogic::Operation; end
```
For anonymous functions:
```ruby
JsonLogic.add_operation("starts_with", lazy: false) do; end
```
- **Lazy**
```ruby
class JsonLogic::Operations::StartsWith < JsonLogic::LazyOperation; end
```
For anonymous functions:
- First parameter: **array of operator arguments** (you can destructure it).

- Second parameter: current **data**.
```ruby
JsonLogic.add_operation("starts_with", lazy: true) do; end
->((string, prefix), data) { string.to_s.start_with?(prefix.to_s) }
```

See [§How](#how) for details.
### Proc / Lambda

### 2) Enable JsonLogic Semantics (optional)
Enable semantics to mirror JsonLogic’s comparison/truthiness in Ruby:
Pick the Operation type.

[Default Operation](#1-default-operations) mode passes values.

```ruby
using JsonLogic::Semantics
JsonLogic.add_operation("starts_with") do |(string_value, prefix_value), _data|
string_value.to_s.start_with?(prefix_value.to_s)
end
```
[Lazy Operation](#2-lazy-operations) mode passes raw rules (you evaluate them):

```ruby
JsonLogic.add_operation("starts_with", lazy: true) do |(string_rule, prefix_rule), data|
string_value = JsonLogic.apply(string_rule, data)
prefix_value = JsonLogic.apply(prefix_rule, data)
string_value.to_s.start_with?(prefix_value.to_s)
end
```

See [§JsonLogic Semantic](#jsonlogic-semantic) for details.
See [§How](https://github.com/tavrelkate/json-logic-rb?tab=readme-ov-file#how) for details.

### 3) Create an Operation and provide a machine name
Use immediately:

Operation methods use a consistent call shape.
```ruby
JsonLogic.apply({ "starts_with" => [ { "var" => "email" }, "admin@" ] })
```

- The first parameter is the **array of operator arguments**.
- The second is the current **data**.

### Class

Pick the Operation type. It has the same call shape.

Thanks to Ruby’s destructuring, you can unpack the argument array right in the method signature.
[Default Operation](#1-default-operations) – Inherit `JsonLogic::Operation`.

```ruby
class JsonLogic::Operations::StartsWith < JsonLogic::Operation
def self.name = "starts_with"
def call((str, prefix), _data)
# str, prefix are ALREADY evaluated to Ruby values
str.to_s.start_with?(prefix.to_s)
end
def call(string_value, prefix_value), _data) = string_value.to_s.start_with?(prefix_value.to_s)
end
```

### 4) Register the new operation
[Lazy Operation](#2-lazy-operations) – Inherit `JsonLogic::LazyOperation`.

Register explicitly:

```ruby
JsonLogic::Engine.default.registry.register(JsonLogic::Operations::StartsWith)
```

After registration, use it in rules:
Now, Class is ready to use.

```json
{ "starts_with": [ { "var": "email" }, "admin@" ] }
```ruby
JsonLogic.apply({ "starts_with" => [ { "var" => "email" }, "admin@" ] })
```








## JsonLogic Semantic

All supported Operations follow JsonLogic semantics.
Expand All @@ -281,28 +292,29 @@ As JsonLogic primary developed in JavaScript it inherits JavaScript's type coerc
```ruby
using JsonLogic::Semantics

1 >= "1.0" # => true
1 >= "1.0"
# => true
```

### Truthiness

JsonLogic’s truthiness differs from Ruby’s (see <https://jsonlogic.com/truthy.html>).
In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty arrays are also falsey.
JsonLogic’s truthiness differs from Ruby’s (see [Json Logic Website Truthy and Falsy](https://jsonlogic.com/truthy.html)).
In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty arrays are falsey too.

**In Ruby:**
```ruby
!![]
# => true
```

While JsonLogic as was mentioned before has it's own truthiness:
While JsonLogic as was mentioned before has it's own truthiness.

**In Ruby (with JsonLogic Semantic):**

```ruby
include JsonLogic::Semantics
using JsonLogic::Semantics

truthy?([])
!![]
# => false
```

Expand Down Expand Up @@ -343,9 +355,10 @@ ruby script/compliance.rb spec/tmp/tests.json

## Security

- Rules are **data**, not code; no Ruby eval.
- Operations are **pure** (no IO, no network, no shell).
- Rules have **no write** access to anything.
- RULES ARE DATA; NO RUBY EVAL;
- OPERATIONS ARE PURE; NO IO, NO NETWORK; NO SHELL;
- RULES HAVE NO WRITE ACCESS TO ANYTHING;


## License

Expand Down