Skip to content

Add JSON Logic iteration operators (none, map, filter, reduce)#3553

Merged
ajpallares merged 30 commits into
mainfrom
pallares/json-logic-iteration-mapping-operators
Jun 9, 2026
Merged

Add JSON Logic iteration operators (none, map, filter, reduce)#3553
ajpallares merged 30 commits into
mainfrom
pallares/json-logic-iteration-mapping-operators

Conversation

@ajpallares

@ajpallares ajpallares commented Jun 7, 2026

Copy link
Copy Markdown
Member

Motivation

Completes the iteration-operator family alongside #3552, mirroring json-logic-js. Resolves SDK-4350.

iOS counterpart: RevenueCat/purchases-ios#6834.

Description

  • Adds none / map / filter / reduce, sharing the per-item scope helper; reduce rebinds vars to {"current", "accumulator"} (the only asymmetric case).
  • Non-array source: nonetrue (JS guards with !Array.isArray(x) || !x.length); map / filter[]; reduce → seed unchanged.

Note

Medium Risk
Changes rule evaluation semantics for new operators; incorrect scope or edge-case handling could alter predicate outcomes in production rules, though coverage is heavy on json-logic-js fixtures.

Overview
Adds none, map, filter, and reduce to the rules engine’s JSON Logic iteration set (alongside existing some/all), aligned with json-logic-js behavior and wired through Operators.dispatch.

none, map, and filter reuse the same [arrayExpr, predicate] shape as some/all: the array is evaluated in the outer scope; the predicate runs per element with vars bound to the item only (no parent scope). none is the inverse of some (true when no item matches; empty/non-array sources → true). map returns evaluated results per item; filter keeps original items where the predicate is truthy; non-array sources → [].

reduce takes [arrayExpr, predicate, initialAccumulator], evaluates source and seed in the outer scope, and folds with predicate scope {current, accumulator} only. Non-array sources return the evaluated seed unchanged.

parseIterationArgs docs are extended so callers can treat non-array vs empty arrays differently where the spec requires it. 40 new predicate conformance fixtures (none, map, filter, reduce) bring the expected fixture count to 347.

Reviewed by Cursor Bugbot for commit e600a64. Bugbot is set up for automated code reviews on this repo. Configure here.

ajpallares and others added 11 commits June 5, 2026 14:11
…e fixtures

Adds per-operator JSON predicate fixtures (exact copies of the iOS #6885
corpus) under src/test/resources/predicate-fixtures, run by a shared
parameterized runner. Deletes the migrated arithmetic/comparison/equality/
logic suites and trims the accessor/evaluator/string-array suites to the
cases not expressible as predicate->boolean. Test-only change.

Co-authored-by: Cursor <cursoragent@cursor.com>
Expresses the "empty-string key resolves to the whole scope and is not
missing" case as a `{"!!":{"missing":[""]}}` -> false fixture (the string
coercion the other missing fixtures use can't distinguish [] from [""]).
Removes the now-covered Kotlin test and bumps the fixture count to 237.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds fixtures for the empty-segment dot-path splits, var default-vs-null-leaf
behavior, and the non-numeric missing_some threshold (7 new cases), and drops
the now-redundant Kotlin tests. AccessorOperatorsTest keeps only the cases
that cannot be a predicate->boolean fixture (top-level array scope or a
whole-object return). Fixture count is now 244.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Mirror the iOS rules-engine implementation: Kotlin some/all operators
matching json-logic-js semantics, shared JSON predicate fixtures, and
pinned fixture count bumped to 271.

Co-authored-by: Cursor <cursoragent@cursor.com>
place operator docs on each op instead of duplicating at the type level; drop "not vacuous truth" wording

Co-authored-by: Cursor <cursoragent@cursor.com>
Implement variadic min/max with JS Math.min/Math.max semantics: operands
coerce via Number() (toNumberOrNull), empty input yields ±∞, and NaN
operands poison the result. Wire into the operator dispatcher and add
byte-identical min.json/max.json fixtures from purchases-ios, bumping
the pinned fixture count to 305.

