pub trait Plugin:
Send
+ Sync
+ 'static {
Show 19 methods
// Required method
fn name(&self) -> &'static str;
// Provided methods
fn dependencies(&self) -> &'static [&'static str] { ... }
fn models(&self) -> Vec<ModelMeta> { ... }
fn routes(&self) -> Router { ... }
fn route_paths(&self) -> Vec<RouteSpec> { ... }
fn openapi_paths(&self) -> Vec<(String, Value)> { ... }
fn system_checks(&self) -> Vec<SystemCheck> { ... }
fn provides_storage(&self) -> bool { ... }
fn database(&self) -> Option<&'static str> { ... }
fn templates_dirs(&self) -> Vec<PathBuf> { ... }
fn template_registrars(&self) -> Vec<TemplateRegistrar> ⓘ { ... }
fn wrap_router(&self, router: Router) -> Router { ... }
fn middleware(&self) -> Vec<Arc<dyn Middleware>> { ... }
fn static_files(&self) -> Vec<StaticFile> { ... }
fn static_dirs(&self) -> Vec<StaticDir> { ... }
fn static_root_dirs(&self) -> Vec<PathBuf> { ... }
fn commands(&self) -> Vec<Box<dyn PluginCommand>> { ... }
fn api_endpoints(&self) -> Vec<ApiEndpoint> { ... }
fn on_ready(&self, _ctx: &AppContext) -> Result<(), PluginError> { ... }
}Expand description
The contract every umbral extension implements.
Every method except name() has a default that returns the empty
contribution. A plugin opts in only to what it contributes: a
pure-route plugin overrides routes(); a pure-data plugin
overrides models(); the auth plugin overrides almost all of them.
The trait is Send + Sync + 'static so App::builder() can store a
homogeneous Vec<Box<dyn Plugin>> and the runtime can hand the
plugin reference to threads (e.g. for background tasks spawned in
on_ready). The bounds are deliberately permissive: any
reasonable Rust struct meets them by default.
Required Methods§
Provided Methods§
Sourcefn dependencies(&self) -> &'static [&'static str]
fn dependencies(&self) -> &'static [&'static str]
Names of plugins that must load before this one. The
App::builder() topological sort uses this; cycles surface as
BuildError::PluginCycle. The default is no dependencies.
Sourcefn models(&self) -> Vec<ModelMeta>
fn models(&self) -> Vec<ModelMeta>
The plugin’s models, in declaration order. The M7 migration
engine collects these across every registered plugin and uses
them as the diff target for makemigrations.
Default: no models. A pure-route or pure-middleware plugin leaves this alone.
Sourcefn routes(&self) -> Router
fn routes(&self) -> Router
The plugin’s HTTP routes. Merged into the app router after the
hand-written one passed to AppBuilder::routes(). Plugins
choose their own path prefixes (spec 02 §“What a plugin can
contribute”: routes are flat, not auto-prefixed).
Sourcefn route_paths(&self) -> Vec<RouteSpec>
fn route_paths(&self) -> Vec<RouteSpec>
Declared URL routes this plugin contributes — a companion to
routes used for surfacing route lists outside the request
flow (currently: the dev-mode default 404 page). axum doesn’t
expose its internal route table, so plugins report what they
declare here; the framework treats this as informational only
— not a source of truth for routing.
Each entry carries a path pattern and the HTTP methods it
accepts; the dev-mode 404 page renders method badges so a
developer can tell at a glance which verb to use. Conversions
(see RouteSpec’s From impls) cover the ergonomic shapes:
"/admin/login".into(), ("GET", "/articles").into(),
(&["GET", "POST"][..], "/api/post").into().
Default empty. Mismatch with the real routes() is a stale-
list bug, not a correctness bug.
Sourcefn openapi_paths(&self) -> Vec<(String, Value)>
fn openapi_paths(&self) -> Vec<(String, Value)>
OpenAPI path items the plugin contributes. Returned as a
Vec<(path, value)> where path is the URL template
(/api/auth/login, /api/foo/{id}) and value is the
matching OpenAPI 3.0 Path Item Object serialised as
a serde_json::Value.
umbral-openapi walks every registered plugin’s
contribution at spec-build time and merges them into the
emitted document’s paths object. Closes BUG-20 from
bugs/tests/testBugs.md — auto-generated CRUD routes were
the only thing the spec described before; plugin-
contributed routes (auth, custom actions) were invisible
to Swagger UI.
Plugins that don’t ship OpenAPI documentation leave this
alone. The umbral-openapi plugin’s own routes (the
/openapi.json and Swagger UI mount) are not in the
generated spec — they’re delivery, not API.
Sourcefn system_checks(&self) -> Vec<SystemCheck>
fn system_checks(&self) -> Vec<SystemCheck>
Boot-time checks the plugin needs to pass. Run in phase 4 of
App::build() alongside the framework’s built-in checks.
Severity::Error blocks boot; Severity::Warning logs and
continues.
Sourcefn provides_storage(&self) -> bool
fn provides_storage(&self) -> bool
true if this plugin registers a Storage
backend (e.g. StoragePlugin, which calls
crate::storage::set_storage in Plugin::on_ready).
The boot system check field.storage_backend reads this flag to
decide whether a model that declares a FileField / ImageField
has somewhere to resolve its uploads. It checks the capability
flag rather than the ambient storage_opt() because storage is
registered in on_ready, which runs after the system-check
phase — at check time the ambient backend isn’t published yet, but
the declared capability is knowable from the plugin list. Override
this (return true) in any plugin whose on_ready registers a
backend.
Sourcefn database(&self) -> Option<&'static str>
fn database(&self) -> Option<&'static str>
The database alias every model this plugin contributes should
be read from and written to. Returns None to use the
"default" pool (the same one umbral::db::pool() returns).
This is umbral’s per-plugin database routing hook. The
builder reads it during phase 3 and the QuerySet’s
resolve_pool defers to it when no .on(&pool) override is
set on the chain. Per-plugin granularity (every model the
plugin owns goes to one database) is the v1 shape; per-model
overrides via attribute lands when a real workload needs it.
The named alias must have been registered via
AppBuilder::database(alias, pool) or
Settings.databases[alias] before App::build(). A reference
to an unregistered alias surfaces as
BuildError::PluginDatabaseAlias at boot.
Sourcefn templates_dirs(&self) -> Vec<PathBuf>
fn templates_dirs(&self) -> Vec<PathBuf>
Template directories this plugin contributes.
Each path is added to the global template search list in plugin
registration order. The app-level templates_dir (set via
AppBuilder::templates_dir) is always searched first; plugin
directories follow in topological dependency order so a plugin
with no dependencies appears before its dependents.
When two plugins (or the app directory and a plugin) ship a template with the same name, the first directory in the list wins and a tracing warning is emitted at boot so the collision is visible. First-match-wins across all template directories.
Default: no directories. A plugin that renders no HTML leaves this alone.
Sourcefn template_registrars(&self) -> Vec<TemplateRegistrar> ⓘ
fn template_registrars(&self) -> Vec<TemplateRegistrar> ⓘ
Custom template tags / filters this plugin contributes (feature #67 - a loadable template tag/filter library).
Each returned [TemplateRegistrar] is a closure that mutates the
minijinja Environment at engine-build
time — env.add_filter(...), env.add_function(...),
env.add_global(...). They are collected across all plugins in
topological order and applied after the framework built-ins
(static, media_url, markdown, now, currency, …), so a
plugin may deliberately override a built-in by re-registering the
same name.
The closures must be owned and 'static (no borrow of self) so
the framework can stash them and re-run them on every dev-mode
hot-reload rebuild. Capture any per-plugin config by value.
fn template_registrars(&self) -> Vec<TemplateRegistrar> {
vec![Box::new(|env| {
env.add_filter("shout", |s: String| s.to_uppercase());
})]
}Default: no custom tags. A plugin that ships none leaves this alone.
Sourcefn wrap_router(&self, router: Router) -> Router
fn wrap_router(&self, router: Router) -> Router
Wrap the app router with the plugin’s middleware layers.
Called once per plugin during App::build’s phase 5, in
topological dependency order. The plugin receives the router
after its routes have already been merged in, applies any
.layer(...) calls it needs (tower layers, axum’s middleware
fn helpers, etc.), and returns the wrapped router.
Returning the router shape (instead of a Vec<Layer> like
the spec sketched) sidesteps the trait-object lifetime
problem Layer’s generics produce. Plugins keep full access
to the axum / tower API at the call site.
Default: return the router unchanged. A pure-data plugin (models only) inherits this and never touches the router.
Sourcefn middleware(&self) -> Vec<Arc<dyn Middleware>>
fn middleware(&self) -> Vec<Arc<dyn Middleware>>
Framework-level request/response middleware this plugin contributes (feature #68).
Where wrap_router hands you the raw axum
Router for arbitrary tower Layers, this is the ergonomic
surface: each Middleware gets a
before_request / after_response hook and nothing else to wire.
All plugins’ middleware (plus the app’s) are collected into one
stack and installed as a single layer at App::build, in plugin
topological order — a plugin’s before_request runs after those of
the plugins it depends on, and its after_response runs before
them (onion order).
Reach for wrap_router when you need a real tower Layer (timeouts,
tracing spans, body-limit); reach for this when you just want to
look at the request or response.
Default: no middleware.
Sourcefn static_files(&self) -> Vec<StaticFile>
fn static_files(&self) -> Vec<StaticFile>
Static files the plugin ships baked into its binary.
Each entry produces one GET <url_path> route that returns the
file body with the supplied Content-Type and Cache-Control.
Bodies are &'static [u8] — typically include_bytes! —
because the canonical use is “the binary ships its own CSS / JS
/ fonts.”
Use cases:
umbral-adminships its precompiled Tailwind CSS this way.- A plugin that adds an HTMX page can ship an icon or font.
- User code can register arbitrary embedded assets.
Conflicts across plugins (two plugins claiming the same
url_path) surface as the axum Router::route panic at
App::build time, with the second registrant losing.
Default: no files. Plugins that ship no embedded assets leave this alone.
Sourcefn static_dirs(&self) -> Vec<StaticDir>
fn static_dirs(&self) -> Vec<StaticDir>
On-disk source directories this plugin contributes to the unified static pipeline.
Where static_files bakes assets into the binary (zero-config,
always available), static_dirs declares a filesystem source
the framework’s static handler serves live. Each entry pairs a
namespace (the per-plugin URL/disk segment that prevents
collisions — "admin", "playground") with the absolute
source_dir holding that plugin’s source assets (plugins
typically compute it from env!("CARGO_MANIFEST_DIR")).
At App::build() the framework walks every plugin’s
static_dirs() into a namespace -> source_dir registry and
mounts one handler at the configured static_url (default
/static/). A request /static/<namespace>/<rest> resolves:
- Dev —
<source_dir>/<rest>first (live source serving: drop a rebuilt file and it’s served on the next request), falling back to<static_root>/<namespace>/<rest>when the namespace isn’t registered or the file is missing. - Prod / Test —
<static_root>/<namespace>/<rest>only.
Two plugins declaring the same namespace is a boot-time error
(BuildError::DuplicateStaticNamespace) — collisions fail
loudly, never silently shadow.
Default: no directories. A plugin that ships no filesystem assets leaves this alone.
Sourcefn static_root_dirs(&self) -> Vec<PathBuf>
fn static_root_dirs(&self) -> Vec<PathBuf>
On-disk directories served at the root of static_url — with
no namespace segment.
Where static_dirs serves a plugin’s assets under a namespaced
path (/static/<namespace>/<file>), these directories back the
bare /static/<file> space for app/site-level static (a project’s
own CSS, images, favicon). The framework’s single static handler
resolves a request by trying registered namespaces first, then
these root directories with the full request path.
This is the seam that lets the framework own static_url as a
single mount: a StoragePlugin’s static side pointed at the configured
static_url contributes its directory here instead of nesting its
own (conflicting) catch-all route. A plugin serving its directory
at a different mount returns nothing here and nests as usual.
Default: none.
Sourcefn commands(&self) -> Vec<Box<dyn PluginCommand>>
fn commands(&self) -> Vec<Box<dyn PluginCommand>>
CLI subcommands the plugin contributes.
Each command implements crate::cli::PluginCommand and ships
a clap::Command plus an async run handler. The framework’s
binary (or any user-written one) calls
crate::cli::dispatch with the App’s plugin list to wire
these into a single CLI tree.
Default: no commands. Plugins that only contribute models, routes, or middleware leave this alone.
Sourcefn api_endpoints(&self) -> Vec<ApiEndpoint>
fn api_endpoints(&self) -> Vec<ApiEndpoint>
Callable HTTP endpoints this plugin wants advertised in a machine-readable index (e.g. a REST API root, or a client’s service-discovery fetch).
This is not how a plugin mounts routes — that’s routes.
It’s a declaration of which of those routes are worth surfacing
to an API client, with a human label and a grouping key. The
framework collects every plugin’s list at App::build() into a
global readable via crate::migrate::registered_api_endpoints;
a plugin like umbral-rest reads that global to render an API
root without ever naming the plugins that contributed.
Paths are relative (/oauth/google/login) — the core type stays
origin-agnostic; a consumer joins its own origin when it needs an
absolute URL.
Default: nothing advertised. Plugins that don’t expose a client-facing API leave this alone.
Sourcefn on_ready(&self, _ctx: &AppContext) -> Result<(), PluginError>
fn on_ready(&self, _ctx: &AppContext) -> Result<(), PluginError>
Wire signals, start background work, seal admin registrations.
Called after phase 4 (system checks) passes, in topological
dependency order. Sync, on purpose; spawn async work via
ctx.runtime() when the runtime handle lands.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".