Rule Cascade
ReferenceExample READMEs

Spring Boot example

A small payments API that enforces examples/contracts/payments-transfer.ruleset.yaml with the Java runtime.

A small payments API that enforces examples/contracts/payments-transfer.ruleset.yaml with the Java runtime.

FileWhat it shows
RuleCascadeConfigurationLoad the ruleset once at startup, either compiled from the YAML contracts or read from a precompiled bundle; a bad ruleset or a missing custom operator stops the application
CustomOperatorsCustom operators (x-*) the service supplies to its rulesets
TransferControllerThe four steps on every operation: evaluate, refuse on deny, persist, run commands
RuleCascadeControllerServe the client-safe manifest with an ETag, and a dry-run evaluation endpoint
ApiExceptionHandlerReturn a denied evaluation as RFC 9457 problem details (422)
TransferApiTestThe behaviour above, end to end through MockMvc, with the ruleset compiled from source
BundleStartupTestThe same service started from a bundle
CustomOperatorTestA ruleset that needs a custom operator: with it registered, and failing closed without it

Run it

Not yet run where this repository was assembled (Maven Central was unreachable there); CI builds and tests it.

mvn -f ../../packages/java/pom.xml install     # puts rule-cascade-core in your local Maven repository
mvn spring-boot:run
# Allowed, with a non-blocking warning about the missing memo
curl -s localhost:8080/transfers -H 'Content-Type: application/json' -d '{
  "transfer": {"type": "domestic", "amount": 120.50, "currency": "USD",
               "beneficiary": {"name": "Jo Lee", "country": "US"}}}'

# Denied by a server-only rule the browser never sees
curl -s localhost:8080/transfers -H 'Content-Type: application/json' -d '{
  "transfer": {"type": "international", "amount": 500, "currency": "USD", "memo": "gift",
               "beneficiary": {"name": "X", "country": "KP", "swiftCode": "ABCDKPPY"}}}'

# Over the limit: only a risk officer can accept the risk, with a justification
curl -s localhost:8080/transfers -H 'Content-Type: application/json' \
  -H 'X-Actor-Id: u-9' -H 'X-Actor-Roles: risk-officer' -d '{
  "transfer": {"type": "domestic", "amount": 30000, "currency": "USD", "memo": "house",
               "beneficiary": {"name": "Sam", "country": "US"}},
  "resolutions": [
    {"rule": "transfer.large.review-warning", "type": "acknowledge"},
    {"rule": "org.transfer.amount-limit", "type": "accept-risk", "justification": "verified source of funds"}]}'

# What the React app fetches
curl -si localhost:8080/rulesets/acme.payments.transfer/manifest | head -20

Source or bundle

By default the service compiles contracts/*.ruleset.yaml from the classpath at startup (RuleSet.load). Set rule-cascade.bundle to start from a compiled bundle instead (RuleSet.fromBundle); the service then needs no YAML and repeats no load-time check:

# from the repository root: compile the contract into a bundle
python tools/rulecheck.py compile examples/contracts/payments-transfer.ruleset.yaml \
  -o /tmp/acme.payments.transfer.bundle.json

# from this directory: start from the bundle
mvn spring-boot:run \
  -Dspring-boot.run.arguments=--rule-cascade.bundle=file:/tmp/acme.payments.transfer.bundle.json

rule-cascade compile (the Go command) writes the same bundle; any compiler does.

A bundle contains the server manifest, so treat it like any other internal artefact: build it in CI, store it immutably, and never serve it to a browser. The tests read the bundles published in conformance/bundles, which are the compiled form of the example contracts.

Custom operators

A ruleset may use operators the host application supplies. They are registered once, in CustomOperators, and attached with RuleSet.withOperators. At startup the configuration asks the ruleset for its missingOperators() and refuses to start when there is one. The payments contract declares no custom operator; CustomOperatorTest evaluates the onboarding catalog, whose loyalty-number rule calls x-luhn.

The actor

The actor comes from two request headers to keep the example short. A real service takes the actor from its security context and never trusts a role a caller claims for itself.

On this page