Reventless PPX Guide
The Reventless PPX eliminates boilerplate from application code. Instead of manually declaring let name, module Id, let moduleUrl, and @s.matches(DcbTag.string), you add a single annotation and the PPX injects everything at compile time.
Setup
Add the PPX to your package's rescript.json. It must come before sury-ppx:
{
"ppx-flags": ["@reventlessdev/reventless-ppx/bin", "sury-ppx/bin"]
}
The PPX reads package.json (for the npm package name) and rescript.json (for namespace and dependencies) from the nearest parent directory. Both files must exist.
Annotations
@@reventless.spec
Use on all spec files: aggregate specs, read model specs, extension point specs, DCB slice specs, event mapping specs, and side effect specs.
What it injects:
| Binding | Condition | Value |
|---|---|---|
let name | Not already declared | Derived from filename |
module Id | Not already declared, and reventless-spec is a dependency | Reventless.Id.String |
let moduleUrl | Not already declared | Computed npm specifier |
open Reventless.ReadModel + let config + let subIdConfig | Filename contains ReadModel, @schema type state present, let config not declared | ReadModel defaults |
Name derivation strips known component suffixes from the filename:
| Filename | Derived name |
|---|---|
Category.res | "Category" |
ProductsReadModel.res | "Products" |
AddCategory.res | "AddCategory" |
CategoriesView.res | "Categories" |
ProductsExtensionPoint.res | "Products" |
ProductBehavior.res | "Product" |
Stripped suffixes: ExtensionPointMapping, ExtensionPoint, ReadModel, Behavior, Projections, Projection, Aggregate, Plugin, Slice, Spec, View.
Dotted names in spec packages: When the rescript.json namespace ends in Spec (e.g., CatalogSpec), the PPX automatically prefixes the derived name with the plugin name:
| Filename | Namespace | Derived name |
|---|---|---|
ProductsExtensionPoint.res | CatalogSpec | "Catalog.Products" |
OrdersExtensionPoint.res | OrderingSpec | "Ordering.Orders" |
The plugin name is the namespace with Spec stripped.
Explicit name override:
@@reventless.spec("CustomName")
Use this when the derived name doesn't match your intent. The PPX still injects module Id and moduleUrl.
module Id is skipped when reventless-spec is not in the package's rescript.json dependencies. This allows lightweight spec packages (extension point specs) to use @@reventless.spec without depending on the full framework.
@@reventless.behavior
Use on all behavior files.
What it injects:
| Binding | Condition | Value |
|---|---|---|
open Spec | Not already present | Opens the spec module |
module Spec = Spec | Not already declared | Aliases the spec module |
let moduleUrl | Not already declared | Computed npm specifier |
Spec module derivation: strips Behavior from the filename.
| Filename | Derived spec |
|---|---|
CategoryBehavior.res | open Category; module Spec = Category |
OrderBehavior.res | open Order; module Spec = Order |
ProductDemandBehavior.res | open ProductDemand; module Spec = ProductDemand |
Explicit spec override:
@@reventless.behavior(PluginSpec)
Use this when the spec module name doesn't match {Filename minus Behavior} — for example when the spec file is named differently than the behavior file's prefix.
@@reventless.dcbTags
Use on DCB slice files outside *Slice/ folders that have entity ID fields in @schema types. Files inside any *Slice/ folder (StateChangeSlice, StateViewSlice, AutomationSlice, InboundTranslationSlice, OutboundTranslationSlice) get dcbTags automatically via @@reventless.spec — no explicit @@reventless.dcbTags needed.
What it does: Scans all @schema-annotated variant types and injects @s.matches(Reventless.DcbTag.string) on fields that match these rules (unless @s.matches(...) is already present):
| Field pattern | Type | Injection |
|---|---|---|
*Id: string | scalar | @s.matches(DcbTag.string) on the type |
*Id: array<string> | array (singular name) | @s.matches(DcbTag.string) on the element type — for cross-entity queries |
*Ids: array<string> | array (plural name) | @s.matches(DcbTag.string) on the element type — for multi-value storage |
The PPX generates the fully qualified Reventless.DcbTag.string, so no open Reventless is needed just for DCB tags.
Before (manual):
@schema
type command = AddProduct({
productId: @s.matches(DcbTag.string) string,
name: string,
})
@schema
type event = ProductAdded({
productId: @s.matches(DcbTag.string) string,
name: string,
})
After (with PPX):
@@reventless.dcbTags
@schema
type command = AddProduct({
productId: string,
name: string,
})
@schema
type event = ProductAdded({
productId: string,
name: string,
})
Combine with @@reventless.spec: Most DCB files outside slice folders use both annotations:
@@reventless.spec
@@reventless.dcbTags