Files
Antonio PallaresandCursor 2deacfd190 Add Scope plumbing and custom operator extension point (#7433)
* 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>

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 08:04:21 +00:00

83 lines
3.1 KiB
Swift

//
// LogicOperators.swift
//
// Created by Antonio Pallares.
//
import Foundation
extension RulesEngine {
/// Logic operators: `!`, `!!`, `and`, `or`, `if`.
///
/// `and`, `or`, and `if` short-circuit and therefore can't use the eager
/// `evalArgs` helpers — they evaluate their arguments one at a time.
enum LogicOperators {
/// `{"!": x}` — boolean negation. Coerces to bool first per JSON Logic
/// truthiness rules.
static func opNot(args: Value, vars: Scope) throws -> Value {
let value = try Operators.firstArgEvaluated(args, vars: vars)
return .bool(!value.isTruthy)
}
/// `{"!!": x}` — boolean cast. Spec: equivalent to `!!x` in JS.
static func opNotNot(args: Value, vars: Scope) throws -> Value {
let value = try Operators.firstArgEvaluated(args, vars: vars)
return .bool(value.isTruthy)
}
/// `{"and": [a, b, c]}` — short-circuit AND. Returns the first falsy
/// value or, if all are truthy, the last value (matches JS / JSON Logic:
/// `and` returns the actual value, not a coerced boolean). Empty input
/// returns `.undefined` (json-logic-js reduces an empty `and` to
/// `undefined`, which is falsy but `!== null`).
static func opAnd(args: Value, vars: Scope) throws -> Value {
let items = Operators.argsAsList(args)
var last: Value = .undefined
for item in items {
last = try Evaluator.evaluateValue(item, vars: vars)
if !last.isTruthy {
return last
}
}
return last
}
/// `{"or": [a, b, c]}` — short-circuit OR. Returns the first truthy
/// value or, if all are falsy, the last value. Empty input returns
/// `.undefined` (json-logic-js reduces an empty `or` to `undefined`,
/// which is falsy but `!== null`).
static func opOr(args: Value, vars: Scope) throws -> Value {
let items = Operators.argsAsList(args)
var last: Value = .undefined
for item in items {
last = try Evaluator.evaluateValue(item, vars: vars)
if last.isTruthy {
return last
}
}
return last
}
/// `{"if": [cond, then, else]}` — also supports chained
/// `[c1, t1, c2, t2, ..., else]` (think `else if`). Without an `else`
/// clause and with no truthy condition, returns `null`.
static func opIf(args: Value, vars: Scope) throws -> Value {
let items = Operators.argsAsList(args)
var index = 0
while index + 1 < items.count {
let condition = try Evaluator.evaluateValue(items[index], vars: vars)
if condition.isTruthy {
return try Evaluator.evaluateValue(items[index + 1], vars: vars)
}
index += 2
}
if index < items.count {
return try Evaluator.evaluateValue(items[index], vars: vars)
}
return .null
}
}
}