Skip to main content

rango_core/
app.rs

1//! Application assembly and serving.
2
3use std::{any::Any, future::Future, pin::Pin, sync::Arc};
4
5use axum::{Router, extract::Extension};
6#[cfg(feature = "forgery")]
7use axum::{extract::DefaultBodyLimit, middleware::from_fn};
8use axum::{
9    http::StatusCode,
10    response::{IntoResponse, Response},
11};
12use tower_http::{catch_panic::CatchPanicLayer, services::ServeDir, trace::TraceLayer};
13
14#[cfg(feature = "forgery")]
15use crate::forgery;
16use crate::{error::Error, settings::Settings, store::Store, urls::Routes};
17
18/// Rango application: settings, store, routes, and lifecycle.
19pub struct App {
20    settings: Settings,
21    store: Arc<dyn Store>,
22    router: Router,
23    ready: Option<Ready>,
24}
25
26type Ready = Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>;
27
28impl App {
29    /// New app with the given settings and store.
30    pub fn new(settings: Settings, store: Arc<dyn Store>) -> Self {
31        Self {
32            settings,
33            store,
34            router: Router::new(),
35            ready: None,
36        }
37    }
38
39    /// Merge routes into the root router.
40    pub fn urls(mut self, routes: Routes) -> Self {
41        self.router = self.router.merge(routes.into_router());
42        self
43    }
44
45    /// Nest routes under the given URL prefix.
46    pub fn mount(mut self, prefix: &str, routes: Routes) -> Self {
47        self.router = self.router.nest(prefix, routes.into_router());
48        self
49    }
50
51    /// Serve settings-assets as static files under `/assets`.
52    pub fn mount_static(mut self) -> Self {
53        let dir = self.settings.assets.clone();
54        self.router = self.router.nest_service("/assets", ServeDir::new(dir));
55        self
56    }
57
58    /// Run the given closure as a startup hook before serving starts.
59    pub fn ready<F, Fut>(mut self, ready: F) -> Self
60    where
61        F: FnOnce() -> Fut + Send + 'static,
62        Fut: Future<Output = ()> + Send + 'static,
63    {
64        self.ready = Some(Box::new(move || Box::pin(ready())));
65        self
66    }
67
68    fn build(self) -> Router {
69        let mut router = self
70            .router
71            .fallback(|| async { Error::NotFound.into_response() });
72        router = router.layer(Extension(self.store));
73        #[cfg(feature = "forgery")]
74        if self.settings.forgery {
75            router = router.layer(from_fn(forgery::guard));
76        }
77        router = router
78            .layer(TraceLayer::new_for_http())
79            .layer(catch_panic(self.settings.debug));
80        #[cfg(feature = "forgery")]
81        if self.settings.forgery {
82            router = router.layer(DefaultBodyLimit::max(forgery::LIMIT));
83        }
84        router
85    }
86
87    /// Bind and serve the app until it shuts down.
88    pub async fn run(mut self) -> Result<(), std::io::Error> {
89        if self.settings.secret.is_none() {
90            return Err(std::io::Error::other("set a secret before serving"));
91        }
92        let address = (self.settings.host.as_str(), self.settings.port);
93        let listener = tokio::net::TcpListener::bind(address).await?;
94        tracing::info!(
95            "rango running on http://{}:{}",
96            self.settings.host,
97            self.settings.port
98        );
99        if let Some(ready) = self.ready.take() {
100            ready().await;
101        }
102        axum::serve(listener, self.build()).await
103    }
104}
105
106fn catch_panic(
107    debug: bool,
108) -> CatchPanicLayer<impl FnMut(Box<dyn Any + Send + 'static>) -> Response + Clone> {
109    CatchPanicLayer::custom(move |panic: Box<dyn Any + Send + 'static>| {
110        let body = if debug {
111            panic
112                .downcast_ref::<&str>()
113                .map(|msg| msg.to_string())
114                .or_else(|| panic.downcast_ref::<String>().cloned())
115                .unwrap_or_else(|| "Internal Server Error".to_string())
116        } else {
117            "Internal Server Error".to_string()
118        };
119        (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
120    })
121}