Skip to content

Add JSON Logic string + array operators#6793

Merged
ajpallares merged 198 commits into
mainfrom
pallares/json-logic-string-array-operators
Jun 2, 2026
Merged

Add JSON Logic string + array operators#6793
ajpallares merged 198 commits into
mainfrom
pallares/json-logic-string-array-operators

Conversation

@ajpallares

@ajpallares ajpallares commented May 14, 2026

Copy link
Copy Markdown
Member

Resolves SDK-4331

Motivation

Adds in, cat, substr, merge, and missing_some so v1 targeting predicates can express string-content, array-membership, and "any N of these fields populated" checks.

Summary

  • New StringArrayOperators covers in, cat, substr, and merge per the JSON Logic spec.
  • missing_some lives next to missing in AccessorOperators to reuse the same dot-path lookup.
  • Operators dispatch table extended with all five operators.

Tests

  • StringArrayOperatorsTests covers each operator (basic, coercion, edge cases, arity tolerance).
  • AccessorOperatorsTests extends missing_some coverage (threshold met, below threshold, zero required, dot-paths).
  • EvaluatorTests adds two integration tests through dispatch: a country in [...] membership check and a missing_some gate inside an if.

Notes

Made with Cursor


Note

Low Risk
Isolated RulesEngineInternal operator additions with extensive spec-parity tests; affects targeting predicate evaluation but not purchases, auth, or network I/O.

Overview
Extends the internal JSON Logic rules engine with string/array operators (in, cat, substr, merge) and missing_some, wired through Operators.dispatch and a new StringArrayOperators module. missing_some reuses missing dot-path semantics and compares present-field count against a threshold via new jsToNumber coercion (distinct from asNumber for NaN edge cases). substr uses clampedInt so NaN/±Infinity indices don’t trap when coercing to Int.

Broad unit coverage (StringArrayOperatorsTests, AccessorOperatorsTests, EvaluatorTests integration) documents json-logic-js parity, including empty-string in haystacks, strict array membership, and Unicode code-point slicing (noted divergence from JS only for surrogate pairs).

Reviewed by Cursor Bugbot for commit fee8661. Bugbot is set up for automated code reviews on this repo. Configure here.

ajpallares and others added 7 commits May 14, 2026 10:35
Sets up the plumbing for an internal RulesEngine framework that the SDK
can ship as a separate library without coupling RevenueCat and
RevenueCatUI to each other through it. No actual rules logic yet —
this is just the wiring across all distribution methods so the upcoming
JSON Logic engine can land incrementally.

What's here:

- New `RulesEngine/` source dir with a `public enum RulesEngine {}`
  placeholder, plus `Tests/RulesEngineTests/` with a smoke test.
- `Package.swift`: new `RulesEngine` library product, target, and
  `RulesEngineTests` test target.
- Tuist: new `Projects/RulesEngine/Project.swift`, registered in
  `Workspace.swift` under `localXcodeProject` mode (mirroring how
  RevenueCat / RevenueCatUI are wired), and added to the
  `productTypes` map in `Tuist/Package.swift`.
- Legacy `RevenueCat.xcodeproj`: new `RulesEngine` framework target +
  `RulesEngineTests` test target + shared `RulesEngine.xcscheme`,
  added programmatically via `scripts/add_rules_engine_to_xcodeproj.rb`
  (kept under `scripts/` for repeatability). This is what enables
  Carthage and any future xcframework export to see the framework.
- `RevenueCatRulesEngine.podspec` at the repo root, with
  `s.module_name = 'RulesEngine'` so consumers `import RulesEngine`.
  The pod has no dependency on `RevenueCat`.
- `Tests/TestPlans/CI-AllTests.xctestplan`: `RulesEngineTests` added
  so the existing `test_ios` Fastlane / CI matrix executes them
  automatically — no separate lane or job required for tests.
- `fastlane/Fastfile`: `RevenueCatRulesEngine.podspec` added to the
  version-bump map and the `check_pods` lint matrix; new
  `push_rules_engine_pod` lane.
- `.circleci/default_config.yml`: new `push-rules-engine-pod` job
  wired into the release workflow alongside the existing pod pushes;
  `make-release` now also requires it.
