Add JSON Logic iteration operators (none, map, filter, reduce)#3553
Merged
ajpallares merged 30 commits intoJun 9, 2026
Conversation
…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>
…-logic-iteration-operators
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
a07f0f8 to
3d0f607
Compare
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>
…-logic-iteration-operators
…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>
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
…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
Co-authored-by: Cursor <cursoragent@cursor.com>
tonidero
approved these changes
Jun 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Completes the iteration-operator family alongside #3552, mirroring json-logic-js. Resolves SDK-4350.
iOS counterpart: RevenueCat/purchases-ios#6834.
Description
none/map/filter/reduce, sharing the per-item scope helper;reducerebindsvarsto{"current", "accumulator"}(the only asymmetric case).none→true(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, andreduceto the rules engine’s JSON Logic iteration set (alongside existingsome/all), aligned with json-logic-js behavior and wired throughOperators.dispatch.none,map, andfilterreuse the same[arrayExpr, predicate]shape assome/all: the array is evaluated in the outer scope; the predicate runs per element withvarsbound to the item only (no parent scope).noneis the inverse ofsome(true when no item matches; empty/non-array sources →true).mapreturns evaluated results per item;filterkeeps original items where the predicate is truthy; non-array sources →[].reducetakes[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.parseIterationArgsdocs 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.