Files
purchases-ios/Sources/RulesEngine/CustomOperators/CustomOperators.swift
T
Antonio PallaresandCursor 8ba422da6a Add rc.lower and rc.upper custom operators (#7436)
* 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>

* 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>

* Add rc.lower and rc.upper for case-insensitive rules

Locale-independent case conversion via jsString coercion, with predicate
fixtures covering coercion, Unicode behavior, and motivating match cases.

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>

* Pin locale-independent uppercasing of ASCII i

rc.lower already had a fixture asserting the Unicode default mapping for
the Turkish I, but rc.upper had no equivalent. The uppercase direction is
the more consequential one: Turkish maps i to U+0130, a scalar with no
ASCII counterpart, so a locale-sensitive implementation would silently
change what rules match.

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

* Require string input for rc.lower and rc.upper

Both operators coerced non-strings through jsString, mirroring JS
String(x).toLowerCase(). That makes absent data look like real data: a
missing var resolves to null, so comparing a lowercased path against
"null" would quietly match.

Throw typeMismatch instead, matching rc.length and rc.entries. The four
coercion fixtures are replaced by nine type-error fixtures covering null,
undefined, numbers, booleans, arrays, objects and missing vars, with
rc.upper cases pinning that it shares the guard.

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

* Correct the jsNumberString divergence note

The note claimed the JS mismatch only surfaces through var path coercion
and looseEq, and only for large magnitudes. cat, substr and in stringify
numbers through the same helper, and small values diverge too: 1e-6
renders as "1e-06" rather than "0.000001".

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 07:41:56 +02:00

40 lines
1.0 KiB
Swift

//
// CustomOperators.swift
//
// Created by Antonio Pallares.
//
import Foundation
extension RulesEngine {
/// RevenueCat-specific JSON Logic operators.
///
/// Operators here are namespaced under `rc.` so they cannot collide with a
/// future standard JSON Logic operator.
enum CustomOperators {
static func dispatch(
op operatorName: String,
args: Value,
vars: Scope
) throws -> Value {
switch operatorName {
case "rc.length":
return try LengthOperator.opLength(args: args, vars: vars)
case "rc.lower":
return try CaseOperators.opLower(args: args, vars: vars)
case "rc.upper":
return try CaseOperators.opUpper(args: args, vars: vars)
case "rc.rootVar":
return try RootVarOperator.opRootVar(args: args, vars: vars)
default:
throw EvaluationError.unsupportedOperator(name: operatorName)
}
}
}
}