Files
purchases-ios/Sources/RulesEngine/CustomOperators/CustomOperators.swift
T
Antonio PallaresandCursor aa30cdcf1c Add rc.regexMatch custom operator (#7556)
* Add rc.regexMatch custom operator

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

* Report why a regex pattern failed to compile

The engine discarded the compile error and said only that the pattern was
bad, which leaves whoever authored the rule to find the mistake by eye.

NSRegularExpression has little to add today: every syntax failure is the
same NSCocoaErrorDomain 2048 whose description restates the pattern. The
catch is here so the message keeps its shape across the engines, where
the JVM names the fault and points at the offset.

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

* Name the engines the regex operators actually run on

The divergence list said ICU here and java.util.regex on Android, which
is wrong: Android's java.util.regex has wrapped ICU4C since 2.3, so both
devices run ICU and Funnels is the outlier. Read the old note and you
would expect \d to behave differently on the two devices when it behaves
differently in Funnels instead.

Adds the rule the old list was missing — ICU rejects a bare literal }
that JS accepts — and the one genuine device difference, unknown escapes.

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

* Pin how the regex operators read an empty input

An empty input was untested, and it is the input a variable lookup is
most likely to produce by accident. All three engines agree: the string
is still searched, so a pattern that can match nothing succeeds on it
and one that needs a character does not.

The empty *pattern* is deliberately not fixtured. ICU refuses to compile
it while JS reads it as a match at every position, so it is a divergence
rather than a shared rule, and it is now recorded as one.

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

* Delegate regex syntax to the platform's own documentation

The divergence list restated what each engine does, which the vendor
pages already document and do more reliably than a comment that has to be
kept true by hand. The syntax reference now lives behind a link, leaving
only what this file decides for itself.

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

* Name only the pattern when a regex fails to compile

The Android branch hand-assembled the first line of PatternSyntaxException's
own message, and its iOS counterpart appended a localizedDescription that
only repeats the pattern back. Neither is asserted anywhere, and the useful
half is OpenJDK phrasing that a device's ICU-backed engine words differently.

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

* Bind both regex operands before reading them

The arity guard proves the count matches what the caller asked for, not
that it reaches two, so a caller passing a smaller count would index past
the end. An out-of-bounds read traps, and a trap is the one failure the
engine cannot hand back as an error.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:09:58 +02:00

61 lines
1.8 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 {
// swiftlint:disable:next cyclomatic_complexity
static func dispatch(
op operatorName: String,
args: Value,
vars: Scope
) throws -> Value {
switch operatorName {
case "rc.entries":
return try EntriesOperators.opEntries(args: args, vars: vars)
case "rc.fromEntries":
return try EntriesOperators.opFromEntries(args: args, vars: vars)
case "rc.let":
return try LetOperator.opLet(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.regexMatch":
return try RegexOperators.opRegexMatch(args: args, vars: vars)
case "rc.rootVar":
return try RootVarOperator.opRootVar(args: args, vars: vars)
case "rc.semverCompare":
return try SemverOperator.opSemverCompare(args: args, vars: vars)
case "rc.slice":
return try SliceOperator.opSlice(args: args, vars: vars)
case "rc.sortBy":
return try SortByOperator.opSortBy(args: args, vars: vars)
case "rc.split":
return try SplitOperator.opSplit(args: args, vars: vars)
default:
throw EvaluationError.unsupportedOperator(name: operatorName)
}
}
}
}