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§
Sourcefn requires(&self) -> &'static [Port]
fn requires(&self) -> &'static [Port]
Ports the module cannot run without; missing = build error.
Sourcefn migrations(&self) -> Migrations
fn migrations(&self) -> Migrations
The module’s migrations, embedded per dialect.
Sourcefn validate_config(&self, cfg: &dyn Config) -> Result<(), ConfigError>
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.
Sourcefn router(&self, ctx: ModuleContext) -> Router
fn router(&self, ctx: ModuleContext) -> Router
The module’s router, nested under /v1/<name>.
Provided Methods§
Sourcefn harness_api(&self) -> u32
fn harness_api(&self) -> u32
Contract version, checked by Harness::build.
Sourcefn depends_on(&self) -> &'static [&'static str]
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.
Sourcefn tables(&self) -> &'static [&'static str]
fn tables(&self) -> &'static [&'static str]
Table names this module owns; duplicates across modules are a build error.
Sourcefn personal_data(&self) -> &'static [PersonalDataSet]
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.
Sourcefn emits(&self) -> &'static [&'static str]
fn emits(&self) -> &'static [&'static str]
Event names this module emits ("<module>.<event>"), listed by
/__health.
Sourcefn public_writes(&self) -> bool
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.
Sourcefn public_write_policy(&self) -> RoutePolicy
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.
Sourcefn signature_verification(&self) -> SignatureVerification
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 withPayments::verify_webhookplus the Inbox dedup ledger, and production requires an effectivePaymentsport. Every module built before this existed gets this, unchanged.SignatureVerification::Hmac: the handler proves deliveries with the corewebhook_signaturescheme (Svix, Stripe-style, or a provider-specific layout), keyed by the module-scoped config secret named here — aModuleConfigsuffix, so"WEBHOOK_SECRET"on moduleposis read fromPOS_WEBHOOK_SECRET. Production requires that key and noPaymentsport.
Sourcefn max_body_bytes(&self, _cfg: &dyn Config) -> usize
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.
Sourcefn streaming_routes(&self) -> &'static [StreamRoute]
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.
Sourcefn self_check(&self) -> Vec<String>
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.
Sourcefn well_known(&self) -> Option<Router>
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.
Sourcefn surface(&self) -> Surface
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.
Sourcefn events(&self) -> Vec<(EventName, EventHandler)>
fn events(&self) -> Vec<(EventName, EventHandler)>
Handlers for events other modules emit; registered at
Harness::build.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".