//! 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;