Skip to content

Add JSON Logic iteration operators (some, all)#6817

Merged
ajpallares merged 29 commits into
mainfrom
pallares/json-logic-iteration-operators
Jun 8, 2026
Merged

Add JSON Logic iteration operators (some, all)#6817
ajpallares merged 29 commits into
mainfrom
pallares/json-logic-iteration-operators

Conversation

@ajpallares

@ajpallares ajpallares commented May 19, 2026

Copy link
Copy Markdown
Member

Checklist

  • Tests (as JSON predicate fixtures)
  • Follow-up issues for purchases-android and hybrids — SDK-4342

Motivation

Adds the json-logic-js iteration predicates some / all that v1 audience predicates rely on. Resolves SDK-4341. Stacked on #6885 (the JSON predicate-fixture migration).

Description

  • {"some": [arrayExpr, predicate]} / {"all": [...]}: the array is evaluated in the outer scope; the predicate is re-evaluated per item with vars rebound to the current item; a non-array source short-circuits to false.
  • Empty all returns false per json-logic-js (not vacuous truth), diverging from khepri — pinned by a fixture.
  • Coverage is declarative JSON (some.json / all.json) run by the shared fixture runner from Migrate base RulesEngineInternal operator unit tests to JSON predicate fixtures #6885; no Swift unit tests.

Note

Medium Risk
Changes audience predicate evaluation semantics for array conditions; risk is mitigated by extensive conformance fixtures but incorrect iteration scope could mis-target users.

Overview
Adds JSON Logic some and all iteration operators to the rules engine, aligned with json-logic-js, so v1 audience predicates can express “any / every element matches.”

some is true when the per-item predicate is truthy for at least one element (short-circuits on first match); all requires every element to pass, with empty all → false (not vacuous truth). The array expression is evaluated in the outer scope; each predicate run uses the current item as vars with no parent-scope inheritance. Non-array sources yield false.

Implementation lives in new IterationOperators and is wired through Operators.dispatch. Behavior is covered by some.json / all.json fixture cases (nested iteration, scope, truthiness, edge sources); the shared fixture count is bumped to 273.

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

Comment thread RulesEngineInternal/Operators/IterationOperators.swift
Base automatically changed from pallares/json-logic-string-array-operators to main June 2, 2026 12:38
@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-operators branch 2 times, most recently from d11529c to f6c4c80 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-iteration-operators branch from f6c4c80 to 874649b Compare June 2, 2026 14:12
@ajpallares ajpallares changed the base branch from main to pallares/rules-engine-json-fixtures June 2, 2026 14:12

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 874649b. Configure here.

Comment thread RulesEngineInternal/Operators/IterationOperators.swift
ajpallares and others added 7 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)
@ajpallares ajpallares force-pushed the pallares/json-logic-iteration-operators branch from 874649b to db50103 Compare June 3, 2026 12:59
ajpallares added a commit that referenced this pull request Jun 3, 2026
ajpallares added a commit that referenced this pull request Jun 5, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj
ajpallares added a commit that referenced this pull request Jun 5, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj
ajpallares and others added 5 commits June 5, 2026 17:37
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 [""].
Bumps the pinned fixture count to 237. Keeps the corpus in sync with the
purchases-android counterpart.

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), bumping
the pinned count to 244. Also adds the var-null-path Swift test so the
non-fixturizable cases match the purchases-android counterpart. Keeps the
fixture corpus byte-identical across both repos.

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

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

# Conflicts:
#	Tests/RulesEngineInternalTests/PredicateFixtureTests.swift
Co-authored-by: Cursor <cursoragent@cursor.com>
- 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>
ajpallares and others added 3 commits June 6, 2026 18:56
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>

@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.

Looking great!

case "merge":
return try StringArrayOperators.opMerge(args: args, vars: vars)

case "some":

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.

Supernitpicky: I see we leave an empty line between some of the operators, I think it corresponds with groupings of this operators? If so, I think it might be good to add a small comment on each section so it's easier to understand?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point! Done in 9f98885
Thanks!

},
{
"id": "all_returns_false_for_empty_array_per_json_logic_spec",
"description": "json-logic-js returns false (not vacuous truth) for all over an empty array; we follow the spec even though the khepri backend returns true",

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.

Interesting... You might already be on it, but could be good to sync to see if it makes sense to change this in the backend, rather than forgetting this difference later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Seems like this was wrong and khepri already returns false for this (see https://github.com/RevenueCat/khepri/blob/main/khepri/services/audience/clickhouse_query_builder_compiler.py#L211-L218 where requires_non_empty is passed as true, which means that an empty array returns `false).

Thanks for flagging!

{
"fixtures": [
{
"id": "some_returns_true_when_at_least_one_item_matches",

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.

Hmm in this test, all match. Should we also add some tests for having some that do pass the predicate and some that do not? Also, some where the first element does not match, to make sure it's not order dependent.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch! Done in de532c2
Thanks!

Base automatically changed from pallares/rules-engine-json-fixtures to main June 8, 2026 08:15
ajpallares and others added 5 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>
@ajpallares ajpallares enabled auto-merge (squash) June 8, 2026 08:54
@ajpallares ajpallares merged commit 9dec04f into main Jun 8, 2026
17 of 20 checks passed
@ajpallares ajpallares deleted the pallares/json-logic-iteration-operators branch June 8, 2026 09:05
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