- `.swiftlint.yml`: `Tests/RulesEngineTests` added to the
  `xctestcase_superclass` exclude list (same treatment as
  `ReceiptParserTests`).

Decisions worth flagging:

- Pod name is `RevenueCatRulesEngine`, module name is `RulesEngine`.
  The longer pod name avoids namespace collisions in the CocoaPods
  global namespace; the module name keeps `import RulesEngine` clean.
- Wired into the legacy `.xcodeproj` from day one so Carthage works
  the same day a real type lands here. The pbxproj edits live in a
  scripted helper rather than hand-written diffs to keep the change
  reviewable and reproducible.
- No consumer (`RevenueCat` / `RevenueCatUI`) depends on `RulesEngine`
  yet. That choice is intentionally a follow-up so we can decide
  ownership without churn here.

Verification:

- `swift build --target RulesEngine` ✔
- `swift test --filter RulesEngineTests` ✔
- `xcodebuild -project RevenueCat.xcodeproj -scheme RulesEngine ... build` ✔
- `xcodebuild -project RevenueCat.xcodeproj -scheme RulesEngine ... test` ✔
- `bundle exec pod lib lint RevenueCatRulesEngine.podspec --quick` ✔
- `tuist generate` (default `localSwiftPackage`) ✔
- `TUIST_RC_XCODE_PROJECT=true tuist generate` ✔
- `swiftlint RulesEngine Tests/RulesEngineTests` ✔

Co-authored-by: Cursor <cursoragent@cursor.com>
Every public declaration in this module is intended to be visible only
to the rest of the SDK (RevenueCat / RevenueCatUI / hybrid bridges),
not to app developers, so put the SPI gate in place from day one
instead of bolting it on later.

- `RulesEngine.RulesEngine` is now `@_spi(Internal) public`. Comment
  explains the convention so future declarations follow it.
- The test bundle imports the module with `@_spi(Internal) @testable
  import RulesEngine` so it can reach SPI-public symbols.

Verified: `swift test --filter RulesEngineTests`,
`xcodebuild ... -scheme RulesEngine test`,
`pod lib lint RevenueCatRulesEngine.podspec --quick`,
`swiftlint RulesEngine Tests/RulesEngineTests` — all green.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Mirror the RulesEngine product + target wiring into
  `Package@swift-5.8.swift` so consumers on Swift 5.8 toolchains see
  the new module too. Caught by reviewer; my original SPM change only
  touched the main `Package.swift`.
- Trim the verbose explainer doc on the `RulesEngine` placeholder
  type. The `@_spi(Internal)` annotation speaks for itself; future
  declarations don't need a per-file reminder.
- Delete `scripts/add_rules_engine_to_xcodeproj.rb`. It was a one-shot
  helper used to add the framework / test targets / scheme to
  `RevenueCat.xcodeproj` without hand-editing 290 lines of pbxproj.
  The pbxproj is now the source of truth for those targets, and
  keeping the script around just risks drift between the script's
  hardcoded settings and the actual project file.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a fastlane lane (check_rules_engine_no_public_api) that builds RulesEngine for iphonesimulator with BUILD_LIBRARY_FOR_DISTRIBUTION=YES and asserts the public swiftinterface contains zero declarations. @_spi(Internal) public symbols only appear in the .private.swiftinterface, so they cannot leak through this check by construction. Unlike a baseline diff, no committed reference file exists for a contributor to regenerate to silence the check.

Wires the lane into a new check-rules-engine-public-api CI job alongside the existing check-api-changes-* jobs in run-all-tests, release-or-main, all-tasks-passed, and all-tests-succeeded workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
First slice of the JSON Logic rules engine in the new RulesEngine module
(built on top of the framework skeleton from
`pallares/rules-engine-skeleton`). Pure Swift, no public surface yet —
every new declaration is module-internal so the engine can land
incrementally without touching the SDK's exported API.

What's in:

- `Logger.swift` — internal `RulesEngineLogger` protocol + default
  `PrintLogger` (stderr) + test `CapturingLogger`. Kept internal so a
  future foreign-trait logger injected by the host SDK can be adapted
  to the same protocol without API churn.
