Skip to content

Add JSON Logic comparison operators (<, <=, >, >=)#3484

Merged
ajpallares merged 98 commits into
mainfrom
pallares/json-logic-comparison-operators
May 29, 2026
Merged

Add JSON Logic comparison operators (<, <=, >, >=)#3484
ajpallares merged 98 commits into
mainfrom
pallares/json-logic-comparison-operators

Conversation

@ajpallares

@ajpallares ajpallares commented May 14, 2026

Copy link
Copy Markdown
Member

Resolves SDK-4328

Motivation

Adds the JSON Logic comparison operators to express numeric thresholds and ranges.
iOS counterpart: RevenueCat/purchases-ios#6792

Summary

  • Implements <, <=, >, >= per the JSON Logic spec.
  • < / <= accept the 3-arg between form ({"<=": [1, x, 10]} reads as 1 <= x <= 10); > / >= are binary only, matching the JS reference.
  • Two-string operands compare lexicographically; mixed types coerce through Value.toNumberOrNull and compare as Double (any comparison against NaN is false, so a malformed operand fails closed).

Made with Cursor


Note

Medium Risk
New predicate surface for targeting rules; semantics are subtle (lex vs numeric, NaN) but heavily tested and aligned with json-logic-js.

Overview
Adds JSON Logic comparison operators (<, <=, >, >=) to the rules engine so predicates can express thresholds and ranges (e.g. launch count ≥ 3).

A new ComparisonOperators module implements spec-aligned behavior: two strings compare lexicographically; mixed types use numeric coercion via toNumberOrNull, with NaN comparisons always false. < and <= support the 3-argument “between” form (a < b < c); > and >= stay binary only, matching json-logic-js. Arrays/objects are stringified for comparison when needed.

Operators.dispatch routes the four operator names to this module. Unit tests cover edge cases (lex vs numeric, missing args, extra args dropped) plus evaluator integration tests with var and between-form predicates.

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

ajpallares and others added 15 commits May 14, 2026 10:21
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>
…merge)

Co-authored-by: Cursor <cursoragent@cursor.com>
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>
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>
@ajpallares ajpallares added the pr:feat A new feature label May 14, 2026
ajpallares and others added 10 commits May 14, 2026 18:28
- 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
@ajpallares ajpallares added pr:other and removed pr:feat A new feature labels May 15, 2026
ajpallares and others added 2 commits May 15, 2026 20:00
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>
`aed6e2f88` committed an `api.txt` for parity with other public-library
convention-plugin modules, but the immediate follow-up #3480
(no-public-apis enforcement) replaces that baseline with an explicit
annotate-or-fail check that doesn't keep a file in source. Committing
the baseline here just to delete it there is churn, so:

- Drop `rules-engine/api.txt`.
- Set `enforceCheck = false` so the standard `check` task no longer runs
  `metalavaCheckCompatibility*` against a missing baseline.
- Redirect `metalavaGenerateSignature*` output to `build/api-dump.txt`
  so local spot-checks don't drop a file at the module root.
- Keep `hiddenAnnotations.add(...InternalRulesEngineAPI)` so the
  generated dump filters gated symbols.
