Skip to main content

Module

Trait Module 

Source
pub trait Module:
    Send
    + Sync
    + 'static {
Show 22 methods // Required methods fn name(&self) -> &'static str; fn version(&self) -> &'static str; fn requires(&self) -> &'static [Port]; fn migrations(&self) -> Migrations; fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError>; fn router(&self, ctx: ModuleContext) -> Router; // Provided methods fn harness_api(&self) -> u32 { ... } fn optional(&self) -> &'static [Port] { ... } fn depends_on(&self) -> &'static [&'static str] { ... } fn tables(&self) -> &'static [&'static str] { ... } fn personal_data(&self) -> &'static [PersonalDataSet] { ... } fn emits(&self) -> &'static [&'static str] { ... } fn public_writes(&self) -> bool { ... } fn public_write_policy(&self) -> RoutePolicy { ... } fn signature_verification(&self) -> SignatureVerification { ... } fn max_body_bytes(&self, _cfg: &dyn Config) -> usize { ... } fn streaming_routes(&self) -> &'static [StreamRoute] { ... } fn self_check(&self) -> Vec<String> { ... } fn well_known(&self) -> Option<Router> { ... } fn surface(&self) -> Surface { ... } fn events(&self) -> Vec<(EventName, EventHandler)> { ... } fn scheduled<'a>( &'a self, ctx: &'a ModuleContext, cron: &'a str, ) -> BoxFuture<'a, Result<(), AnyError>> { ... }
}
Expand description

A Factory Zero module. Object-safe; composed as Arc<dyn Module>.

Handlers get the request scope as an axum extractor: async fn join(scope: Scope, State(ctx): State<Arc<ModuleContext>>, ...). There is no ambient “current request” (ADR 0007).

Required Methods§

Source

fn name(&self) -> &'static str

Kebab-case name; mounted at /v1/<name>.

Source

fn version(&self) -> &'static str

env!("CARGO_PKG_VERSION"), surfaced by /__health.

Source

fn requires(&self) -> &'static [Port]

Ports the module cannot run without; missing = build error.

Source

fn migrations(&self) -> Migrations

The module’s migrations, embedded per dialect.

Source

fn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError>

Rejects invalid configuration: missing required keys or malformed values, reported together with the module name.

When it runs. The conformance kit calls it, and a module’s own tests should. It cannot run at Harness::build or in fz doctor, because neither has the deploy config — the values live on the runtime Env and only exist per request. The Cloudflare runtime therefore runs it once at cold start and logs any failure loudly (console_error!, so it reaches Workers Logs); it does not fail the boot, so a misconfigured module still degrades per request (issue #101) rather than taking the whole Worker down. Turning that into a hard boot failure is a deployment decision for an ADR.

§Errors

Err listing every invalid or missing key for this module.

Source

fn router(&self, ctx: ModuleContext) -> Router

The module’s router, nested under /v1/<name>.

Provided Methods§

Source

fn harness_api(&self) -> u32

Contract version, checked by Harness::build.

Source

fn optional(&self) -> &'static [Port]

Ports the module uses when present.

Source

fn depends_on(&self) -> &'static [&'static str]

Modules this one’s schema sits on top of, by name.

A partial order, not a load order for routes: it exists so migrations apply in an order where a foreign key can point at a table another module owns (RECONCILIATION.md §2). Naming a module the venture did not compose is a build error, and so is a cycle — both at HarnessBuilder::build, never at boot, because a boot error is one a deployment discovers in production.

Declaring nothing is the common case and costs nothing: with no dependencies anywhere the resolved order is composition order, unchanged.

Source

fn tables(&self) -> &'static [&'static str]

Table names this module owns; duplicates across modules are a build error.

Source

fn personal_data(&self) -> &'static [PersonalDataSet]

What this module holds about a person, per table.

Read by cratefield-module-privacy to export a subject’s data, to erase it, and to publish the “what is stored” table. Declaring it here rather than in the privacy module is what makes that module reusable: it never names a venture’s tables, so a schema change cannot leave it describing something that no longer exists.

Validated at HarnessBuilder::build: a table this module does not own is an error, and so is a declaration that cannot mean anything — an Anonymise naming no columns, a Retain with no reason, a published description left blank. See PersonalDataSet::none for a table that legitimately holds nothing personal, which says so rather than staying silent.

Defaulting to empty keeps every existing module compiling unchanged, and a module with no tables has nothing to say. A module that owns tables and declares nothing is a different thing, and since issue #244 it fails cratefield_testing::conformance rather than the build: see undeclared_tables for why the kit is the place that refuses it and a running deployment is not.

Source

fn emits(&self) -> &'static [&'static str]

Event names this module emits ("<module>.<event>"), listed by /__health.

Source

fn public_writes(&self) -> bool

Whether the module has public write endpoints.

Advisory since issue #133. The enforcement point is each route’s RoutePolicy on the declared surface, collected by WriteGuards::collect and gated at HarnessBuilder::build. A module that writes publicly but declares no policy on any action counts as a HumanForm writer (the conservative fallback for pre-surface modules); declaring an explicit Open/Signature policy on every action opts out of the fallback.

Source

fn public_write_policy(&self) -> RoutePolicy

