Skip to content

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

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

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

Conversation

@ajpallares

@ajpallares ajpallares commented May 22, 2026

Copy link
Copy Markdown
Member

Motivation

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

Android counterpart: RevenueCat/purchases-android/pull/3553

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
Expands predicate evaluation semantics (scoping and non-array edge cases) in the rules engine; risk is mitigated by alignment with json-logic-js and broad fixture coverage rather than touching auth or I/O.

Overview
Adds none, map, filter, and reduce to the JSON Logic rules engine so iteration behavior matches json-logic-js alongside existing some / all.

none, map, and filter reuse the same [arrayExpr, predicate] shape and parseIterationArgs: the array is evaluated in the outer scope; the predicate runs per element with vars set to the item only (no parent scope). none is the inverse of some (short-circuits on the first truthy item). map returns unevaluated predicate results; filter keeps original items when the predicate is truthy. Non-array sources yield true for none, [] for map / filter.

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

Operators.dispatch registers all four operators. Predicate conformance fixtures grow by 40 cases (307 → 347) in filter.json, map.json, none.json, and reduce.json (edge cases, scope isolation, composition with reduce).

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

@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-mapping-operators branch from 426aee0 to 4aa50c4 Compare May 22, 2026 15:07
@ajpallares ajpallares changed the base branch from pallares/json-logic-min-max-operators to pallares/json-logic-iteration-operators May 22, 2026 15:07
@ajpallares ajpallares changed the base branch from pallares/json-logic-iteration-operators to pallares/json-logic-min-max-operators May 27, 2026 08:49
@ajpallares ajpallares force-pushed the pallares/json-logic-min-max-operators branch from d2ca6d6 to e73eabe Compare June 2, 2026 12:43
@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-mapping-operators branch from 2a0a33c to 46d6925 Compare June 2, 2026 12:43
@ajpallares ajpallares force-pushed the pallares/json-logic-min-max-operators branch from e73eabe to d2ca6d6 Compare June 2, 2026 12:48
@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-mapping-operators branch from 46d6925 to 2a0a33c Compare June 2, 2026 12:48
…tures

Convert the hand-written operator unit tests for operators available at the
string+array level into declarative JSON predicate fixtures sharing khepri's
conformance format, run by a shared runner that auto-discovers every file in
PredicateFixtures/. `expected` is a khepri-compatible superset (Bool or
{ "error": ... }) plus optional `description` and `expectedWarnings`.

Adds a test-only Value: Decodable conformance so predicates and variables
decode straight into the engine's value model; production Value is unchanged.
Tests that can't be expressed as predicate->Bool stay in Swift; ValueTests
untouched. Test-only change.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 40901e5)
@ajpallares ajpallares force-pushed the pallares/json-logic-min-max-operators branch from d2ca6d6 to 03295d9 Compare June 2, 2026 14:13
@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-mapping-operators branch from 2a0a33c to df4b140 Compare June 2, 2026 14:14
ajpallares and others added 8 commits June 2, 2026 17:07
Load the in-repo fixtures once and feed the decoded cases straight into
the parameterized test, instead of collecting IDs and re-reading every
file to find the matching case per ID. PredicateConformanceFixtureCase
gains Identifiable + Sendable (for the arguments) and a
CustomTestStringConvertible extension so each case still displays by id.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Asserting an exact warning count coupled the fixtures to engine
internals. Match warnings by substring only (count-agnostic); an empty
`contains` now asserts that no warning is emitted, preserving the
"does not warn" cases. The three count-only fixtures gain the
"missing variable" substring they actually emit.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the khepri single-file plumbing (defaultFixtureURL, fixtureURL,
the no-arg loadCases(), the env var, and the unused fixtureNotFound
error). This PR only runs the in-repo PredicateFixtures/ directory; the
khepri conformance loader lands with its test in the conformance PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
Implements the json-logic-js some/all iteration predicates. Operator coverage
is expressed as JSON predicate fixtures (some.json / all.json) run by the
shared fixture runner introduced downstack, replacing hand-written unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit d11529c)
Implements the json-logic-js min/max operators (variadic flat list, ±∞ for
empty input, NaN propagation for non-numeric operands). Coverage is expressed
as JSON predicate fixtures (min.json / max.json) run by the shared fixture
runner, replacing hand-written unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit e73eabe)
@ajpallares ajpallares force-pushed the pallares/json-logic-min-max-operators branch from 03295d9 to 8a445e2 Compare June 3, 2026 12:59
Completes the json-logic-js iteration family (none/map/filter/reduce) on top
of some/all. Coverage is expressed as JSON predicate fixtures
(none.json / map.json / filter.json / reduce.json) run by the shared fixture
runner, replacing hand-written unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 46d6925)
@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-mapping-operators branch from df4b140 to 6d0efcb Compare June 3, 2026 13:00
ajpallares added a commit that referenced this pull request Jun 3, 2026
@emerge-tools