- Update the trailing comment to call out both follow-up PRs
  (publishing/distribution + #3480 enforcement).

Co-authored-by: Cursor <cursoragent@cursor.com>
Cover null coercing to zero, null vs NaN operands, and null in the
between form alongside the existing non-numeric NaN cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
ajpallares and others added 6 commits May 28, 2026 14:25
Cover missing operands and extra args on <= (like <) and >= (like >).

Co-authored-by: Cursor <cursoragent@cursor.com>
Describe omitted arguments as null → NaN → false instead of
undefined/Number(undefined) jargon.

Co-authored-by: Cursor <cursoragent@cursor.com>
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>
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.91%. Comparing base (1b2b0e1) to head (c74d95c).

Additional details and impacted files
@@                            Coverage Diff                            @@
##           pallares/json-logic-arithmetic-operators    #3484   +/-   ##
=========================================================================
  Coverage                                     79.91%   79.91%           
=========================================================================
  Files                                           369      369           
  Lines                                         14934    14934           
  Branches                                       2058     2058           
=========================================================================
  Hits                                          11934    11934           
  Misses                                         2163     2163           
  Partials                                        837      837           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ajpallares ajpallares marked this pull request as ready for review May 28, 2026 15:47
@ajpallares ajpallares requested a review from a team as a code owner May 28, 2026 15:47

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a comment about changing the tests to make them easier to read/add and extract to share with other platforms... But not a blocker I would say.

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class ComparisonOperatorsTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have you thought about writing all these as parameterized tests? IMO, it might be cleaner, and also easier to share later on with other tests once we extract these accross platforms.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes and no.

So, we do intend to have a unified set of tests driven by a conformance fixtures file in khepri (WIP RevenueCat/purchases-ios#6822). So, for now, I'm thinking of these tests as documentation (and validation) of how the Rules Engine should behave.

That said, I agree that they could be rewritten as parameterized tests that are easier to read + understand and easier to share with the iOS counterpart if we want to.

That would still mean having the same redundancy we have now (local tests + khepri's fixtures) but we can iterate on that.

For now, I like the idea of parameterizing these so that we can more easily share the file with iOS, even if initially the file is duplicated in both repos. Thank you for the suggestion! I will tackle that in a follow up PR 👌

ajpallares and others added 3 commits May 29, 2026 11:37
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>
Base automatically changed from pallares/json-logic-arithmetic-operators to main May 29, 2026 10:23
ajpallares and others added 2 commits May 29, 2026 12:29
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>
@ajpallares ajpallares enabled auto-merge May 29, 2026 10:31
@ajpallares ajpallares disabled auto-merge May 29, 2026 10:43
ajpallares and others added 2 commits May 29, 2026 12:48
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>
@ajpallares ajpallares enabled auto-merge May 29, 2026 11:55
@ajpallares ajpallares added this pull request to the merge queue May 29, 2026
Merged via the queue into main with commit e401d47 May 29, 2026
34 of 35 checks passed
@ajpallares ajpallares deleted the pallares/json-logic-comparison-operators branch May 29, 2026 12:32
matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
Resolves SDK-4332

## Motivation

Implements `in`, `cat`, `substr`, `merge`, and `missing_some` for v1
targeting predicates. Android behavior and tests match iOS (#6793) and
the json-logic-js spec.

## Summary

- `StringArrayOperators` covers `in`, `cat`, `substr`, and `merge` using
shared `jsString` and `Operators.clampedInt`
- `AccessorOperators.opMissingSome` reuses dot-path lookup from
`missing`
- 34 `StringArrayOperatorsTest` cases mirror iOS; 8 `missing_some`
accessor tests plus 2 evaluator integration tests

## Notes

- `substr` slices by Unicode code points (differs from JS UTF-16 only
for surrogate pairs)
- Stacked on RevenueCat#3484

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes rule-evaluation semantics for new operators (including edge
cases like empty `in` haystacks and `substr` indexing); well-tested and
scoped to the rules engine, not auth or payments.
> 
> **Overview**
> Adds JSON Logic **string/array** operators and **`missing_some`** so
v1 targeting predicates can express membership, concatenation, slicing,
array flattening, and “at least *N* of these fields present” guards.
> 
> **`StringArrayOperators`** implements **`in`** (substring on strings
with empty-haystack false; strict `===` on arrays), **`cat`**,
**`substr`** (Unicode code points via `clampedInt`; documented
divergence from JS UTF-16 for surrogates), and **`merge`** (one-level
flatten). **`Operators.dispatch`** routes these plus **`missing_some`**,
which reuses **`missing`** dot-path semantics and compares present count
to a threshold via new **`jsToNumber`** (unparseable thresholds → never
satisfied). **`jsArrayElementString`** is exposed for **`cat`**; shared
coercion stays on existing **`jsString`** / **`strictEq`** helpers.
> 
> Coverage: large **`StringArrayOperatorsTest`**, **`missing_some`**
accessor tests, and evaluator integration for **`in`** and
**`missing_some`** inside **`if`**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3056262. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
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 -->
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