modo-rs 0.8.0

Rust web framework for small monolithic apps
Documentation
//! # modo::tenant
//!
//! Multi-tenant request routing.
//!
//! A two-step pipeline resolves a tenant on every request: a [`TenantStrategy`]
//! extracts a raw [`TenantId`] from `http::request::Parts`, and a
//! [`TenantResolver`] maps that identifier to an app-defined tenant type. The
//! resolved tenant is inserted into request extensions and surfaced to handlers
//! via the [`Tenant<T>`] axum extractor.
//!
//! Provides:
//! - [`TenantId`] — raw identifier extracted from the request (`Slug`, `Domain`, `Id`, `ApiKey`)
//! - [`TenantStrategy`] — trait for extracting a [`TenantId`] from request parts
//! - [`TenantResolver`] — trait for mapping a [`TenantId`] to an app-defined tenant type (not object-safe; uses RPITIT)
//! - [`HasTenantId`] — required bound on the resolved tenant; provides the tracing field value
//! - [`Tenant<T>`] — axum extractor for the resolved tenant
//! - [`TenantLayer`] — Tower layer produced by [`middleware()`]
//! - [`TenantMiddleware`] — Tower service that resolves the tenant on every request
//! - [`middleware()`] — primary entry point that builds a [`TenantLayer`] from a strategy and resolver
//!
//! The [`domain`] submodule provides [`DomainService`](domain::DomainService)
//! for registering and DNS-verifying custom per-tenant domains.
//!
//! # How it works
//!
//! 1. A [`TenantStrategy`] extracts a [`TenantId`] from `http::request::Parts`.
//! 2. A [`TenantResolver`] maps that `TenantId` to the app's concrete tenant type.
//! 3. [`middleware()`] combines the two into a Tower [`TenantLayer`] that inserts the
//!    resolved tenant into request extensions and records `tenant_id` in the
//!    current tracing span.
//! 4. Handler functions use the [`Tenant<T>`] extractor to access the resolved value.
//!
//! # Strategies
//!
//! | Constructor | Struct | Produces |
//! |---|---|---|
//! | [`subdomain()`] | [`SubdomainStrategy`] | `TenantId::Slug` |
//! | [`domain()`] | [`DomainStrategy`] | `TenantId::Domain` |
//! | [`subdomain_or_domain()`] | [`SubdomainOrDomainStrategy`] | `TenantId::Slug` or `TenantId::Domain` |
//! | [`header()`] | [`HeaderStrategy`] | `TenantId::Id` |
//! | [`api_key_header()`] | [`ApiKeyHeaderStrategy`] | `TenantId::ApiKey` (redacted in `Display`/`Debug`) |
//! | [`path_prefix()`] | [`PathPrefixStrategy`] | `TenantId::Slug` (rewrites URI) |
//! | [`path_param()`] | [`PathParamStrategy`] | `TenantId::Slug` (requires `.route_layer()`) |
//!
//! # Quick start
//!
//! ```rust,ignore
//! use modo::tenant::{HasTenantId, Tenant, TenantId, TenantResolver, middleware, subdomain};
//!
//! #[derive(Clone)]
//! struct MyTenant { id: String }
//!
//! impl HasTenantId for MyTenant {
//!     fn tenant_id(&self) -> &str { &self.id }
//! }
//!
//! struct MyResolver;
//! impl TenantResolver for MyResolver {
//!     type Tenant = MyTenant;
//!     async fn resolve(&self, id: &TenantId) -> modo::Result<MyTenant> {
//!         Ok(MyTenant { id: id.as_str().to_string() })
//!     }
//! }
//!
//! let app = axum::Router::new()
//!     .route("/dashboard", axum::routing::get(|t: Tenant<MyTenant>| async move {
//!         format!("tenant: {}", t.id)
//!     }))
//!     .layer(middleware(subdomain("example.com"), MyResolver));
//! ```

pub mod domain;

mod extractor;
mod id;
mod middleware;
mod strategy;
mod traits;

pub use extractor::Tenant;
pub use id::TenantId;
pub use middleware::{TenantLayer, TenantMiddleware, middleware};
pub use strategy::{
    ApiKeyHeaderStrategy, DomainStrategy, HeaderStrategy, PathParamStrategy, PathPrefixStrategy,
    SubdomainOrDomainStrategy, SubdomainStrategy, api_key_header, domain, header, path_param,
    path_prefix, subdomain, subdomain_or_domain,
};
pub use traits::{HasTenantId, TenantResolver, TenantStrategy};