Add JSON Logic iteration operators (some, all)#6817
Conversation
d11529c to
f6c4c80
Compare
…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)
f6c4c80 to
874649b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ 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.
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)
874649b to
db50103
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # RevenueCat.xcodeproj/project.pbxproj
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>
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
| case "merge": | ||
| return try StringArrayOperators.opMerge(args: args, vars: vars) | ||
|
|
||
| case "some": |
There was a problem hiding this comment.
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?
| }, | ||
| { | ||
| "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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
…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>

Checklist
purchases-androidand hybrids — SDK-4342Motivation
Adds the json-logic-js iteration predicates
some/allthat 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 withvarsrebound to the current item; a non-array source short-circuits tofalse.allreturnsfalseper json-logic-js (not vacuous truth), diverging from khepri — pinned by a fixture.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
someandalliteration operators to the rules engine, aligned with json-logic-js, so v1 audience predicates can express “any / every element matches.”someis true when the per-item predicate is truthy for at least one element (short-circuits on first match);allrequires every element to pass, with emptyall→ false (not vacuous truth). The array expression is evaluated in the outer scope; each predicate run uses the current item asvarswith no parent-scope inheritance. Non-array sources yield false.Implementation lives in new
IterationOperatorsand is wired throughOperators.dispatch. Behavior is covered bysome.json/all.jsonfixture 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.