use axum::{ extract::{OriginalUri, RawQuery, State}, http::{header, HeaderMap, StatusCode}, response::{IntoResponse, Response}, routing::get, Router, }; use forma_ir::PageProps; use forma_server::{ navigation::NAVIGATION_MEDIA_TYPE, NavigationBranch, NavigationConfig, PageHandle, RenderCtx, RenderError, SealedNavigationPolicy, }; use std::sync::Arc; #[derive(PageProps)] pub(crate) struct ShellProps { count: u32, theme: String, theme_next: String, } #[derive(PageProps)] pub(crate) struct LeafProps { count: u32, } pub(crate) struct RouterLab { shell: PageHandle, overview: PageHandle, details: PageHandle, navigation: NavigationConfig, } impl RouterLab { pub(crate) fn new( shell: PageHandle, overview: PageHandle, details: PageHandle, ) -> Result> { // Public, stateless demonstration. GET forms need no enhanced action // allowlist; the cosmetic theme POST remains a document-navigation form. // PageResponse must remain the complete effective document policy. let navigation = NavigationConfig::new("router-lab", "layout-v1", "public-v1")? .with_sealed_policy(SealedNavigationPolicy::page_response_only( std::iter::empty::<&str>(), )?); Ok(Self { shell, overview, details, navigation, }) } fn branch( &self, path: &str, count: u32, details: bool, headers: &HeaderMap, ) -> Result { let ctx = RenderCtx::new() .with_title(if details { "Details · Native router — Forma" } else { "Overview · Native router — Forma" }) .with_navigation(self.navigation.clone()); let shell = self.shell.render_owner( &ShellProps { count: 0, theme: crate::site_theme::current(headers), theme_next: crate::site_theme::next(headers), }, &ctx, "persistent-router-lab-layout", )?; let page = if details { &self.details } else { &self.overview }; let leaf = page.render_owner(&LeafProps { count }, &ctx, "request-seed")?; NavigationBranch::new(path, vec![shell, leaf]) } } pub(crate) fn routes() -> Router> { Router::new() .route("/lab/router", get(overview)) .route("/lab/router/", get(overview)) .route("/lab/router/details", get(details)) .route("/lab/router/details/", get(details)) .route( "/lab/router/source/server.rs", get(|| async { crate::source(include_str!("router_lab.rs")) }), ) .route( "/lab/router/source/shell.tsx", get(|| async { crate::source(include_str!("router-shell.tsx")) }), ) .route( "/lab/router/source/overview.tsx", get(|| async { crate::source(include_str!("router-overview.tsx")) }), ) .route( "/lab/router/source/details.tsx", get(|| async { crate::source(include_str!("router-details.tsx")) }), ) .route( "/lab/router/source/navigation.ts", get(|| async { crate::source(include_str!("router-navigation.ts")) }), ) } fn seed(query: Option<&str>, details: bool) -> Result { if query.is_none_or(str::is_empty) { return Ok(if details { 12 } else { 7 }); } crate::request_count(query) } fn render( state: Arc, path: &str, query: Option<&str>, headers: HeaderMap, details: bool, ) -> Response { let count = match seed(query, details) { Ok(count) => count, Err(message) => { return ( StatusCode::BAD_REQUEST, [ (header::CACHE_CONTROL, "no-store"), (header::X_CONTENT_TYPE_OPTIONS, "nosniff"), ], message, ) .into_response() } }; let enhanced = headers .get(header::ACCEPT) .and_then(|value| value.to_str().ok()) .is_some_and(|value| value == NAVIGATION_MEDIA_TYPE); let rendered = state .router_lab .branch(path, count, details, &headers) .and_then(|branch| { if enhanced { branch .into_navigation_response() .map(IntoResponse::into_response) } else { branch .into_document_response() .map(IntoResponse::into_response) } }); match rendered { Ok(response) => response, Err(error) => { tracing::error!(code = error.code(), route = error.route(), "{error}"); ( StatusCode::INTERNAL_SERVER_ERROR, [ (header::CACHE_CONTROL, "no-store"), (header::X_CONTENT_TYPE_OPTIONS, "nosniff"), ], "The router demonstration could not be rendered.", ) .into_response() } } } async fn overview( State(state): State>, OriginalUri(uri): OriginalUri, RawQuery(query): RawQuery, headers: HeaderMap, ) -> Response { render(state, uri.path(), query.as_deref(), headers, false) } async fn details( State(state): State>, OriginalUri(uri): OriginalUri, RawQuery(query): RawQuery, headers: HeaderMap, ) -> Response { render(state, uri.path(), query.as_deref(), headers, true) } #[cfg(test)] mod tests { use super::seed; #[test] fn request_seeds_are_bounded_and_routes_have_distinct_defaults() { assert_eq!(seed(None, false), Ok(7)); assert_eq!(seed(None, true), Ok(12)); assert_eq!(seed(Some("count=9999"), true), Ok(9999)); for query in [ "count=10000", "count=-1", "count=1&count=2", "other=9", "count=%3Cscript%3E", ] { assert!(seed(Some(query), false).is_err(), "{query}"); assert!(seed(Some(query), true).is_err(), "{query}"); } } }