//! The axum host adapter for [`quasi_router`].
//!
//!
//!
//! A host adapter is thin by design, and since the Tauri protocol adapter
//! arrived it is thinner still: decoding, status mapping and the retarget
//! header live in [`quasi_http`], which both hosts call. What is left here is
//! the part that is genuinely axum's, which is how it mounts, how it reads a
//! body, and where it puts the blocking hop.
//!
//! Hosted web was described as free rather than aspirational, and this is what
//! that meant: a router returning descriptions plus a renderer emitting HTML is
//! already the shape MNW's server has, so a hosted app is a second adapter and
//! not a port.
//!
//! # What it owns
//!
//! - Mounting, as a fallback. See [`Adapter::into_router`].
//! - Reading the body, with a limit, before anything is parsed.
//! - Resolving the state a request is answered against, for the one host that
//! serves many viewers out of one process. See [`Adapter::per_viewer`].
//! - The blocking hop. The router is sync per decision 6, so the call happens
//! on a blocking thread, which is what an axum handler over a blocking store
//! pays anyway.
//!
//! # What it does not own
//!
//! Markup. See [`Serves`], and the reason it is a parameter: the Tauri
//! custom-protocol adapter serves HTML to a webview too, and generating it here
//! would guarantee a second implementation.
//!
//! # Mounting it
//!
//! quasi matches paths itself, so the adapter mounts as a fallback rather than
//! registering each route with axum. One matcher, not two agreeing ones. Real
//! axum routes merged in front of it keep working, which is how static files,
//! a health endpoint and anything else outside the description layer are
//! served.
//!
//! ```no_run
//! use std::sync::Arc;
//! use quasi_axum::{Adapter, Serves};
//! use quasi_router::{Node, Request, Response, RouteError, Router, Screen, Slot, RegionKind};
//!
//! struct App;
//! struct Html;
//!
//! impl Serves for Html {
//! fn screen(&self, screen: &Screen) -> String { format!("
{}
", screen.title) }
//! fn fragment(&self, _node: &Node) -> String { String::new() }
//! fn suggestions(&self, _: &str, _: &[quasi_router::Candidate]) -> String { String::new() }
//! }
//!
//! fn home(_app: &App, _request: Request) -> Result {
//! Ok(Screen::sidebar_content("Home")
//! .with(Slot::new("content", RegionKind::Pane))
//! .into())
//! }
//!
//! # async fn run() {
//! let quasi = Router::::new().get("/", home);
//! let app = axum::Router::new()
//! .merge(Adapter::new(quasi, Arc::new(App), Arc::new(Html)).into_router());
//!
//! let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
//! axum::serve(listener, app).await.unwrap();
//! # }
//! ```
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use axum::body::{Body, Bytes};
use axum::extract::Request;
use axum::response::Response as HttpResponse;
use http::request::Parts;
use quasi_http::Refusal;
use quasi_router::{Params, Response, RouteError, Router};
pub use quasi_http::{DEFAULT_BODY_LIMIT, Serves, htmx};
/// The state one request is answered against, once it is resolved.
///
/// A future, because what a viewer is resolved from is a lookup: a session
/// cookie against a store, a token against an introspection endpoint. That is
/// async work, and it happens here rather than in a handler because a handler
/// is sync by decision 6 and this is the one layer that can await.
///
/// `Err` is the resolution itself failing, not the viewer being anonymous. A
/// signed-out reader is a state the factory builds; a store that would not
/// answer is a [`RouteError`], and the request never reaches the router.
pub type StateFuture = Pin> + Send>>;
/// Builds the state one request is answered against.
///
/// The returned future is `'static`, so a factory takes what it needs out of
/// the request parts first and the future owns it. That is the discipline
/// rather than a limitation: a future holding a borrow on the request head
/// would be a second lifetime running through every host that mounts this.
pub type StateFactory = dyn Fn(&Parts) -> StateFuture + Send + Sync;
/// Builds the renderer for one request.
///
/// It is handed the state, the request's parameters and the answer the router
/// gave, and returns the renderer that answer is rendered with. The
/// [`Response`] is there so the factory can walk the screen and do work only
/// for the regions that are actually on it: a factory that highlighted a blob
/// without looking would run syntect for every request that never shows a file.
///
/// `None` is the router refusing. There is no screen, the answer is a notice
/// fragment, and a renderer built for it has nothing per-request to say.
pub type RenderFactory = dyn Fn(&S, &Params, Option<&Response>) -> R + Send + Sync;
/// Where the renderer for a request comes from.
///
/// A host with nothing per-request to say shares one. A host serving a public
/// website builds one per request, because that is the only moment it knows
/// what the page contains. See [`Adapter::per_request`].
enum Renderers {
Shared(Arc),
PerRequest(Box>),
}
/// Where the state for a request comes from.
///
/// `Handler = fn(&S, Request)` builds `S` once and reads it for the life of
/// the program, which is correct for the hosts quasi was designed against: a
/// Tauri window, an egui frame and a terminal loop each have one user, so one
/// of everything is the truth. A hosted server is the one host that breaks
/// that assumption, because it has one process and many viewers.
///
/// So the adapter builds `S` per request, and nothing above it moves: the
/// router, the handler signature and the description layer are untouched. See
/// [`Adapter::per_viewer`].
enum States {
Shared(Arc),
PerRequest(Box>),
}
/// The router, the app's state and a renderer, mounted as an axum service.
pub struct Adapter {
router: Router,
state: States,
render: Renderers,
body_limit: usize,
}
/// What a request needs, once, behind one `Arc`.
struct Context {
router: Router,
state: States,
render: Renderers,
body_limit: usize,
}
impl Adapter
where
S: Send + Sync + 'static,
R: Serves,
{
/// Mount a router over this state, rendered by this renderer.
///
/// One renderer, shared by every request. Right for a host whose renderer
/// is configuration: where the assets live and what the classes are called
/// do not vary by request.
#[must_use]
pub fn new(router: Router, state: Arc, render: Arc) -> Self {
Self {
router,
state: States::Shared(state),
render: Renderers::Shared(render),
body_limit: DEFAULT_BODY_LIMIT,
}
}
/// Mount a router over this state, building a renderer per request.
///
/// For the host that has something to say about this page and not about
/// the next one: the markup filling a bespoke region, a per-screen head, a
/// theme block computed from the signed-in reader. A renderer is
/// configuration and an allocation, so building one per request is cheap;
/// what it buys is that the host gets its say at the one moment it knows
/// what the answer contains.
///
/// This is the only channel for it, and deliberately. A handler is
/// `fn(&S, Request) -> Result` with no request object
/// and no side channel, so anything it computed could only ride in the
/// [`Response`], and host markup in the router's response type would make
/// the router host-aware. So the factory does the work and the handler
/// stops doing it.
///
/// ```no_run
/// # use std::sync::Arc;
/// # use quasi_axum::Adapter;
/// # use quasi_router::{Outcome, Router};
/// # struct App;
/// # #[derive(Default)] struct Html { fills: std::collections::HashMap }
/// # impl quasi_axum::Serves for Html {
/// # fn screen(&self, _: &quasi_router::Screen) -> String { String::new() }
/// # fn fragment(&self, _: &quasi_router::Node) -> String { String::new() }
/// # fn suggestions(&self, _: &str, _: &[quasi_router::Candidate]) -> String { String::new() }
/// # }
/// # let quasi = Router::::new();
/// Adapter::per_request(quasi, Arc::new(App), |_app, _params, answer| {
/// let mut render = Html::default();
/// if let Some(Outcome::Screen(screen)) = answer.map(|a| &a.outcome) {
/// for slot in &screen.slots {
/// // Only the regions this screen actually has.
/// render.fills.insert(slot.id.clone(), String::new());
/// }
/// }
/// render
/// });
/// ```
#[must_use]
pub fn per_request(router: Router, state: Arc, factory: F) -> Self
where
F: Fn(&S, &Params, Option<&Response>) -> R + Send + Sync + 'static,
{
Self {
router,
state: States::Shared(state),
render: Renderers::PerRequest(Box::new(factory)),
body_limit: DEFAULT_BODY_LIMIT,
}
}
/// Mount a router that resolves both its state and its renderer per
/// request.
///
/// For the host with many viewers behind one process. The factory is handed
/// the request head and runs in async context, so the session lookup, the
/// viewer and the theme resolved from them are loaded before the router is
/// called, and a handler reads them off `&S` as it reads anything else.
///
/// This is why the handler signature did not have to grow. A request-scoped
/// `S` is the same ruling the host boundary already made, that a host fact a
/// described screen needs is one the app puts in `S` while it still has a
/// handle to ask. All that changes for a server is how long `S` lives, and
/// the alternatives all cost more: a second parameter cascades to every
/// repo that has written a handler, an async handler kind was refused by
/// decision 6 for the hosts that have no runtime, and a capability protocol
/// was settled against when the host boundary was drawn.
///
/// Both factories, because a host that resolves a viewer per request is a
/// host whose renderer has something to say about that viewer: their theme,
/// their chrome, whether the page is signed in. A host that genuinely wants
/// one renderer clones a configured template in the render factory, which is
/// an allocation per request against a store round trip it is already
/// paying.
///
/// ```no_run
/// # use std::sync::Arc;
/// # use quasi_axum::Adapter;
/// # use quasi_router::Router;
/// # struct Store;
/// # impl Store { async fn viewer(&self, _: Option) -> Option { None } }
/// # struct Viewer { store: Arc, viewer: Option }
/// # #[derive(Clone, Default)] struct Html;
/// # impl quasi_axum::Serves for Html {
/// # fn screen(&self, _: &quasi_router::Screen) -> String { String::new() }
/// # fn fragment(&self, _: &quasi_router::Node) -> String { String::new() }
/// # fn suggestions(&self, _: &str, _: &[quasi_router::Candidate]) -> String { String::new() }
/// # }
/// # let store = Arc::new(Store);
/// # let quasi = Router::::new();
/// Adapter::per_viewer(
/// quasi,
/// move |parts| {
/// // Taken out of the head first: the future owns what it needs.
/// let cookie = parts
/// .headers
/// .get(http::header::COOKIE)
/// .and_then(|value| value.to_str().ok())
/// .map(str::to_owned);
/// let store = Arc::clone(&store);
/// Box::pin(async move {
/// let viewer = store.viewer(cookie).await;
/// Ok(Viewer { store, viewer })
/// })
/// },
/// |_viewer, _params, _answer| Html,
/// );
/// ```
#[must_use]
pub fn per_viewer(router: Router, state: F, render: G) -> Self
where
F: Fn(&Parts) -> StateFuture + Send + Sync + 'static,
G: Fn(&S, &Params, Option<&Response>) -> R + Send + Sync + 'static,
{
Self {
router,
state: States::PerRequest(Box::new(state)),
render: Renderers::PerRequest(Box::new(render)),
body_limit: DEFAULT_BODY_LIMIT,
}
}
/// Read at most this many bytes of a form body.
#[must_use]
pub fn body_limit(mut self, bytes: usize) -> Self {
self.body_limit = bytes;
self
}
/// An axum router serving every path through quasi.
///
/// A fallback, so anything merged in front of it wins. That is deliberate:
/// a health endpoint, static assets and a file upload are all things that
/// are not descriptions, and they should stay ordinary axum routes rather
/// than being forced through a description layer that has no word for them.
pub fn into_router(self) -> axum::Router {
let context = Arc::new(Context {
router: self.router,
state: self.state,
render: self.render,
body_limit: self.body_limit,
});
axum::Router::new().fallback(move |request: Request| {
let context = Arc::clone(&context);
async move { serve(context, request).await }
})
}
}
/// Answer one request.
async fn serve(context: Arc>, request: Request) -> HttpResponse
where
S: Send + Sync + 'static,
R: Serves,
{
let (parts, body) = request.into_parts();
// The limit is applied while reading rather than after, because a hosted
// server is the one host where the sender is not our own webview and the
// bytes should never be buffered in the first place.
let bytes: Bytes = match axum::body::to_bytes(body, context.body_limit).await {
Ok(bytes) => bytes,
Err(_) => return convert(quasi_http::refuse(Refusal::TooLarge)),
};
let incoming = match quasi_http::decode(
&parts.method,
&parts.uri,
&parts.headers,
&bytes,
context.body_limit,
) {
Ok(incoming) => incoming,
Err(refusal) => return convert(quasi_http::refuse(refusal)),
};
// Kept because the router consumes them and a per-request renderer is
// built after dispatch, when what the factory needs to see is both what
// was asked and what was answered. The view is what a renderer factory
// reads — which theme, which viewer, which list you are on — so it is the
// carried half that is kept rather than the write's payload.
let params = incoming.carried.clone();
// Kept before the router consumes the request, because whether the answer
// is a place is decided from what was asked and the answer together.
let asked = quasi_http::Asked::new(&incoming);
// After the envelope is checked and before the router is called. An
// oversized body should not cost a session lookup, and a handler must not
// run before the viewer it reads is resolved.
let state = match &context.state {
States::Shared(state) => Arc::clone(state),
States::PerRequest(factory) => match factory(&parts).await {
Ok(state) => Arc::new(state),
Err(error) => return convert(unresolved(&error)),
},
};
// Decision 6: the router is sync. Calling it directly would block the
// executor for however long the store takes.
let dispatch = {
let context = Arc::clone(&context);
let state = Arc::clone(&state);
tokio::task::spawn_blocking(move || context.router.handle(&state, incoming.into())).await
};
let outcome = dispatch.unwrap_or_else(|_| {
// A panic in a handler. Reported as ours, because it is.
Err(RouteError::internal("the request could not be completed"))
});
match &context.render {
Renderers::Shared(render) => convert(quasi_http::respond(&**render, outcome, &asked)),
Renderers::PerRequest(factory) => {
// Built from the answer rather than before it, so the factory can
// fill the regions this screen has and skip the work for the ones
// it does not.
let render = factory(&state, ¶ms, outcome.as_ref().ok());
convert(quasi_http::respond(&render, outcome, &asked))
}
}
}
/// A state factory's failure, which has no description and no renderer.
///
/// Bodyless, for the reason [`quasi_http::refuse`] is: nothing was reached, so
/// there is nothing to say that the status does not already say. A renderer
/// cannot be built either, since the one thing a per-request renderer is handed
/// is the state that could not be resolved.
fn unresolved(error: &RouteError) -> http::Response> {
let mut builder = http::Response::builder().status(error.class.http_status());
// Bodyless still owes `Allow` if the class is the one that claims verbs.
// A state factory cannot produce that class today, and hard-coding the
// assumption here is how it would stop being true silently.
if let Some(allow) = error.allow_header() {
builder = builder.header(http::header::ALLOW, allow);
}
builder
.body(Vec::new())
.expect("a response with no body and no headers is always valid")
}
/// An `http` response with a `Vec` body becomes an axum one.
fn convert(response: http::Response>) -> HttpResponse {
let (parts, body) = response.into_parts();
HttpResponse::from_parts(parts, Body::from(body))
}
#[cfg(test)]
mod tests;