What actually protects those public writes, for a module with no ADR 0010 surface to hang a per-action RoutePolicy on (issue #143).

Consulted only when public_writes is true and the surface declares no guarded action, so a module with a surface is unaffected and the conservative default is unchanged: say nothing and you are still a HumanForm writer. The auth login methods are the reason it exists — they are public writers whose proof is a single-use challenge or a signed link, never a CAPTCHA widget, and before this they could only be filed under a gate they can never satisfy.

Source

fn signature_verification(&self) -> SignatureVerification

Which verifier proves this module’s RoutePolicy::Signature deliveries (issue #533): how a webhook handler checks a machine signature before acting.

This is the default for the module’s signature routes. A route that declares its own verifier — .verification(..) on the Action (issue #595) — overrides it, so one module can verify a Stripe webhook through Payments and a RevenueCat webhook with its own HMAC secret.

Consulted only when the module actually has signature-guarded routes — WriteGuards::collect resolves each route’s verifier (its own, else this) and the production gate demands exactly what was declared:

  • SignatureVerification::Payments (the default): the handler proves deliveries with Payments::verify_webhook plus the Inbox dedup ledger, and production requires an effective Payments port. Every module built before this existed gets this, unchanged.
  • SignatureVerification::Hmac: the handler proves deliveries with the core webhook_signature scheme (Svix, Stripe-style, or a provider-specific layout), keyed by the module-scoped config secret named here — a ModuleConfig suffix, so "WEBHOOK_SECRET" on module pos is read from POS_WEBHOOK_SECRET. Production requires that key and no Payments port.
Source

fn max_body_bytes(&self, _cfg: &dyn Config) -> usize

The largest request body any route of this module accepts, so a runtime can refuse an oversize body before buffering it (issue #440).

This is a coarse per-module ceiling, not a per-route limit. A runtime with a fixed memory budget — a Workers isolate, say — reads it through Harness::max_body_bytes when a request arrives and refuses everything above it at the door, because the precise enforcement, axum’s DefaultBodyLimit inside Module::router, only fires once the body is already resident in memory. It must never return less than the harness-wide MAX_BODY_BYTES: the runtime guard sits in front of the router, and a module that tightened it would 413 requests the router itself would have accepted. Raising it is legitimate — LinkedIn’s image upload does — but the route that justifies the raise keeps its own DefaultBodyLimit, which stays the precise per-route enforcer.

cfg is the live deployment config, so a module that moves a cap from configuration must read it here too: if an operator raises MAX_IMAGE_BYTES above the compiled default, the runtime guard must not refuse a body the route would have accepted.

Default: MAX_BODY_BYTES, so a module that says nothing changes nothing.

Source

fn streaming_routes(&self) -> &'static [StreamRoute]

Routes this module serves in streaming mode (issue #585), each with its own StreamRoute::max_bytes ceiling. Opt-in: the default is empty, and every existing route keeps the buffered behaviour it always had.

On a declared route the router reads the body as a stream instead of buffering it: the handler extracts a RequestStream, the route’s max_bytes replaces Module::max_body_bytes for the declared content-length pre-check, and the handler may answer with a ResponseStream. The paths are relative to the module’s /v1/<name> mount and use axum’s pattern syntax (/upload, /files/{id}, /files/{*rest}). A (method, path) that names no declared route is untouched.

Source

fn self_check(&self) -> Vec<String>

Problems the module can find in itself, with no configuration and no environment: its own embedded data, checked against its own declarations.

This is the half of Module::validate_config that fz doctor can actually run. The doctor has no deploy config — those values live on the runtime Env and exist only per request — so it cannot ask a module whether its deployment is valid. It can ask whether what the module compiled in is coherent, and that answer is the same on a laptop, in CI and in production.

The notifications module answers with every translation its catalog is missing (issue #190), so a translation gap is a pull request rather than a person reading a message id on a lock screen.

Each string is one problem, already naming the module. Return identifiers, never values: this is printed by a CLI and copied into issues, so nothing read from the environment, a recipient or a URL may appear in one. Default: nothing, so no existing module changes.

Source

fn well_known(&self) -> Option<Router>

Routes this module serves at the root under /.well-known, for spec-mandated discovery documents (OIDC openid-configuration, jwks.json) that must live outside /v1 (issue #46). Paths are relative to the prefix: register /jwks.json, not /.well-known/jwks.json.

At most one module may provide one: discovery URLs are a singleton namespace, so Harness::build fails (naming every provider) when two modules return a router here. None by default.

Source

fn surface(&self) -> Surface

The module’s UI surface (ADR 0010): the actions a renderer may offer and the views that compose them. Input schemas come from the handler’s own body types (Action::input::<Body>()), so the declaration cannot drift from the route. Harness::build validates it; GET /__surface serves the composition. Default: nothing, and a module that declares nothing renders nothing.

Source

fn events(&self) -> Vec<(EventName, EventHandler)>

Handlers for events other modules emit; registered at Harness::build.

Source

fn scheduled<'a>( &'a self, ctx: &'a ModuleContext, cron: &'a str, ) -> BoxFuture<'a, Result<(), AnyError>>

Scheduled work (cron is the trigger expression). Default: none.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§