Files
purchases-ios/RulesEngineInternal/Operators/LogicOperators.swift
T
Antonio PallaresandCursor 0ff0e3dafe Merge branch 'pallares/rules-engine-enforce-internal-api' into pallares/json-logic-evaluator
Brings in:
- The `RulesEngine` → `RulesEngineInternal` module rename from #6787.
- The `no_leaking_rules_engine_internal_import` SwiftLint rule update from #6788.
- Plus the `main`-merge cascade carried by #6788.

Conflicts resolved:

- `RulesEngineInternal/RulesEngineInternal.swift`: kept this branch's
  `Rules.logger` / `LoggerStorage` storage, dropped the
  `@_spi(Internal)` marker on `public enum Rules` (since #6787 dropped
  those markers; the new SwiftLint rule replaces that gate).
- `RevenueCat.xcodeproj/project.pbxproj`: kept this branch's full file
  list (Evaluator, Logger, Value, RuleError, Operators/*, plus the new
  test files and helpers), then applied the producer-side rename to
  every `RulesEngine\b` / `RulesEngineTests\b` token (target name,
  group name, path, productName, bundle IDs `com.revenuecat.RulesEngineInternal[Tests]`,
  framework / xctest output names, and the `RulesEngine.swift` → `RulesEngineInternal.swift`
  file ref).

Other rename follow-ups in this branch's added files:

- `Tests/RulesEngineInternalTests/*.swift` (8 files) — every
  `[@_spi(Internal) ]@testable import RulesEngine` updated to
  `@testable import RulesEngineInternal`. The `@_spi(Internal)`
  attribute is no longer needed because the producer no longer uses
  SPI markers.
- `Projects/RulesEngineInternal/`, `RulesEngineInternal/`, and
  `Tests/RulesEngineInternalTests/` directories were moved
  automatically by git's directory-rename detection (15 file renames).

Left intentionally unchanged:

- `RulesEngineInternal/Logger.swift` — the development-only
  `PrintLogger` keeps its `[RulesEngine]` log prefix because that
  string identifies the subsystem (the rules engine) for log readers,
  not the producer-side module name. The injected production logger
  replaces this struct entirely once the engine is wired into the SDK,
  so the prefix is throwaway.
- `RulesEngineLogger` protocol name — internal type name within the
  module, not part of the import-site signal that motivated the
  module rename.
- The `Rules` namespace name — separate identifier from the module
  name, kept stable so call sites still write `Rules.something`.

Verified locally:
- `swift build --target RulesEngineInternal` ✔
- `swift test --filter RulesEngineInternalTests` ✔ — 75 tests passed
- `xcodebuild -scheme RulesEngineInternal -destination 'generic/platform=iOS' build` ✔
- `swiftlint --no-cache` ✔ — 0 violations across 1332 files
- `git ls-files | xargs perl -ne 'if (/\bRulesEngine\b/ && !/RulesEngineInternal/) { ... }'` returns only the `[RulesEngine]` log prefix mentioned above.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 17:51:26 +02:00

96 lines
3.5 KiB
Swift

//
// LogicOperators.swift
//
// Created by Antonio Pallares.
//
import Foundation
/// 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: Value) throws -> Value {
let value = try firstArgEvaluated(args, vars: vars)
return .bool(!value.isTruthy)
}
/// `{"!!": x}` — boolean cast. Spec: equivalent to `!!x` in JS.
static func opNotNot(args: Value, vars: Value) throws -> Value {
let value = try 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
/// semantics: `and` returns the actual value, not a coerced boolean).
///
/// Empty input returns `.null` to mirror the json-logic-js spec, where
/// the empty-args case falls through to an uninitialized `current`
/// (i.e. `undefined`). Both are falsy, so observers that only check
/// truthiness keep working; the visible difference shows up in
/// constructs like `{"if": [{"and": []}, then, else]}`, which the spec
/// routes to `else`.
static func opAnd(args: Value, vars: Value) throws -> Value {
let items = Operators.argsAsList(args)
// Empty input falls through with `last == .null` (≈ JS `undefined`).
var last: Value = .null
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 `.null` to mirror the json-logic-js spec — same
/// reasoning as `opAnd`.
static func opOr(args: Value, vars: Value) throws -> Value {
let items = Operators.argsAsList(args)
// Empty input falls through with `last == .null` (≈ JS `undefined`).
var last: Value = .null
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: Value) 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
}
// MARK: - Helpers
private static func firstArgEvaluated(_ args: Value, vars: Value) throws -> Value {
let items = Operators.argsAsList(args)
let first = items.first ?? .null
return try Evaluator.evaluateValue(first, vars: vars)
}
}