Add :rules-engine skeleton module#3478
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>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3478 +/- ##
=======================================
Coverage 79.89% 79.89%
=======================================
Files 369 369
Lines 14871 14871
Branches 2048 2048
=======================================
Hits 11881 11881
Misses 2157 2157
Partials 833 833 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- 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>
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>
`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>
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>
Removes the trailing block describing the publishing/distribution and no-public-APIs enforcement follow-ups. The comment isn't load-bearing — the PR descriptions and split branches already make this clear, so it just adds noise to the build script. Co-authored-by: Cursor <cursoragent@cursor.com>
tonidero
left a comment
There was a problem hiding this comment.
It's good in general! Just a thought about whether we need the new annotation... Lmk what you think!
| * API. Annotate every new public declaration in this module with | ||
| * [InternalRulesEngineAPI]. | ||
| */ | ||
| @InternalRulesEngineAPI |
There was a problem hiding this comment.
Hmm do we need this annotation? I guess someone could depend on the rules engine module directly, but then, I think it's safe to say they are shooting themselves in the foot. We could name the artifact something like rules-engine-internal or something to make it clearer.
I'm just a bit reticent to have to add this to all API, since it could get annoying/noisy. And as long as we use this module as implementation and not api, these APIs shouldn't be exposed to our customers.
There was a problem hiding this comment.
100%. We don't have to, really. Definitely, it would be simpler for us not to have to deal with this annotation at all.
And as long as we use this module as
implementationand notapi
This, I think we should enforce somehow, to avoid using some of these as api by mistake. WDYT?
There was a problem hiding this comment.
Hmm indeed... not sure of ways to enforce this, other than by using some danger rule or something like that... But yeah, I would say not super critical. I think we should always be very careful with anything we use api with (since it ends up being public API.
There was a problem hiding this comment.
True, true. In iOS, I did come up with a very simple lint rule that indirectly prevents us from doing it (RevenueCat/purchases-ios#6788). But I agree, it's not super critical and not a blocker at all 👍
Mirrors the iOS counterpart shift on PR #6787 (commit `931f36373`): the per-declaration opt-in marker is dropped in favor of a structural guarantee. On Android the structural guarantee was already in place — `:rules-engine` is consumed by `:purchases` / `:ui:revenuecatui` as an `implementation` dependency, which keeps every symbol off the SDK's transitive compile classpath, so third-party consumers never see the declarations regardless of their visibility modifier. The annotation was therefore signaling intent rather than enforcing it. Removing it cleans up: - `InternalRulesEngineAPI.kt` (the `@RequiresOptIn` annotation) is deleted outright. - `RulesEngine` becomes a plain `public object`. Doc comment trimmed to point readers at the structural-via-`implementation` guarantee rather than at the now-gone annotation. - `RulesEngineTest` drops its `@OptIn(InternalRulesEngineAPI::class)`. - `rules-engine/build.gradle.kts` no longer needs the `metalava` block. With no annotation to hide, the convention plugin's defaults produce a committed `rules-engine/api.txt` at the module root with `enforceCheck` on — same shape as `:feature:amazon` and the rest of the published modules. This also removes the `build/api-dump.txt` spot-check redirection. - `rules-engine/api.txt` baseline is committed for the first time. Skeleton surface is exactly `RulesEngine` (the namespace object). The follow-up commit on PR #3480 will drop `scripts/check-rules-engine-internal-only.sh` (its premise — "api dump must contain only the annotation declaration" — no longer holds) and the corresponding CircleCI step. PR #3480 itself ends up empty as a result; iOS `4c32721b8` made the same call and rescoped its enforcement PR. No platform-specific Detekt equivalent of iOS's `no_leaking_rules_engine_import` SwiftLint rule is added at this time. Per-module `implementation` dependencies plus Kotlin's `internal` visibility on every non-namespace declaration cover the intended surface for now. Verified: `:rules-engine:testDefaultsDebugUnitTest`, `detektAll`, `metalavaGenerateSignatureDefaultsRelease`, and `metalavaCheckDefaultsRelease` all green. Co-authored-by: Cursor <cursoragent@cursor.com>
Checklist
purchases-iosand hybridsMotivation
Plumbing for an internal
:rules-enginemodule so the upcoming JSON Logic engine can stack on top without coupling to:purchasesor:ui:revenuecatui. Sibling iOS PR: RevenueCat/purchases-ios#6787.Description
RulesEngine+ smoke test, single-flavor (nobc7/bc8— rules eval has no Billing Client dep), wired intosettings.gradle.kts, no consumer yet.mavenPublishing,:bom,ConfigureConditionalPublishingrevert,dokkaHtmlPartial) deferred to a separate "flip the switch" PR — Central versions are immutable, so we avoid permanently shipping an empty artifact.Note
Low Risk
Low risk: adds a new placeholder module and a build-time publishing exclusion, with no runtime behavior changes and no external API surface intended for consumers.
Overview
Introduces a new
:rules-enginemodule as a skeleton Android library, including a placeholderRulesEnginenamespace, API signature stub, and a smoke unit test.Wires the module into the Gradle build via
settings.gradle.kts, and updatesconfigureConditionalPublishingto skip applying the Maven publish plugin for:rules-engineso no empty artifact is published to Maven Central yet.Reviewed by Cursor Bugbot for commit 1291337. Bugbot is set up for automated code reviews on this repo. Configure here.