1use crate::error::Result;
4use crate::interceptor::{InterceptorChain, RequestInterceptor, ResponseInterceptor};
5use crate::middleware::{BodyLimitLayer, LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT};
6use crate::response::IntoResponse;
7use crate::router::{MethodRouter, Router};
8use crate::server::Server;
9use std::collections::HashMap;
10use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
11
12pub struct RustApi {
30 router: Router,
31 openapi_spec: rustapi_openapi::OpenApiSpec,
32 layers: LayerStack,
33 body_limit: Option<usize>,
34 interceptors: InterceptorChain,
35 #[cfg(feature = "http3")]
36 http3_config: Option<crate::http3::Http3Config>,
37}
38
39impl RustApi {
40 pub fn new() -> Self {
42 let _ = tracing_subscriber::registry()
44 .with(
45 EnvFilter::try_from_default_env()
46 .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
47 )
48 .with(tracing_subscriber::fmt::layer())
49 .try_init();
50
51 Self {
52 router: Router::new(),
53 openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
54 .register::<rustapi_openapi::ErrorSchema>()
55 .register::<rustapi_openapi::ErrorBodySchema>()
56 .register::<rustapi_openapi::ValidationErrorSchema>()
57 .register::<rustapi_openapi::ValidationErrorBodySchema>()
58 .register::<rustapi_openapi::FieldErrorSchema>(),
59 layers: LayerStack::new(),
60 body_limit: Some(DEFAULT_BODY_LIMIT), interceptors: InterceptorChain::new(),
62 #[cfg(feature = "http3")]
63 http3_config: None,
64 }
65 }
66
67 #[cfg(feature = "swagger-ui")]
91 pub fn auto() -> Self {
92 Self::new().mount_auto_routes_grouped().docs("/docs")
94 }
95
96 #[cfg(not(feature = "swagger-ui"))]
101 pub fn auto() -> Self {
102 Self::new().mount_auto_routes_grouped()
103 }
104
105 pub fn config() -> RustApiConfig {
123 RustApiConfig::new()
124 }
125
126 pub fn body_limit(mut self, limit: usize) -> Self {
147 self.body_limit = Some(limit);
148 self
149 }
150
151 pub fn no_body_limit(mut self) -> Self {
164 self.body_limit = None;
165 self
166 }
167
168 pub fn layer<L>(mut self, layer: L) -> Self
188 where
189 L: MiddlewareLayer,
190 {
191 self.layers.push(Box::new(layer));
192 self
193 }
194
195 pub fn request_interceptor<I>(mut self, interceptor: I) -> Self
227 where
228 I: RequestInterceptor,
229 {
230 self.interceptors.add_request_interceptor(interceptor);
231 self
232 }
233
234 pub fn response_interceptor<I>(mut self, interceptor: I) -> Self
266 where
267 I: ResponseInterceptor,
268 {
269 self.interceptors.add_response_interceptor(interceptor);
270 self
271 }
272
273 pub fn state<S>(self, _state: S) -> Self
289 where
290 S: Clone + Send + Sync + 'static,
291 {
292 let state = _state;
294 let mut app = self;
295 app.router = app.router.state(state);
296 app
297 }
298
299 pub fn register_schema<T: for<'a> rustapi_openapi::Schema<'a>>(mut self) -> Self {
311 self.openapi_spec = self.openapi_spec.register::<T>();
312 self
313 }
314
315 pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
317 self.openapi_spec.info.title = title.to_string();
320 self.openapi_spec.info.version = version.to_string();
321 self.openapi_spec.info.description = description.map(|d| d.to_string());
322 self
323 }
324
325 pub fn openapi_spec(&self) -> &rustapi_openapi::OpenApiSpec {
327 &self.openapi_spec
328 }
329
330 fn mount_auto_routes_grouped(mut self) -> Self {
331 let routes = crate::auto_route::collect_auto_routes();
332 let mut by_path: HashMap<String, MethodRouter> = HashMap::new();
333
334 for route in routes {
335 let method_enum = match route.method {
336 "GET" => http::Method::GET,
337 "POST" => http::Method::POST,
338 "PUT" => http::Method::PUT,
339 "DELETE" => http::Method::DELETE,
340 "PATCH" => http::Method::PATCH,
341 _ => http::Method::GET,
342 };
343
344 let path = if route.path.starts_with('/') {
345 route.path.to_string()
346 } else {
347 format!("/{}", route.path)
348 };
349
350 let entry = by_path.entry(path).or_default();
351 entry.insert_boxed_with_operation(method_enum, route.handler, route.operation);
352 }
353
354 #[cfg(feature = "tracing")]
355 let route_count: usize = by_path.values().map(|mr| mr.allowed_methods().len()).sum();
356 #[cfg(feature = "tracing")]
357 let path_count = by_path.len();
358
359 for (path, method_router) in by_path {
360 self = self.route(&path, method_router);
361 }
362
363 crate::trace_info!(
364 paths = path_count,
365 routes = route_count,
366 "Auto-registered routes"
367 );
368
369 crate::auto_schema::apply_auto_schemas(&mut self.openapi_spec);
371
372 self
373 }
374
375 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
386 for (method, op) in &method_router.operations {
388 let mut op = op.clone();
389 add_path_params_to_operation(path, &mut op, &std::collections::HashMap::new());
390 self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op);
391 }
392
393 self.router = self.router.route(path, method_router);
394 self
395 }
396
397 pub fn typed<P: crate::typed_path::TypedPath>(self, method_router: MethodRouter) -> Self {
399 self.route(P::PATH, method_router)
400 }
401
402 #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
406 pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
407 self.route(path, method_router)
408 }
409
410 pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
428 let method_enum = match route.method {
429 "GET" => http::Method::GET,
430 "POST" => http::Method::POST,
431 "PUT" => http::Method::PUT,
432 "DELETE" => http::Method::DELETE,
433 "PATCH" => http::Method::PATCH,
434 _ => http::Method::GET,
435 };
436
437 let mut op = route.operation;
439 add_path_params_to_operation(route.path, &mut op, &route.param_schemas);
440 self.openapi_spec = self.openapi_spec.path(route.path, route.method, op);
441
442 self.route_with_method(route.path, method_enum, route.handler)
443 }
444
445 fn route_with_method(
447 self,
448 path: &str,
449 method: http::Method,
450 handler: crate::handler::BoxedHandler,
451 ) -> Self {
452 use crate::router::MethodRouter;
453 let path = if !path.starts_with('/') {
462 format!("/{}", path)
463 } else {
464 path.to_string()
465 };
466
467 let mut handlers = std::collections::HashMap::new();
476 handlers.insert(method, handler);
477
478 let method_router = MethodRouter::from_boxed(handlers);
479 self.route(&path, method_router)
480 }
481
482 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
498 let normalized_prefix = normalize_prefix_for_openapi(prefix);
500
501 for (matchit_path, method_router) in router.method_routers() {
504 let display_path = router
506 .registered_routes()
507 .get(matchit_path)
508 .map(|info| info.path.clone())
509 .unwrap_or_else(|| matchit_path.clone());
510
511 let prefixed_path = if display_path == "/" {
513 normalized_prefix.clone()
514 } else {
515 format!("{}{}", normalized_prefix, display_path)
516 };
517
518 for (method, op) in &method_router.operations {
520 let mut op = op.clone();
521 add_path_params_to_operation(
522 &prefixed_path,
523 &mut op,
524 &std::collections::HashMap::new(),
525 );
526 self.openapi_spec = self.openapi_spec.path(&prefixed_path, method.as_str(), op);
527 }
528 }
529
530 self.router = self.router.nest(prefix, router);
532 self
533 }
534
535 pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
564 self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
565 }
566
567 pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
584 use crate::router::MethodRouter;
585 use std::collections::HashMap;
586
587 let prefix = config.prefix.clone();
588 let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
589
590 let handler: crate::handler::BoxedHandler =
592 std::sync::Arc::new(move |req: crate::Request| {
593 let config = config.clone();
594 let path = req.uri().path().to_string();
595
596 Box::pin(async move {
597 let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
598
599 match crate::static_files::StaticFile::serve(relative_path, &config).await {
600 Ok(response) => response,
601 Err(err) => err.into_response(),
602 }
603 })
604 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
605 });
606
607 let mut handlers = HashMap::new();
608 handlers.insert(http::Method::GET, handler);
609 let method_router = MethodRouter::from_boxed(handlers);
610
611 self.route(&catch_all_path, method_router)
612 }
613
614 #[cfg(feature = "compression")]
631 pub fn compression(self) -> Self {
632 self.layer(crate::middleware::CompressionLayer::new())
633 }
634
635 #[cfg(feature = "compression")]
651 pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
652 self.layer(crate::middleware::CompressionLayer::with_config(config))
653 }
654
655 #[cfg(feature = "swagger-ui")]
679 pub fn docs(self, path: &str) -> Self {
680 let title = self.openapi_spec.info.title.clone();
681 let version = self.openapi_spec.info.version.clone();
682 let description = self.openapi_spec.info.description.clone();
683
684 self.docs_with_info(path, &title, &version, description.as_deref())
685 }
686
687 #[cfg(feature = "swagger-ui")]
696 pub fn docs_with_info(
697 mut self,
698 path: &str,
699 title: &str,
700 version: &str,
701 description: Option<&str>,
702 ) -> Self {
703 use crate::router::get;
704 self.openapi_spec.info.title = title.to_string();
706 self.openapi_spec.info.version = version.to_string();
707 if let Some(desc) = description {
708 self.openapi_spec.info.description = Some(desc.to_string());
709 }
710
711 let path = path.trim_end_matches('/');
712 let openapi_path = format!("{}/openapi.json", path);
713
714 let spec_json =
716 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
717 let openapi_url = openapi_path.clone();
718
719 let spec_handler = move || {
721 let json = spec_json.clone();
722 async move {
723 http::Response::builder()
724 .status(http::StatusCode::OK)
725 .header(http::header::CONTENT_TYPE, "application/json")
726 .body(crate::response::Body::from(json))
727 .unwrap()
728 }
729 };
730
731 let docs_handler = move || {
733 let url = openapi_url.clone();
734 async move {
735 let response = rustapi_openapi::swagger_ui_html(&url);
736 response.map(crate::response::Body::Full)
737 }
738 };
739
740 self.route(&openapi_path, get(spec_handler))
741 .route(path, get(docs_handler))
742 }
743
744 #[cfg(feature = "swagger-ui")]
760 pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
761 let title = self.openapi_spec.info.title.clone();
762 let version = self.openapi_spec.info.version.clone();
763 let description = self.openapi_spec.info.description.clone();
764
765 self.docs_with_auth_and_info(
766 path,
767 username,
768 password,
769 &title,
770 &version,
771 description.as_deref(),
772 )
773 }
774
775 #[cfg(feature = "swagger-ui")]
791 pub fn docs_with_auth_and_info(
792 mut self,
793 path: &str,
794 username: &str,
795 password: &str,
796 title: &str,
797 version: &str,
798 description: Option<&str>,
799 ) -> Self {
800 use crate::router::MethodRouter;
801 use base64::{engine::general_purpose::STANDARD, Engine};
802 use std::collections::HashMap;
803
804 self.openapi_spec.info.title = title.to_string();
806 self.openapi_spec.info.version = version.to_string();
807 if let Some(desc) = description {
808 self.openapi_spec.info.description = Some(desc.to_string());
809 }
810
811 let path = path.trim_end_matches('/');
812 let openapi_path = format!("{}/openapi.json", path);
813
814 let credentials = format!("{}:{}", username, password);
816 let encoded = STANDARD.encode(credentials.as_bytes());
817 let expected_auth = format!("Basic {}", encoded);
818
819 let spec_json =
821 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
822 let openapi_url = openapi_path.clone();
823 let expected_auth_spec = expected_auth.clone();
824 let expected_auth_docs = expected_auth;
825
826 let spec_handler: crate::handler::BoxedHandler =
828 std::sync::Arc::new(move |req: crate::Request| {
829 let json = spec_json.clone();
830 let expected = expected_auth_spec.clone();
831 Box::pin(async move {
832 if !check_basic_auth(&req, &expected) {
833 return unauthorized_response();
834 }
835 http::Response::builder()
836 .status(http::StatusCode::OK)
837 .header(http::header::CONTENT_TYPE, "application/json")
838 .body(crate::response::Body::from(json))
839 .unwrap()
840 })
841 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
842 });
843
844 let docs_handler: crate::handler::BoxedHandler =
846 std::sync::Arc::new(move |req: crate::Request| {
847 let url = openapi_url.clone();
848 let expected = expected_auth_docs.clone();
849 Box::pin(async move {
850 if !check_basic_auth(&req, &expected) {
851 return unauthorized_response();
852 }
853 let response = rustapi_openapi::swagger_ui_html(&url);
854 response.map(crate::response::Body::Full)
855 })
856 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
857 });
858
859 let mut spec_handlers = HashMap::new();
861 spec_handlers.insert(http::Method::GET, spec_handler);
862 let spec_router = MethodRouter::from_boxed(spec_handlers);
863
864 let mut docs_handlers = HashMap::new();
865 docs_handlers.insert(http::Method::GET, docs_handler);
866 let docs_router = MethodRouter::from_boxed(docs_handlers);
867
868 self.route(&openapi_path, spec_router)
869 .route(path, docs_router)
870 }
871
872 pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
883 if let Some(limit) = self.body_limit {
885 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
887 }
888
889 let server = Server::new(self.router, self.layers, self.interceptors);
890 server.run(addr).await
891 }
892
893 pub async fn run_with_shutdown<F>(
895 mut self,
896 addr: impl AsRef<str>,
897 signal: F,
898 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
899 where
900 F: std::future::Future<Output = ()> + Send + 'static,
901 {
902 if let Some(limit) = self.body_limit {
904 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
906 }
907
908 let server = Server::new(self.router, self.layers, self.interceptors);
909 server.run_with_shutdown(addr.as_ref(), signal).await
910 }
911
912 pub fn into_router(self) -> Router {
914 self.router
915 }
916
917 pub fn layers(&self) -> &LayerStack {
919 &self.layers
920 }
921
922 pub fn interceptors(&self) -> &InterceptorChain {
924 &self.interceptors
925 }
926
927 #[cfg(feature = "http3")]
941 pub async fn run_http3(
942 mut self,
943 config: crate::http3::Http3Config,
944 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
945 use std::sync::Arc;
946
947 if let Some(limit) = self.body_limit {
949 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
950 }
951
952 let server = crate::http3::Http3Server::new(
953 &config,
954 Arc::new(self.router),
955 Arc::new(self.layers),
956 Arc::new(self.interceptors),
957 )
958 .await?;
959
960 server.run().await
961 }
962
963 #[cfg(feature = "http3-dev")]
977 pub async fn run_http3_dev(
978 mut self,
979 addr: &str,
980 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
981 use std::sync::Arc;
982
983 if let Some(limit) = self.body_limit {
985 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
986 }
987
988 let server = crate::http3::Http3Server::new_with_self_signed(
989 addr,
990 Arc::new(self.router),
991 Arc::new(self.layers),
992 Arc::new(self.interceptors),
993 )
994 .await?;
995
996 server.run().await
997 }
998
999 #[cfg(feature = "http3")]
1023 pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
1024 self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
1025 self
1026 }
1027
1028 #[cfg(feature = "http3")]
1043 pub async fn run_dual_stack(
1044 mut self,
1045 _http_addr: &str,
1046 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1047 let config = self
1055 .http3_config
1056 .take()
1057 .ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
1058
1059 tracing::warn!("run_dual_stack currently only runs HTTP/3. HTTP/1.1 support coming soon.");
1060 self.run_http3(config).await
1061 }
1062}
1063
1064fn add_path_params_to_operation(
1065 path: &str,
1066 op: &mut rustapi_openapi::Operation,
1067 param_schemas: &std::collections::HashMap<String, String>,
1068) {
1069 let mut params: Vec<String> = Vec::new();
1070 let mut in_brace = false;
1071 let mut current = String::new();
1072
1073 for ch in path.chars() {
1074 match ch {
1075 '{' => {
1076 in_brace = true;
1077 current.clear();
1078 }
1079 '}' => {
1080 if in_brace {
1081 in_brace = false;
1082 if !current.is_empty() {
1083 params.push(current.clone());
1084 }
1085 }
1086 }
1087 _ => {
1088 if in_brace {
1089 current.push(ch);
1090 }
1091 }
1092 }
1093 }
1094
1095 if params.is_empty() {
1096 return;
1097 }
1098
1099 let op_params = op.parameters.get_or_insert_with(Vec::new);
1100
1101 for name in params {
1102 let already = op_params
1103 .iter()
1104 .any(|p| p.location == "path" && p.name == name);
1105 if already {
1106 continue;
1107 }
1108
1109 let schema = if let Some(schema_type) = param_schemas.get(&name) {
1111 schema_type_to_openapi_schema(schema_type)
1112 } else {
1113 infer_path_param_schema(&name)
1114 };
1115
1116 op_params.push(rustapi_openapi::Parameter {
1117 name,
1118 location: "path".to_string(),
1119 required: true,
1120 description: None,
1121 schema,
1122 });
1123 }
1124}
1125
1126fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef {
1128 match schema_type.to_lowercase().as_str() {
1129 "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1130 "type": "string",
1131 "format": "uuid"
1132 })),
1133 "integer" | "int" | "int64" | "i64" => {
1134 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1135 "type": "integer",
1136 "format": "int64"
1137 }))
1138 }
1139 "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1140 "type": "integer",
1141 "format": "int32"
1142 })),
1143 "number" | "float" | "f64" | "f32" => {
1144 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1145 "type": "number"
1146 }))
1147 }
1148 "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1149 "type": "boolean"
1150 })),
1151 _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1152 "type": "string"
1153 })),
1154 }
1155}
1156
1157fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef {
1166 let lower = name.to_lowercase();
1167
1168 let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid");
1170
1171 if is_uuid {
1172 return rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1173 "type": "string",
1174 "format": "uuid"
1175 }));
1176 }
1177
1178 let is_integer = lower == "id"
1180 || lower.ends_with("_id")
1181 || (lower.ends_with("id") && lower.len() > 2) || lower == "page"
1183 || lower == "limit"
1184 || lower == "offset"
1185 || lower == "count"
1186 || lower.ends_with("_count")
1187 || lower.ends_with("_num")
1188 || lower == "year"
1189 || lower == "month"
1190 || lower == "day"
1191 || lower == "index"
1192 || lower == "position";
1193
1194 if is_integer {
1195 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1196 "type": "integer",
1197 "format": "int64"
1198 }))
1199 } else {
1200 rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1201 }
1202}
1203
1204fn normalize_prefix_for_openapi(prefix: &str) -> String {
1211 if prefix.is_empty() {
1213 return "/".to_string();
1214 }
1215
1216 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
1218
1219 if segments.is_empty() {
1221 return "/".to_string();
1222 }
1223
1224 let mut result = String::with_capacity(prefix.len() + 1);
1226 for segment in segments {
1227 result.push('/');
1228 result.push_str(segment);
1229 }
1230
1231 result
1232}
1233
1234impl Default for RustApi {
1235 fn default() -> Self {
1236 Self::new()
1237 }
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242 use super::RustApi;
1243 use crate::extract::{FromRequestParts, State};
1244 use crate::path_params::PathParams;
1245 use crate::request::Request;
1246 use crate::router::{get, post, Router};
1247 use bytes::Bytes;
1248 use http::Method;
1249 use proptest::prelude::*;
1250
1251 #[test]
1252 fn state_is_available_via_extractor() {
1253 let app = RustApi::new().state(123u32);
1254 let router = app.into_router();
1255
1256 let req = http::Request::builder()
1257 .method(Method::GET)
1258 .uri("/test")
1259 .body(())
1260 .unwrap();
1261 let (parts, _) = req.into_parts();
1262
1263 let request = Request::new(
1264 parts,
1265 crate::request::BodyVariant::Buffered(Bytes::new()),
1266 router.state_ref(),
1267 PathParams::new(),
1268 );
1269 let State(value) = State::<u32>::from_request_parts(&request).unwrap();
1270 assert_eq!(value, 123u32);
1271 }
1272
1273 #[test]
1274 fn test_path_param_type_inference_integer() {
1275 use super::infer_path_param_schema;
1276
1277 let int_params = [
1279 "id",
1280 "user_id",
1281 "userId",
1282 "postId",
1283 "page",
1284 "limit",
1285 "offset",
1286 "count",
1287 "item_count",
1288 "year",
1289 "month",
1290 "day",
1291 "index",
1292 "position",
1293 ];
1294
1295 for name in int_params {
1296 let schema = infer_path_param_schema(name);
1297 match schema {
1298 rustapi_openapi::SchemaRef::Inline(v) => {
1299 assert_eq!(
1300 v.get("type").and_then(|v| v.as_str()),
1301 Some("integer"),
1302 "Expected '{}' to be inferred as integer",
1303 name
1304 );
1305 }
1306 _ => panic!("Expected inline schema for '{}'", name),
1307 }
1308 }
1309 }
1310
1311 #[test]
1312 fn test_path_param_type_inference_uuid() {
1313 use super::infer_path_param_schema;
1314
1315 let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
1317
1318 for name in uuid_params {
1319 let schema = infer_path_param_schema(name);
1320 match schema {
1321 rustapi_openapi::SchemaRef::Inline(v) => {
1322 assert_eq!(
1323 v.get("type").and_then(|v| v.as_str()),
1324 Some("string"),
1325 "Expected '{}' to be inferred as string",
1326 name
1327 );
1328 assert_eq!(
1329 v.get("format").and_then(|v| v.as_str()),
1330 Some("uuid"),
1331 "Expected '{}' to have uuid format",
1332 name
1333 );
1334 }
1335 _ => panic!("Expected inline schema for '{}'", name),
1336 }
1337 }
1338 }
1339
1340 #[test]
1341 fn test_path_param_type_inference_string() {
1342 use super::infer_path_param_schema;
1343
1344 let string_params = ["name", "slug", "code", "token", "username"];
1346
1347 for name in string_params {
1348 let schema = infer_path_param_schema(name);
1349 match schema {
1350 rustapi_openapi::SchemaRef::Inline(v) => {
1351 assert_eq!(
1352 v.get("type").and_then(|v| v.as_str()),
1353 Some("string"),
1354 "Expected '{}' to be inferred as string",
1355 name
1356 );
1357 assert!(
1358 v.get("format").is_none()
1359 || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
1360 "Expected '{}' to NOT have uuid format",
1361 name
1362 );
1363 }
1364 _ => panic!("Expected inline schema for '{}'", name),
1365 }
1366 }
1367 }
1368
1369 #[test]
1370 fn test_schema_type_to_openapi_schema() {
1371 use super::schema_type_to_openapi_schema;
1372
1373 let uuid_schema = schema_type_to_openapi_schema("uuid");
1375 match uuid_schema {
1376 rustapi_openapi::SchemaRef::Inline(v) => {
1377 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1378 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
1379 }
1380 _ => panic!("Expected inline schema for uuid"),
1381 }
1382
1383 for schema_type in ["integer", "int", "int64", "i64"] {
1385 let schema = schema_type_to_openapi_schema(schema_type);
1386 match schema {
1387 rustapi_openapi::SchemaRef::Inline(v) => {
1388 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1389 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
1390 }
1391 _ => panic!("Expected inline schema for {}", schema_type),
1392 }
1393 }
1394
1395 let int32_schema = schema_type_to_openapi_schema("int32");
1397 match int32_schema {
1398 rustapi_openapi::SchemaRef::Inline(v) => {
1399 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1400 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
1401 }
1402 _ => panic!("Expected inline schema for int32"),
1403 }
1404
1405 for schema_type in ["number", "float"] {
1407 let schema = schema_type_to_openapi_schema(schema_type);
1408 match schema {
1409 rustapi_openapi::SchemaRef::Inline(v) => {
1410 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
1411 }
1412 _ => panic!("Expected inline schema for {}", schema_type),
1413 }
1414 }
1415
1416 for schema_type in ["boolean", "bool"] {
1418 let schema = schema_type_to_openapi_schema(schema_type);
1419 match schema {
1420 rustapi_openapi::SchemaRef::Inline(v) => {
1421 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
1422 }
1423 _ => panic!("Expected inline schema for {}", schema_type),
1424 }
1425 }
1426
1427 let string_schema = schema_type_to_openapi_schema("string");
1429 match string_schema {
1430 rustapi_openapi::SchemaRef::Inline(v) => {
1431 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1432 }
1433 _ => panic!("Expected inline schema for string"),
1434 }
1435 }
1436
1437 proptest! {
1444 #![proptest_config(ProptestConfig::with_cases(100))]
1445
1446 #[test]
1451 fn prop_nested_routes_in_openapi_spec(
1452 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1454 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1456 has_param in any::<bool>(),
1457 ) {
1458 async fn handler() -> &'static str { "handler" }
1459
1460 let prefix = format!("/{}", prefix_segments.join("/"));
1462
1463 let mut route_path = format!("/{}", route_segments.join("/"));
1465 if has_param {
1466 route_path.push_str("/{id}");
1467 }
1468
1469 let nested_router = Router::new().route(&route_path, get(handler));
1471 let app = RustApi::new().nest(&prefix, nested_router);
1472
1473 let expected_openapi_path = format!("{}{}", prefix, route_path);
1475
1476 let spec = app.openapi_spec();
1478
1479 prop_assert!(
1481 spec.paths.contains_key(&expected_openapi_path),
1482 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1483 expected_openapi_path,
1484 spec.paths.keys().collect::<Vec<_>>()
1485 );
1486
1487 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1489 prop_assert!(
1490 path_item.get.is_some(),
1491 "GET operation should exist for path '{}'",
1492 expected_openapi_path
1493 );
1494 }
1495
1496 #[test]
1501 fn prop_multiple_methods_preserved_in_openapi(
1502 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1503 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1504 ) {
1505 async fn get_handler() -> &'static str { "get" }
1506 async fn post_handler() -> &'static str { "post" }
1507
1508 let prefix = format!("/{}", prefix_segments.join("/"));
1510 let route_path = format!("/{}", route_segments.join("/"));
1511
1512 let get_route_path = format!("{}/get", route_path);
1515 let post_route_path = format!("{}/post", route_path);
1516 let nested_router = Router::new()
1517 .route(&get_route_path, get(get_handler))
1518 .route(&post_route_path, post(post_handler));
1519 let app = RustApi::new().nest(&prefix, nested_router);
1520
1521 let expected_get_path = format!("{}{}", prefix, get_route_path);
1523 let expected_post_path = format!("{}{}", prefix, post_route_path);
1524
1525 let spec = app.openapi_spec();
1527
1528 prop_assert!(
1530 spec.paths.contains_key(&expected_get_path),
1531 "Expected OpenAPI path '{}' not found",
1532 expected_get_path
1533 );
1534 prop_assert!(
1535 spec.paths.contains_key(&expected_post_path),
1536 "Expected OpenAPI path '{}' not found",
1537 expected_post_path
1538 );
1539
1540 let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1542 prop_assert!(
1543 get_path_item.get.is_some(),
1544 "GET operation should exist for path '{}'",
1545 expected_get_path
1546 );
1547
1548 let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1550 prop_assert!(
1551 post_path_item.post.is_some(),
1552 "POST operation should exist for path '{}'",
1553 expected_post_path
1554 );
1555 }
1556
1557 #[test]
1562 fn prop_path_params_in_openapi_after_nesting(
1563 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1564 param_name in "[a-z][a-z0-9]{0,5}",
1565 ) {
1566 async fn handler() -> &'static str { "handler" }
1567
1568 let prefix = format!("/{}", prefix_segments.join("/"));
1570 let route_path = format!("/{{{}}}", param_name);
1571
1572 let nested_router = Router::new().route(&route_path, get(handler));
1574 let app = RustApi::new().nest(&prefix, nested_router);
1575
1576 let expected_openapi_path = format!("{}{}", prefix, route_path);
1578
1579 let spec = app.openapi_spec();
1581
1582 prop_assert!(
1584 spec.paths.contains_key(&expected_openapi_path),
1585 "Expected OpenAPI path '{}' not found",
1586 expected_openapi_path
1587 );
1588
1589 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1591 let get_op = path_item.get.as_ref().unwrap();
1592
1593 prop_assert!(
1594 get_op.parameters.is_some(),
1595 "Operation should have parameters for path '{}'",
1596 expected_openapi_path
1597 );
1598
1599 let params = get_op.parameters.as_ref().unwrap();
1600 let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1601 prop_assert!(
1602 has_param,
1603 "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1604 param_name,
1605 params.iter().map(|p| &p.name).collect::<Vec<_>>()
1606 );
1607 }
1608 }
1609
1610 proptest! {
1618 #![proptest_config(ProptestConfig::with_cases(100))]
1619
1620 #[test]
1625 fn prop_rustapi_nest_delegates_to_router_nest(
1626 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1627 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1628 has_param in any::<bool>(),
1629 ) {
1630 async fn handler() -> &'static str { "handler" }
1631
1632 let prefix = format!("/{}", prefix_segments.join("/"));
1634
1635 let mut route_path = format!("/{}", route_segments.join("/"));
1637 if has_param {
1638 route_path.push_str("/{id}");
1639 }
1640
1641 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1643 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1644
1645 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1647 let rustapi_router = rustapi_app.into_router();
1648
1649 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1651
1652 let rustapi_routes = rustapi_router.registered_routes();
1654 let router_routes = router_app.registered_routes();
1655
1656 prop_assert_eq!(
1657 rustapi_routes.len(),
1658 router_routes.len(),
1659 "RustApi and Router should have same number of routes"
1660 );
1661
1662 for (path, info) in router_routes {
1664 prop_assert!(
1665 rustapi_routes.contains_key(path),
1666 "Route '{}' from Router should exist in RustApi routes",
1667 path
1668 );
1669
1670 let rustapi_info = rustapi_routes.get(path).unwrap();
1671 prop_assert_eq!(
1672 &info.path, &rustapi_info.path,
1673 "Display paths should match for route '{}'",
1674 path
1675 );
1676 prop_assert_eq!(
1677 info.methods.len(), rustapi_info.methods.len(),
1678 "Method count should match for route '{}'",
1679 path
1680 );
1681 }
1682 }
1683
1684 #[test]
1689 fn prop_rustapi_nest_includes_routes_in_openapi(
1690 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1691 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1692 has_param in any::<bool>(),
1693 ) {
1694 async fn handler() -> &'static str { "handler" }
1695
1696 let prefix = format!("/{}", prefix_segments.join("/"));
1698
1699 let mut route_path = format!("/{}", route_segments.join("/"));
1701 if has_param {
1702 route_path.push_str("/{id}");
1703 }
1704
1705 let nested_router = Router::new().route(&route_path, get(handler));
1707 let app = RustApi::new().nest(&prefix, nested_router);
1708
1709 let expected_openapi_path = format!("{}{}", prefix, route_path);
1711
1712 let spec = app.openapi_spec();
1714
1715 prop_assert!(
1717 spec.paths.contains_key(&expected_openapi_path),
1718 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1719 expected_openapi_path,
1720 spec.paths.keys().collect::<Vec<_>>()
1721 );
1722
1723 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1725 prop_assert!(
1726 path_item.get.is_some(),
1727 "GET operation should exist for path '{}'",
1728 expected_openapi_path
1729 );
1730 }
1731
1732 #[test]
1737 fn prop_rustapi_nest_route_matching_identical(
1738 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1739 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1740 param_value in "[a-z0-9]{1,10}",
1741 ) {
1742 use crate::router::RouteMatch;
1743
1744 async fn handler() -> &'static str { "handler" }
1745
1746 let prefix = format!("/{}", prefix_segments.join("/"));
1748 let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1749
1750 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1752 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1753
1754 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1756 let rustapi_router = rustapi_app.into_router();
1757 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1758
1759 let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1761
1762 let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1764 let router_match = router_app.match_route(&full_path, &Method::GET);
1765
1766 match (rustapi_match, router_match) {
1768 (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1769 prop_assert_eq!(
1770 rustapi_params.len(),
1771 router_params.len(),
1772 "Parameter count should match"
1773 );
1774 for (key, value) in &router_params {
1775 prop_assert!(
1776 rustapi_params.contains_key(key),
1777 "RustApi should have parameter '{}'",
1778 key
1779 );
1780 prop_assert_eq!(
1781 rustapi_params.get(key).unwrap(),
1782 value,
1783 "Parameter '{}' value should match",
1784 key
1785 );
1786 }
1787 }
1788 (rustapi_result, router_result) => {
1789 prop_assert!(
1790 false,
1791 "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1792 match rustapi_result {
1793 RouteMatch::Found { .. } => "Found",
1794 RouteMatch::NotFound => "NotFound",
1795 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1796 },
1797 match router_result {
1798 RouteMatch::Found { .. } => "Found",
1799 RouteMatch::NotFound => "NotFound",
1800 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1801 }
1802 );
1803 }
1804 }
1805 }
1806 }
1807
1808 #[test]
1810 fn test_openapi_operations_propagated_during_nesting() {
1811 async fn list_users() -> &'static str {
1812 "list users"
1813 }
1814 async fn get_user() -> &'static str {
1815 "get user"
1816 }
1817 async fn create_user() -> &'static str {
1818 "create user"
1819 }
1820
1821 let users_router = Router::new()
1824 .route("/", get(list_users))
1825 .route("/create", post(create_user))
1826 .route("/{id}", get(get_user));
1827
1828 let app = RustApi::new().nest("/api/v1/users", users_router);
1830
1831 let spec = app.openapi_spec();
1832
1833 assert!(
1835 spec.paths.contains_key("/api/v1/users"),
1836 "Should have /api/v1/users path"
1837 );
1838 let users_path = spec.paths.get("/api/v1/users").unwrap();
1839 assert!(users_path.get.is_some(), "Should have GET operation");
1840
1841 assert!(
1843 spec.paths.contains_key("/api/v1/users/create"),
1844 "Should have /api/v1/users/create path"
1845 );
1846 let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1847 assert!(create_path.post.is_some(), "Should have POST operation");
1848
1849 assert!(
1851 spec.paths.contains_key("/api/v1/users/{id}"),
1852 "Should have /api/v1/users/{{id}} path"
1853 );
1854 let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1855 assert!(
1856 user_path.get.is_some(),
1857 "Should have GET operation for user by id"
1858 );
1859
1860 let get_user_op = user_path.get.as_ref().unwrap();
1862 assert!(get_user_op.parameters.is_some(), "Should have parameters");
1863 let params = get_user_op.parameters.as_ref().unwrap();
1864 assert!(
1865 params
1866 .iter()
1867 .any(|p| p.name == "id" && p.location == "path"),
1868 "Should have 'id' path parameter"
1869 );
1870 }
1871
1872 #[test]
1874 fn test_openapi_spec_empty_without_routes() {
1875 let app = RustApi::new();
1876 let spec = app.openapi_spec();
1877
1878 assert!(
1880 spec.paths.is_empty(),
1881 "OpenAPI spec should have no paths without routes"
1882 );
1883 }
1884
1885 #[test]
1890 fn test_rustapi_nest_delegates_to_router_nest() {
1891 use crate::router::RouteMatch;
1892
1893 async fn list_users() -> &'static str {
1894 "list users"
1895 }
1896 async fn get_user() -> &'static str {
1897 "get user"
1898 }
1899 async fn create_user() -> &'static str {
1900 "create user"
1901 }
1902
1903 let users_router = Router::new()
1905 .route("/", get(list_users))
1906 .route("/create", post(create_user))
1907 .route("/{id}", get(get_user));
1908
1909 let app = RustApi::new().nest("/api/v1/users", users_router);
1911 let router = app.into_router();
1912
1913 let routes = router.registered_routes();
1915 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1916
1917 assert!(
1919 routes.contains_key("/api/v1/users"),
1920 "Should have /api/v1/users route"
1921 );
1922 assert!(
1923 routes.contains_key("/api/v1/users/create"),
1924 "Should have /api/v1/users/create route"
1925 );
1926 assert!(
1927 routes.contains_key("/api/v1/users/:id"),
1928 "Should have /api/v1/users/:id route"
1929 );
1930
1931 match router.match_route("/api/v1/users", &Method::GET) {
1933 RouteMatch::Found { params, .. } => {
1934 assert!(params.is_empty(), "Root route should have no params");
1935 }
1936 _ => panic!("GET /api/v1/users should be found"),
1937 }
1938
1939 match router.match_route("/api/v1/users/create", &Method::POST) {
1940 RouteMatch::Found { params, .. } => {
1941 assert!(params.is_empty(), "Create route should have no params");
1942 }
1943 _ => panic!("POST /api/v1/users/create should be found"),
1944 }
1945
1946 match router.match_route("/api/v1/users/123", &Method::GET) {
1947 RouteMatch::Found { params, .. } => {
1948 assert_eq!(
1949 params.get("id"),
1950 Some(&"123".to_string()),
1951 "Should extract id param"
1952 );
1953 }
1954 _ => panic!("GET /api/v1/users/123 should be found"),
1955 }
1956
1957 match router.match_route("/api/v1/users", &Method::DELETE) {
1959 RouteMatch::MethodNotAllowed { allowed } => {
1960 assert!(allowed.contains(&Method::GET), "Should allow GET");
1961 }
1962 _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
1963 }
1964 }
1965
1966 #[test]
1971 fn test_rustapi_nest_includes_routes_in_openapi_spec() {
1972 async fn list_items() -> &'static str {
1973 "list items"
1974 }
1975 async fn get_item() -> &'static str {
1976 "get item"
1977 }
1978
1979 let items_router = Router::new()
1981 .route("/", get(list_items))
1982 .route("/{item_id}", get(get_item));
1983
1984 let app = RustApi::new().nest("/api/items", items_router);
1986
1987 let spec = app.openapi_spec();
1989
1990 assert!(
1992 spec.paths.contains_key("/api/items"),
1993 "Should have /api/items in OpenAPI"
1994 );
1995 assert!(
1996 spec.paths.contains_key("/api/items/{item_id}"),
1997 "Should have /api/items/{{item_id}} in OpenAPI"
1998 );
1999
2000 let list_path = spec.paths.get("/api/items").unwrap();
2002 assert!(
2003 list_path.get.is_some(),
2004 "Should have GET operation for /api/items"
2005 );
2006
2007 let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2008 assert!(
2009 get_path.get.is_some(),
2010 "Should have GET operation for /api/items/{{item_id}}"
2011 );
2012
2013 let get_op = get_path.get.as_ref().unwrap();
2015 assert!(get_op.parameters.is_some(), "Should have parameters");
2016 let params = get_op.parameters.as_ref().unwrap();
2017 assert!(
2018 params
2019 .iter()
2020 .any(|p| p.name == "item_id" && p.location == "path"),
2021 "Should have 'item_id' path parameter"
2022 );
2023 }
2024}
2025
2026#[cfg(feature = "swagger-ui")]
2028fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2029 req.headers()
2030 .get(http::header::AUTHORIZATION)
2031 .and_then(|v| v.to_str().ok())
2032 .map(|auth| auth == expected)
2033 .unwrap_or(false)
2034}
2035
2036#[cfg(feature = "swagger-ui")]
2038fn unauthorized_response() -> crate::Response {
2039 http::Response::builder()
2040 .status(http::StatusCode::UNAUTHORIZED)
2041 .header(
2042 http::header::WWW_AUTHENTICATE,
2043 "Basic realm=\"API Documentation\"",
2044 )
2045 .header(http::header::CONTENT_TYPE, "text/plain")
2046 .body(crate::response::Body::from("Unauthorized"))
2047 .unwrap()
2048}
2049
2050pub struct RustApiConfig {
2052 docs_path: Option<String>,
2053 docs_enabled: bool,
2054 api_title: String,
2055 api_version: String,
2056 api_description: Option<String>,
2057 body_limit: Option<usize>,
2058 layers: LayerStack,
2059}
2060
2061impl Default for RustApiConfig {
2062 fn default() -> Self {
2063 Self::new()
2064 }
2065}
2066
2067impl RustApiConfig {
2068 pub fn new() -> Self {
2069 Self {
2070 docs_path: Some("/docs".to_string()),
2071 docs_enabled: true,
2072 api_title: "RustAPI".to_string(),
2073 api_version: "1.0.0".to_string(),
2074 api_description: None,
2075 body_limit: None,
2076 layers: LayerStack::new(),
2077 }
2078 }
2079
2080 pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2082 self.docs_path = Some(path.into());
2083 self
2084 }
2085
2086 pub fn docs_enabled(mut self, enabled: bool) -> Self {
2088 self.docs_enabled = enabled;
2089 self
2090 }
2091
2092 pub fn openapi_info(
2094 mut self,
2095 title: impl Into<String>,
2096 version: impl Into<String>,
2097 description: Option<impl Into<String>>,
2098 ) -> Self {
2099 self.api_title = title.into();
2100 self.api_version = version.into();
2101 self.api_description = description.map(|d| d.into());
2102 self
2103 }
2104
2105 pub fn body_limit(mut self, limit: usize) -> Self {
2107 self.body_limit = Some(limit);
2108 self
2109 }
2110
2111 pub fn layer<L>(mut self, layer: L) -> Self
2113 where
2114 L: MiddlewareLayer,
2115 {
2116 self.layers.push(Box::new(layer));
2117 self
2118 }
2119
2120 pub fn build(self) -> RustApi {
2122 let mut app = RustApi::new().mount_auto_routes_grouped();
2123
2124 if let Some(limit) = self.body_limit {
2126 app = app.body_limit(limit);
2127 }
2128
2129 app = app.openapi_info(
2130 &self.api_title,
2131 &self.api_version,
2132 self.api_description.as_deref(),
2133 );
2134
2135 #[cfg(feature = "swagger-ui")]
2136 if self.docs_enabled {
2137 if let Some(path) = self.docs_path {
2138 app = app.docs(&path);
2139 }
2140 }
2141
2142 app.layers.extend(self.layers);
2145
2146 app
2147 }
2148
2149 pub async fn run(
2151 self,
2152 addr: impl AsRef<str>,
2153 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2154 self.build().run(addr.as_ref()).await
2155 }
2156}