- `Value.swift` — typed `Value` enum (`null` / `bool` / `int` / `float` /
  `string` / `array` / `object`), JSON Logic truthiness + loose (`==`)
  / strict (`===`) equality with type coercion.
- `RuleError.swift` — `RuleError` enum (`parse`, `typeMismatch`,
  `unsupportedOperator`).
- `Evaluator.swift` — top-level dispatcher; takes a typed `Value`
  predicate (no JSON parsing in the engine) and returns `Bool`.
- `Operators/` — MVP set: `var` (strict dot-path), `missing`, `==`,
  `!=`, `===`, `!==`, `!`, `!!`, `and`, `or`, `if`.
- Test bundle includes a `Value+JSON.swift` helper (Foundation's
  `JSONSerialization` + `CFNumber` type sniffing) so test predicates
  can be expressed as JSON literals — production callers will
  construct `Value` trees from the host SDK's own JSON parser.

Key decisions:

- **JSON parsing lives outside the engine.** The engine accepts a typed
  `Value` tree (the JSON-shaped enum is what cross-language bridges
  can express across the boundary). The `Foundation`-backed JSON
  helper is gated behind the test target only.
- **Missing variables** resolve to `null` and emit a warning, per JSON
  Logic spec. No `missingVariable` case in v1; reserved for a future
  strict mode.
- **`var` lookup** uses strict dot-path navigation. Flat-key fallback
  considered and deferred — pure addition if we need it later.

Verification:

- `swift test --filter RulesEngineTests` — 63 tests, 0 failures.
- `xcodebuild test -scheme RulesEngine -destination 'platform=macOS,arch=arm64'`
  — all RulesEngineTests pass.
- `swiftlint lint RulesEngine Tests/RulesEngineTests` — clean.
- `pod lib lint RevenueCatRulesEngine.podspec --quick` — passed.

Files were added to the legacy `RevenueCat.xcodeproj` programmatically
via a one-shot `xcodeproj`-gem helper (mirroring how the framework
target itself was bootstrapped); the helper script was deleted after
running so the pbxproj remains the source of truth.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ajpallares ajpallares added the pr:feat A new feature label May 14, 2026
ajpallares and others added 16 commits May 14, 2026 18:51
- Add the standard RevenueCat MIT license header to
  `RulesEngine.swift` and `RulesEngineTests.swift`, matching every other
  Swift source in `Sources/`, `Tests/`, and `RevenueCatUI/`.
- Drop the explicit `Foundation.framework in Frameworks` entries (and
  the orphaned `Foundation.framework` PBXFileReference / `iOS` group)
  from the `RulesEngine` and `RulesEngineTests` targets. Swift
  auto-links Foundation, and the hardcoded `iPhoneOS18.0.sdk` path was
  inconsistent with `RevenueCat`, `RevenueCatUI`, and `ReceiptParser`,
  none of which link Foundation explicitly.

Verified: `xcodebuild build`, `xcodebuild test`, `swift build`,
`swift test`, and `pod lib lint --quick` all pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
- SPM: drop the `RulesEngine` library product. The target stays — it's
  consumed as an internal dependency of `RevenueCat` / `RevenueCatUI`
  (once we wire it in) and used by `RulesEngineTests` — but without a
  `.library(...)` product it can't be added directly by SPM
  integrators. Mirrored in `Package@swift-5.8.swift`.

- CI: drop `push-rules-engine-pod` from the `deploy-tag` workflow and
  from `make-release`'s `requires`. The module is currently a skeleton
  with no functionality and no consumer, so publishing
  `RevenueCatRulesEngine` to CocoaPods trunk on every SDK release would
  ship an empty pod forever. The job definition, the
  `push_rules_engine_pod` fastlane lane, the `pod_lib_lint` inside
  `check_pods`, and the `version_replacements` entry are all kept, so
  the podspec keeps getting linted and version-bumped — a follow-up PR
  will re-add the two workflow lines alongside the first real consumer
  of `RulesEngine`.

Verified: `swift build --target RulesEngine`, `swift test --filter
RulesEngineTests`, `xcodebuild -scheme RulesEngine test`,
`pod lib lint RevenueCatRulesEngine.podspec --quick`, and YAML parsing
of `.circleci/default_config.yml` all pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
The `RulesEngine` module is currently a skeleton with no functionality
and no consumer. Keeping the CocoaPods publishing pieces in this PR
means we'd be carrying podspec / fastlane / CircleCI artifacts that
nothing exercises end-to-end.

