use std::sync::Arc;
use axum::{
Router,
body::Body,
extract::{DefaultBodyLimit, Request},
handler::Handler,
http::Extensions,
middleware::{self, Next},
routing::{MethodRouter, delete, get, head, options, patch, post, put, trace},
};
use frunk::{HCons, HNil, hlist::HList};
use crate::{
app::{App, MountedApp},
capability::{CapStore, Capability},
components::slots::{SharedChromeFolder, SlotTag},
tag::Tagged,
traits::{
add::{AddCapability, CapTagAbsent},
get::GetByTag,
},
};
pub const REQUEST_BODY_LIMIT_BYTES: usize = 50 * 1024 * 1024;
pub mod route_tag;
pub use route_tag::{
AppPaneGet, AppPanePost, BoostPost, FileDownloadGet, FileDownloadPost, FkSelectGet,
FragmentGet, FragmentPost, GenerationPost, ModalGet, RouteQueryBuilder, RouteTag, RouteUrl,
nav_url, trailing_slash,
};
pub struct HttpTag;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Method {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
Trace,
}
#[derive(Clone)]
pub struct Route {
pub path: String,
pub method: Method,
method_router: MethodRouter<()>,
}
impl Route {
fn new(path: impl Into<String>, method: Method, method_router: MethodRouter<()>) -> Self {
Self {
path: normalize_route_path(path),
method,
method_router,
}
}
pub fn get<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Get, get(handler))
}
pub fn post<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Post, post(handler))
}
pub fn put<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Put, put(handler))
}
pub fn delete<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Delete, delete(handler))
}
pub fn patch<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Patch, patch(handler))
}
pub fn head<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Head, head(handler))
}
pub fn options<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Options, options(handler))
}
pub fn trace<H, T>(path: impl Into<String>, handler: H) -> Self
where
H: Handler<T, ()>,
T: 'static,
{
Self::new(path, Method::Trace, trace(handler))
}
}
fn normalize_route_path(path: impl Into<String>) -> String {
let path = path.into();
if path.len() > 1 && path.ends_with('/') {
path.trim_end_matches('/').to_owned()
} else {
path
}
}
pub trait RouteRegistrar<Http, Proof = ()>: Sized {
type Output;
fn register_routes(self, http: Http) -> Self::Output;
}
pub trait FoldMountRoutes<Http, Proof = ()>: Sized {
type Output;
fn fold_mount_routes(self, http: Http) -> Self::Output;
}
impl<Http> FoldMountRoutes<Http> for HNil {
type Output = Http;
fn fold_mount_routes(self, http: Http) -> Self::Output {
http
}
}
impl<Plugin, Hook, Tail, Http, TailProof, Proof> FoldMountRoutes<Http, (TailProof, Proof)>
for HCons<Tagged<Plugin, Hook>, Tail>
where
Tail: FoldMountRoutes<Http, TailProof>,
Hook: RouteRegistrar<Tail::Output, Proof>,
{
type Output = <Hook as RouteRegistrar<Tail::Output, Proof>>::Output;
fn fold_mount_routes(self, http: Http) -> Self::Output {
let http = self.tail.fold_mount_routes(http);
self.head.value.register_routes(http)
}
}
pub trait MountRoutes {
fn mount_routes(self, router: Router<()>) -> Router<()>;
}
trait PushRoutes {
fn push_routes(self, routes: &mut Vec<Route>);
}
impl PushRoutes for HNil {
fn push_routes(self, _routes: &mut Vec<Route>) {}
}
impl<Tag, Tail> PushRoutes for HCons<Tagged<Tag, Route>, Tail>
where
Tail: PushRoutes,
{
fn push_routes(self, routes: &mut Vec<Route>) {
routes.push(self.head.value);
self.tail.push_routes(routes);
}
}
impl<Routes> MountRoutes for Routes
where
Routes: PushRoutes,
{
fn mount_routes(self, mut router: Router<()>) -> Router<()> {
let mut routes = Vec::new();
self.push_routes(&mut routes);
let mut kept = Vec::with_capacity(routes.len());
for route in routes {
if kept
.iter()
.any(|r: &Route| r.path == route.path && r.method == route.method)
{
continue;
}
kept.push(route);
}
for route in kept {
router = router.route(&route.path, route.method_router);
}
router
}
}
pub trait ProvideRequestCaps {
fn provide_request_caps(&self, extensions: &mut Extensions);
}
impl ProvideRequestCaps for HNil {
fn provide_request_caps(&self, _: &mut Extensions) {}
}
impl<Tag, V, Tail> ProvideRequestCaps for HCons<Tagged<Tag, V>, Tail>
where
V: Clone + Send + Sync + 'static,
Tail: ProvideRequestCaps,
{
fn provide_request_caps(&self, extensions: &mut Extensions) {
extensions.insert(self.head.value.clone());
self.tail.provide_request_caps(extensions);
}
}
pub struct Cap<T>(pub T);
impl<S, T> axum::extract::FromRequestParts<S> for Cap<T>
where
T: Clone + Send + Sync + 'static,
S: Send + Sync,
{
type Rejection = (axum::http::StatusCode, &'static str);
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
parts.extensions.get::<T>().cloned().map(Cap).ok_or((
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"missing capability in request extensions",
))
}
}
#[derive(Clone)]
pub struct HttpCapability<Routes> {
pub routes: Routes,
}
impl HttpCapability<HNil> {
pub fn new() -> Self {
Self { routes: HNil }
}
}
impl Default for HttpCapability<HNil> {
fn default() -> Self {
Self::new()
}
}
impl<Routes> HttpCapability<Routes> {
pub fn prepend<Tag>(self, route: Route) -> HttpCapability<HCons<Tagged<Tag, Route>, Routes>>
where
Routes: HList,
{
HttpCapability {
routes: HCons {
head: Tagged::new(route),
tail: self.routes,
},
}
}
pub fn get_route<Tag, Index>(&self) -> &Route
where
Routes: GetByTag<Tag, Index, Value = Route>,
{
self.routes.get_by_tag()
}
pub fn into_router(self) -> Router<()>
where
Routes: MountRoutes,
{
self.routes.mount_routes(Router::new())
}
}
pub type HttpCap<Hooks, Http> = CapStore<HttpTag, Hooks, Http>;
impl<Hooks, Routes> HttpCap<Hooks, HttpCapability<Routes>> {
pub fn resolve_route_hooks<Proof>(
self,
) -> HttpCap<HNil, <Hooks as FoldMountRoutes<HttpCapability<Routes>, Proof>>::Output>
where
Hooks: FoldMountRoutes<HttpCapability<Routes>, Proof>,
{
let http = self.hooks.fold_mount_routes(self.items);
CapStore::with_items(http)
}
}
impl<Http> Capability for HttpCap<HNil, Http> {
type Value = Arc<Http>;
type Output = Tagged<HttpTag, Arc<Http>>;
type Hooks = HNil;
type Items = Http;
fn mount(self) -> Self::Output {
Tagged::new(Arc::new(self.items))
}
}
pub fn with_http<L, Proof>(app: App<L>) -> App<HCons<HttpCap<HNil, HttpCapability<HNil>>, L>>
where
L: HList + CapTagAbsent<HttpTag, Proof>,
{
app.add_capability(CapStore::with_items(HttpCapability::new()))
}
pub fn into_axum_router<M, HttpIdx, Routes, SlotIdx>(app: &MountedApp<M>) -> Router
where
M: GetByTag<HttpTag, HttpIdx, Value = Arc<HttpCapability<Routes>>>,
M: GetByTag<SlotTag, SlotIdx, Value = SharedChromeFolder>,
M: ProvideRequestCaps + Clone + Send + Sync + 'static,
Routes: MountRoutes + Clone,
{
let router = app
.get_capability_output::<HttpTag, HttpIdx>()
.as_ref()
.clone()
.into_router();
let caps = Arc::new(app.capabilities.clone());
router
.layer(middleware::from_fn(crate::web::htmx_middleware))
.layer(middleware::from_fn(
move |mut req: Request<Body>, next: Next| {
let caps = Arc::clone(&caps);
async move {
caps.provide_request_caps(req.extensions_mut());
next.run(req).await
}
},
))
.layer(DefaultBodyLimit::max(REQUEST_BODY_LIMIT_BYTES))
}