emerge-tools Bot commented Jun 5, 2026

Copy link
Copy Markdown

📸 Snapshot Test

274 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
RevenueCat
com.revenuecat.PaywallsTester
0 0 0 0 274 0 N/A

🛸 Powered by Emerge Tools

ajpallares and others added 8 commits June 5, 2026 20:04
- Rename the object-array some/all fixtures to describe behavior instead
  of referencing the khepri oracle
- Add fixtures for literal predicates and non-boolean truthy predicate
  results
- Drop the unused opName parameter from parseIterationArgs and return an
  optional items list so a non-array source is distinguishable from an
  empty one
- Bump pinned fixture count 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>
Add var-fed cases (including the single-array-arg NaN gotcha), min
fail-closed and empty-list fail-closed coverage. Collapse the two-pass
NaN scan + extremum into a single reducing fold. Pinned fixture count
294 -> 301.

Co-authored-by: Cursor <cursoragent@cursor.com>
…erators' into pallares/json-logic-min-max-operators

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

# Conflicts:
#	Tests/RulesEngineInternalTests/PredicateFixtureTests.swift
…ators' into pallares/json-logic-iteration-mapping-operators

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

# Conflicts:
#	RulesEngineInternal/Operators/IterationOperators.swift
#	Tests/RulesEngineInternalTests/PredicateFixtureTests.swift
Strengthens iteration-operator coverage for none/map/filter/reduce:
- Add object-item named-property fixtures (none/map/filter) — the most
  common real-world shape, previously untested (only scalar/empty items).
- Assert map/filter return real arrays by folding through reduce
  (count/sum) instead of relying solely on string coercion.
- Add a reduce fixture pinning that the initial accumulator is evaluated
  even for a non-array source (matches initial = apply(values[2], data)).
- Reword the none non-array rationale: current json-logic-js implements
  none directly with !Array.isArray(x) || !x.length, not via filter.

Expected fixture count 341 -> 345.

Co-authored-by: Cursor <cursoragent@cursor.com>
ajpallares and others added 8 commits June 8, 2026 10:24
…teration-operators

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

# Conflicts:
#	Tests/RulesEngineInternalTests/PredicateFixtureTests.swift
Label each operator grouping in Operators.dispatch per PR review feedback.

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.

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.

Co-authored-by: Cursor <cursoragent@cursor.com>
…erators' into pallares/json-logic-min-max-operators

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

# Conflicts:
#	Tests/RulesEngineInternalTests/PredicateFixtureTests.swift
Remove the duplicate WorkflowsCacheTests.swift PBXFileReference introduced
during a merge, and add a dedicated "Min and max" grouping comment so the
min/max cases are no longer lumped under arithmetic.

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

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

# Conflicts:
#	Tests/RulesEngineInternalTests/PredicateFixtureTests.swift
Base automatically changed from pallares/json-logic-min-max-operators to main June 8, 2026 13:44
ajpallares and others added 3 commits June 8, 2026 16:37
…teration-mapping-operators

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

# Conflicts:
#	Tests/RulesEngineInternalTests/PredicateFixtureTests.swift
#	Tests/RulesEngineInternalTests/PredicateFixtures/max.json
#	Tests/RulesEngineInternalTests/PredicateFixtures/min.json
Co-authored-by: Cursor <cursoragent@cursor.com>
@ajpallares ajpallares marked this pull request as ready for review June 8, 2026 15:28
@ajpallares ajpallares requested a review from a team as a code owner June 8, 2026 15:28

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense! Just a question for tests.

}
]
},
""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this isn't actually an empty array but an empty string, which is a bit confusing... I guess we're relying on it being equivalent... Probably NABD though.

@ajpallares ajpallares merged commit 2de7a18 into main Jun 9, 2026
18 of 20 checks passed
@ajpallares ajpallares deleted the pallares/json-logic-iteration-mapping-operators branch June 9, 2026 11:47
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