Co-authored-by: Cursor <cursoragent@cursor.com>
Completes the json-logic-js iteration family on top of some/all.
none/map/filter rebind vars to the current item; reduce rebinds to
{current, accumulator}. Non-array sources: none -> true, map/filter
-> [], reduce -> seed unchanged. Coverage is the shared declarative
JSON fixtures (none/map/filter/reduce.json, byte-identical to iOS),
bumping the pinned fixture count to 345. Android counterpart of
RevenueCat/purchases-ios#6834.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov

codecov Bot commented Jun 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.18%. Comparing base (689b9e7) to head (e600a64).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3553   +/-   ##
=======================================
  Coverage   80.18%   80.18%           
=======================================
  Files         371      371           
  Lines       15233    15233           
  Branches     2111     2111           
=======================================
  Hits        12214    12214           
  Misses       2169     2169           
  Partials      850      850           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-mapping-operators branch from a07f0f8 to 3d0f607 Compare June 7, 2026 13:49
ajpallares and others added 14 commits June 8, 2026 09:45
Co-authored-by: Cursor <cursoragent@cursor.com>
Label each operator grouping in Operators.dispatch per PR review feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move the string/array group after comparison so the Android dispatcher
mirrors the iOS Operators.dispatch grouping order.

Co-authored-by: Cursor <cursoragent@cursor.com>
Rename the all-match fixture and add cases where only the first or only
the middle item matches, addressing PR review feedback that the existing
some coverage was order dependent. Fixtures kept byte-identical with iOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
khepri guards all with notEmpty, so it returns false for an empty array
like json-logic-js; drop the inaccurate claim that khepri returns true.
Fixture kept byte-identical with iOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
…teration-operators

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	rules-engine-internal/src/test/kotlin/com/revenuecat/purchases/rules/PredicateFixtureLoaderTest.kt
…erators' into pallares/json-logic-min-max-operators

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	rules-engine-internal/src/main/kotlin/com/revenuecat/purchases/rules/operators/Operators.kt
#	rules-engine-internal/src/test/kotlin/com/revenuecat/purchases/rules/PredicateFixtureLoaderTest.kt
…in-max-operators

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	rules-engine-internal/src/main/kotlin/com/revenuecat/purchases/rules/operators/Operators.kt
#	rules-engine-internal/src/test/kotlin/com/revenuecat/purchases/rules/PredicateFixtureLoaderTest.kt
Add a dedicated "Min and max" grouping comment so the min/max cases are not
lumped under arithmetic, matching the iOS dispatcher.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ators' into pallares/json-logic-iteration-mapping-operators

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	rules-engine-internal/src/test/kotlin/com/revenuecat/purchases/rules/PredicateFixtureLoaderTest.kt
Self-documenting replacement for the {"/": [-10,0]} workaround; constants
seeded only by the conformance harness (reservedConstants), engine untouched;
NaN still uses x != x; fixtures kept byte-identical with iOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
substr.json and missing_some.json now use the seeded "+Infinity" and
"-Infinity" variables instead of division-by-zero operands. NaN cases
and evaluator.json are unchanged. Fixtures are byte-identical to iOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
Base automatically changed from pallares/json-logic-min-max-operators to main June 8, 2026 14:03
Mirror iOS predicate fixture updates: remove the redundant
"comes from the test-only" clause from 7 fixture descriptions
across min, max, substr, and missing_some. Fixtures kept
byte-identical with iOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nfinity-fixture-vars

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	rules-engine-internal/src/test/resources/predicate-fixtures/max.json
#	rules-engine-internal/src/test/resources/predicate-fixtures/min.json
@ajpallares ajpallares changed the base branch from main to pallares/json-logic-infinity-fixture-vars June 8, 2026 14:32
…ture-vars' into pallares/json-logic-iteration-mapping-operators
Base automatically changed from pallares/json-logic-infinity-fixture-vars to main June 8, 2026 14:58
@ajpallares ajpallares added this pull request to the merge queue Jun 9, 2026
Merged via the queue into main with commit 85b12f9 Jun 9, 2026
38 checks passed
@ajpallares ajpallares deleted the pallares/json-logic-iteration-mapping-operators branch June 9, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants