1use std::any::{Any, TypeId};
36use std::collections::HashMap;
37use std::future::Future;
38use std::pin::Pin;
39use std::sync::Arc;
40
41use crate::context::RequestContext;
42use crate::middleware::{BoxFuture, Handler, Middleware, MiddlewareStack};
43use crate::request::{Method, Request};
44use crate::response::{Response, StatusCode};
45use crate::shutdown::ShutdownController;
46use fastapi_router::{Route, RouteLookup, Router};
47
48pub enum StartupHook {
54 Sync(Box<dyn FnOnce() -> Result<(), StartupHookError> + Send>),
56 AsyncFactory(
58 Box<
59 dyn FnOnce() -> Pin<Box<dyn Future<Output = Result<(), StartupHookError>> + Send>>
60 + Send,
61 >,
62 ),
63}
64
65impl StartupHook {
66 pub fn sync<F>(f: F) -> Self
68 where
69 F: FnOnce() -> Result<(), StartupHookError> + Send + 'static,
70 {
71 Self::Sync(Box::new(f))
72 }
73
74 pub fn async_fn<F, Fut>(f: F) -> Self
76 where
77 F: FnOnce() -> Fut + Send + 'static,
78 Fut: Future<Output = Result<(), StartupHookError>> + Send + 'static,
79 {
80 Self::AsyncFactory(Box::new(move || Box::pin(f())))
81 }
82
83 pub fn run(
87 self,
88 ) -> Result<
89 Option<Pin<Box<dyn Future<Output = Result<(), StartupHookError>> + Send>>>,
90 StartupHookError,
91 > {
92 match self {
93 Self::Sync(f) => f().map(|()| None),
94 Self::AsyncFactory(f) => Ok(Some(f())),
95 }
96 }
97}
98
99#[derive(Debug)]
101pub struct StartupHookError {
102 pub hook_name: Option<String>,
104 pub message: String,
106 pub abort: bool,
108}
109
110impl StartupHookError {
111 pub fn new(message: impl Into<String>) -> Self {
113 Self {
114 hook_name: None,
115 message: message.into(),
116 abort: true,
117 }
118 }
119
120 #[must_use]
122 pub fn with_hook_name(mut self, name: impl Into<String>) -> Self {
123 self.hook_name = Some(name.into());
124 self
125 }
126
127 #[must_use]
129 pub fn with_abort(mut self, abort: bool) -> Self {
130 self.abort = abort;
131 self
132 }
133
134 pub fn non_fatal(message: impl Into<String>) -> Self {
136 Self {
137 hook_name: None,
138 message: message.into(),
139 abort: false,
140 }
141 }
142}
143
144impl std::fmt::Display for StartupHookError {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 if let Some(name) = &self.hook_name {
147 write!(f, "Startup hook '{}' failed: {}", name, self.message)
148 } else {
149 write!(f, "Startup hook failed: {}", self.message)
150 }
151 }
152}
153
154impl std::error::Error for StartupHookError {}
155
156#[derive(Debug)]
158pub enum StartupOutcome {
159 Success,
161 PartialSuccess {
163 warnings: usize,
165 },
166 Aborted(StartupHookError),
168}
169
170impl StartupOutcome {
171 #[must_use]
173 pub fn can_proceed(&self) -> bool {
174 !matches!(self, Self::Aborted(_))
175 }
176
177 pub fn into_error(self) -> Option<StartupHookError> {
179 match self {
180 Self::Aborted(e) => Some(e),
181 _ => None,
182 }
183 }
184}
185
186pub type BoxHandler = Box<
190 dyn Fn(
191 &RequestContext,
192 &mut Request,
193 ) -> std::pin::Pin<Box<dyn Future<Output = Response> + Send>>
194 + Send
195 + Sync,
196>;
197
198pub type BoxWebSocketHandler = Box<
200 dyn Fn(
201 &RequestContext,
202 &mut Request,
203 crate::websocket::WebSocket,
204 ) -> std::pin::Pin<
205 Box<dyn Future<Output = Result<(), crate::websocket::WebSocketError>> + Send>,
206 > + Send
207 + Sync,
208>;
209
210#[derive(Clone)]
212pub struct RouteEntry {
213 pub method: Method,
215 pub path: String,
217 meta: Option<fastapi_router::Route>,
222 handler: Arc<BoxHandler>,
224}
225
226impl RouteEntry {
227 pub fn new<H, Fut>(method: Method, path: impl Into<String>, handler: H) -> Self
233 where
234 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
235 Fut: Future<Output = Response> + Send + 'static,
236 {
237 let handler: BoxHandler = Box::new(move |ctx, req| {
238 let fut = handler(ctx, req);
239 Box::pin(fut)
240 });
241 Self {
242 method,
243 path: path.into(),
244 meta: None,
245 handler: Arc::new(handler),
246 }
247 }
248
249 pub fn from_route<H, Fut>(route: fastapi_router::Route, handler: H) -> Self
254 where
255 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
256 Fut: Future<Output = Response> + Send + 'static,
257 {
258 let method = route.method;
259 let path = route.path.clone();
260 let mut entry = Self::new(method, path, handler);
261 entry.meta = Some(route);
262 entry
263 }
264
265 pub fn route_meta(&self) -> Option<&fastapi_router::Route> {
267 self.meta.as_ref()
268 }
269
270 pub async fn call(&self, ctx: &RequestContext, req: &mut Request) -> Response {
272 (self.handler)(ctx, req).await
273 }
274}
275
276impl std::fmt::Debug for RouteEntry {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 f.debug_struct("RouteEntry")
279 .field("method", &self.method)
280 .field("path", &self.path)
281 .field("meta", &self.meta.as_ref().map(|r| r.operation_id.as_str()))
282 .finish_non_exhaustive()
283 }
284}
285
286#[derive(Clone)]
288pub struct WebSocketRouteEntry {
289 pub path: String,
291 handler: Arc<BoxWebSocketHandler>,
293}
294
295impl WebSocketRouteEntry {
296 pub fn new<H, Fut>(path: impl Into<String>, handler: H) -> Self
298 where
299 H: Fn(&RequestContext, &mut Request, crate::websocket::WebSocket) -> Fut
300 + Send
301 + Sync
302 + 'static,
303 Fut: Future<Output = Result<(), crate::websocket::WebSocketError>> + Send + 'static,
304 {
305 let handler: BoxWebSocketHandler = Box::new(move |ctx, req, ws| {
306 let fut = handler(ctx, req, ws);
307 Box::pin(fut)
308 });
309 Self {
310 path: path.into(),
311 handler: Arc::new(handler),
312 }
313 }
314
315 pub async fn call(
317 &self,
318 ctx: &RequestContext,
319 req: &mut Request,
320 ws: crate::websocket::WebSocket,
321 ) -> Result<(), crate::websocket::WebSocketError> {
322 (self.handler)(ctx, req, ws).await
323 }
324}
325
326impl std::fmt::Debug for WebSocketRouteEntry {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 f.debug_struct("WebSocketRouteEntry")
329 .field("path", &self.path)
330 .finish_non_exhaustive()
331 }
332}
333
334#[derive(Default)]
339pub struct StateContainer {
340 state: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
341}
342
343impl StateContainer {
344 #[must_use]
346 pub fn new() -> Self {
347 Self {
348 state: HashMap::new(),
349 }
350 }
351
352 pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
356 self.state.insert(TypeId::of::<T>(), Arc::new(value));
357 }
358
359 pub fn get<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
361 self.state
362 .get(&TypeId::of::<T>())
363 .and_then(|v| Arc::clone(v).downcast::<T>().ok())
364 }
365
366 pub fn contains<T: 'static>(&self) -> bool {
368 self.state.contains_key(&TypeId::of::<T>())
369 }
370
371 pub fn len(&self) -> usize {
373 self.state.len()
374 }
375
376 pub fn is_empty(&self) -> bool {
378 self.state.is_empty()
379 }
380}
381
382impl std::fmt::Debug for StateContainer {
383 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384 f.debug_struct("StateContainer")
385 .field("count", &self.state.len())
386 .finish()
387 }
388}
389
390pub type BoxExceptionHandler = Box<
398 dyn Fn(&RequestContext, Box<dyn std::error::Error + Send + Sync>) -> Response + Send + Sync,
399>;
400
401#[derive(Default)]
436pub struct ExceptionHandlers {
437 handlers: HashMap<TypeId, BoxExceptionHandler>,
438}
439
440impl ExceptionHandlers {
441 #[must_use]
443 pub fn new() -> Self {
444 Self {
445 handlers: HashMap::new(),
446 }
447 }
448
449 #[must_use]
451 pub fn with_defaults() -> Self {
452 let mut handlers = Self::new();
453
454 handlers.register::<crate::HttpError>(|_ctx, err| {
456 use crate::IntoResponse;
457 err.into_response()
458 });
459
460 handlers.register::<crate::ValidationErrors>(|_ctx, err| {
462 use crate::IntoResponse;
463 err.into_response()
464 });
465
466 handlers
467 }
468
469 pub fn register<E>(
474 &mut self,
475 handler: impl Fn(&RequestContext, E) -> Response + Send + Sync + 'static,
476 ) where
477 E: std::error::Error + Send + Sync + 'static,
478 {
479 let boxed_handler: BoxExceptionHandler = Box::new(move |ctx, err| {
480 match err.downcast::<E>() {
482 Ok(typed_err) => handler(ctx, *typed_err),
483 Err(_) => {
484 Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
486 }
487 }
488 });
489 self.handlers.insert(TypeId::of::<E>(), boxed_handler);
490 }
491
492 #[must_use]
494 pub fn handler<E>(
495 mut self,
496 handler: impl Fn(&RequestContext, E) -> Response + Send + Sync + 'static,
497 ) -> Self
498 where
499 E: std::error::Error + Send + Sync + 'static,
500 {
501 self.register::<E>(handler);
502 self
503 }
504
505 pub fn handle<E>(&self, ctx: &RequestContext, err: E) -> Option<Response>
510 where
511 E: std::error::Error + Send + Sync + 'static,
512 {
513 let type_id = TypeId::of::<E>();
514 self.handlers
515 .get(&type_id)
516 .map(|handler| handler(ctx, Box::new(err)))
517 }
518
519 pub fn handle_or_default<E>(&self, ctx: &RequestContext, err: E) -> Response
521 where
522 E: std::error::Error + Send + Sync + 'static,
523 {
524 self.handle(ctx, err)
525 .unwrap_or_else(|| Response::with_status(StatusCode::INTERNAL_SERVER_ERROR))
526 }
527
528 pub fn has_handler<E: 'static>(&self) -> bool {
530 self.handlers.contains_key(&TypeId::of::<E>())
531 }
532
533 pub fn len(&self) -> usize {
535 self.handlers.len()
536 }
537
538 pub fn is_empty(&self) -> bool {
540 self.handlers.is_empty()
541 }
542
543 pub fn merge(&mut self, other: ExceptionHandlers) {
547 self.handlers.extend(other.handlers);
548 }
549}
550
551impl std::fmt::Debug for ExceptionHandlers {
552 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553 f.debug_struct("ExceptionHandlers")
554 .field("count", &self.handlers.len())
555 .finish()
556 }
557}
558
559#[derive(Debug, Clone)]
561pub struct AppConfig {
562 pub name: String,
564 pub version: String,
566 pub debug: bool,
568 pub root_path: String,
572 pub root_path_in_servers: bool,
574 pub trailing_slash_mode: crate::routing::TrailingSlashMode,
576 pub debug_config: crate::error::DebugConfig,
578 pub max_body_size: usize,
580 pub request_timeout_ms: u64,
582}
583
584impl Default for AppConfig {
585 fn default() -> Self {
586 Self {
587 name: String::from("fastapi_rust"),
588 version: String::from("0.1.0"),
589 debug: false,
590 root_path: String::new(),
591 root_path_in_servers: false,
592 trailing_slash_mode: crate::routing::TrailingSlashMode::Strict,
593 debug_config: crate::error::DebugConfig::default(),
594 max_body_size: 1024 * 1024, request_timeout_ms: 30_000, }
597 }
598}
599
600impl AppConfig {
601 #[must_use]
603 pub fn new() -> Self {
604 Self::default()
605 }
606
607 #[must_use]
609 pub fn name(mut self, name: impl Into<String>) -> Self {
610 self.name = name.into();
611 self
612 }
613
614 #[must_use]
616 pub fn version(mut self, version: impl Into<String>) -> Self {
617 self.version = version.into();
618 self
619 }
620
621 #[must_use]
623 pub fn debug(mut self, debug: bool) -> Self {
624 self.debug = debug;
625 self
626 }
627
628 #[must_use]
634 pub fn root_path(mut self, root_path: impl Into<String>) -> Self {
635 let mut rp = root_path.into();
636 while rp.ends_with('/') {
638 rp.pop();
639 }
640 self.root_path = rp;
641 self
642 }
643
644 #[must_use]
646 pub fn root_path_in_servers(mut self, enabled: bool) -> Self {
647 self.root_path_in_servers = enabled;
648 self
649 }
650
651 #[must_use]
653 pub fn trailing_slash_mode(mut self, mode: crate::routing::TrailingSlashMode) -> Self {
654 self.trailing_slash_mode = mode;
655 self
656 }
657
658 #[must_use]
660 pub fn debug_config(mut self, config: crate::error::DebugConfig) -> Self {
661 self.debug_config = config;
662 self
663 }
664
665 #[must_use]
667 pub fn max_body_size(mut self, size: usize) -> Self {
668 self.max_body_size = size;
669 self
670 }
671
672 #[must_use]
674 pub fn request_timeout_ms(mut self, timeout: u64) -> Self {
675 self.request_timeout_ms = timeout;
676 self
677 }
678}
679
680#[derive(Debug, Clone)]
702pub struct OpenApiConfig {
703 pub enabled: bool,
705 pub title: String,
707 pub version: String,
709 pub description: Option<String>,
711 pub openapi_path: String,
713 pub servers: Vec<(String, Option<String>)>,
715 pub tags: Vec<(String, Option<String>)>,
717}
718
719impl Default for OpenApiConfig {
720 fn default() -> Self {
721 Self {
722 enabled: true,
723 title: "FastAPI Rust".to_string(),
724 version: "0.1.0".to_string(),
725 description: None,
726 openapi_path: "/openapi.json".to_string(),
727 servers: Vec::new(),
728 tags: Vec::new(),
729 }
730 }
731}
732
733impl OpenApiConfig {
734 #[must_use]
736 pub fn new() -> Self {
737 Self::default()
738 }
739
740 #[must_use]
742 pub fn title(mut self, title: impl Into<String>) -> Self {
743 self.title = title.into();
744 self
745 }
746
747 #[must_use]
749 pub fn version(mut self, version: impl Into<String>) -> Self {
750 self.version = version.into();
751 self
752 }
753
754 #[must_use]
756 pub fn description(mut self, description: impl Into<String>) -> Self {
757 self.description = Some(description.into());
758 self
759 }
760
761 #[must_use]
763 pub fn path(mut self, path: impl Into<String>) -> Self {
764 self.openapi_path = path.into();
765 self
766 }
767
768 #[must_use]
770 pub fn server(mut self, url: impl Into<String>, description: Option<String>) -> Self {
771 self.servers.push((url.into(), description));
772 self
773 }
774
775 #[must_use]
777 pub fn tag(mut self, name: impl Into<String>, description: Option<String>) -> Self {
778 self.tags.push((name.into(), description));
779 self
780 }
781
782 #[must_use]
784 pub fn disable(mut self) -> Self {
785 self.enabled = false;
786 self
787 }
788}
789
790pub struct AppBuilder {
816 config: AppConfig,
817 routes: Vec<RouteEntry>,
818 ws_routes: Vec<WebSocketRouteEntry>,
819 middleware: Vec<Arc<dyn Middleware>>,
820 state: StateContainer,
821 exception_handlers: ExceptionHandlers,
822 startup_hooks: Vec<StartupHook>,
823 shutdown_hooks: Vec<Box<dyn FnOnce() + Send>>,
824 async_shutdown_hooks: Vec<Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>>,
825 openapi_config: Option<OpenApiConfig>,
826 docs_config: Option<crate::docs::DocsConfig>,
827}
828
829impl Default for AppBuilder {
830 fn default() -> Self {
831 Self {
832 config: AppConfig::default(),
833 routes: Vec::new(),
834 ws_routes: Vec::new(),
835 middleware: Vec::new(),
836 state: StateContainer::default(),
837 exception_handlers: ExceptionHandlers::default(),
838 startup_hooks: Vec::new(),
839 shutdown_hooks: Vec::new(),
840 async_shutdown_hooks: Vec::new(),
841 openapi_config: None,
842 docs_config: None,
843 }
844 }
845}
846
847impl AppBuilder {
848 #[must_use]
850 pub fn new() -> Self {
851 Self::default()
852 }
853
854 #[must_use]
856 pub fn config(mut self, config: AppConfig) -> Self {
857 self.config = config;
858 self
859 }
860
861 #[must_use]
877 pub fn openapi(mut self, config: OpenApiConfig) -> Self {
878 self.openapi_config = Some(config);
879 self
880 }
881
882 #[must_use]
891 pub fn enable_docs(mut self, mut config: crate::docs::DocsConfig) -> Self {
892 if config.title == crate::docs::DocsConfig::default().title {
894 config.title.clone_from(&self.config.name);
895 }
896
897 match self.openapi_config.take() {
899 Some(mut openapi) => {
900 openapi.openapi_path.clone_from(&config.openapi_path);
901 self.openapi_config = Some(openapi);
902 }
903 None => {
904 self.openapi_config = Some(
905 OpenApiConfig::new()
906 .title(self.config.name.clone())
907 .version(self.config.version.clone())
908 .path(config.openapi_path.clone()),
909 );
910 }
911 }
912
913 self.docs_config = Some(config);
914 self
915 }
916
917 #[must_use]
921 pub fn route<H, Fut>(mut self, path: impl Into<String>, method: Method, handler: H) -> Self
922 where
923 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
924 Fut: Future<Output = Response> + Send + 'static,
925 {
926 self.routes.push(RouteEntry::new(method, path, handler));
927 self
928 }
929
930 #[must_use]
935 pub fn route_entry(mut self, entry: RouteEntry) -> Self {
936 self.routes.push(entry);
937 self
938 }
939
940 #[must_use]
945 pub fn websocket<H, Fut>(mut self, path: impl Into<String>, handler: H) -> Self
946 where
947 H: Fn(&RequestContext, &mut Request, crate::websocket::WebSocket) -> Fut
948 + Send
949 + Sync
950 + 'static,
951 Fut: Future<Output = Result<(), crate::websocket::WebSocketError>> + Send + 'static,
952 {
953 self.ws_routes.push(WebSocketRouteEntry::new(path, handler));
954 self
955 }
956
957 #[must_use]
959 pub fn get<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
960 where
961 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
962 Fut: Future<Output = Response> + Send + 'static,
963 {
964 self.route(path, Method::Get, handler)
965 }
966
967 #[must_use]
969 pub fn post<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
970 where
971 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
972 Fut: Future<Output = Response> + Send + 'static,
973 {
974 self.route(path, Method::Post, handler)
975 }
976
977 #[must_use]
979 pub fn put<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
980 where
981 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
982 Fut: Future<Output = Response> + Send + 'static,
983 {
984 self.route(path, Method::Put, handler)
985 }
986
987 #[must_use]
989 pub fn delete<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
990 where
991 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
992 Fut: Future<Output = Response> + Send + 'static,
993 {
994 self.route(path, Method::Delete, handler)
995 }
996
997 #[must_use]
999 pub fn patch<H, Fut>(self, path: impl Into<String>, handler: H) -> Self
1000 where
1001 H: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
1002 Fut: Future<Output = Response> + Send + 'static,
1003 {
1004 self.route(path, Method::Patch, handler)
1005 }
1006
1007 #[must_use]
1013 pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> Self {
1014 self.middleware.push(Arc::new(middleware));
1015 self
1016 }
1017
1018 #[must_use]
1022 pub fn state<T: Send + Sync + 'static>(mut self, state: T) -> Self {
1023 self.state.insert(state);
1024 self
1025 }
1026
1027 #[must_use]
1055 pub fn exception_handler<E, H>(mut self, handler: H) -> Self
1056 where
1057 E: std::error::Error + Send + Sync + 'static,
1058 H: Fn(&RequestContext, E) -> Response + Send + Sync + 'static,
1059 {
1060 self.exception_handlers.register::<E>(handler);
1061 self
1062 }
1063
1064 #[must_use]
1068 pub fn exception_handlers(mut self, handlers: ExceptionHandlers) -> Self {
1069 self.exception_handlers = handlers;
1070 self
1071 }
1072
1073 #[must_use]
1079 pub fn with_default_exception_handlers(mut self) -> Self {
1080 self.exception_handlers = ExceptionHandlers::with_defaults();
1081 self
1082 }
1083
1084 #[must_use]
1108 pub fn on_startup<F>(mut self, hook: F) -> Self
1109 where
1110 F: FnOnce() -> Result<(), StartupHookError> + Send + 'static,
1111 {
1112 self.startup_hooks.push(StartupHook::Sync(Box::new(hook)));
1113 self
1114 }
1115
1116 #[must_use]
1131 pub fn on_startup_async<F, Fut>(mut self, hook: F) -> Self
1132 where
1133 F: FnOnce() -> Fut + Send + 'static,
1134 Fut: Future<Output = Result<(), StartupHookError>> + Send + 'static,
1135 {
1136 self.startup_hooks.push(StartupHook::AsyncFactory(Box::new(
1137 move || Box::pin(hook()),
1138 )));
1139 self
1140 }
1141
1142 #[must_use]
1160 pub fn on_shutdown<F>(mut self, hook: F) -> Self
1161 where
1162 F: FnOnce() + Send + 'static,
1163 {
1164 self.shutdown_hooks.push(Box::new(hook));
1165 self
1166 }
1167
1168 #[must_use]
1182 pub fn on_shutdown_async<F, Fut>(mut self, hook: F) -> Self
1183 where
1184 F: FnOnce() -> Fut + Send + 'static,
1185 Fut: Future<Output = ()> + Send + 'static,
1186 {
1187 self.async_shutdown_hooks
1188 .push(Box::new(move || Box::pin(hook())));
1189 self
1190 }
1191
1192 #[must_use]
1194 pub fn startup_hook_count(&self) -> usize {
1195 self.startup_hooks.len()
1196 }
1197
1198 #[must_use]
1200 pub fn shutdown_hook_count(&self) -> usize {
1201 self.shutdown_hooks.len() + self.async_shutdown_hooks.len()
1202 }
1203
1204 #[must_use]
1212 #[allow(clippy::too_many_lines)]
1213 pub fn build(mut self) -> App {
1214 let (openapi_spec, openapi_path) = if let Some(ref openapi_config) = self.openapi_config {
1216 if openapi_config.enabled {
1217 let spec = self.generate_openapi_spec(openapi_config);
1218 let spec_json =
1219 serde_json::to_string_pretty(&spec).unwrap_or_else(|_| "{}".to_string());
1220 (
1221 Some(Arc::new(spec_json)),
1222 Some(openapi_config.openapi_path.clone()),
1223 )
1224 } else {
1225 (None, None)
1226 }
1227 } else {
1228 (None, None)
1229 };
1230
1231 if let (Some(spec), Some(path)) = (&openapi_spec, &openapi_path) {
1233 let spec_clone = Arc::clone(spec);
1234 self.routes.push(RouteEntry::new(
1235 Method::Get,
1236 path.clone(),
1237 move |_ctx: &RequestContext, _req: &mut Request| {
1238 let spec = Arc::clone(&spec_clone);
1239 async move {
1240 Response::ok()
1241 .header("content-type", b"application/json".to_vec())
1242 .body(crate::response::ResponseBody::Bytes(
1243 spec.as_bytes().to_vec(),
1244 ))
1245 }
1246 },
1247 ));
1248 }
1249
1250 if let (Some(openapi_url), Some(docs_config)) = (openapi_path.clone(), self.docs_config) {
1254 let docs_config = Arc::new(docs_config);
1255 let openapi_url = Arc::new(openapi_url);
1256
1257 if let Some(docs_path) = docs_config.docs_path.clone() {
1258 let cfg = Arc::clone(&docs_config);
1259 let url = Arc::clone(&openapi_url);
1260 self.routes.push(RouteEntry::new(
1261 Method::Get,
1262 docs_path.clone(),
1263 move |_ctx: &RequestContext, _req: &mut Request| {
1264 let cfg = Arc::clone(&cfg);
1265 let url = Arc::clone(&url);
1266 async move { crate::docs::swagger_ui_response(&cfg, &url) }
1267 },
1268 ));
1269
1270 let docs_prefix = docs_path.trim_end_matches('/');
1272 let oauth2_redirect_path = if docs_prefix.is_empty() {
1273 "/oauth2-redirect".to_string()
1274 } else {
1275 format!("{docs_prefix}/oauth2-redirect")
1276 };
1277 self.routes.push(RouteEntry::new(
1278 Method::Get,
1279 oauth2_redirect_path,
1280 |_ctx: &RequestContext, _req: &mut Request| async move {
1281 crate::docs::oauth2_redirect_response()
1282 },
1283 ));
1284 }
1285
1286 if let Some(redoc_path) = docs_config.redoc_path.clone() {
1287 let cfg = Arc::clone(&docs_config);
1288 let url = Arc::clone(&openapi_url);
1289 self.routes.push(RouteEntry::new(
1290 Method::Get,
1291 redoc_path,
1292 move |_ctx: &RequestContext, _req: &mut Request| {
1293 let cfg = Arc::clone(&cfg);
1294 let url = Arc::clone(&url);
1295 async move { crate::docs::redoc_response(&cfg, &url) }
1296 },
1297 ));
1298 }
1299 }
1300
1301 let mut middleware_stack = MiddlewareStack::with_capacity(self.middleware.len());
1302 for mw in self.middleware {
1303 middleware_stack.push_arc(mw);
1304 }
1305
1306 let mut router = Router::new();
1308 for entry in &self.routes {
1309 let route = entry
1310 .route_meta()
1311 .cloned()
1312 .unwrap_or_else(|| Route::new(entry.method, &entry.path));
1313 router
1314 .add(route)
1315 .expect("route conflict during App::build()");
1316 }
1317
1318 let mut ws_router = Router::new();
1320 for entry in &self.ws_routes {
1321 ws_router
1322 .add(Route::new(Method::Get, &entry.path))
1323 .expect("websocket route conflict during App::build()");
1324 }
1325
1326 App {
1327 config: self.config,
1328 routes: self.routes,
1329 ws_routes: self.ws_routes,
1330 router,
1331 ws_router,
1332 middleware: middleware_stack,
1333 state: Arc::new(self.state),
1334 exception_handlers: Arc::new(self.exception_handlers),
1335 dependency_overrides: Arc::new(crate::dependency::DependencyOverrides::new()),
1336 startup_hooks: parking_lot::Mutex::new(self.startup_hooks),
1337 shutdown_hooks: parking_lot::Mutex::new(self.shutdown_hooks),
1338 async_shutdown_hooks: parking_lot::Mutex::new(self.async_shutdown_hooks),
1339 openapi_spec,
1340 }
1341 }
1342
1343 fn generate_openapi_spec(&self, config: &OpenApiConfig) -> fastapi_openapi::OpenApi {
1345 use fastapi_openapi::{OpenApiBuilder, Operation, Response as OAResponse};
1346 use std::collections::HashMap;
1347
1348 let mut builder = OpenApiBuilder::new(&config.title, &config.version);
1349
1350 if let Some(ref desc) = config.description {
1352 builder = builder.description(desc);
1353 }
1354
1355 for (url, desc) in &config.servers {
1357 builder = builder.server(url, desc.clone());
1358 }
1359
1360 for (name, desc) in &config.tags {
1362 builder = builder.tag(name, desc.clone());
1363 }
1364
1365 for entry in &self.routes {
1367 if let Some(route) = entry.route_meta() {
1368 builder.add_route(route);
1369 continue;
1370 }
1371
1372 let mut responses = HashMap::new();
1374 responses.insert(
1375 "200".to_string(),
1376 OAResponse {
1377 description: "Successful response".to_string(),
1378 content: HashMap::new(),
1379 },
1380 );
1381
1382 let operation = Operation {
1383 operation_id: Some(format!(
1384 "{}_{}",
1385 entry.method.as_str().to_lowercase(),
1386 entry
1387 .path
1388 .replace('/', "_")
1389 .replace(['{', '}'], "")
1390 .trim_matches('_')
1391 )),
1392 summary: None,
1393 description: None,
1394 tags: Vec::new(),
1395 parameters: Vec::new(),
1396 request_body: None,
1397 responses,
1398 deprecated: false,
1399 };
1400
1401 builder = builder.operation(entry.method.as_str(), &entry.path, operation);
1402 }
1403
1404 builder.build()
1405 }
1406}
1407
1408impl std::fmt::Debug for AppBuilder {
1409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1410 f.debug_struct("AppBuilder")
1411 .field("config", &self.config)
1412 .field("routes", &self.routes.len())
1413 .field("middleware", &self.middleware.len())
1414 .field("state", &self.state)
1415 .field("exception_handlers", &self.exception_handlers)
1416 .field("startup_hooks", &self.startup_hooks.len())
1417 .field("shutdown_hooks", &self.shutdown_hook_count())
1418 .finish()
1419 }
1420}
1421
1422pub struct App {
1427 config: AppConfig,
1428 routes: Vec<RouteEntry>,
1429 ws_routes: Vec<WebSocketRouteEntry>,
1430 router: Router,
1431 ws_router: Router,
1432 middleware: MiddlewareStack,
1433 state: Arc<StateContainer>,
1434 exception_handlers: Arc<ExceptionHandlers>,
1435 dependency_overrides: Arc<crate::dependency::DependencyOverrides>,
1436 startup_hooks: parking_lot::Mutex<Vec<StartupHook>>,
1437 shutdown_hooks: parking_lot::Mutex<Vec<Box<dyn FnOnce() + Send>>>,
1438 async_shutdown_hooks: parking_lot::Mutex<
1439 Vec<Box<dyn FnOnce() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send>>,
1440 >,
1441 openapi_spec: Option<Arc<String>>,
1443}
1444
1445impl App {
1446 #[must_use]
1448 pub fn builder() -> AppBuilder {
1449 AppBuilder::new()
1450 }
1451
1452 #[must_use]
1457 pub fn test_client(self: Arc<Self>) -> crate::testing::TestClient<Arc<Self>> {
1458 crate::testing::TestClient::new(self)
1459 }
1460
1461 #[must_use]
1463 pub fn test_client_with_seed(
1464 self: Arc<Self>,
1465 seed: u64,
1466 ) -> crate::testing::TestClient<Arc<Self>> {
1467 crate::testing::TestClient::with_seed(self, seed)
1468 }
1469
1470 #[must_use]
1472 pub fn config(&self) -> &AppConfig {
1473 &self.config
1474 }
1475
1476 #[must_use]
1478 pub fn route_count(&self) -> usize {
1479 self.routes.len()
1480 }
1481
1482 #[must_use]
1484 pub fn websocket_route_count(&self) -> usize {
1485 self.ws_routes.len()
1486 }
1487
1488 #[must_use]
1490 pub fn has_websocket_route(&self, path: &str) -> bool {
1491 matches!(
1492 self.ws_router.lookup(path, Method::Get),
1493 RouteLookup::Match(_)
1494 )
1495 }
1496
1497 pub fn routes(&self) -> impl Iterator<Item = (Method, &str)> {
1501 self.routes.iter().map(|r| (r.method, r.path.as_str()))
1502 }
1503
1504 #[must_use]
1518 pub fn openapi_spec(&self) -> Option<&str> {
1519 self.openapi_spec.as_ref().map(|s| s.as_str())
1520 }
1521
1522 #[must_use]
1524 pub fn state(&self) -> &Arc<StateContainer> {
1525 &self.state
1526 }
1527
1528 pub fn get_state<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
1530 self.state.get::<T>()
1531 }
1532
1533 #[must_use]
1535 pub fn exception_handlers(&self) -> &Arc<ExceptionHandlers> {
1536 &self.exception_handlers
1537 }
1538
1539 pub fn override_dependency_value<T>(&self, value: T)
1544 where
1545 T: crate::dependency::FromDependency,
1546 {
1547 self.dependency_overrides.insert_value(value);
1548 }
1549
1550 pub fn clear_dependency_overrides(&self) {
1552 self.dependency_overrides.clear();
1553 }
1554
1555 #[must_use]
1557 pub fn dependency_overrides(&self) -> Arc<crate::dependency::DependencyOverrides> {
1558 Arc::clone(&self.dependency_overrides)
1559 }
1560
1561 pub fn take_background_tasks(req: &mut Request) -> Option<crate::request::BackgroundTasks> {
1566 req.take_extension::<crate::request::BackgroundTasks>()
1567 }
1568
1569 pub fn handle_error<E>(&self, ctx: &RequestContext, err: E) -> Option<Response>
1574 where
1575 E: std::error::Error + Send + Sync + 'static,
1576 {
1577 self.exception_handlers.handle(ctx, err)
1578 }
1579
1580 pub fn handle_error_or_default<E>(&self, ctx: &RequestContext, err: E) -> Response
1582 where
1583 E: std::error::Error + Send + Sync + 'static,
1584 {
1585 self.exception_handlers.handle_or_default(ctx, err)
1586 }
1587
1588 pub async fn handle(&self, ctx: &RequestContext, req: &mut Request) -> Response {
1593 match self.router.lookup(req.path(), req.method()) {
1595 RouteLookup::Match(route_match) => {
1596 let entry = self.routes.iter().find(|e| {
1598 e.method == route_match.route.method && e.path == route_match.route.path
1599 });
1600
1601 let Some(entry) = entry else {
1602 return Response::with_status(StatusCode::INTERNAL_SERVER_ERROR);
1604 };
1605
1606 if !route_match.params.is_empty() {
1608 let path_params = crate::extract::PathParams::from_pairs(
1609 route_match
1610 .params
1611 .iter()
1612 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1613 .collect(),
1614 );
1615 req.insert_extension(path_params);
1616 }
1617
1618 let handler = RouteHandler { entry };
1620 self.middleware.execute(&handler, ctx, req).await
1621 }
1622 RouteLookup::MethodNotAllowed { allowed } => {
1623 if req.method() == Method::Options {
1626 let mut methods = allowed.methods().to_vec();
1627 if !methods.contains(&Method::Options) {
1628 methods.push(Method::Options);
1629 }
1630 let allow = fastapi_router::AllowedMethods::new(methods);
1631 Response::with_status(StatusCode::NO_CONTENT)
1632 .header("allow", allow.header_value().as_bytes().to_vec())
1633 } else {
1634 Response::with_status(StatusCode::METHOD_NOT_ALLOWED)
1635 .header("allow", allowed.header_value().as_bytes().to_vec())
1636 }
1637 }
1638 RouteLookup::NotFound => Response::with_status(StatusCode::NOT_FOUND),
1639 }
1640 }
1641
1642 pub async fn handle_websocket(
1647 &self,
1648 ctx: &RequestContext,
1649 req: &mut Request,
1650 ws: crate::websocket::WebSocket,
1651 ) -> Result<(), crate::websocket::WebSocketError> {
1652 match self.ws_router.lookup(req.path(), Method::Get) {
1653 RouteLookup::Match(route_match) => {
1654 let entry = self
1655 .ws_routes
1656 .iter()
1657 .find(|e| e.path == route_match.route.path);
1658 let Some(entry) = entry else {
1659 return Err(crate::websocket::WebSocketError::Protocol(
1660 "websocket route missing handler",
1661 ));
1662 };
1663
1664 if !route_match.params.is_empty() {
1665 let path_params = crate::extract::PathParams::from_pairs(
1666 route_match
1667 .params
1668 .iter()
1669 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
1670 .collect(),
1671 );
1672 req.insert_extension(path_params);
1673 }
1674
1675 entry.call(ctx, req, ws).await
1676 }
1677 _ => Err(crate::websocket::WebSocketError::Protocol(
1678 "no websocket route matched",
1679 )),
1680 }
1681 }
1682
1683 pub async fn run_startup_hooks(&self) -> StartupOutcome {
1700 let hooks: Vec<StartupHook> = std::mem::take(&mut *self.startup_hooks.lock());
1701 let mut warnings = 0;
1702
1703 for hook in hooks {
1704 match hook.run() {
1705 Ok(None) => {
1706 }
1708 Ok(Some(fut)) => {
1709 match fut.await {
1711 Ok(()) => {}
1712 Err(e) if e.abort => {
1713 return StartupOutcome::Aborted(e);
1714 }
1715 Err(_) => {
1716 warnings += 1;
1717 }
1718 }
1719 }
1720 Err(e) if e.abort => {
1721 return StartupOutcome::Aborted(e);
1722 }
1723 Err(_) => {
1724 warnings += 1;
1725 }
1726 }
1727 }
1728
1729 if warnings > 0 {
1730 StartupOutcome::PartialSuccess { warnings }
1731 } else {
1732 StartupOutcome::Success
1733 }
1734 }
1735
1736 pub async fn run_shutdown_hooks(&self) {
1743 let async_hooks: Vec<_> = std::mem::take(&mut *self.async_shutdown_hooks.lock());
1745 for hook in async_hooks.into_iter().rev() {
1746 let fut = hook();
1747 fut.await;
1748 }
1749
1750 let sync_hooks: Vec<_> = std::mem::take(&mut *self.shutdown_hooks.lock());
1752 for hook in sync_hooks.into_iter().rev() {
1753 hook();
1754 }
1755 }
1756
1757 pub fn transfer_shutdown_hooks(&self, controller: &ShutdownController) {
1764 let sync_hooks: Vec<_> = std::mem::take(&mut *self.shutdown_hooks.lock());
1766 for hook in sync_hooks {
1767 controller.register_hook(hook);
1768 }
1769
1770 let async_hooks: Vec<_> = std::mem::take(&mut *self.async_shutdown_hooks.lock());
1772 for hook in async_hooks {
1773 controller.register_async_hook(move || hook());
1774 }
1775 }
1776
1777 #[must_use]
1779 pub fn pending_startup_hooks(&self) -> usize {
1780 self.startup_hooks.lock().len()
1781 }
1782
1783 #[must_use]
1785 pub fn pending_shutdown_hooks(&self) -> usize {
1786 self.shutdown_hooks.lock().len() + self.async_shutdown_hooks.lock().len()
1787 }
1788}
1789
1790impl std::fmt::Debug for App {
1791 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1792 f.debug_struct("App")
1793 .field("config", &self.config)
1794 .field("routes", &self.routes.len())
1795 .field("middleware", &self.middleware.len())
1796 .field("state", &self.state)
1797 .field("exception_handlers", &self.exception_handlers)
1798 .field("startup_hooks", &self.startup_hooks.lock().len())
1799 .field("shutdown_hooks", &self.pending_shutdown_hooks())
1800 .finish()
1801 }
1802}
1803
1804impl Handler for App {
1806 fn call<'a>(
1807 &'a self,
1808 ctx: &'a RequestContext,
1809 req: &'a mut Request,
1810 ) -> BoxFuture<'a, Response> {
1811 Box::pin(async move { self.handle(ctx, req).await })
1812 }
1813
1814 fn dependency_overrides(&self) -> Option<Arc<crate::dependency::DependencyOverrides>> {
1815 Some(Arc::clone(&self.dependency_overrides))
1816 }
1817}
1818
1819struct RouteHandler<'a> {
1821 entry: &'a RouteEntry,
1822}
1823
1824impl<'a> Handler for RouteHandler<'a> {
1825 fn call<'b>(
1826 &'b self,
1827 ctx: &'b RequestContext,
1828 req: &'b mut Request,
1829 ) -> BoxFuture<'b, Response> {
1830 let handler = self.entry.handler.clone();
1831 Box::pin(async move { handler(ctx, req).await })
1832 }
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837 use super::*;
1838
1839 use crate::response::ResponseBody;
1840
1841 fn test_handler(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
1843 std::future::ready(Response::ok().body(ResponseBody::Bytes(b"Hello, World!".to_vec())))
1844 }
1845
1846 fn health_handler(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
1847 std::future::ready(Response::ok().body(ResponseBody::Bytes(b"OK".to_vec())))
1848 }
1849
1850 fn test_context() -> RequestContext {
1851 let cx = asupersync::Cx::for_testing();
1852 RequestContext::new(cx, 1)
1853 }
1854
1855 #[test]
1856 fn app_builder_creates_app() {
1857 let app = App::builder()
1858 .config(AppConfig::new().name("Test App"))
1859 .get("/", test_handler)
1860 .get("/health", health_handler)
1861 .build();
1862
1863 assert_eq!(app.route_count(), 2);
1864 assert_eq!(app.config().name, "Test App");
1865 }
1866
1867 #[test]
1868 fn app_config_builder() {
1869 let config = AppConfig::new()
1870 .name("My API")
1871 .version("1.0.0")
1872 .debug(true)
1873 .max_body_size(2 * 1024 * 1024)
1874 .request_timeout_ms(60_000);
1875
1876 assert_eq!(config.name, "My API");
1877 assert_eq!(config.version, "1.0.0");
1878 assert!(config.debug);
1879 assert_eq!(config.max_body_size, 2 * 1024 * 1024);
1880 assert_eq!(config.request_timeout_ms, 60_000);
1881 }
1882
1883 #[test]
1884 fn state_container_insert_and_get() {
1885 #[derive(Debug, PartialEq)]
1886 struct MyState {
1887 value: i32,
1888 }
1889
1890 let mut container = StateContainer::new();
1891 container.insert(MyState { value: 42 });
1892
1893 let state = container.get::<MyState>();
1894 assert!(state.is_some());
1895 assert_eq!(state.unwrap().value, 42);
1896 }
1897
1898 #[test]
1899 fn state_container_multiple_types() {
1900 struct TypeA(i32);
1901 struct TypeB(String);
1902
1903 let mut container = StateContainer::new();
1904 container.insert(TypeA(1));
1905 container.insert(TypeB("hello".to_string()));
1906
1907 assert!(container.contains::<TypeA>());
1908 assert!(container.contains::<TypeB>());
1909 assert!(!container.contains::<i64>());
1910
1911 assert_eq!(container.get::<TypeA>().unwrap().0, 1);
1912 assert_eq!(container.get::<TypeB>().unwrap().0, "hello");
1913 }
1914
1915 #[test]
1916 fn app_builder_with_state() {
1917 struct DbPool {
1918 connection_count: usize,
1919 }
1920
1921 let app = App::builder()
1922 .state(DbPool {
1923 connection_count: 10,
1924 })
1925 .get("/", test_handler)
1926 .build();
1927
1928 let pool = app.get_state::<DbPool>();
1929 assert!(pool.is_some());
1930 assert_eq!(pool.unwrap().connection_count, 10);
1931 }
1932
1933 #[test]
1934 fn app_handles_get_request() {
1935 let app = App::builder().get("/", test_handler).build();
1936
1937 let ctx = test_context();
1938 let mut req = Request::new(Method::Get, "/");
1939
1940 let response = futures_executor::block_on(app.handle(&ctx, &mut req));
1941 assert_eq!(response.status().as_u16(), 200);
1942 }
1943
1944 #[test]
1945 fn app_returns_404_for_unknown_path() {
1946 let app = App::builder().get("/", test_handler).build();
1947
1948 let ctx = test_context();
1949 let mut req = Request::new(Method::Get, "/unknown");
1950
1951 let response = futures_executor::block_on(app.handle(&ctx, &mut req));
1952 assert_eq!(response.status().as_u16(), 404);
1953 }
1954
1955 #[test]
1956 fn app_returns_405_for_wrong_method() {
1957 let app = App::builder().get("/", test_handler).build();
1958
1959 let ctx = test_context();
1960 let mut req = Request::new(Method::Post, "/");
1961
1962 let response = futures_executor::block_on(app.handle(&ctx, &mut req));
1963 assert_eq!(response.status().as_u16(), 405);
1964 }
1965
1966 #[test]
1967 fn app_builder_all_methods() {
1968 let app = App::builder()
1969 .get("/get", test_handler)
1970 .post("/post", test_handler)
1971 .put("/put", test_handler)
1972 .delete("/delete", test_handler)
1973 .patch("/patch", test_handler)
1974 .build();
1975
1976 assert_eq!(app.route_count(), 5);
1977 }
1978
1979 #[test]
1980 fn route_entry_debug() {
1981 let entry = RouteEntry::new(Method::Get, "/test", test_handler);
1982 let debug = format!("{:?}", entry);
1983 assert!(debug.contains("RouteEntry"));
1984 assert!(debug.contains("Get"));
1985 assert!(debug.contains("/test"));
1986 }
1987
1988 #[test]
1989 fn app_with_middleware() {
1990 use crate::middleware::NoopMiddleware;
1991
1992 let app = App::builder()
1993 .middleware(NoopMiddleware)
1994 .middleware(NoopMiddleware)
1995 .get("/", test_handler)
1996 .build();
1997
1998 let ctx = test_context();
1999 let mut req = Request::new(Method::Get, "/");
2000
2001 let response = futures_executor::block_on(app.handle(&ctx, &mut req));
2002 assert_eq!(response.status().as_u16(), 200);
2003 }
2004
2005 #[derive(Debug)]
2011 struct TestError {
2012 message: String,
2013 code: u32,
2014 }
2015
2016 impl std::fmt::Display for TestError {
2017 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2018 write!(f, "TestError({}): {}", self.code, self.message)
2019 }
2020 }
2021
2022 impl std::error::Error for TestError {}
2023
2024 #[derive(Debug)]
2026 struct AnotherError(String);
2027
2028 impl std::fmt::Display for AnotherError {
2029 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2030 write!(f, "AnotherError: {}", self.0)
2031 }
2032 }
2033
2034 impl std::error::Error for AnotherError {}
2035
2036 #[test]
2039 fn exception_handlers_new_is_empty() {
2040 let handlers = ExceptionHandlers::new();
2041 assert!(handlers.is_empty());
2042 assert_eq!(handlers.len(), 0);
2043 }
2044
2045 #[test]
2046 fn exception_handlers_register_single() {
2047 let mut handlers = ExceptionHandlers::new();
2048 handlers.register::<TestError>(|_ctx, err| {
2049 Response::with_status(StatusCode::BAD_REQUEST)
2050 .body(ResponseBody::Bytes(err.message.as_bytes().to_vec()))
2051 });
2052
2053 assert!(handlers.has_handler::<TestError>());
2054 assert!(!handlers.has_handler::<AnotherError>());
2055 assert_eq!(handlers.len(), 1);
2056 }
2057
2058 #[test]
2059 fn exception_handlers_register_multiple() {
2060 let mut handlers = ExceptionHandlers::new();
2061 handlers.register::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2062 handlers.register::<AnotherError>(|_ctx, _err| {
2063 Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2064 });
2065
2066 assert!(handlers.has_handler::<TestError>());
2067 assert!(handlers.has_handler::<AnotherError>());
2068 assert_eq!(handlers.len(), 2);
2069 }
2070
2071 #[test]
2072 fn exception_handlers_builder_pattern() {
2073 let handlers = ExceptionHandlers::new()
2074 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST))
2075 .handler::<AnotherError>(|_ctx, _err| {
2076 Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2077 });
2078
2079 assert!(handlers.has_handler::<TestError>());
2080 assert!(handlers.has_handler::<AnotherError>());
2081 assert_eq!(handlers.len(), 2);
2082 }
2083
2084 #[test]
2085 fn exception_handlers_with_defaults() {
2086 let handlers = ExceptionHandlers::with_defaults();
2087
2088 assert!(handlers.has_handler::<crate::HttpError>());
2089 assert!(handlers.has_handler::<crate::ValidationErrors>());
2090 assert_eq!(handlers.len(), 2);
2091 }
2092
2093 #[test]
2094 fn exception_handlers_merge() {
2095 let mut handlers1 = ExceptionHandlers::new()
2096 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2097
2098 let handlers2 = ExceptionHandlers::new().handler::<AnotherError>(|_ctx, _err| {
2099 Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2100 });
2101
2102 handlers1.merge(handlers2);
2103
2104 assert!(handlers1.has_handler::<TestError>());
2105 assert!(handlers1.has_handler::<AnotherError>());
2106 assert_eq!(handlers1.len(), 2);
2107 }
2108
2109 #[test]
2112 fn exception_handlers_handle_registered_error() {
2113 let handlers = ExceptionHandlers::new().handler::<TestError>(|_ctx, err| {
2114 Response::with_status(StatusCode::BAD_REQUEST)
2115 .body(ResponseBody::Bytes(err.message.as_bytes().to_vec()))
2116 });
2117
2118 let ctx = test_context();
2119 let err = TestError {
2120 message: "test error".into(),
2121 code: 42,
2122 };
2123
2124 let response = handlers.handle(&ctx, err);
2125 assert!(response.is_some());
2126
2127 let response = response.unwrap();
2128 assert_eq!(response.status().as_u16(), 400);
2129 }
2130
2131 #[test]
2132 fn exception_handlers_handle_unregistered_error() {
2133 let handlers = ExceptionHandlers::new()
2134 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2135
2136 let ctx = test_context();
2137 let err = AnotherError("unhandled".into());
2138
2139 let response = handlers.handle(&ctx, err);
2140 assert!(response.is_none());
2141 }
2142
2143 #[test]
2144 fn exception_handlers_handle_or_default_registered() {
2145 let handlers = ExceptionHandlers::new()
2146 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2147
2148 let ctx = test_context();
2149 let err = TestError {
2150 message: "test".into(),
2151 code: 1,
2152 };
2153
2154 let response = handlers.handle_or_default(&ctx, err);
2155 assert_eq!(response.status().as_u16(), 400);
2156 }
2157
2158 #[test]
2159 fn exception_handlers_handle_or_default_unregistered() {
2160 let handlers = ExceptionHandlers::new();
2161
2162 let ctx = test_context();
2163 let err = TestError {
2164 message: "test".into(),
2165 code: 1,
2166 };
2167
2168 let response = handlers.handle_or_default(&ctx, err);
2169 assert_eq!(response.status().as_u16(), 500);
2170 }
2171
2172 #[test]
2173 fn exception_handlers_error_values_passed_to_handler() {
2174 use std::sync::atomic::{AtomicU32, Ordering};
2175
2176 let captured_code = Arc::new(AtomicU32::new(0));
2177 let captured_code_clone = captured_code.clone();
2178
2179 let handlers = ExceptionHandlers::new().handler::<TestError>(move |_ctx, err| {
2180 captured_code_clone.store(err.code, Ordering::SeqCst);
2181 Response::with_status(StatusCode::BAD_REQUEST)
2182 });
2183
2184 let ctx = test_context();
2185 let err = TestError {
2186 message: "test".into(),
2187 code: 12345,
2188 };
2189
2190 let _ = handlers.handle(&ctx, err);
2191 assert_eq!(captured_code.load(Ordering::SeqCst), 12345);
2192 }
2193
2194 #[test]
2197 fn app_builder_exception_handler_single() {
2198 let app = App::builder()
2199 .exception_handler::<TestError, _>(|_ctx, err| {
2200 Response::with_status(StatusCode::BAD_REQUEST)
2201 .body(ResponseBody::Bytes(err.message.as_bytes().to_vec()))
2202 })
2203 .get("/", test_handler)
2204 .build();
2205
2206 assert!(app.exception_handlers().has_handler::<TestError>());
2207 }
2208
2209 #[test]
2210 fn app_builder_exception_handler_multiple() {
2211 let app = App::builder()
2212 .exception_handler::<TestError, _>(|_ctx, _err| {
2213 Response::with_status(StatusCode::BAD_REQUEST)
2214 })
2215 .exception_handler::<AnotherError, _>(|_ctx, _err| {
2216 Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2217 })
2218 .get("/", test_handler)
2219 .build();
2220
2221 assert!(app.exception_handlers().has_handler::<TestError>());
2222 assert!(app.exception_handlers().has_handler::<AnotherError>());
2223 }
2224
2225 #[test]
2226 fn app_builder_with_default_exception_handlers() {
2227 let app = App::builder()
2228 .with_default_exception_handlers()
2229 .get("/", test_handler)
2230 .build();
2231
2232 assert!(app.exception_handlers().has_handler::<crate::HttpError>());
2233 assert!(
2234 app.exception_handlers()
2235 .has_handler::<crate::ValidationErrors>()
2236 );
2237 }
2238
2239 #[test]
2240 fn app_builder_exception_handlers_registry() {
2241 let handlers = ExceptionHandlers::new()
2242 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST))
2243 .handler::<AnotherError>(|_ctx, _err| {
2244 Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
2245 });
2246
2247 let app = App::builder()
2248 .exception_handlers(handlers)
2249 .get("/", test_handler)
2250 .build();
2251
2252 assert!(app.exception_handlers().has_handler::<TestError>());
2253 assert!(app.exception_handlers().has_handler::<AnotherError>());
2254 }
2255
2256 #[test]
2257 fn app_handle_error_registered() {
2258 let app = App::builder()
2259 .exception_handler::<TestError, _>(|_ctx, _err| {
2260 Response::with_status(StatusCode::BAD_REQUEST)
2261 })
2262 .get("/", test_handler)
2263 .build();
2264
2265 let ctx = test_context();
2266 let err = TestError {
2267 message: "test".into(),
2268 code: 1,
2269 };
2270
2271 let response = app.handle_error(&ctx, err);
2272 assert!(response.is_some());
2273 assert_eq!(response.unwrap().status().as_u16(), 400);
2274 }
2275
2276 #[test]
2277 fn app_handle_error_unregistered() {
2278 let app = App::builder().get("/", test_handler).build();
2279
2280 let ctx = test_context();
2281 let err = TestError {
2282 message: "test".into(),
2283 code: 1,
2284 };
2285
2286 let response = app.handle_error(&ctx, err);
2287 assert!(response.is_none());
2288 }
2289
2290 #[test]
2291 fn app_handle_error_or_default() {
2292 let app = App::builder().get("/", test_handler).build();
2293
2294 let ctx = test_context();
2295 let err = TestError {
2296 message: "test".into(),
2297 code: 1,
2298 };
2299
2300 let response = app.handle_error_or_default(&ctx, err);
2301 assert_eq!(response.status().as_u16(), 500);
2302 }
2303
2304 #[test]
2307 fn exception_handlers_override_on_register() {
2308 let handlers = ExceptionHandlers::new()
2309 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST))
2310 .handler::<TestError>(|_ctx, _err| {
2311 Response::with_status(StatusCode::UNPROCESSABLE_ENTITY)
2312 });
2313
2314 assert_eq!(handlers.len(), 1);
2316
2317 let ctx = test_context();
2318 let err = TestError {
2319 message: "test".into(),
2320 code: 1,
2321 };
2322
2323 let response = handlers.handle(&ctx, err);
2325 assert!(response.is_some());
2326 assert_eq!(response.unwrap().status().as_u16(), 422);
2327 }
2328
2329 #[test]
2330 fn exception_handlers_merge_overrides() {
2331 let mut handlers1 = ExceptionHandlers::new()
2332 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2333
2334 let handlers2 = ExceptionHandlers::new().handler::<TestError>(|_ctx, _err| {
2335 Response::with_status(StatusCode::UNPROCESSABLE_ENTITY)
2336 });
2337
2338 handlers1.merge(handlers2);
2339
2340 assert_eq!(handlers1.len(), 1);
2342
2343 let ctx = test_context();
2344 let err = TestError {
2345 message: "test".into(),
2346 code: 1,
2347 };
2348
2349 let response = handlers1.handle(&ctx, err);
2351 assert!(response.is_some());
2352 assert_eq!(response.unwrap().status().as_u16(), 422);
2353 }
2354
2355 #[test]
2356 fn exception_handlers_override_default_http_error() {
2357 let mut handlers = ExceptionHandlers::with_defaults();
2359
2360 handlers.register::<crate::HttpError>(|_ctx, err| {
2362 let detail = err.detail.as_deref().unwrap_or("Unknown error");
2364 Response::with_status(err.status)
2365 .header("x-custom-error", b"true".to_vec())
2366 .body(ResponseBody::Bytes(detail.as_bytes().to_vec()))
2367 });
2368
2369 assert_eq!(handlers.len(), 2);
2371
2372 let ctx = test_context();
2373 let err = crate::HttpError::bad_request().with_detail("test error");
2374
2375 let response = handlers.handle(&ctx, err);
2376 assert!(response.is_some());
2377
2378 let response = response.unwrap();
2379 assert_eq!(response.status().as_u16(), 400);
2380
2381 let custom_header = response
2383 .headers()
2384 .iter()
2385 .find(|(name, _)| name.eq_ignore_ascii_case("x-custom-error"))
2386 .map(|(_, v)| v.as_slice());
2387 assert_eq!(custom_header, Some(b"true".as_slice()));
2388 }
2389
2390 #[test]
2391 fn exception_handlers_override_default_validation_errors() {
2392 let mut handlers = ExceptionHandlers::with_defaults();
2394
2395 handlers.register::<crate::ValidationErrors>(|_ctx, errs| {
2397 Response::with_status(StatusCode::BAD_REQUEST)
2399 .header("x-error-count", errs.len().to_string().as_bytes().to_vec())
2400 });
2401
2402 let ctx = test_context();
2403 let mut errs = crate::ValidationErrors::new();
2404 errs.push(crate::ValidationError::missing(
2405 crate::error::loc::body_field("name"),
2406 ));
2407 errs.push(crate::ValidationError::missing(
2408 crate::error::loc::body_field("email"),
2409 ));
2410
2411 let response = handlers.handle(&ctx, errs);
2412 assert!(response.is_some());
2413
2414 let response = response.unwrap();
2415 assert_eq!(response.status().as_u16(), 400);
2417
2418 let count_header = response
2420 .headers()
2421 .iter()
2422 .find(|(name, _)| name.eq_ignore_ascii_case("x-error-count"))
2423 .map(|(_, v)| v.as_slice());
2424 assert_eq!(count_header, Some(b"2".as_slice()));
2425 }
2426
2427 #[test]
2428 fn exception_handlers_debug_format() {
2429 let handlers = ExceptionHandlers::new()
2430 .handler::<TestError>(|_ctx, _err| Response::with_status(StatusCode::BAD_REQUEST));
2431
2432 let debug = format!("{:?}", handlers);
2433 assert!(debug.contains("ExceptionHandlers"));
2434 assert!(debug.contains("count"));
2435 assert!(debug.contains("1"));
2436 }
2437
2438 #[test]
2439 fn app_debug_includes_exception_handlers() {
2440 let app = App::builder()
2441 .exception_handler::<TestError, _>(|_ctx, _err| {
2442 Response::with_status(StatusCode::BAD_REQUEST)
2443 })
2444 .get("/", test_handler)
2445 .build();
2446
2447 let debug = format!("{:?}", app);
2448 assert!(debug.contains("exception_handlers"));
2449 }
2450
2451 #[test]
2452 fn app_builder_debug_includes_exception_handlers() {
2453 let builder = App::builder().exception_handler::<TestError, _>(|_ctx, _err| {
2454 Response::with_status(StatusCode::BAD_REQUEST)
2455 });
2456
2457 let debug = format!("{:?}", builder);
2458 assert!(debug.contains("exception_handlers"));
2459 }
2460
2461 #[test]
2468 fn app_builder_startup_hook_registration() {
2469 let builder = App::builder().on_startup(|| Ok(())).on_startup(|| Ok(()));
2470
2471 assert_eq!(builder.startup_hook_count(), 2);
2472 }
2473
2474 #[test]
2475 fn app_builder_shutdown_hook_registration() {
2476 let builder = App::builder().on_shutdown(|| {}).on_shutdown(|| {});
2477
2478 assert_eq!(builder.shutdown_hook_count(), 2);
2479 }
2480
2481 #[test]
2482 fn app_builder_mixed_hooks() {
2483 let builder = App::builder()
2484 .on_startup(|| Ok(()))
2485 .on_shutdown(|| {})
2486 .on_startup(|| Ok(()))
2487 .on_shutdown(|| {});
2488
2489 assert_eq!(builder.startup_hook_count(), 2);
2490 assert_eq!(builder.shutdown_hook_count(), 2);
2491 }
2492
2493 #[test]
2494 fn app_pending_hooks_count() {
2495 let app = App::builder()
2496 .on_startup(|| Ok(()))
2497 .on_startup(|| Ok(()))
2498 .on_shutdown(|| {})
2499 .get("/", test_handler)
2500 .build();
2501
2502 assert_eq!(app.pending_startup_hooks(), 2);
2503 assert_eq!(app.pending_shutdown_hooks(), 1);
2504 }
2505
2506 #[test]
2509 fn startup_hooks_run_in_fifo_order() {
2510 let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
2511
2512 let order1 = Arc::clone(&order);
2513 let order2 = Arc::clone(&order);
2514 let order3 = Arc::clone(&order);
2515
2516 let app = App::builder()
2517 .on_startup(move || {
2518 order1.lock().push(1);
2519 Ok(())
2520 })
2521 .on_startup(move || {
2522 order2.lock().push(2);
2523 Ok(())
2524 })
2525 .on_startup(move || {
2526 order3.lock().push(3);
2527 Ok(())
2528 })
2529 .get("/", test_handler)
2530 .build();
2531
2532 let outcome = futures_executor::block_on(app.run_startup_hooks());
2533 assert!(outcome.can_proceed());
2534
2535 assert_eq!(*order.lock(), vec![1, 2, 3]);
2537
2538 assert_eq!(app.pending_startup_hooks(), 0);
2540 }
2541
2542 #[test]
2545 fn shutdown_hooks_run_in_lifo_order() {
2546 let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
2547
2548 let order1 = Arc::clone(&order);
2549 let order2 = Arc::clone(&order);
2550 let order3 = Arc::clone(&order);
2551
2552 let app = App::builder()
2553 .on_shutdown(move || {
2554 order1.lock().push(1);
2555 })
2556 .on_shutdown(move || {
2557 order2.lock().push(2);
2558 })
2559 .on_shutdown(move || {
2560 order3.lock().push(3);
2561 })
2562 .get("/", test_handler)
2563 .build();
2564
2565 futures_executor::block_on(app.run_shutdown_hooks());
2566
2567 assert_eq!(*order.lock(), vec![3, 2, 1]);
2569
2570 assert_eq!(app.pending_shutdown_hooks(), 0);
2572 }
2573
2574 #[test]
2577 fn startup_hooks_success_outcome() {
2578 let app = App::builder()
2579 .on_startup(|| Ok(()))
2580 .on_startup(|| Ok(()))
2581 .get("/", test_handler)
2582 .build();
2583
2584 let outcome = futures_executor::block_on(app.run_startup_hooks());
2585 assert!(matches!(outcome, StartupOutcome::Success));
2586 assert!(outcome.can_proceed());
2587 }
2588
2589 #[test]
2592 fn startup_hooks_fatal_error_aborts() {
2593 let app = App::builder()
2594 .on_startup(|| Ok(()))
2595 .on_startup(|| Err(StartupHookError::new("database connection failed")))
2596 .on_startup(|| Ok(())) .get("/", test_handler)
2598 .build();
2599
2600 let outcome = futures_executor::block_on(app.run_startup_hooks());
2601 assert!(!outcome.can_proceed());
2602
2603 if let StartupOutcome::Aborted(err) = outcome {
2604 assert!(err.message.contains("database connection failed"));
2605 assert!(err.abort);
2606 } else {
2607 panic!("Expected Aborted outcome");
2608 }
2609 }
2610
2611 #[test]
2614 fn startup_hooks_non_fatal_error_continues() {
2615 let app = App::builder()
2616 .on_startup(|| Ok(()))
2617 .on_startup(|| Err(StartupHookError::non_fatal("optional feature unavailable")))
2618 .on_startup(|| Ok(())) .get("/", test_handler)
2620 .build();
2621
2622 let outcome = futures_executor::block_on(app.run_startup_hooks());
2623 assert!(outcome.can_proceed());
2624
2625 if let StartupOutcome::PartialSuccess { warnings } = outcome {
2626 assert_eq!(warnings, 1);
2627 } else {
2628 panic!("Expected PartialSuccess outcome");
2629 }
2630 }
2631
2632 #[test]
2635 fn startup_hook_error_builder() {
2636 let err = StartupHookError::new("test error")
2637 .with_hook_name("database_init")
2638 .with_abort(false);
2639
2640 assert_eq!(err.hook_name.as_deref(), Some("database_init"));
2641 assert_eq!(err.message, "test error");
2642 assert!(!err.abort);
2643 }
2644
2645 #[test]
2646 fn startup_hook_error_display() {
2647 let err = StartupHookError::new("connection failed").with_hook_name("redis_init");
2648
2649 let display = format!("{}", err);
2650 assert!(display.contains("redis_init"));
2651 assert!(display.contains("connection failed"));
2652 }
2653
2654 #[test]
2655 fn startup_hook_error_non_fatal() {
2656 let err = StartupHookError::non_fatal("optional feature");
2657 assert!(!err.abort);
2658 }
2659
2660 #[test]
2663 fn transfer_shutdown_hooks_to_controller() {
2664 let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
2665
2666 let order1 = Arc::clone(&order);
2667 let order2 = Arc::clone(&order);
2668
2669 let app = App::builder()
2670 .on_shutdown(move || {
2671 order1.lock().push(1);
2672 })
2673 .on_shutdown(move || {
2674 order2.lock().push(2);
2675 })
2676 .get("/", test_handler)
2677 .build();
2678
2679 let controller = ShutdownController::new();
2680 app.transfer_shutdown_hooks(&controller);
2681
2682 assert_eq!(app.pending_shutdown_hooks(), 0);
2684
2685 assert_eq!(controller.hook_count(), 2);
2687
2688 while let Some(hook) = controller.pop_hook() {
2690 hook.run();
2691 }
2692
2693 assert_eq!(*order.lock(), vec![2, 1]);
2695 }
2696
2697 #[test]
2700 fn app_debug_includes_hooks() {
2701 let app = App::builder()
2702 .on_startup(|| Ok(()))
2703 .on_shutdown(|| {})
2704 .get("/", test_handler)
2705 .build();
2706
2707 let debug = format!("{:?}", app);
2708 assert!(debug.contains("startup_hooks"));
2709 assert!(debug.contains("shutdown_hooks"));
2710 }
2711
2712 #[test]
2713 fn app_builder_debug_includes_hooks() {
2714 let builder = App::builder().on_startup(|| Ok(())).on_shutdown(|| {});
2715
2716 let debug = format!("{:?}", builder);
2717 assert!(debug.contains("startup_hooks"));
2718 assert!(debug.contains("shutdown_hooks"));
2719 }
2720
2721 #[test]
2724 fn startup_outcome_success() {
2725 let outcome = StartupOutcome::Success;
2726 assert!(outcome.can_proceed());
2727 assert!(outcome.into_error().is_none());
2728 }
2729
2730 #[test]
2731 fn startup_outcome_partial_success() {
2732 let outcome = StartupOutcome::PartialSuccess { warnings: 2 };
2733 assert!(outcome.can_proceed());
2734 assert!(outcome.into_error().is_none());
2735 }
2736
2737 #[test]
2738 fn startup_outcome_aborted() {
2739 let err = StartupHookError::new("fatal");
2740 let outcome = StartupOutcome::Aborted(err);
2741 assert!(!outcome.can_proceed());
2742
2743 let err = outcome.into_error();
2744 assert!(err.is_some());
2745 assert_eq!(err.unwrap().message, "fatal");
2746 }
2747
2748 #[test]
2751 fn startup_hooks_multiple_non_fatal_errors() {
2752 let app = App::builder()
2753 .on_startup(|| Err(StartupHookError::non_fatal("warning 1")))
2754 .on_startup(|| Ok(()))
2755 .on_startup(|| Err(StartupHookError::non_fatal("warning 2")))
2756 .on_startup(|| Err(StartupHookError::non_fatal("warning 3")))
2757 .get("/", test_handler)
2758 .build();
2759
2760 let outcome = futures_executor::block_on(app.run_startup_hooks());
2761 assert!(outcome.can_proceed());
2762
2763 if let StartupOutcome::PartialSuccess { warnings } = outcome {
2764 assert_eq!(warnings, 3);
2765 } else {
2766 panic!("Expected PartialSuccess");
2767 }
2768 }
2769
2770 #[test]
2773 fn empty_startup_hooks() {
2774 let app = App::builder().get("/", test_handler).build();
2775
2776 let outcome = futures_executor::block_on(app.run_startup_hooks());
2777 assert!(matches!(outcome, StartupOutcome::Success));
2778 }
2779
2780 #[test]
2781 fn empty_shutdown_hooks() {
2782 let app = App::builder().get("/", test_handler).build();
2783
2784 futures_executor::block_on(app.run_shutdown_hooks());
2786 }
2787
2788 #[test]
2791 fn startup_hooks_consumed_after_run() {
2792 let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
2793 let counter_clone = Arc::clone(&counter);
2794
2795 let app = App::builder()
2796 .on_startup(move || {
2797 counter_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2798 Ok(())
2799 })
2800 .get("/", test_handler)
2801 .build();
2802
2803 futures_executor::block_on(app.run_startup_hooks());
2805 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2806
2807 futures_executor::block_on(app.run_startup_hooks());
2809 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2810 }
2811
2812 #[test]
2813 fn shutdown_hooks_consumed_after_run() {
2814 let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
2815 let counter_clone = Arc::clone(&counter);
2816
2817 let app = App::builder()
2818 .on_shutdown(move || {
2819 counter_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2820 })
2821 .get("/", test_handler)
2822 .build();
2823
2824 futures_executor::block_on(app.run_shutdown_hooks());
2826 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2827
2828 futures_executor::block_on(app.run_shutdown_hooks());
2830 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
2831 }
2832
2833 #[test]
2834 fn root_path_strips_trailing_slashes() {
2835 let config = AppConfig::new().root_path("/api/");
2836 assert_eq!(config.root_path, "/api");
2837
2838 let config = AppConfig::new().root_path("/api///");
2839 assert_eq!(config.root_path, "/api");
2840
2841 let config = AppConfig::new().root_path("/api");
2842 assert_eq!(config.root_path, "/api");
2843
2844 let config = AppConfig::new().root_path("");
2846 assert_eq!(config.root_path, "");
2847 }
2848}