3 Commits
Author SHA1 Message Date
Antonio PallaresandCursor a9be1ba1d5 Compare object keys by UTF-16 code unit in the rules engine (#7562)
* Compare strings by UTF-16 code unit in the rules engine

A JS string is a sequence of UTF-16 code units and every string operation
works on those units. Swift's String compares canonical equivalence over
Unicode scalars, so <, <=, >, >=, ==, ===, in and rc.entries key ordering
all had cases where a predicate evaluated one way on iOS and the other in
JS and Kotlin.

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

* Keep the new fixture descriptions platform-neutral

The fixture files are byte-identical across the iOS and Android engines, so
a description that explains the contract in terms of one host language's
string comparison does not read correctly in the other repository.

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

* Name the characters under test in the UTF-16 fixture descriptions

The predicates spell these strings as \uXXXX escapes so the exact code
units are unambiguous, which leaves a reader unable to tell which
character a fixture is about. Each description now shows the glyph and
its code point, and spells out the surrogate pair where one is the
point of the case.

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

* Route rc.sortBy string keys through jsStringPrecedes

The operator open-coded the UTF-16 comparison because the shared helper
did not exist yet when it landed. It does now, and the rationale lives
in the helper's doc, so the operator no longer repeats it.

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

* Compare object keys by UTF-16 code unit

A Swift dictionary keys by canonical equivalence, so é (U+00E9) and e
followed by a combining acute (U+0301) were one key: an object holding
both spellings kept only one, and a var path spelled either way resolved
against the other. JS and Kotlin keep the two apart, so a rule reading a
non-ASCII key could answer differently per platform.

Value.object now stores an ObjectValue, whose keys compare and hash by
code unit, and the JSON parser iterates the NSDictionary rather than
casting to [String: Any], which merges the keys before the engine sees
them.

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

* Fold the ObjectValue.filter condition into a where clause

SwiftLint's for_where rule, and it reads better.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 11:19:16 +02:00
Antonio PallaresandCursor e6a0d9531c Compare strings by UTF-16 code unit in the rules engine (#7518)
* Compare strings by UTF-16 code unit in the rules engine

A JS string is a sequence of UTF-16 code units and every string operation
works on those units. Swift's String compares canonical equivalence over
Unicode scalars, so <, <=, >, >=, ==, ===, in and rc.entries key ordering
all had cases where a predicate evaluated one way on iOS and the other in
JS and Kotlin.

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

* Keep the new fixture descriptions platform-neutral

The fixture files are byte-identical across the iOS and Android engines, so
a description that explains the contract in terms of one host language's
string comparison does not read correctly in the other repository.

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

* Name the characters under test in the UTF-16 fixture descriptions

The predicates spell these strings as \uXXXX escapes so the exact code
units are unambiguous, which leaves a reader unable to tell which
character a fixture is about. Each description now shows the glyph and
its code point, and spells out the surrogate pair where one is the
point of the case.

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

* Route rc.sortBy string keys through jsStringPrecedes

The operator open-coded the UTF-16 comparison because the shared helper
did not exist yet when it landed. It does now, and the rationale lives
in the helper's doc, so the operator no longer repeats it.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:51:39 +02:00
Antonio PallaresandCursor 267322158d Add rc.entries and rc.fromEntries custom operators (#7435)
* Add Scope plumbing and custom operator extension point

Thread evaluation scope through the rules engine and delegate unknown
operators to an empty CustomOperators dispatcher, without changing behavior.

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

* Fix project.pbxproj: restore RewardedAdTrackingMetadata entries

The RulesEngine registration edit accidentally dropped four
RewardedAdTrackingMetadata.swift entries from the Xcode project.
Restore them and normalize CustomOperators identifiers to 24 hex chars.

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

* Add rc.entries and rc.fromEntries for keyed object iteration

Introduce pair conversion operators so filter/map/some can iterate
subscriptions, entitlements, and other keyed objects via existing iteration ops.

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

* Use firstArgEvaluated for rc.entries argument convention

Promote the shared unary-arg helper to Operators and align rc.entries
and rc.fromEntries with json-logic-js spread semantics. Update fixtures
for wrapped array operands and add a multi-arg interop case.

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

* Promote firstArgEvaluated to shared Operators helpers

Move the unary-arg helper from LogicOperators to Operators so custom
operators can use the same json-logic-js spread convention.

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

* Address review comments on custom operator extension point

Drop the cross-engine registration note from CustomOperators, and restore
the single-line var extra-args warning that was reflowed without changing
its text.

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

* Reformat rc.entries fixtures so subexpressions read as units

The one-token-per-line formatting spread predicates over dozens of lines,
so a comparison like {"===": [{"var": "0"}, "a"]} took seven lines and its
shape was impossible to see at a glance. Subtrees that fit within 120
columns are now inlined, which keeps each operator and its operands
together on one line while deeply nested structures still expand.

No fixture content changed; the document is byte-identical after parsing,
key order included.

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

* Trim EntriesOperators docs of cross-repo references

Drops the cross-engine contract paragraph and the Audience.swift pointer.
Both described obligations outside this file, which will drift as those
places change.

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

* Make rc.entries throw typeMismatch on non-object, non-array input

Returning [] with a warning let a misaimed rule keep evaluating: a some
over an empty list is simply false, so a rule pointed at a string or a
missing variable reported a confident answer rather than a failure. Null,
undefined, bool, number and string now throw, matching rc.length.

Strings in particular do not yield the character pairs JS Object.entries
produces. This is JSON, not JS, and a string here means the rule targets
the wrong field.

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

* Make rc.fromEntries throw typeMismatch on malformed input

Skipping bad entries produced a partial object indistinguishable from a
legitimate one, letting a rule match on whichever keys happened to survive.
Non-array arguments and entries that are not two-element [key, value] pairs
now throw, so rc.fromEntries fails the same way as rc.entries.

Entry arity is checked exactly: a one-element entry no longer defaults its
value to null, and an over-long entry no longer silently ignores the extra
elements the way JS Object.fromEntries does.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 10:57:04 +00:00