Drop them entirely from this PR so the skeleton stays minimal:

- Delete `RevenueCatRulesEngine.podspec`.
- Remove `push_rules_engine_pod` fastlane lane, `version_replacements`
  entry, and the `pod_lib_lint` call from `check_pods`.
- Remove the `push-rules-engine-pod` CircleCI job definition and the
  explanatory comment in the `deploy-tag` workflow.

The full distribution wiring (including the workflow entry and
`make-release` requirement) will land in a separate draft PR that we
can keep open and only merge alongside the first real consumer of
`RulesEngine`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the duplicated platform-independence note from the `check_pods`
`RulesEngine` swiftinterface check; the remaining lines already cover
why @_spi(Internal) can't leak through it.

Co-authored-by: Cursor <cursoragent@cursor.com>
…into pallares/rules-engine-enforce-internal-api
Co-authored-by: Cursor <cursoragent@cursor.com>
…into pallares/rules-engine-enforce-internal-api
`RulesEngine` is intentionally never exposed as an SPM `.library`
product nor as an `.external(name:)` target in any of our Tuist
projects — it's only ever pulled in transitively as an internal
target of `RevenueCat`/`RevenueCatUI`. That removes the duplicate-
framework concern that gates `RevenueCat`/`RevenueCatUI` behind
`TUIST_RC_XCODE_PROJECT=true`, because no workspace project links
both the local Tuist `RulesEngine.framework` and the SPM-resolved
transitive one into the same binary.

Moving the `./Projects/RulesEngine` append out of the
`localXcodeProject` switch lets developers run
`tuist generate RulesEngine` (or pick the `RulesEngine` scheme from
the workspace) in the default dependency mode, without needing to
set any environment variables.

`RevenueCat` / `RevenueCatUI` stay gated as before — they're exposed
as SPM library products, so including the local Tuist projects
alongside the SPM-resolved ones would still produce the
"Multiple commands produce" duplicate-framework error.

Co-authored-by: Cursor <cursoragent@cursor.com>
Define `RulesEngineTests` as a Tuist target alongside `RulesEngine` and
add it to the `RulesEngine` scheme's `testAction` so the generated
workspace can build and run the module's tests.

Use `tuist generate RulesEngineTests` (or a full `tuist generate`) to
get a workspace with both targets — `tuist generate RulesEngine`
filters to the framework alone, since tests depend on the framework
rather than the other way around.

Co-authored-by: Cursor <cursoragent@cursor.com>
`TargetReference` is `ExpressibleByStringLiteral`, so the explicit
`.init(stringLiteral:)` is unnecessary — passing the string directly
produces the same result with less noise.

Co-authored-by: Cursor <cursoragent@cursor.com>
Match the established pattern used by every other framework target in
`RevenueCat.xcodeproj` (`RevenueCat`, `RevenueCatUI`, `ReceiptParser`):
Debug uses `SKIP_INSTALL = NO`, Release uses `SKIP_INSTALL = YES`.

