Skip to main content

Plugin

Trait Plugin 

Source
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 match Django’s AppConfig ergonomics: any reasonable Rust struct meets them by default.

Required Methods§

Source

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

A stable identifier. Used as the key in the migration tracking table, in dependency lists, and as the directory name under migrations/. Plugin names live in the same namespace as migrate::APP_PLUGIN_NAME ("app"), so user crates must not pick the name "app".

Provided Methods§

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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 answer to Django’s DATABASE_ROUTERS. 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.

Source

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. This matches Django’s APP_DIRS loader semantics.

Default: no directories. A plugin that renders no HTML leaves this alone.

Source

fn template_registrars(&self) -> Vec<TemplateRegistrar>

Custom template tags / filters this plugin contributes (feature #67 — Django’s {% load %}-able template 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.

Source

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.

Source

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.

Source

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-admin ships 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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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".

Implementors§