max / quasi
| 1 | //! The axum host adapter for [`quasi_router`]. |
| 2 | //! |
| 3 | //! <!-- wiki: quasi-overview --> |
| 4 | //! |
| 5 | //! A host adapter is thin by design, and since the Tauri protocol adapter |
| 6 | //! arrived it is thinner still: decoding, status mapping and the retarget |
| 7 | //! header live in [`quasi_http`], which both hosts call. What is left here is |
| 8 | //! the part that is genuinely axum's, which is how it mounts, how it reads a |
| 9 | //! body, and where it puts the blocking hop. |
| 10 | //! |
| 11 | //! Hosted web was described as free rather than aspirational, and this is what |
| 12 | //! that meant: a router returning descriptions plus a renderer emitting HTML is |
| 13 | //! already the shape MNW's server has, so a hosted app is a second adapter and |
| 14 | //! not a port. |
| 15 | //! |
| 16 | //! # What it owns |
| 17 | //! |
| 18 | //! - Mounting, as a fallback. See [`Adapter::into_router`]. |
| 19 | //! - Reading the body, with a limit, before anything is parsed. |
| 20 | //! - Resolving the state a request is answered against, for the one host that |
| 21 | //! serves many viewers out of one process. See [`Adapter::per_viewer`]. |
| 22 | //! - The blocking hop. The router is sync per decision 6, so the call happens |
| 23 | //! on a blocking thread, which is what an axum handler over a blocking store |
| 24 | //! pays anyway. |
| 25 | //! |
| 26 | //! # What it does not own |
| 27 | //! |
| 28 | //! Markup. See [`Serves`], and the reason it is a parameter: the Tauri |
| 29 | //! custom-protocol adapter serves HTML to a webview too, and generating it here |
| 30 | //! would guarantee a second implementation. |
| 31 | //! |
| 32 | //! # Mounting it |
| 33 | //! |
| 34 | //! quasi matches paths itself, so the adapter mounts as a fallback rather than |
| 35 | //! registering each route with axum. One matcher, not two agreeing ones. Real |
| 36 | //! axum routes merged in front of it keep working, which is how static files, |
| 37 | //! a health endpoint and anything else outside the description layer are |
| 38 | //! served. |
| 39 | //! |
| 40 | //! ```no_run |
| 41 | //! use std::sync::Arc; |
| 42 | //! use quasi_axum::{Adapter, Serves}; |
| 43 | //! use quasi_router::{Node, Request, Response, RouteError, Router, Screen, Slot, RegionKind}; |
| 44 | //! |
| 45 | //! struct App; |
| 46 | //! struct Html; |
| 47 | //! |
| 48 | //! impl Serves for Html { |
| 49 | //! fn screen(&self, screen: &Screen) -> String { format!("<h1>{}</h1>", screen.title) } |
| 50 | //! fn fragment(&self, _node: &Node) -> String { String::new() } |
| 51 | //! } |
| 52 | //! |
| 53 | //! fn home(_app: &App, _request: Request) -> Result<Response, RouteError> { |
| 54 | //! Ok(Screen::sidebar_content("Home") |
| 55 | //! .with(Slot::new("content", RegionKind::Pane)) |
| 56 | //! .into()) |
| 57 | //! } |
| 58 | //! |
| 59 | //! # async fn run() { |
| 60 | //! let quasi = Router::<App>::new().get("/", home); |
| 61 | //! let app = axum::Router::new() |
| 62 | //! .merge(Adapter::new(quasi, Arc::new(App), Arc::new(Html)).into_router()); |
| 63 | //! |
| 64 | //! let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); |
| 65 | //! axum::serve(listener, app).await.unwrap(); |
| 66 | //! # } |
| 67 | //! ``` |
| 68 | |
| 69 | use Future; |
| 70 | use Pin; |
| 71 | use Arc; |
| 72 | |
| 73 | use ; |
| 74 | use Request; |
| 75 | use Response as HttpResponse; |
| 76 | use Parts; |
| 77 | use Refusal; |
| 78 | use ; |
| 79 | |
| 80 | pub use ; |
| 81 | |
| 82 | /// The state one request is answered against, once it is resolved. |
| 83 | /// |
| 84 | /// A future, because what a viewer is resolved from is a lookup: a session |
| 85 | /// cookie against a store, a token against an introspection endpoint. That is |
| 86 | /// async work, and it happens here rather than in a handler because a handler |
| 87 | /// is sync by decision 6 and this is the one layer that can await. |
| 88 | /// |
| 89 | /// `Err` is the resolution itself failing, not the viewer being anonymous. A |
| 90 | /// signed-out reader is a state the factory builds; a store that would not |
| 91 | /// answer is a [`RouteError`], and the request never reaches the router. |
| 92 | pub type StateFuture<S> = ; |
| 93 | |
| 94 | /// Builds the state one request is answered against. |
| 95 | /// |
| 96 | /// The returned future is `'static`, so a factory takes what it needs out of |
| 97 | /// the request parts first and the future owns it. That is the discipline |
| 98 | /// rather than a limitation: a future holding a borrow on the request head |
| 99 | /// would be a second lifetime running through every host that mounts this. |
| 100 | pub type StateFactory<S> = dyn Fn + Send + Sync; |
| 101 | |
| 102 | /// Builds the renderer for one request. |
| 103 | /// |
| 104 | /// It is handed the state, the request's parameters and the answer the router |
| 105 | /// gave, and returns the renderer that answer is rendered with. The |
| 106 | /// [`Response`] is there so the factory can walk the screen and do work only |
| 107 | /// for the regions that are actually on it: a factory that highlighted a blob |
| 108 | /// without looking would run syntect for every request that never shows a file. |
| 109 | /// |
| 110 | /// `None` is the router refusing. There is no screen, the answer is a notice |
| 111 | /// fragment, and a renderer built for it has nothing per-request to say. |
| 112 | pub type RenderFactory<S, R> = dyn Fn + Send + Sync; |
| 113 | |
| 114 | /// Where the renderer for a request comes from. |
| 115 | /// |
| 116 | /// A host with nothing per-request to say shares one, which is what every |
| 117 | /// client-side host wants and what it cost before this existed. A host serving |
| 118 | /// a public website builds one per request, because that is the only moment it |
| 119 | /// knows what the page contains. See [`Adapter::per_request`]. |
| 120 | |
| 121 | Shared, |
| 122 | PerRequest, |
| 123 | |
| 124 | |
| 125 | /// Where the state for a request comes from. |
| 126 | /// |
| 127 | /// `Handler<S> = fn(&S, Request)` builds `S` once and reads it for the life of |
| 128 | /// the program, which is correct for the hosts quasi was designed against: a |
| 129 | /// Tauri window, an egui frame and a terminal loop each have one user, so one |
| 130 | /// of everything is the truth. A hosted server is the one host that breaks |
| 131 | /// that assumption, because it has one process and many viewers. |
| 132 | /// |
| 133 | /// So the adapter builds `S` per request, and nothing above it moves: the |
| 134 | /// router, the handler signature and the description layer are untouched. See |
| 135 | /// [`Adapter::per_viewer`]. |
| 136 | |
| 137 | Shared, |
| 138 | PerRequest, |
| 139 | |
| 140 | |
| 141 | /// The router, the app's state and a renderer, mounted as an axum service. |
| 142 | |
| 143 | router: , |
| 144 | state: , |
| 145 | render: , |
| 146 | body_limit: usize, |
| 147 | |
| 148 | |
| 149 | /// What a request needs, once, behind one `Arc`. |
| 150 | |
| 151 | router: , |
| 152 | state: , |
| 153 | render: , |
| 154 | body_limit: usize, |
| 155 | |
| 156 | |
| 157 | |
| 158 | |
| 159 | S: Send + Sync + 'static, |
| 160 | R: Serves, |
| 161 | |
| 162 | /// Mount a router over this state, rendered by this renderer. |
| 163 | /// |
| 164 | /// One renderer, shared by every request. Right for a host whose renderer |
| 165 | /// is configuration: where the assets live and what the classes are called |
| 166 | /// do not vary by request. |
| 167 | |
| 168 | |
| 169 | Self |
| 170 | router, |
| 171 | state: Shared, |
| 172 | render: Shared, |
| 173 | body_limit: DEFAULT_BODY_LIMIT, |
| 174 | |
| 175 | |
| 176 | |
| 177 | /// Mount a router over this state, building a renderer per request. |
| 178 | /// |
| 179 | /// For the host that has something to say about this page and not about |
| 180 | /// the next one: the markup filling a bespoke region, a per-screen head, a |
| 181 | /// theme block computed from the signed-in reader. A renderer is |
| 182 | /// configuration and an allocation, so building one per request is cheap; |
| 183 | /// what it buys is that the host gets its say at the one moment it knows |
| 184 | /// what the answer contains. |
| 185 | /// |
| 186 | /// This is the only channel for it, and deliberately. A handler is |
| 187 | /// `fn(&S, Request) -> Result<Response, RouteError>` with no request object |
| 188 | /// and no side channel, so anything it computed could only ride in the |
| 189 | /// [`Response`], and host markup in the router's response type would make |
| 190 | /// the router host-aware. So the factory does the work and the handler |
| 191 | /// stops doing it. |
| 192 | /// |
| 193 | /// ```no_run |
| 194 | /// # use std::sync::Arc; |
| 195 | /// # use quasi_axum::Adapter; |
| 196 | /// # use quasi_router::{Outcome, Router}; |
| 197 | /// # struct App; |
| 198 | /// # #[derive(Default)] struct Html { fills: std::collections::HashMap<String, String> } |
| 199 | /// # impl quasi_axum::Serves for Html { |
| 200 | /// # fn screen(&self, _: &quasi_router::Screen) -> String { String::new() } |
| 201 | /// # fn fragment(&self, _: &quasi_router::Node) -> String { String::new() } |
| 202 | /// # } |
| 203 | /// # let quasi = Router::<App>::new(); |
| 204 | /// Adapter::per_request(quasi, Arc::new(App), |_app, _params, answer| { |
| 205 | /// let mut render = Html::default(); |
| 206 | /// if let Some(Outcome::Screen(screen)) = answer.map(|a| &a.outcome) { |
| 207 | /// for slot in &screen.slots { |
| 208 | /// // Only the regions this screen actually has. |
| 209 | /// render.fills.insert(slot.id.clone(), String::new()); |
| 210 | /// } |
| 211 | /// } |
| 212 | /// render |
| 213 | /// }); |
| 214 | /// ``` |
| 215 | |
| 216 | |
| 217 | |
| 218 | F: Fn(&S, &Params, ) -> R + Send + Sync + 'static, |
| 219 | |
| 220 | Self |
| 221 | router, |
| 222 | state: Shared, |
| 223 | render: PerRequest, |
| 224 | body_limit: DEFAULT_BODY_LIMIT, |
| 225 | |
| 226 | |
| 227 | |
| 228 | /// Mount a router that resolves both its state and its renderer per |
| 229 | /// request. |
| 230 | /// |
| 231 | /// For the host with many viewers behind one process. The factory is handed |
| 232 | /// the request head and runs in async context, so the session lookup, the |
| 233 | /// viewer and the theme resolved from them are loaded before the router is |
| 234 | /// called, and a handler reads them off `&S` as it reads anything else. |
| 235 | /// |
| 236 | /// This is why the handler signature did not have to grow. A request-scoped |
| 237 | /// `S` is the same ruling the host boundary already made, that a host fact a |
| 238 | /// described screen needs is one the app puts in `S` while it still has a |
| 239 | /// handle to ask. All that changes for a server is how long `S` lives, and |
| 240 | /// the alternatives all cost more: a second parameter cascades to every |
| 241 | /// repo that has written a handler, an async handler kind was refused by |
| 242 | /// decision 6 for the hosts that have no runtime, and a capability protocol |
| 243 | /// was settled against when the host boundary was drawn. |
| 244 | /// |
| 245 | /// Both factories, because a host that resolves a viewer per request is a |
| 246 | /// host whose renderer has something to say about that viewer: their theme, |
| 247 | /// their chrome, whether the page is signed in. A host that genuinely wants |
| 248 | /// one renderer clones a configured template in the render factory, which is |
| 249 | /// an allocation per request against a store round trip it is already |
| 250 | /// paying. |
| 251 | /// |
| 252 | /// ```no_run |
| 253 | /// # use std::sync::Arc; |
| 254 | /// # use quasi_axum::Adapter; |
| 255 | /// # use quasi_router::Router; |
| 256 | /// # struct Store; |
| 257 | /// # impl Store { async fn viewer(&self, _: Option<String>) -> Option<u64> { None } } |
| 258 | /// # struct Viewer { store: Arc<Store>, viewer: Option<u64> } |
| 259 | /// # #[derive(Clone, Default)] struct Html; |
| 260 | /// # impl quasi_axum::Serves for Html { |
| 261 | /// # fn screen(&self, _: &quasi_router::Screen) -> String { String::new() } |
| 262 | /// # fn fragment(&self, _: &quasi_router::Node) -> String { String::new() } |
| 263 | /// # } |
| 264 | /// # let store = Arc::new(Store); |
| 265 | /// # let quasi = Router::<Viewer>::new(); |
| 266 | /// Adapter::per_viewer( |
| 267 | /// quasi, |
| 268 | /// move |parts| { |
| 269 | /// // Taken out of the head first: the future owns what it needs. |
| 270 | /// let cookie = parts |
| 271 | /// .headers |
| 272 | /// .get(http::header::COOKIE) |
| 273 | /// .and_then(|value| value.to_str().ok()) |
| 274 | /// .map(str::to_owned); |
| 275 | /// let store = Arc::clone(&store); |
| 276 | /// Box::pin(async move { |
| 277 | /// let viewer = store.viewer(cookie).await; |
| 278 | /// Ok(Viewer { store, viewer }) |
| 279 | /// }) |
| 280 | /// }, |
| 281 | /// |_viewer, _params, _answer| Html, |
| 282 | /// ); |
| 283 | /// ``` |
| 284 | |
| 285 | |
| 286 | |
| 287 | F: Fn(&Parts) -> + Send + Sync + 'static, |
| 288 | G: Fn(&S, &Params, ) -> R + Send + Sync + 'static, |
| 289 | |
| 290 | Self |
| 291 | router, |
| 292 | state: PerRequest, |
| 293 | render: PerRequest, |
| 294 | body_limit: DEFAULT_BODY_LIMIT, |
| 295 | |
| 296 | |
| 297 | |
| 298 | /// Read at most this many bytes of a form body. |
| 299 | |
| 300 | |
| 301 | self.body_limit = bytes; |
| 302 | self |
| 303 | |
| 304 | |
| 305 | /// An axum router serving every path through quasi. |
| 306 | /// |
| 307 | /// A fallback, so anything merged in front of it wins. That is deliberate: |
| 308 | /// a health endpoint, static assets and a file upload are all things that |
| 309 | /// are not descriptions, and they should stay ordinary axum routes rather |
| 310 | /// than being forced through a description layer that has no word for them. |
| 311 | |
| 312 | let context = new |
| 313 | router: self.router, |
| 314 | state: self.state, |
| 315 | render: self.render, |
| 316 | body_limit: self.body_limit, |
| 317 | ; |
| 318 | |
| 319 | new.fallback |
| 320 | let context = clone; |
| 321 | async move |
| 322 | |
| 323 | |
| 324 | |
| 325 | |
| 326 | /// Answer one request. |
| 327 | async |
| 328 | |
| 329 | S: Send + Sync + 'static, |
| 330 | R: Serves, |
| 331 | |
| 332 | let = request.into_parts; |
| 333 | |
| 334 | // The limit is applied while reading rather than after, because a hosted |
| 335 | // server is the one host where the sender is not our own webview and the |
| 336 | // bytes should never be buffered in the first place. |
| 337 | let bytes: Bytes = match to_bytes.await |
| 338 | Ok => bytes, |
| 339 | Err => return convert, |
| 340 | ; |
| 341 | |
| 342 | let incoming = match decode |
| 343 | &parts.method, |
| 344 | &parts.uri, |
| 345 | &parts.headers, |
| 346 | &bytes, |
| 347 | context.body_limit, |
| 348 | |
| 349 | Ok => incoming, |
| 350 | Err => return convert, |
| 351 | ; |
| 352 | |
| 353 | // Kept because the router consumes them and a per-request renderer is |
| 354 | // built after dispatch, when what the factory needs to see is both what |
| 355 | // was asked and what was answered. The view is what a renderer factory |
| 356 | // reads — which theme, which viewer, which list you are on — so it is the |
| 357 | // carried half that is kept rather than the write's payload. |
| 358 | let params = incoming.carried.clone; |
| 359 | |
| 360 | // Kept before the router consumes the request, because whether the answer |
| 361 | // is a place is decided from what was asked and the answer together. |
| 362 | let asked = new; |
| 363 | |
| 364 | // After the envelope is checked and before the router is called. An |
| 365 | // oversized body should not cost a session lookup, and a handler must not |
| 366 | // run before the viewer it reads is resolved. |
| 367 | let state = match &context.state |
| 368 | Shared => clone, |
| 369 | PerRequest => match factory.await |
| 370 | Ok => new, |
| 371 | Err => return convert, |
| 372 | , |
| 373 | ; |
| 374 | |
| 375 | // Decision 6: the router is sync. Calling it directly would block the |
| 376 | // executor for however long the store takes. |
| 377 | let dispatch = |
| 378 | let context = clone; |
| 379 | let state = clone; |
| 380 | spawn_blocking.await |
| 381 | ; |
| 382 | |
| 383 | let outcome = dispatch.unwrap_or_else |
| 384 | // A panic in a handler. Reported as ours, because it is. |
| 385 | Err |
| 386 | ; |
| 387 | |
| 388 | match &context.render |
| 389 | Shared => convert, |
| 390 | PerRequest => |
| 391 | // Built from the answer rather than before it, so the factory can |
| 392 | // fill the regions this screen has and skip the work for the ones |
| 393 | // it does not. |
| 394 | let render = factory; |
| 395 | convert |
| 396 | |
| 397 | |
| 398 | |
| 399 | |
| 400 | /// A state factory's failure, which has no description and no renderer. |
| 401 | /// |
| 402 | /// Bodyless, for the reason [`quasi_http::refuse`] is: nothing was reached, so |
| 403 | /// there is nothing to say that the status does not already say. A renderer |
| 404 | /// cannot be built either, since the one thing a per-request renderer is handed |
| 405 | /// is the state that could not be resolved. |
| 406 | |
| 407 | builder |
| 408 | .status |
| 409 | .body |
| 410 | .expect |
| 411 | |
| 412 | |
| 413 | /// An `http` response with a `Vec` body becomes an axum one. |
| 414 | |
| 415 | let = response.into_parts; |
| 416 | from_parts |
| 417 | |
| 418 | |
| 419 | |
| 420 | |
| 421 |