Leaving `SKIP_INSTALL = NO` on Release would copy `RulesEngine.framework`
into the archive's Products directory during `xcodebuild archive`-based
flows (e.g. XCFramework export), which is not the intended behavior for
a framework that's meant to be embedded by consumers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ajpallares ajpallares added pr:other and removed pr:feat A new feature labels May 15, 2026
ajpallares and others added 4 commits May 15, 2026 19:45
…lic_api`

The lane previously invoked `xcodebuild -workspace . -scheme RulesEngine`,
which only works when `RulesEngine` is an SPM library product. After the
skeleton PR removed `RulesEngine` from `Package.swift`'s `products`, no SPM
scheme is auto-generated and CI failed with `The workspace named
"purchases-ios" does not contain a scheme named "RulesEngine"`.

Switch to building via the Tuist-generated `RevenueCat-Tuist.xcworkspace`,
where `RulesEngine` already has a shared scheme. CI now generates the
workspace via the existing `tuist-generate-workspace` step before invoking
the lane; locally, run `tuist install && tuist generate RulesEngine
--no-open` first (the lane fails with a friendly message otherwise).

Co-authored-by: Cursor <cursoragent@cursor.com>
…es/json-logic-evaluator

Conflict in `RevenueCat.xcodeproj/project.pbxproj`: this branch was forked
off the older skeleton state and (via Cursor/Xcode auto-edits) re-introduced
the `Foundation.framework in Frameworks` references with a hardcoded
`iPhoneOS18.0.sdk` path on the `RulesEngine` framework target and the
`RulesEngineTests` target — the exact pollution that was cleaned up on the
skeleton PR (commit `e639329c7 RulesEngine skeleton: PR feedback`).

Resolution: keep all of this branch's legitimate additions (the new JSON
Logic source files and tests, including `EqualityOperatorsTests.swift`) and
re-apply the skeleton-PR cleanup — drop both `Foundation.framework` build
file entries, the file reference with the hardcoded SDK path, the iOS
PBXGroup, and empty out the now-unused entries in the corresponding
Frameworks build phases. Foundation is auto-linked by Swift; no behavior
change.

Verified locally: `swift build --target RulesEngineTests` builds clean,
`tuist generate RulesEngine --no-open` succeeds, and the
`check_rules_engine_no_public_api` lane still passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
`RulesEngine` is not an SPM library product and isn't consumed via
`.external(name: "RulesEngine")` in any Tuist project, so the
`productTypes` mapping has nothing to apply to. Remove it.

Co-authored-by: Cursor <cursoragent@cursor.com>
ajpallares and others added 12 commits May 28, 2026 17:13
Add warn tag parameter with a default, replace the public logger setter with setLogger, and update tests to install loggers through the new API.

Co-authored-by: Cursor <cursoragent@cursor.com>
Nothing in the MVP evaluator throws it yet; add it back in the PR that introduces the first call site.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts: keep arithmetic operators and extended ToNumber
coercion from this branch, take main's evaluator/logger updates and
swiftlint rule, and restore RuleError.typeMismatch for zero-arg mul.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restore ExitOfferHelperTests and ButtonComponentViewTests entries from
main and apply only the RulesEngineInternal arithmetic additions.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts: keep comparison operators alongside arithmetic from main,
update logger API, and merge Xcode project references for both operator sets.

Co-authored-by: Cursor <cursoragent@cursor.com>
Match json-logic-js: after ToPrimitive (number hint), lex compare only when
both sides are strings; compound-vs-string uses jsString, not numeric coercion.

Co-authored-by: Cursor <cursoragent@cursor.com>
…/json-logic-string-array-operators

Co-authored-by: Cursor <cursoragent@cursor.com>
Base automatically changed from pallares/json-logic-comparison-operators to main May 29, 2026 15:01
Use Array.prototype.join semantics for cat null operands and jsToNumber
for missing_some threshold comparisons so NaN and unparseable need
counts never satisfy per json-logic-js.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread RulesEngineInternal/Value.swift
Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit abb68cb. Configure here.

Comment thread RulesEngineInternal/Operators/StringArrayOperators.swift
ajpallares and others added 5 commits June 1, 2026 12:02
Reject empty-string haystacks before substring search per json-logic-js,
and add tests for null substr length, object cat operands, and empty
haystack membership.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…tring-array-operators

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

# Conflicts:
#	RevenueCat.xcodeproj/project.pbxproj
ajpallares added a commit that referenced this pull request Jun 2, 2026
…son-logic-js

cat is Array.prototype.join(""), so a null operand renders as "" (not "null").
missing_some uses a raw JS >= comparison, so a NaN threshold is never satisfied
and the missing keys are returned. The operator source fix lands downstack in

Co-authored-by: Cursor <cursoragent@cursor.com>
#6793; this only updates the JSON predicate fixtures to match.
@ajpallares

Copy link
Copy Markdown
Member Author

Bump on this @RevenueCat/coresdk 🙏

@ajpallares ajpallares merged commit 06439b3 into main Jun 2, 2026
18 of 20 checks passed
@ajpallares ajpallares deleted the pallares/json-logic-string-array-operators branch June 2, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants