1use crate::background::{BackgroundTaskLimits, TaskManager};
17use crate::body::Body;
18use crate::di::{self, DiRegistry};
19use crate::error::{ApiError, ServerError};
20use crate::lifespan::{DEFAULT_SHUTDOWN_BUDGET, Lifespan};
21use crate::response::IntoResponse;
22use crate::routing::{Endpoint, MethodRouter, Route};
23use siderite_openapi::{DocumentBuilder, OpenApi, OpenApiError, ui};
24use siderite_orm::{Databases, Db};
25use std::collections::BTreeMap;
26use std::future::Future;
27use std::pin::Pin;
28use std::sync::Arc;
29
30pub(crate) type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
32
33pub(crate) type RouterLayer = Box<dyn FnOnce(axum::Router) -> axum::Router + Send>;
36
37pub(crate) type LifespanHook = Box<dyn FnOnce() -> BoxFuture<Result<(), ApiError>> + Send>;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct AppMeta {
43 pub title: String,
45 pub version: String,
47 pub description: Option<String>,
49}
50
51impl Default for AppMeta {
52 fn default() -> Self {
53 Self {
54 title: "siderite".to_owned(),
55 version: "0.1.0".to_owned(),
56 description: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct DocsConfig {
64 pub openapi_url: Option<String>,
66 pub swagger_url: Option<String>,
68 pub redoc_url: Option<String>,
70}
71
72impl Default for DocsConfig {
73 fn default() -> Self {
74 Self {
75 openapi_url: Some("/openapi.json".to_owned()),
76 swagger_url: Some("/docs".to_owned()),
77 redoc_url: Some("/redoc".to_owned()),
78 }
79 }
80}
81
82struct Mount {
84 prefix: String,
85 app: App,
86}
87
88pub struct App {
90 pub(crate) meta: AppMeta,
91 pub(crate) docs: DocsConfig,
92 pub(crate) routes: Vec<(String, MethodRouter)>,
93 mounts: Vec<Mount>,
94 pub(crate) router_layers: Vec<RouterLayer>,
96 pub(crate) services: Vec<RouterLayer>,
98 pub(crate) fallback: Option<RouterLayer>,
100 pub(crate) startup_hooks: Vec<LifespanHook>,
101 pub(crate) shutdown_hooks: Vec<LifespanHook>,
102 pub(crate) shutdown_budget: std::time::Duration,
103 pub(crate) background_limits: BackgroundTaskLimits,
104 pub(crate) server_limits: crate::ServerLimits,
105 pub(crate) middleware: Vec<RouterLayer>,
107 pub(crate) di: DiRegistry,
109 databases: Option<Databases>,
111 pub(crate) config_errors: Vec<String>,
114}
115
116impl std::fmt::Debug for App {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("App")
119 .field("meta", &self.meta)
120 .field(
121 "routes",
122 &self.routes.iter().map(|(p, _)| p).collect::<Vec<_>>(),
123 )
124 .field(
125 "mounts",
126 &self.mounts.iter().map(|m| &m.prefix).collect::<Vec<_>>(),
127 )
128 .finish_non_exhaustive()
129 }
130}
131
132impl Default for App {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138pub(crate) async fn route_not_found() -> axum::response::Response {
139 ApiError::not_found("No route matches the requested path.")
140 .into_response()
141 .map(Body::into_inner)
142}
143
144impl App {
145 pub fn new() -> Self {
147 Self {
148 meta: AppMeta::default(),
149 docs: DocsConfig::default(),
150 routes: Vec::new(),
151 mounts: Vec::new(),
152 router_layers: Vec::new(),
153 services: Vec::new(),
154 fallback: None,
155 startup_hooks: Vec::new(),
156 shutdown_hooks: Vec::new(),
157 shutdown_budget: DEFAULT_SHUTDOWN_BUDGET,
158 background_limits: BackgroundTaskLimits::default(),
159 server_limits: crate::ServerLimits::default(),
160 middleware: Vec::new(),
161 di: DiRegistry::default(),
162 databases: None,
163 config_errors: Vec::new(),
164 }
165 }
166
167 #[must_use]
169 pub fn title(mut self, title: impl Into<String>) -> Self {
170 self.meta.title = title.into();
171 self
172 }
173
174 #[must_use]
176 pub fn version(mut self, version: impl Into<String>) -> Self {
177 self.meta.version = version.into();
178 self
179 }
180
181 #[must_use]
183 pub fn description(mut self, description: impl Into<String>) -> Self {
184 self.meta.description = Some(description.into());
185 self
186 }
187
188 #[must_use]
190 pub fn docs(mut self, docs: DocsConfig) -> Self {
191 self.docs = docs;
192 self
193 }
194
195 pub fn meta(&self) -> &AppMeta {
197 &self.meta
198 }
199
200 #[must_use]
202 pub fn route(mut self, path: &str, methods: MethodRouter) -> Self {
203 self.routes.push((path.to_owned(), methods));
204 self
205 }
206
207 #[must_use]
209 pub fn routes(mut self, routes: impl IntoIterator<Item = Route>) -> Self {
210 for route in routes {
211 self = self.route(route.path, route.router);
212 }
213 self
214 }
215
216 #[must_use]
219 pub fn mount(mut self, prefix: &str, app: App) -> Self {
220 self.mounts.push(Mount {
221 prefix: prefix.trim_end_matches('/').to_owned(),
222 app,
223 });
224 self
225 }
226
227 #[must_use]
229 pub fn nest(self, prefix: &str, app: App) -> Self {
230 self.mount(prefix, app)
231 }
232
233 #[must_use]
237 pub fn with_state<T: Send + Sync + 'static>(mut self, state: T) -> Self {
238 let state = Arc::new(state);
239 self.router_layers
240 .push(Box::new(move |router: axum::Router| {
241 router.layer(axum::Extension(state))
242 }));
243 self
244 }
245
246 #[must_use]
251 pub fn database(mut self, alias: impl Into<String>, db: Db) -> Self {
252 let databases = self.databases.take().unwrap_or_default();
253 self.databases = Some(databases.with(alias, db));
254 self
255 }
256
257 #[must_use]
259 pub fn databases(mut self, databases: Databases) -> Self {
260 self.databases = Some(databases);
261 self
262 }
263
264 pub fn database_registry(&self) -> Option<&Databases> {
266 self.databases.as_ref()
267 }
268
269 fn endpoints(&self) -> Vec<(String, &Endpoint)> {
271 let mut out: Vec<(String, &Endpoint)> = self
272 .routes
273 .iter()
274 .flat_map(|(path, r)| r.endpoints.iter().map(move |e| (path.clone(), e)))
275 .collect();
276 for mount in &self.mounts {
277 for (path, e) in mount.app.endpoints() {
278 let full = if path == "/" && !mount.prefix.is_empty() {
279 mount.prefix.clone()
280 } else {
281 format!("{}{path}", mount.prefix)
282 };
283 out.push((full, e));
284 }
285 }
286 out
287 }
288
289 pub fn openapi(&self) -> Result<OpenApi, OpenApiError> {
295 let mut builder = DocumentBuilder::new(&self.meta.title, &self.meta.version);
296 if let Some(d) = &self.meta.description {
297 builder = builder.description(d);
298 }
299 for (path, endpoint) in self.endpoints() {
300 if endpoint.meta.hidden {
301 continue;
302 }
303 if let Some(method) = endpoint.openapi_method() {
304 let op = endpoint.operation(builder.registry());
305 builder.add_operation(&path, method, op);
306 }
307 }
308 builder.build()
309 }
310
311 fn first_config_error(&self) -> Option<&str> {
313 self.config_errors
314 .first()
315 .map(String::as_str)
316 .or_else(|| self.mounts.iter().find_map(|m| m.app.first_config_error()))
317 }
318
319 fn validate(&self) -> Result<(), ServerError> {
321 let mut seen: BTreeMap<(String, String), ()> = BTreeMap::new();
322 for (path, e) in self.endpoints() {
323 if !path.starts_with('/') {
324 return Err(ServerError::Configuration(format!(
325 "route path `{path}` must start with `/`"
326 )));
327 }
328 if e.openapi_method().is_none() {
329 return Err(ServerError::Configuration(format!(
330 "unsupported HTTP method {} for route {path}",
331 e.method
332 )));
333 }
334 if seen
335 .insert((path.clone(), e.method.to_string()), ())
336 .is_some()
337 {
338 return Err(ServerError::Configuration(format!(
339 "duplicate route {} {path}",
340 e.method
341 )));
342 }
343 }
344 for (name, url) in [
345 ("openapi_url", &self.docs.openapi_url),
346 ("swagger_url", &self.docs.swagger_url),
347 ("redoc_url", &self.docs.redoc_url),
348 ] {
349 if let Some(url) = url
350 && !url.starts_with('/')
351 {
352 return Err(ServerError::Configuration(format!(
353 "docs `{name}` `{url}` must start with `/`"
354 )));
355 }
356 }
357 if let Some(message) = self.first_config_error() {
358 return Err(ServerError::Configuration(message.to_owned()));
359 }
360 for m in &self.mounts {
361 if m.prefix.is_empty() && m.app.fallback.is_some() {
364 return Err(ServerError::Configuration(
365 "an app mounted at `/` cannot serve root static files; \
366 call `static_files(\"/\", ..)` on the parent app"
367 .to_owned(),
368 ));
369 }
370 if !m.prefix.is_empty() && !m.prefix.starts_with('/') {
371 return Err(ServerError::Configuration(format!(
372 "mount prefix `{}` must start with `/`",
373 m.prefix
374 )));
375 }
376 }
377 Ok(())
378 }
379
380 fn build_router(
386 mut self,
387 hooks: &mut Lifespan,
388 root: bool,
389 ) -> (axum::Router, Vec<RouterLayer>, Option<RouterLayer>) {
390 hooks.startup.append(&mut self.startup_hooks);
391 hooks.shutdown.append(&mut self.shutdown_hooks);
394
395 let has_background = self.routes.iter().any(|(_, methods)| {
396 methods
397 .endpoints
398 .iter()
399 .any(|endpoint| endpoint.background_tasks)
400 });
401 let background = has_background.then(|| {
402 let manager = TaskManager::new(self.background_limits);
403 hooks.background.push(manager.clone());
404 manager
405 });
406 let mut grouped: BTreeMap<String, Vec<Endpoint>> = BTreeMap::new();
407 for (path, r) in self.routes {
408 grouped.entry(path).or_default().extend(r.endpoints);
409 }
410 let mut router = axum::Router::new();
411 for (path, endpoints) in grouped {
412 let method_router = endpoints.into_iter().filter_map(Endpoint::into_axum).fold(
413 axum::routing::MethodRouter::new(),
414 axum::routing::MethodRouter::merge,
415 );
416 router = router.route(&path, method_router);
417 }
418 for mount in self.mounts {
419 let (child, middleware, _) = mount.app.build_router(hooks, false);
420 let child = apply_middleware(child, middleware);
421 router = if mount.prefix.is_empty() {
422 router.merge(child)
423 } else {
424 router.nest(&mount.prefix, child)
425 };
426 }
427 for service in self.services {
428 router = service(router);
429 }
430 let mut fallback = self.fallback;
432 if !root && let Some(f) = fallback.take() {
433 router = f(router);
434 }
435 if root || !self.di.is_empty() {
436 router = di::install(router, self.di);
437 }
438 router = self
439 .router_layers
440 .into_iter()
441 .fold(router, |r, layer| layer(r));
442 if let Some(manager) = background {
443 router = router.layer(axum::Extension(manager));
444 }
445 (router, self.middleware, fallback)
446 }
447
448 pub(crate) fn build(mut self) -> Result<(axum::Router, Lifespan), ServerError> {
450 self.validate()?;
451 if let Some(databases) = self.databases.take() {
452 self = self.with_state(databases);
453 }
454 let openapi = self
455 .openapi()
456 .map_err(|e| ServerError::Configuration(e.to_string()))?;
457 let docs = self.docs.clone();
458 let title = self.meta.title.clone();
459 let upgrades = crate::server::tasks::TaskOwner::new(self.server_limits.max_websockets);
460 let mut hooks = Lifespan {
461 shutdown_budget: self.shutdown_budget,
462 server: Some(crate::server::ServerOwners::new(self.server_limits)),
463 ..Lifespan::default()
464 };
465 let (mut router, middleware, fallback) = self.build_router(&mut hooks, true);
466 if let Some(spec_url) = docs.openapi_url {
467 let json = serde_json::to_string(&openapi)
468 .map_err(|e| ServerError::Configuration(e.to_string()))?;
469 let mut docs_router =
472 axum::Router::new().route(&spec_url, static_route("application/json", json));
473 if let Some(url) = docs.swagger_url {
474 let html = ui::swagger_ui_html(&title, &spec_url);
475 docs_router =
476 docs_router.route(&url, static_route("text/html; charset=utf-8", html));
477 }
478 if let Some(url) = docs.redoc_url {
479 let html = ui::redoc_html(&title, &spec_url);
480 docs_router =
481 docs_router.route(&url, static_route("text/html; charset=utf-8", html));
482 }
483 router = router.merge(docs_router.route_layer(axum::middleware::from_fn(
484 |req: axum::extract::Request, next: axum::middleware::Next| {
485 let (parts, body) = req.into_parts();
486 crate::middleware::note_matched_path(&parts);
487 next.run(axum::extract::Request::from_parts(parts, body))
488 },
489 )));
490 }
491 let router = match fallback {
492 Some(install) => install(router),
493 None => router.fallback(route_not_found),
494 };
495 let router = apply_middleware(router, middleware);
496 let router = router.layer(axum::Extension(upgrades.handle()));
497 let router = if let Some(server) = &hooks.server {
498 router.layer(axum::Extension(server.readiness.clone()))
499 } else {
500 router
501 };
502 hooks.transports.push(upgrades);
503 Ok((router, hooks))
504 }
505
506 pub fn into_router_service(self) -> Result<crate::RouterService, ServerError> {
512 self.build()
513 .map(|(router, _)| crate::RouterService::new(router))
514 }
515
516 pub fn into_service_with_lifespan(
523 self,
524 ) -> Result<(crate::RouterService, Lifespan), ServerError> {
525 self.build()
526 .map(|(router, lifespan)| (crate::RouterService::new(router), lifespan))
527 }
528
529 pub async fn run(self, addr: &str) -> Result<(), ServerError> {
540 self.run_until(addr, shutdown_signal()).await
541 }
542
543 pub async fn run_until<F>(self, addr: &str, shutdown: F) -> Result<(), ServerError>
559 where
560 F: Future<Output = ()> + Send + 'static,
561 {
562 let budget = self.shutdown_budget;
563 let limits = self.server_limits;
564 let (router, lifespan) = self.build()?;
565 let server = lifespan
566 .server
567 .clone()
568 .ok_or_else(|| ServerError::Configuration("missing server owner".to_owned()))?;
569 let mut owner = lifespan.supervise()?;
570 let mut shutdown = Box::pin(shutdown);
571 tokio::select! {
572 result = owner.ready() => result?,
573 _ = &mut shutdown => return owner.shutdown().await,
574 }
575 let listener = match tokio::net::TcpListener::bind(addr).await {
576 Ok(listener) => listener,
577 Err(source) => {
578 let _ = owner.shutdown().await;
579 return Err(ServerError::Bind {
580 addr: addr.to_owned(),
581 source,
582 });
583 }
584 };
585 tracing::info!(%addr, "listening");
586 let serving = crate::server::serve(listener, router, limits, server, budget, shutdown);
587 let (result, deadline) = serving.await;
588 let stopped = owner.shutdown_at(deadline).await;
589 result?;
590 stopped
591 }
592}
593
594fn apply_middleware(router: axum::Router, layers: Vec<RouterLayer>) -> axum::Router {
596 layers
597 .into_iter()
598 .rev()
599 .fold(router, |router, layer| layer(router))
600}
601
602fn static_route(content_type: &'static str, body: String) -> axum::routing::MethodRouter {
603 let body: Arc<str> = body.into();
604 axum::routing::get(move || {
605 let body = Arc::clone(&body);
606 async move {
607 let mut resp = axum::response::Response::new(axum::body::Body::from(body.to_string()));
608 resp.headers_mut().insert(
609 http::header::CONTENT_TYPE,
610 http::HeaderValue::from_static(content_type),
611 );
612 resp
613 }
614 })
615}
616
617async fn shutdown_signal() {
618 let interrupt = async {
619 if let Err(error) = tokio::signal::ctrl_c().await {
620 tracing::error!(%error, "cannot listen for Ctrl-C");
621 std::future::pending::<()>().await;
622 }
623 };
624 #[cfg(unix)]
625 let terminate = async {
626 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
627 Ok(mut signal) => {
628 signal.recv().await;
629 }
630 Err(error) => {
631 tracing::error!(%error, "cannot listen for SIGTERM");
632 std::future::pending::<()>().await;
633 }
634 }
635 };
636 #[cfg(not(unix))]
637 let terminate = std::future::pending::<()>();
638 tokio::select! { _ = interrupt => {}, _ = terminate => {} }
639}