mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
* 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>
104 lines
4.4 KiB
Swift
104 lines
4.4 KiB
Swift
//
|
|
// EntriesOperators.swift
|
|
//
|
|
// Created by Antonio Pallares.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
extension RulesEngine {
|
|
|
|
/// `rc.entries` and `rc.fromEntries` — convert between keyed objects and
|
|
/// the `[key, value]` pair arrays that JSON Logic iteration operators accept.
|
|
enum EntriesOperators {
|
|
|
|
/// `{"rc.entries": value}` — convert a keyed object or array into an
|
|
/// array of two-element `[key, value]` arrays so `some`, `filter`, etc.
|
|
/// can iterate it.
|
|
///
|
|
/// **Argument form**: unary operators follow `json-logic-js` spread
|
|
/// semantics via `Operators.firstArgEvaluated` — a multi-element
|
|
/// top-level array is a multi-argument call, not a single array
|
|
/// operand. So `{"rc.entries": ["a", "b"]}` uses only `"a"`. To pass a
|
|
/// literal array operand, wrap it:
|
|
/// `{"rc.entries": [["a", "b"]]}`.
|
|
///
|
|
/// - **Object**: pairs sorted **lexicographically by key**. Swift
|
|
/// dictionaries have no insertion order; this deliberately diverges
|
|
/// from JS `Object.entries` insertion order. Rules must not depend on
|
|
/// insertion order.
|
|
/// - **Array**: index/value pairs with **string** keys (`"0"`, `"1"`, …),
|
|
/// matching `Object.entries(["a","b"]) === [["0","a"],["1","b"]]`.
|
|
/// - **Anything else** (null, undefined, bool, number, string): throws
|
|
/// `EvaluationError.typeMismatch`.
|
|
///
|
|
/// Strings throw rather than yielding the character pairs JS
|
|
/// `Object.entries("ab")` produces. A string reaching here means the rule
|
|
/// points at the wrong field, and iterating its characters would let that
|
|
/// rule keep evaluating to a confident wrong answer.
|
|
static func opEntries(args: Value, vars: Scope) throws -> Value {
|
|
let input = try Operators.firstArgEvaluated(args, vars: vars)
|
|
|
|
switch input {
|
|
case .object(let map):
|
|
let sortedKeys = map.keys.sorted()
|
|
let pairs = sortedKeys.map { key in
|
|
Value.array([.string(key), map[key] ?? .null])
|
|
}
|
|
return .array(pairs)
|
|
|
|
case .array(let items):
|
|
let pairs = items.enumerated().map { index, value in
|
|
Value.array([.string(String(index)), value])
|
|
}
|
|
return .array(pairs)
|
|
|
|
default:
|
|
throw EvaluationError.typeMismatch(
|
|
message: "operator 'rc.entries' expected object or array, got \(input)"
|
|
)
|
|
}
|
|
}
|
|
|
|
/// `{"rc.fromEntries": pairs}` — inverse of `rc.entries`.
|
|
///
|
|
/// **Argument form**: same spread semantics as `rc.entries`. A literal
|
|
/// pair list must be wrapped as a single operand, e.g.
|
|
/// `{"rc.fromEntries": [[["a", 1], ["b", 2]]]}`. Extra arguments are
|
|
/// silently ignored, matching `!` and `!!`.
|
|
///
|
|
/// - **Array of two-element arrays**: builds an object. Keys are coerced
|
|
/// via `jsString` (same helper `var` uses for path segments).
|
|
/// - **Duplicate keys**: last occurrence wins, matching JS.
|
|
/// - **Empty array**: `{}`.
|
|
/// - **Non-array argument**, or any entry that is not a two-element
|
|
/// array: throws `EvaluationError.typeMismatch`.
|
|
///
|
|
/// Malformed entries throw rather than being skipped. Dropping them
|
|
/// would build a partial object that reads as a legitimate result, so a
|
|
/// rule could match on the surviving keys alone.
|
|
static func opFromEntries(args: Value, vars: Scope) throws -> Value {
|
|
let input = try Operators.firstArgEvaluated(args, vars: vars)
|
|
|
|
guard case .array(let entries) = input else {
|
|
throw EvaluationError.typeMismatch(
|
|
message: "operator 'rc.fromEntries' expected array, got \(input)"
|
|
)
|
|
}
|
|
|
|
var result: [String: Value] = [:]
|
|
for entry in entries {
|
|
guard case .array(let pair) = entry, pair.count == 2 else {
|
|
throw EvaluationError.typeMismatch(
|
|
message: "operator 'rc.fromEntries' expected a two-element "
|
|
+ "[key, value] entry, got \(entry)"
|
|
)
|
|
}
|
|
|
|
result[jsString(pair[0])] = pair[1]
|
|
}
|
|
return .object(result)
|
|
}
|
|
}
|
|
}
|