Add JSON Logic string + array operators#3485
Merged
Merged
Conversation
Sets up the plumbing for an internal rules-engine module that the SDK can
depend on without coupling to :purchases or :ui:revenuecatui. Includes:
- New :rules-engine Gradle module using the existing
`revenuecat-public-library` convention plugin (Metalava, Dokka, Kover,
Vanniktech publish, baseline profile, explicit-API mode).
- Single-flavor (`apis { defaults }`); no `billingclient` dimension since
the rules engine has no Billing Client dependency. Publishes a single
`purchases-rules-engine` artifact instead of a bc7/bc8 split.
- Module-scoped `mavenPublishing.configure(AndroidSingleVariantLibrary("defaultsRelease"))`
override so the global `ANDROID_VARIANT_TO_PUBLISH=defaultsBc8Release`
default doesn't apply here.
- Placeholder `RulesEngine` Kotlin object plus a smoke test so CI exercises
the module from day one. No actual rules logic yet.
- BOM constraint added so consumers using the BOM get an aligned version.
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 (`:purchases`, `:ui:revenuecatui`, hybrid bridges), not to app developers, so put the opt-in gate in place from day one instead of bolting it on later. Sibling annotation, not the existing one ---------------------------------------- `@InternalRevenueCatAPI` lives in `:purchases` and we deliberately keep `:rules-engine` standalone (no dependency on `:purchases`), so this defines a parallel `@InternalRulesEngineAPI` in `com.revenuecat.purchases.rules` with identical `@RequiresOptIn(level=ERROR)` semantics. Two annotations doing the same job is mildly redundant but unambiguous in the IDE and avoids coupling the two modules just for an annotation. Changes ------- - New `InternalRulesEngineAPI.kt` mirroring the shape of `:purchases`'s `InternalRevenueCatAPI`. - `RulesEngine` object annotated with `@InternalRulesEngineAPI`. - Test class opts in with `@OptIn(InternalRulesEngineAPI::class)`. - Metalava configured (per-module) to add `com.revenuecat.purchases.rules.InternalRulesEngineAPI` to the hidden-annotations list, on top of the `com.revenuecat.purchases.InternalRevenueCatAPI` entry already added by the `revenuecat-public-library` convention plugin. - `api.txt` regenerated: `RulesEngine` is now hidden; the annotation itself remains public so consumers can opt in. Verified -------- - `./gradlew :rules-engine:testDefaultsDebugUnitTest` ✔ - `./gradlew :rules-engine:metalavaCheckCompatibilityDefaultsRelease` ✔ - `./gradlew detektAll` ✔ Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the explainer comment above the `mavenPublishing.configure(...)` override in `rules-engine/build.gradle.kts` and the doc on `InternalRulesEngineAPI`. The behavior is self-evident from the code and the annotation already carries a `@RequiresOptIn` message. Co-authored-by: Cursor <cursoragent@cursor.com>
…public API Adds scripts/check-rules-engine-internal-only.sh which regenerates rules-engine/api.txt and asserts it contains only the @InternalRulesEngineAPI annotation declaration. Unlike a baseline diff, this check is intrinsic and cannot be silenced by regenerating api.txt. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ine-enforce-internal-api
…merge) Co-authored-by: Cursor <cursoragent@cursor.com>
…github.com/RevenueCat/purchases-android into pallares/rules-engine-enforce-internal-api
First slice of the JSON Logic rules engine in the new `:rules-engine` module (built on top of the module skeleton from `pallares/rules-engine-skeleton`). Pure Kotlin, no exported API surface — every new declaration is module-`internal` so the engine can land incrementally without changing `rules-engine/api.txt` or pulling anything into the SDK's metalava signature. What's in: - `Logger.kt` — internal `RulesEngineLogger` interface + default `PrintlnLogger` (stderr) + test `CapturingLogger`. Kept internal so a future host-supplied logger can be adapted to the same interface without API churn. - `Value.kt` — typed `Value` sealed class (`Null` / `BoolValue` / `IntValue` / `FloatValue` / `StringValue` / `ArrayValue` / `ObjectValue`), JSON Logic truthiness + loose (`==`) / strict (`===`) equality with type coercion. - `RuleError.kt` — `RuleError` sealed `RuntimeException` (`Parse`, `TypeMismatch`, `UnsupportedOperator`). - `Evaluator.kt` — top-level dispatcher; takes a typed `Value` predicate (no JSON parsing in the engine) and returns `Boolean`. - `operators/` — MVP set: `var` (strict dot-path), `missing`, `==`, `!=`, `===`, `!==`, `!`, `!!`, `and`, `or`, `if`. - Test source set adds a `helpers/ValueJsonHelper.kt` (`org.json` + `BigDecimal` scale check to preserve int-vs-decimal intent) 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 sealed class is what cross-language bridges can express across the boundary). The `org.json`-backed JSON helper is gated to the test source set only via a new `testImplementation(libs.json)` dep. - **Missing variables** resolve to `Null` and emit a warning, per JSON Logic spec. No `MissingVariable` subclass 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: - `./gradlew :rules-engine:test` — 63 tests, 0 failures. - `./gradlew :rules-engine:check` — `lint` + `metalavaCheckCompatibility` pass; `api.txt` unchanged because every new declaration is `internal`. - `./gradlew detektAll` — clean. Co-authored-by: Cursor <cursoragent@cursor.com>
…(do not merge)" This reverts commit b7bbd5e.
…rge)" This reverts commit 6e38341.
Extends the JSON Logic operator set with arithmetic so predicates can express counter and sum conditions before the comparison and string operators land. What's new: - `+`, `-`, `*`, `/`, `%` per the JSON Logic spec, including variadic `+` and `*`, the 1-arg numeric-cast form of `+`, and unary negation via `-`. All arithmetic returns `Value.FloatValue(Double)` for consistency with the JS reference (which `parseFloat`s every operand). `looseEq` and `strictEq` already bridge `IntValue(n) ↔ FloatValue(n.0)`, so existing comparisons keep working. - Non-numeric operands (`ObjectValue`, `ArrayValue`, unparseable strings) coerce to `Double.NaN` and propagate through arithmetic — the result is `FloatValue(NaN)`, falsy under `isTruthy`. - Division and modulo by zero return `Value.Null` (deliberate deviation from JS's `Infinity` / `NaN` — friendlier for rule authors and matches the engine's "missing value" convention). Documented in the type's KDoc. Tests: - `ArithmeticOperatorsTest` covers each operator (basic, variadic, coercion, NaN propagation, divide/mod by zero, arity errors). - `EvaluatorTest` adds two integration tests through dispatch: `var * 2 == 6` and the divide-by-zero → null → falsy flow. Co-authored-by: Cursor <cursoragent@cursor.com>
Extends the JSON Logic operator set with the comparison operators rule
authors need to express numeric thresholds and ranges.
What's new:
- `<`, `<=`, `>`, `>=` per the JSON Logic spec.
- `<` and `<=` accept the 3-arg between form (`{"<=": [1, x, 10]}`
reads as `1 <= x <= 10`). `>` and `>=` are binary only, matching
the JS reference.
- All operators coerce operands through `Value.toNumberOrNull` and
compare as `Double`. Non-numeric operands (`ObjectValue`,
`ArrayValue`, unparseable strings) become `Double.NaN`; per
IEEE 754 every comparison against NaN is `false`, so a malformed
operand makes the predicate fail closed.
String semantics deviate from the JS reference (which compares two
strings lexicographically, e.g. `"10" < "9"` is true). We always
coerce numerically — `"10" < "9"` is false. Documented in the type's
KDoc.
Tests:
- `ComparisonOperatorsTest` covers each operator (basic, between
form, coercion, NaN propagation, no-between for `>` / `>=`, arity
errors).
- `EvaluatorTest` adds two integration tests through dispatch:
`var >= 3` and the 3-arg `1 <= var <= 10` between form.
Co-authored-by: Cursor <cursoragent@cursor.com>
Implements `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.
What's new:
- New `StringArrayOperators` covers `in`, `cat`, `substr`, and
`merge`.
- `opMissingSome` lives next to `opMissing` in `AccessorOperators` —
it reuses the same dot-path lookup, so co-locating beats spawning a
parallel module.
- `Operators` dispatch table extended with all five operators.
Behavior notes (deviations from the JSON Logic JS reference, both
documented in type-level KDocs and unit-tested):
- **`in` array membership uses `looseEq` instead of strict `===`.**
Rule authors regularly write integer literals against
backend-supplied string lists (`{"in": [{"var": "tier_id"},
["1", "2", "3"]]}` etc.); strict equality would silently fail
those, loose equality matches the rest of our equality story.
- **`substr` slices by Unicode code points**, not UTF-16 code units.
Matches Kotlin's `String.codePointCount` semantics and gives the
intuitive answer for multibyte strings. Differs from JS only for
surrogate-pair characters, which are vanishingly rare in real
rule data.
A few smaller decisions that follow JS:
- `cat` stringifies via a JS-style `String(value)` helper (`null` →
`"null"`, arrays → comma-joined, objects → `"[object Object]"`).
- `substr` with negative `length` mirrors the JS reference's
two-step impl (drop from the right of the substring-from-start).
- `missing_some` falls back to `0` for non-numeric `min_required`,
matching our other operators' lenient numeric coercion.
Tests:
- `StringArrayOperatorsTest` covers each of the four operators.
- `AccessorOperatorsTest` extended with `missing_some` cases
(threshold met, below threshold, zero required, dot-paths, arity
errors).
- `EvaluatorTest` adds two integration tests through dispatch: a
`country in [...]` membership check, and a `missing_some` gate
inside an `if`.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Gate the `mavenPublishing { ... }` configuration on the
`com.vanniktech.maven.publish` plugin actually being applied.
`ConfigureConditionalPublishing` in the convention plugin skips the
publish plugin when `ANDROID_VARIANT_TO_PUBLISH` contains
`customEntitlementComputation` (which `:rules-engine` doesn't have),
so the unconditional block was breaking
`./gradlew :purchases:publish -PANDROID_VARIANT_TO_PUBLISH=customEntitlementComputationBc8Release`
— i.e. the "Deploying Custom Entitlements Computation version"
fastlane step — with `Unresolved reference: mavenPublishing` while
configuring `:rules-engine`.
- Suppress `:rules-engine` from `dokkaHtmlMultiModule`. The existing
`HideInternalRevenueCatAPIPlugin` is hardcoded to
`com.revenuecat.purchases.InternalRevenueCatAPI` and only applied to
`:purchases`, so without this `RulesEngine` and `InternalRulesEngineAPI`
would leak into the published docs at `docs/{version}/`. Every public
symbol in this module is gated by `@InternalRulesEngineAPI`, so a
dedicated docs page would be empty anyway — suppressing the module
is simpler than generalizing the hide-plugin.
- Add `:rules-engine:metalavaGenerateSignatureDefaultsRelease` to
`scripts/api-dump.sh`. The script previously only invoked the
`Bc8`/`Bc7`/`customEntitlement` task names, so `:rules-engine`
(single-flavor, no `billingclient` dimension) was never regenerated,
meaning the committed `rules-engine/api.txt` couldn't act as a
tripwire via `scripts/api-check.sh`. With this change a leaked
non-internal API would show up as a diff in CI.
Verified:
- `:purchases:publish --dry-run -PANDROID_VARIANT_TO_PUBLISH=customEntitlementComputationBc8Release` now only fails on the unrelated `mavenCentralUsername`/`Password` credentials error.
- `:rules-engine:dokkaHtmlPartial` reports `Exiting Generation: Nothing to document`.
- `scripts/api-dump.sh` runs the rules-engine task and `api.txt` round-trips clean.
- `:rules-engine:testDefaultsReleaseUnitTest`, `:rules-engine:metalavaCheckCompatibilityDefaultsRelease`, and `detektAll` all pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
# Conflicts: # scripts/api-dump.sh
The module is currently a skeleton with no functionality and no
consumers, so publishing `purchases-rules-engine` would ship an empty
artifact whose Maven Central version we'd then be on the hook to keep
publishing forever. Defer the publishing wiring until the JSON Logic
engine lands.
- Short-circuit `:rules-engine` in `ConfigureConditionalPublishing` so
`com.vanniktech.maven.publish` is never applied to it. The module
still compiles, gets detekt'd, runs tests, and is dokka-suppressed on
every PR — there's just no AAR pushed to Sonatype.
- Drop the `mavenPublishing { configure(AndroidSingleVariantLibrary(…)) }`
block (and its imports) from `rules-engine/build.gradle.kts`. With the
publish plugin no longer applied, it's unreachable.
- Remove `api(project(":rules-engine"))` from `:bom` so consumers
exploring the BoM don't see a real-looking `purchases-rules-engine`
they could pull in and get nothing.
- Drop the `:rules-engine:metalavaGenerateSignatureDefaultsRelease`
entry from `scripts/api-dump.sh`. It will be re-added in the same
follow-up PR that flips publishing back on, to keep all the "publish
wiring" in one switch-flip.
Follow-up PR (alongside the first real consumer of `RulesEngine`):
revert the `:rules-engine` short-circuit, restore the
`mavenPublishing { … }` block (gated with `plugins.withId(...)` so it
doesn't break CE deploys), restore the `:bom` entry, and re-add the
api-dump invocation.
Verified:
- `:purchases:publish --dry-run -PANDROID_VARIANT_TO_PUBLISH=customEntitlementComputationBc8Release` only fails on the unrelated `mavenCentralUsername`/`Password` credentials error — `:rules-engine` configures cleanly.
- `:rules-engine:tasks --all` lists no Maven publish tasks (only the unrelated `prepareLintJarForPublish` Android Lint internal).
- `:bom:tasks` resolves without `:rules-engine`.
- `:rules-engine:testDefaultsReleaseUnitTest`, `:rules-engine:metalavaCheckCompatibilityDefaultsRelease`, `:rules-engine:dokkaHtmlPartial`, and `detektAll` all pass.
- `scripts/api-dump.sh` leaves all `api*.txt` files unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep this PR minimal: an internal kotlin android library that
compiles, tests, and gets detekt'd. Everything that's only relevant
when the module ships an artifact lives in a separate draft PR:
- Drop the module-level `metalava { … }` block (and the committed
`api.txt` baseline). Without a CI step regenerating it, the file
was just static; we'll re-add both — and wire them into
`scripts/api-dump.sh` — once publishing flips on.
- Drop the `dokkaHtmlPartial` suppression. The module would only show
up in `dokkaHtmlMultiModule` output once it's published, so there's
nothing to hide today.
Replace the two trailing comments with a single short pointer to the
follow-up PR and the `ConfigureConditionalPublishing` short-circuit.
Co-authored-by: Cursor <cursoragent@cursor.com>
…into pallares/rules-engine-enforce-internal-api
The `metalava { hiddenAnnotations.add(…) }` block lives in this branch
(rather than the skeleton) because the
`scripts/check-rules-engine-internal-only.sh` guardrail is the only
thing that depends on it: it asserts that, once
`@InternalRulesEngineAPI`-annotated declarations are hidden, the
generated `api.txt` contains nothing but the annotation interface
itself. Anything else is a leaked non-internal public API.
Without this block, the skeleton would have nothing exercising metalava
on `:rules-engine` (publishing wiring also lives in a separate draft
PR), so we keep it co-located with the check that needs it.
Co-authored-by: Cursor <cursoragent@cursor.com>
`metalava { hiddenAnnotations.add(InternalRulesEngineAPI) }` + a
committed `api.txt` are standard hygiene for every module that uses
`revenuecat.public.library`. The CocoaPods/Maven publishing wiring is
the only thing that's truly "exists but unwired" in this PR and that
already moved to its own draft.
Keeping the metalava config here also avoids a duplicate `metalava {}`
block landing in both the distribution PR (which adds the
`api-dump.sh` entry to enforce drift) and the enforce-internal-api PR
(which uses metalava to verify nothing leaks outside
`@InternalRulesEngineAPI`).
The `api.txt` file is not yet regenerated by CI in this PR — that
follow-up lives in the distribution draft PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
…into pallares/rules-engine-enforce-internal-api
The comment described what `ConfigureConditionalPublishing` already explains via its own short-circuit comment, so it was duplicating context. Co-authored-by: Cursor <cursoragent@cursor.com>
Distinguish null receiver (NaN) from JSON null (coerces to 0). Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts: keep arithmetic operators and extended ToNumber coercion from this branch, take main's RulesEngine.setLogger API and evaluator visibility, and merge TypeMismatch with UnsupportedOperator. Co-authored-by: Cursor <cursoragent@cursor.com>
The internal module opts out of Metalava; drop the accidental api.txt and revert the unrelated api-dump.sh comment so this PR stays focused on arithmetic operators only. Co-authored-by: Cursor <cursoragent@cursor.com>
…/json-logic-comparison-operators
Resolve conflicts: keep comparison operators with arithmetic from main and align rules-engine-internal logger and evaluator APIs with main. 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>
Use a when expression instead of multiple return statements. Co-authored-by: Cursor <cursoragent@cursor.com>
…/json-logic-string-array-operators Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
…github.com/RevenueCat/purchases-android into pallares/json-logic-string-array-operators
tonidero
approved these changes
Jun 2, 2026
| * `NaN` → `0` (matches JS `ToInteger`); `±Infinity` and | ||
| * out-of-range finite values clamp to [Int.MAX_VALUE] / [Int.MIN_VALUE]. | ||
| */ | ||
| fun clampedInt(value: Double): Int { |
Contributor
There was a problem hiding this comment.
Hmm I wonder if this should be separated to some utils class... Also, I wonder what happens if we need higher range than int... But maybe that's not a problem for now.
| fun opIn(args: Value, vars: Value): Value { | ||
| val evaluated = Operators.evalArgs(args, vars) | ||
| val needle = evaluated.firstOrNull() ?: Value.Null | ||
| val haystack = if (evaluated.size >= 2) evaluated[1] else Value.Null |
Contributor
There was a problem hiding this comment.
I must say, these property names 😅
Member
Author
There was a problem hiding this comment.
hahah right. Seems it's the convention for this? 😄
…tring-array-operators
Suppress ReturnCount on clampedInt, name substr operand counts, and avoid String.codePoints (API 24) for minSdk 23 compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3485 +/- ##
=======================================
Coverage 80.03% 80.03%
=======================================
Files 370 370
Lines 15048 15048
Branches 2074 2074
=======================================
Hits 12044 12044
Misses 2161 2161
Partials 843 843 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
matteinn
pushed a commit
to matteinn/purchases-android
that referenced
this pull request
Jun 5, 2026
**This is an automatic release.** ## RevenueCat SDK ### ✨ New Features * Add presented offering context to custom paywall events (RevenueCat#3424) via Rick (@rickvdl) * Add Workflows list endpoint (RevenueCat#3509) via Cesar de la Vega (@vegaro) ## RevenueCatUI SDK ### Paywalls_v2 #### 🐞 Bugfixes * Fix 1px seam between sliding multipage paywall pages (RevenueCat#3526) via Cesar de la Vega (@vegaro) ### 🔄 Other Changes * refactor: extract Offering.presentedOfferingContext() helper and apply across SDK (RevenueCat#3513) via Rick (@rickvdl) * Add JSON Logic string + array operators (RevenueCat#3485) via Antonio Pallares (@ajpallares) * Add ForbiddenPublicSealedClass detekt rule (RevenueCat#3503) via Toni Rico (@tonidero) * Update baseline profiles (RevenueCat#3519) via RevenueCat Git Bot (@RCGitBot) * build(deps): bump fastlane-plugin-revenuecat_internal from `af7bb5c` to `ce6a7ef` (RevenueCat#3515) via dependabot[bot] (@dependabot[bot]) * Add JSON Logic comparison operators (<, <=, >, >=) (RevenueCat#3484) via Antonio Pallares (@ajpallares) * Add JSON Logic arithmetic operators (+, -, *, /, %) (RevenueCat#3483) via Antonio Pallares (@ajpallares) * Add WorkflowEvent model and backend serialization (RevenueCat#3486) via Cesar de la Vega (@vegaro) * RulesEngine: add JSON Logic predicate evaluator (RevenueCat#3482) via Antonio Pallares (@ajpallares) * Add :rules-engine-internal skeleton module (RevenueCat#3478) via Antonio Pallares (@ajpallares) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Version bump and changelog/docs/CI path updates only; no application logic changes in the diff. > > **Overview** > This **automatic release** finalizes **Android SDK 10.8.0** by replacing **`10.8.0-SNAPSHOT`** with **`10.8.0`** across versioning (`gradle.properties`, `.version`, `Config.frameworkVersion`), sample apps, and changelog files. > > Release notes for **10.8.0** are recorded in **`CHANGELOG.md`** / **`CHANGELOG.latest.md`** (workflows list API, paywall offering context on custom events, multipage paywall seam fix, rules-engine/JSON Logic work, etc.). **Docs publishing** now targets **`10.8.0`** on S3, and **`docs/index.html`** redirects to the new doc URL. > > There are **no functional code changes** in this diff beyond version strings and release metadata. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c3048b8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves SDK-4332
Motivation
Implements
in,cat,substr,merge, andmissing_somefor v1 targeting predicates. Android behavior and tests match iOS (#6793) and the json-logic-js spec.Summary
StringArrayOperatorscoversin,cat,substr, andmergeusing sharedjsStringandOperators.clampedIntAccessorOperators.opMissingSomereuses dot-path lookup frommissingStringArrayOperatorsTestcases mirror iOS; 8missing_someaccessor tests plus 2 evaluator integration testsNotes
substrslices by Unicode code points (differs from JS UTF-16 only for surrogate pairs)Note
Medium Risk
Changes rule-evaluation semantics for new operators (including edge cases like empty
inhaystacks andsubstrindexing); well-tested and scoped to the rules engine, not auth or payments.Overview
Adds JSON Logic string/array operators and
missing_someso v1 targeting predicates can express membership, concatenation, slicing, array flattening, and “at least N of these fields present” guards.StringArrayOperatorsimplementsin(substring on strings with empty-haystack false; strict===on arrays),cat,substr(Unicode code points viaclampedInt; documented divergence from JS UTF-16 for surrogates), andmerge(one-level flatten).Operators.dispatchroutes these plusmissing_some, which reusesmissingdot-path semantics and compares present count to a threshold via newjsToNumber(unparseable thresholds → never satisfied).jsArrayElementStringis exposed forcat; shared coercion stays on existingjsString/strictEqhelpers.Coverage: large
StringArrayOperatorsTest,missing_someaccessor tests, and evaluator integration forinandmissing_someinsideif.Reviewed by Cursor Bugbot for commit 3056262. Bugbot is set up for automated code reviews on this repo. Configure here.