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 status_config: Option<crate::status::StatusConfig>,
38}
39
40impl RustApi {
41 pub fn new() -> Self {
43 let _ = tracing_subscriber::registry()
45 .with(
46 EnvFilter::try_from_default_env()
47 .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
48 )
49 .with(tracing_subscriber::fmt::layer())
50 .try_init();
51
52 Self {
53 router: Router::new(),
54 openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
55 .register::<rustapi_openapi::ErrorSchema>()
56 .register::<rustapi_openapi::ErrorBodySchema>()
57 .register::<rustapi_openapi::ValidationErrorSchema>()
58 .register::<rustapi_openapi::ValidationErrorBodySchema>()
59 .register::<rustapi_openapi::FieldErrorSchema>(),
60 layers: LayerStack::new(),
61 body_limit: Some(DEFAULT_BODY_LIMIT), interceptors: InterceptorChain::new(),
63 #[cfg(feature = "http3")]
64 http3_config: None,
65 status_config: None,
66 }
67 }
68
69 #[cfg(feature = "swagger-ui")]
93 pub fn auto() -> Self {
94 Self::new().mount_auto_routes_grouped().docs("/docs")
96 }
97
98 #[cfg(not(feature = "swagger-ui"))]
103 pub fn auto() -> Self {
104 Self::new().mount_auto_routes_grouped()
105 }
106
107 pub fn config() -> RustApiConfig {
125 RustApiConfig::new()
126 }
127
128 pub fn body_limit(mut self, limit: usize) -> Self {
149 self.body_limit = Some(limit);
150 self
151 }
152
153 pub fn no_body_limit(mut self) -> Self {
166 self.body_limit = None;
167 self
168 }
169
170 pub fn layer<L>(mut self, layer: L) -> Self
190 where
191 L: MiddlewareLayer,
192 {
193 self.layers.push(Box::new(layer));
194 self
195 }
196
197 pub fn request_interceptor<I>(mut self, interceptor: I) -> Self
229 where
230 I: RequestInterceptor,
231 {
232 self.interceptors.add_request_interceptor(interceptor);
233 self
234 }
235
236 pub fn response_interceptor<I>(mut self, interceptor: I) -> Self
268 where
269 I: ResponseInterceptor,
270 {
271 self.interceptors.add_response_interceptor(interceptor);
272 self
273 }
274
275 pub fn state<S>(self, _state: S) -> Self
291 where
292 S: Clone + Send + Sync + 'static,
293 {
294 let state = _state;
296 let mut app = self;
297 app.router = app.router.state(state);
298 app
299 }
300
301 pub fn register_schema<T: for<'a> rustapi_openapi::Schema<'a>>(mut self) -> Self {
313 self.openapi_spec = self.openapi_spec.register::<T>();
314 self
315 }
316
317 pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
319 self.openapi_spec.info.title = title.to_string();
322 self.openapi_spec.info.version = version.to_string();
323 self.openapi_spec.info.description = description.map(|d| d.to_string());
324 self
325 }
326
327 pub fn openapi_spec(&self) -> &rustapi_openapi::OpenApiSpec {
329 &self.openapi_spec
330 }
331
332 fn mount_auto_routes_grouped(mut self) -> Self {
333 let routes = crate::auto_route::collect_auto_routes();
334 let mut by_path: HashMap<String, MethodRouter> = HashMap::new();
335
336 for route in routes {
337 let method_enum = match route.method {
338 "GET" => http::Method::GET,
339 "POST" => http::Method::POST,
340 "PUT" => http::Method::PUT,
341 "DELETE" => http::Method::DELETE,
342 "PATCH" => http::Method::PATCH,
343 _ => http::Method::GET,
344 };
345
346 let path = if route.path.starts_with('/') {
347 route.path.to_string()
348 } else {
349 format!("/{}", route.path)
350 };
351
352 let entry = by_path.entry(path).or_default();
353 entry.insert_boxed_with_operation(method_enum, route.handler, route.operation);
354 }
355
356 #[cfg(feature = "tracing")]
357 let route_count: usize = by_path.values().map(|mr| mr.allowed_methods().len()).sum();
358 #[cfg(feature = "tracing")]
359 let path_count = by_path.len();
360
361 for (path, method_router) in by_path {
362 self = self.route(&path, method_router);
363 }
364
365 crate::trace_info!(
366 paths = path_count,
367 routes = route_count,
368 "Auto-registered routes"
369 );
370
371 crate::auto_schema::apply_auto_schemas(&mut self.openapi_spec);
373
374 self
375 }
376
377 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
388 for (method, op) in &method_router.operations {
390 let mut op = op.clone();
391 add_path_params_to_operation(path, &mut op, &std::collections::HashMap::new());
392 self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op);
393 }
394
395 self.router = self.router.route(path, method_router);
396 self
397 }
398
399 pub fn typed<P: crate::typed_path::TypedPath>(self, method_router: MethodRouter) -> Self {
401 self.route(P::PATH, method_router)
402 }
403
404 #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
408 pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
409 self.route(path, method_router)
410 }
411
412 pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
430 let method_enum = match route.method {
431 "GET" => http::Method::GET,
432 "POST" => http::Method::POST,
433 "PUT" => http::Method::PUT,
434 "DELETE" => http::Method::DELETE,
435 "PATCH" => http::Method::PATCH,
436 _ => http::Method::GET,
437 };
438
439 let mut op = route.operation;
441 add_path_params_to_operation(route.path, &mut op, &route.param_schemas);
442 self.openapi_spec = self.openapi_spec.path(route.path, route.method, op);
443
444 self.route_with_method(route.path, method_enum, route.handler)
445 }
446
447 fn route_with_method(
449 self,
450 path: &str,
451 method: http::Method,
452 handler: crate::handler::BoxedHandler,
453 ) -> Self {
454 use crate::router::MethodRouter;
455 let path = if !path.starts_with('/') {
464 format!("/{}", path)
465 } else {
466 path.to_string()
467 };
468
469 let mut handlers = std::collections::HashMap::new();
478 handlers.insert(method, handler);
479
480 let method_router = MethodRouter::from_boxed(handlers);
481 self.route(&path, method_router)
482 }
483
484 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
500 let normalized_prefix = normalize_prefix_for_openapi(prefix);
502
503 for (matchit_path, method_router) in router.method_routers() {
506 let display_path = router
508 .registered_routes()
509 .get(matchit_path)
510 .map(|info| info.path.clone())
511 .unwrap_or_else(|| matchit_path.clone());
512
513 let prefixed_path = if display_path == "/" {
515 normalized_prefix.clone()
516 } else {
517 format!("{}{}", normalized_prefix, display_path)
518 };
519
520 for (method, op) in &method_router.operations {
522 let mut op = op.clone();
523 add_path_params_to_operation(
524 &prefixed_path,
525 &mut op,
526 &std::collections::HashMap::new(),
527 );
528 self.openapi_spec = self.openapi_spec.path(&prefixed_path, method.as_str(), op);
529 }
530 }
531
532 self.router = self.router.nest(prefix, router);
534 self
535 }
536
537 pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
566 self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
567 }
568
569 pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
586 use crate::router::MethodRouter;
587 use std::collections::HashMap;
588
589 let prefix = config.prefix.clone();
590 let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
591
592 let handler: crate::handler::BoxedHandler =
594 std::sync::Arc::new(move |req: crate::Request| {
595 let config = config.clone();
596 let path = req.uri().path().to_string();
597
598 Box::pin(async move {
599 let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
600
601 match crate::static_files::StaticFile::serve(relative_path, &config).await {
602 Ok(response) => response,
603 Err(err) => err.into_response(),
604 }
605 })
606 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
607 });
608
609 let mut handlers = HashMap::new();
610 handlers.insert(http::Method::GET, handler);
611 let method_router = MethodRouter::from_boxed(handlers);
612
613 self.route(&catch_all_path, method_router)
614 }
615
616 #[cfg(feature = "compression")]
633 pub fn compression(self) -> Self {
634 self.layer(crate::middleware::CompressionLayer::new())
635 }
636
637 #[cfg(feature = "compression")]
653 pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
654 self.layer(crate::middleware::CompressionLayer::with_config(config))
655 }
656
657 #[cfg(feature = "swagger-ui")]
681 pub fn docs(self, path: &str) -> Self {
682 let title = self.openapi_spec.info.title.clone();
683 let version = self.openapi_spec.info.version.clone();
684 let description = self.openapi_spec.info.description.clone();
685
686 self.docs_with_info(path, &title, &version, description.as_deref())
687 }
688
689 #[cfg(feature = "swagger-ui")]
698 pub fn docs_with_info(
699 mut self,
700 path: &str,
701 title: &str,
702 version: &str,
703 description: Option<&str>,
704 ) -> Self {
705 use crate::router::get;
706 self.openapi_spec.info.title = title.to_string();
708 self.openapi_spec.info.version = version.to_string();
709 if let Some(desc) = description {
710 self.openapi_spec.info.description = Some(desc.to_string());
711 }
712
713 let path = path.trim_end_matches('/');
714 let openapi_path = format!("{}/openapi.json", path);
715
716 let spec_json =
718 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
719 let openapi_url = openapi_path.clone();
720
721 let spec_handler = move || {
723 let json = spec_json.clone();
724 async move {
725 http::Response::builder()
726 .status(http::StatusCode::OK)
727 .header(http::header::CONTENT_TYPE, "application/json")
728 .body(crate::response::Body::from(json))
729 .unwrap()
730 }
731 };
732
733 let docs_handler = move || {
735 let url = openapi_url.clone();
736 async move {
737 let response = rustapi_openapi::swagger_ui_html(&url);
738 response.map(crate::response::Body::Full)
739 }
740 };
741
742 self.route(&openapi_path, get(spec_handler))
743 .route(path, get(docs_handler))
744 }
745
746 #[cfg(feature = "swagger-ui")]
762 pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
763 let title = self.openapi_spec.info.title.clone();
764 let version = self.openapi_spec.info.version.clone();
765 let description = self.openapi_spec.info.description.clone();
766
767 self.docs_with_auth_and_info(
768 path,
769 username,
770 password,
771 &title,
772 &version,
773 description.as_deref(),
774 )
775 }
776
777 #[cfg(feature = "swagger-ui")]
793 pub fn docs_with_auth_and_info(
794 mut self,
795 path: &str,
796 username: &str,
797 password: &str,
798 title: &str,
799 version: &str,
800 description: Option<&str>,
801 ) -> Self {
802 use crate::router::MethodRouter;
803 use base64::{engine::general_purpose::STANDARD, Engine};
804 use std::collections::HashMap;
805
806 self.openapi_spec.info.title = title.to_string();
808 self.openapi_spec.info.version = version.to_string();
809 if let Some(desc) = description {
810 self.openapi_spec.info.description = Some(desc.to_string());
811 }
812
813 let path = path.trim_end_matches('/');
814 let openapi_path = format!("{}/openapi.json", path);
815
816 let credentials = format!("{}:{}", username, password);
818 let encoded = STANDARD.encode(credentials.as_bytes());
819 let expected_auth = format!("Basic {}", encoded);
820
821 let spec_json =
823 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
824 let openapi_url = openapi_path.clone();
825 let expected_auth_spec = expected_auth.clone();
826 let expected_auth_docs = expected_auth;
827
828 let spec_handler: crate::handler::BoxedHandler =
830 std::sync::Arc::new(move |req: crate::Request| {
831 let json = spec_json.clone();
832 let expected = expected_auth_spec.clone();
833 Box::pin(async move {
834 if !check_basic_auth(&req, &expected) {
835 return unauthorized_response();
836 }
837 http::Response::builder()
838 .status(http::StatusCode::OK)
839 .header(http::header::CONTENT_TYPE, "application/json")
840 .body(crate::response::Body::from(json))
841 .unwrap()
842 })
843 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
844 });
845
846 let docs_handler: crate::handler::BoxedHandler =
848 std::sync::Arc::new(move |req: crate::Request| {
849 let url = openapi_url.clone();
850 let expected = expected_auth_docs.clone();
851 Box::pin(async move {
852 if !check_basic_auth(&req, &expected) {
853 return unauthorized_response();
854 }
855 let response = rustapi_openapi::swagger_ui_html(&url);
856 response.map(crate::response::Body::Full)
857 })
858 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
859 });
860
861 let mut spec_handlers = HashMap::new();
863 spec_handlers.insert(http::Method::GET, spec_handler);
864 let spec_router = MethodRouter::from_boxed(spec_handlers);
865
866 let mut docs_handlers = HashMap::new();
867 docs_handlers.insert(http::Method::GET, docs_handler);
868 let docs_router = MethodRouter::from_boxed(docs_handlers);
869
870 self.route(&openapi_path, spec_router)
871 .route(path, docs_router)
872 }
873
874 pub fn status_page(self) -> Self {
876 self.status_page_with_config(crate::status::StatusConfig::default())
877 }
878
879 pub fn status_page_with_config(mut self, config: crate::status::StatusConfig) -> Self {
881 self.status_config = Some(config);
882 self
883 }
884
885 fn apply_status_page(&mut self) {
887 if let Some(config) = &self.status_config {
888 let monitor = std::sync::Arc::new(crate::status::StatusMonitor::new());
889
890 self.layers
892 .push(Box::new(crate::status::StatusLayer::new(monitor.clone())));
893
894 use crate::router::MethodRouter;
896 use std::collections::HashMap;
897
898 let monitor = monitor.clone();
899 let config = config.clone();
900 let path = config.path.clone(); let handler: crate::handler::BoxedHandler = std::sync::Arc::new(move |_| {
903 let monitor = monitor.clone();
904 let config = config.clone();
905 Box::pin(async move {
906 crate::status::status_handler(monitor, config)
907 .await
908 .into_response()
909 })
910 });
911
912 let mut handlers = HashMap::new();
913 handlers.insert(http::Method::GET, handler);
914 let method_router = MethodRouter::from_boxed(handlers);
915
916 let router = std::mem::take(&mut self.router);
918 self.router = router.route(&path, method_router);
919 }
920 }
921
922 pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
933 self.apply_status_page();
935
936 if let Some(limit) = self.body_limit {
938 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
940 }
941
942 let server = Server::new(self.router, self.layers, self.interceptors);
943 server.run(addr).await
944 }
945
946 pub async fn run_with_shutdown<F>(
948 mut self,
949 addr: impl AsRef<str>,
950 signal: F,
951 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
952 where
953 F: std::future::Future<Output = ()> + Send + 'static,
954 {
955 self.apply_status_page();
957
958 if let Some(limit) = self.body_limit {
959 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
960 }
961
962 let server = Server::new(self.router, self.layers, self.interceptors);
963 server.run_with_shutdown(addr.as_ref(), signal).await
964 }
965
966 pub fn into_router(self) -> Router {
968 self.router
969 }
970
971 pub fn layers(&self) -> &LayerStack {
973 &self.layers
974 }
975
976 pub fn interceptors(&self) -> &InterceptorChain {
978 &self.interceptors
979 }
980
981 #[cfg(feature = "http3")]
995 pub async fn run_http3(
996 mut self,
997 config: crate::http3::Http3Config,
998 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
999 use std::sync::Arc;
1000
1001 self.apply_status_page();
1003
1004 if let Some(limit) = self.body_limit {
1006 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1007 }
1008
1009 let server = crate::http3::Http3Server::new(
1010 &config,
1011 Arc::new(self.router),
1012 Arc::new(self.layers),
1013 Arc::new(self.interceptors),
1014 )
1015 .await?;
1016
1017 server.run().await
1018 }
1019
1020 #[cfg(feature = "http3-dev")]
1034 pub async fn run_http3_dev(
1035 mut self,
1036 addr: &str,
1037 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1038 use std::sync::Arc;
1039
1040 self.apply_status_page();
1042
1043 if let Some(limit) = self.body_limit {
1045 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1046 }
1047
1048 let server = crate::http3::Http3Server::new_with_self_signed(
1049 addr,
1050 Arc::new(self.router),
1051 Arc::new(self.layers),
1052 Arc::new(self.interceptors),
1053 )
1054 .await?;
1055
1056 server.run().await
1057 }
1058
1059 #[cfg(feature = "http3")]
1083 pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
1084 self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
1085 self
1086 }
1087
1088 #[cfg(feature = "http3")]
1103 pub async fn run_dual_stack(
1104 mut self,
1105 _http_addr: &str,
1106 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1107 let config = self
1115 .http3_config
1116 .take()
1117 .ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
1118
1119 tracing::warn!("run_dual_stack currently only runs HTTP/3. HTTP/1.1 support coming soon.");
1120 self.run_http3(config).await
1121 }
1122}
1123
1124fn add_path_params_to_operation(
1125 path: &str,
1126 op: &mut rustapi_openapi::Operation,
1127 param_schemas: &std::collections::HashMap<String, String>,
1128) {
1129 let mut params: Vec<String> = Vec::new();
1130 let mut in_brace = false;
1131 let mut current = String::new();
1132
1133 for ch in path.chars() {
1134 match ch {
1135 '{' => {
1136 in_brace = true;
1137 current.clear();
1138 }
1139 '}' => {
1140 if in_brace {
1141 in_brace = false;
1142 if !current.is_empty() {
1143 params.push(current.clone());
1144 }
1145 }
1146 }
1147 _ => {
1148 if in_brace {
1149 current.push(ch);
1150 }
1151 }
1152 }
1153 }
1154
1155 if params.is_empty() {
1156 return;
1157 }
1158
1159 let op_params = op.parameters.get_or_insert_with(Vec::new);
1160
1161 for name in params {
1162 let already = op_params
1163 .iter()
1164 .any(|p| p.location == "path" && p.name == name);
1165 if already {
1166 continue;
1167 }
1168
1169 let schema = if let Some(schema_type) = param_schemas.get(&name) {
1171 schema_type_to_openapi_schema(schema_type)
1172 } else {
1173 infer_path_param_schema(&name)
1174 };
1175
1176 op_params.push(rustapi_openapi::Parameter {
1177 name,
1178 location: "path".to_string(),
1179 required: true,
1180 description: None,
1181 schema,
1182 });
1183 }
1184}
1185
1186fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef {
1188 match schema_type.to_lowercase().as_str() {
1189 "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1190 "type": "string",
1191 "format": "uuid"
1192 })),
1193 "integer" | "int" | "int64" | "i64" => {
1194 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1195 "type": "integer",
1196 "format": "int64"
1197 }))
1198 }
1199 "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1200 "type": "integer",
1201 "format": "int32"
1202 })),
1203 "number" | "float" | "f64" | "f32" => {
1204 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1205 "type": "number"
1206 }))
1207 }
1208 "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1209 "type": "boolean"
1210 })),
1211 _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1212 "type": "string"
1213 })),
1214 }
1215}
1216
1217fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef {
1226 let lower = name.to_lowercase();
1227
1228 let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid");
1230
1231 if is_uuid {
1232 return rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1233 "type": "string",
1234 "format": "uuid"
1235 }));
1236 }
1237
1238 let is_integer = lower == "page"
1241 || lower == "limit"
1242 || lower == "offset"
1243 || lower == "count"
1244 || lower.ends_with("_count")
1245 || lower.ends_with("_num")
1246 || lower == "year"
1247 || lower == "month"
1248 || lower == "day"
1249 || lower == "index"
1250 || lower == "position";
1251
1252 if is_integer {
1253 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1254 "type": "integer",
1255 "format": "int64"
1256 }))
1257 } else {
1258 rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1259 }
1260}
1261
1262fn normalize_prefix_for_openapi(prefix: &str) -> String {
1269 if prefix.is_empty() {
1271 return "/".to_string();
1272 }
1273
1274 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
1276
1277 if segments.is_empty() {
1279 return "/".to_string();
1280 }
1281
1282 let mut result = String::with_capacity(prefix.len() + 1);
1284 for segment in segments {
1285 result.push('/');
1286 result.push_str(segment);
1287 }
1288
1289 result
1290}
1291
1292impl Default for RustApi {
1293 fn default() -> Self {
1294 Self::new()
1295 }
1296}
1297
1298#[cfg(test)]
1299mod tests {
1300 use super::RustApi;
1301 use crate::extract::{FromRequestParts, State};
1302 use crate::path_params::PathParams;
1303 use crate::request::Request;
1304 use crate::router::{get, post, Router};
1305 use bytes::Bytes;
1306 use http::Method;
1307 use proptest::prelude::*;
1308
1309 #[test]
1310 fn state_is_available_via_extractor() {
1311 let app = RustApi::new().state(123u32);
1312 let router = app.into_router();
1313
1314 let req = http::Request::builder()
1315 .method(Method::GET)
1316 .uri("/test")
1317 .body(())
1318 .unwrap();
1319 let (parts, _) = req.into_parts();
1320
1321 let request = Request::new(
1322 parts,
1323 crate::request::BodyVariant::Buffered(Bytes::new()),
1324 router.state_ref(),
1325 PathParams::new(),
1326 );
1327 let State(value) = State::<u32>::from_request_parts(&request).unwrap();
1328 assert_eq!(value, 123u32);
1329 }
1330
1331 #[test]
1332 fn test_path_param_type_inference_integer() {
1333 use super::infer_path_param_schema;
1334
1335 let int_params = [
1337 "page",
1338 "limit",
1339 "offset",
1340 "count",
1341 "item_count",
1342 "year",
1343 "month",
1344 "day",
1345 "index",
1346 "position",
1347 ];
1348
1349 for name in int_params {
1350 let schema = infer_path_param_schema(name);
1351 match schema {
1352 rustapi_openapi::SchemaRef::Inline(v) => {
1353 assert_eq!(
1354 v.get("type").and_then(|v| v.as_str()),
1355 Some("integer"),
1356 "Expected '{}' to be inferred as integer",
1357 name
1358 );
1359 }
1360 _ => panic!("Expected inline schema for '{}'", name),
1361 }
1362 }
1363 }
1364
1365 #[test]
1366 fn test_path_param_type_inference_uuid() {
1367 use super::infer_path_param_schema;
1368
1369 let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
1371
1372 for name in uuid_params {
1373 let schema = infer_path_param_schema(name);
1374 match schema {
1375 rustapi_openapi::SchemaRef::Inline(v) => {
1376 assert_eq!(
1377 v.get("type").and_then(|v| v.as_str()),
1378 Some("string"),
1379 "Expected '{}' to be inferred as string",
1380 name
1381 );
1382 assert_eq!(
1383 v.get("format").and_then(|v| v.as_str()),
1384 Some("uuid"),
1385 "Expected '{}' to have uuid format",
1386 name
1387 );
1388 }
1389 _ => panic!("Expected inline schema for '{}'", name),
1390 }
1391 }
1392 }
1393
1394 #[test]
1395 fn test_path_param_type_inference_string() {
1396 use super::infer_path_param_schema;
1397
1398 let string_params = [
1400 "name", "slug", "code", "token", "username", "id", "user_id", "userId", "postId",
1401 ];
1402
1403 for name in string_params {
1404 let schema = infer_path_param_schema(name);
1405 match schema {
1406 rustapi_openapi::SchemaRef::Inline(v) => {
1407 assert_eq!(
1408 v.get("type").and_then(|v| v.as_str()),
1409 Some("string"),
1410 "Expected '{}' to be inferred as string",
1411 name
1412 );
1413 assert!(
1414 v.get("format").is_none()
1415 || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
1416 "Expected '{}' to NOT have uuid format",
1417 name
1418 );
1419 }
1420 _ => panic!("Expected inline schema for '{}'", name),
1421 }
1422 }
1423 }
1424
1425 #[test]
1426 fn test_schema_type_to_openapi_schema() {
1427 use super::schema_type_to_openapi_schema;
1428
1429 let uuid_schema = schema_type_to_openapi_schema("uuid");
1431 match uuid_schema {
1432 rustapi_openapi::SchemaRef::Inline(v) => {
1433 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1434 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
1435 }
1436 _ => panic!("Expected inline schema for uuid"),
1437 }
1438
1439 for schema_type in ["integer", "int", "int64", "i64"] {
1441 let schema = schema_type_to_openapi_schema(schema_type);
1442 match schema {
1443 rustapi_openapi::SchemaRef::Inline(v) => {
1444 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1445 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
1446 }
1447 _ => panic!("Expected inline schema for {}", schema_type),
1448 }
1449 }
1450
1451 let int32_schema = schema_type_to_openapi_schema("int32");
1453 match int32_schema {
1454 rustapi_openapi::SchemaRef::Inline(v) => {
1455 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1456 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
1457 }
1458 _ => panic!("Expected inline schema for int32"),
1459 }
1460
1461 for schema_type in ["number", "float"] {
1463 let schema = schema_type_to_openapi_schema(schema_type);
1464 match schema {
1465 rustapi_openapi::SchemaRef::Inline(v) => {
1466 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
1467 }
1468 _ => panic!("Expected inline schema for {}", schema_type),
1469 }
1470 }
1471
1472 for schema_type in ["boolean", "bool"] {
1474 let schema = schema_type_to_openapi_schema(schema_type);
1475 match schema {
1476 rustapi_openapi::SchemaRef::Inline(v) => {
1477 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
1478 }
1479 _ => panic!("Expected inline schema for {}", schema_type),
1480 }
1481 }
1482
1483 let string_schema = schema_type_to_openapi_schema("string");
1485 match string_schema {
1486 rustapi_openapi::SchemaRef::Inline(v) => {
1487 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1488 }
1489 _ => panic!("Expected inline schema for string"),
1490 }
1491 }
1492
1493 proptest! {
1500 #![proptest_config(ProptestConfig::with_cases(100))]
1501
1502 #[test]
1507 fn prop_nested_routes_in_openapi_spec(
1508 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1510 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1512 has_param in any::<bool>(),
1513 ) {
1514 async fn handler() -> &'static str { "handler" }
1515
1516 let prefix = format!("/{}", prefix_segments.join("/"));
1518
1519 let mut route_path = format!("/{}", route_segments.join("/"));
1521 if has_param {
1522 route_path.push_str("/{id}");
1523 }
1524
1525 let nested_router = Router::new().route(&route_path, get(handler));
1527 let app = RustApi::new().nest(&prefix, nested_router);
1528
1529 let expected_openapi_path = format!("{}{}", prefix, route_path);
1531
1532 let spec = app.openapi_spec();
1534
1535 prop_assert!(
1537 spec.paths.contains_key(&expected_openapi_path),
1538 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1539 expected_openapi_path,
1540 spec.paths.keys().collect::<Vec<_>>()
1541 );
1542
1543 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1545 prop_assert!(
1546 path_item.get.is_some(),
1547 "GET operation should exist for path '{}'",
1548 expected_openapi_path
1549 );
1550 }
1551
1552 #[test]
1557 fn prop_multiple_methods_preserved_in_openapi(
1558 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1559 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1560 ) {
1561 async fn get_handler() -> &'static str { "get" }
1562 async fn post_handler() -> &'static str { "post" }
1563
1564 let prefix = format!("/{}", prefix_segments.join("/"));
1566 let route_path = format!("/{}", route_segments.join("/"));
1567
1568 let get_route_path = format!("{}/get", route_path);
1571 let post_route_path = format!("{}/post", route_path);
1572 let nested_router = Router::new()
1573 .route(&get_route_path, get(get_handler))
1574 .route(&post_route_path, post(post_handler));
1575 let app = RustApi::new().nest(&prefix, nested_router);
1576
1577 let expected_get_path = format!("{}{}", prefix, get_route_path);
1579 let expected_post_path = format!("{}{}", prefix, post_route_path);
1580
1581 let spec = app.openapi_spec();
1583
1584 prop_assert!(
1586 spec.paths.contains_key(&expected_get_path),
1587 "Expected OpenAPI path '{}' not found",
1588 expected_get_path
1589 );
1590 prop_assert!(
1591 spec.paths.contains_key(&expected_post_path),
1592 "Expected OpenAPI path '{}' not found",
1593 expected_post_path
1594 );
1595
1596 let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1598 prop_assert!(
1599 get_path_item.get.is_some(),
1600 "GET operation should exist for path '{}'",
1601 expected_get_path
1602 );
1603
1604 let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1606 prop_assert!(
1607 post_path_item.post.is_some(),
1608 "POST operation should exist for path '{}'",
1609 expected_post_path
1610 );
1611 }
1612
1613 #[test]
1618 fn prop_path_params_in_openapi_after_nesting(
1619 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1620 param_name in "[a-z][a-z0-9]{0,5}",
1621 ) {
1622 async fn handler() -> &'static str { "handler" }
1623
1624 let prefix = format!("/{}", prefix_segments.join("/"));
1626 let route_path = format!("/{{{}}}", param_name);
1627
1628 let nested_router = Router::new().route(&route_path, get(handler));
1630 let app = RustApi::new().nest(&prefix, nested_router);
1631
1632 let expected_openapi_path = format!("{}{}", prefix, route_path);
1634
1635 let spec = app.openapi_spec();
1637
1638 prop_assert!(
1640 spec.paths.contains_key(&expected_openapi_path),
1641 "Expected OpenAPI path '{}' not found",
1642 expected_openapi_path
1643 );
1644
1645 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1647 let get_op = path_item.get.as_ref().unwrap();
1648
1649 prop_assert!(
1650 get_op.parameters.is_some(),
1651 "Operation should have parameters for path '{}'",
1652 expected_openapi_path
1653 );
1654
1655 let params = get_op.parameters.as_ref().unwrap();
1656 let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1657 prop_assert!(
1658 has_param,
1659 "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1660 param_name,
1661 params.iter().map(|p| &p.name).collect::<Vec<_>>()
1662 );
1663 }
1664 }
1665
1666 proptest! {
1674 #![proptest_config(ProptestConfig::with_cases(100))]
1675
1676 #[test]
1681 fn prop_rustapi_nest_delegates_to_router_nest(
1682 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1683 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1684 has_param in any::<bool>(),
1685 ) {
1686 async fn handler() -> &'static str { "handler" }
1687
1688 let prefix = format!("/{}", prefix_segments.join("/"));
1690
1691 let mut route_path = format!("/{}", route_segments.join("/"));
1693 if has_param {
1694 route_path.push_str("/{id}");
1695 }
1696
1697 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1699 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1700
1701 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1703 let rustapi_router = rustapi_app.into_router();
1704
1705 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1707
1708 let rustapi_routes = rustapi_router.registered_routes();
1710 let router_routes = router_app.registered_routes();
1711
1712 prop_assert_eq!(
1713 rustapi_routes.len(),
1714 router_routes.len(),
1715 "RustApi and Router should have same number of routes"
1716 );
1717
1718 for (path, info) in router_routes {
1720 prop_assert!(
1721 rustapi_routes.contains_key(path),
1722 "Route '{}' from Router should exist in RustApi routes",
1723 path
1724 );
1725
1726 let rustapi_info = rustapi_routes.get(path).unwrap();
1727 prop_assert_eq!(
1728 &info.path, &rustapi_info.path,
1729 "Display paths should match for route '{}'",
1730 path
1731 );
1732 prop_assert_eq!(
1733 info.methods.len(), rustapi_info.methods.len(),
1734 "Method count should match for route '{}'",
1735 path
1736 );
1737 }
1738 }
1739
1740 #[test]
1745 fn prop_rustapi_nest_includes_routes_in_openapi(
1746 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1747 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1748 has_param in any::<bool>(),
1749 ) {
1750 async fn handler() -> &'static str { "handler" }
1751
1752 let prefix = format!("/{}", prefix_segments.join("/"));
1754
1755 let mut route_path = format!("/{}", route_segments.join("/"));
1757 if has_param {
1758 route_path.push_str("/{id}");
1759 }
1760
1761 let nested_router = Router::new().route(&route_path, get(handler));
1763 let app = RustApi::new().nest(&prefix, nested_router);
1764
1765 let expected_openapi_path = format!("{}{}", prefix, route_path);
1767
1768 let spec = app.openapi_spec();
1770
1771 prop_assert!(
1773 spec.paths.contains_key(&expected_openapi_path),
1774 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1775 expected_openapi_path,
1776 spec.paths.keys().collect::<Vec<_>>()
1777 );
1778
1779 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1781 prop_assert!(
1782 path_item.get.is_some(),
1783 "GET operation should exist for path '{}'",
1784 expected_openapi_path
1785 );
1786 }
1787
1788 #[test]
1793 fn prop_rustapi_nest_route_matching_identical(
1794 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1795 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1796 param_value in "[a-z0-9]{1,10}",
1797 ) {
1798 use crate::router::RouteMatch;
1799
1800 async fn handler() -> &'static str { "handler" }
1801
1802 let prefix = format!("/{}", prefix_segments.join("/"));
1804 let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1805
1806 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1808 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1809
1810 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1812 let rustapi_router = rustapi_app.into_router();
1813 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1814
1815 let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1817
1818 let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1820 let router_match = router_app.match_route(&full_path, &Method::GET);
1821
1822 match (rustapi_match, router_match) {
1824 (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1825 prop_assert_eq!(
1826 rustapi_params.len(),
1827 router_params.len(),
1828 "Parameter count should match"
1829 );
1830 for (key, value) in &router_params {
1831 prop_assert!(
1832 rustapi_params.contains_key(key),
1833 "RustApi should have parameter '{}'",
1834 key
1835 );
1836 prop_assert_eq!(
1837 rustapi_params.get(key).unwrap(),
1838 value,
1839 "Parameter '{}' value should match",
1840 key
1841 );
1842 }
1843 }
1844 (rustapi_result, router_result) => {
1845 prop_assert!(
1846 false,
1847 "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1848 match rustapi_result {
1849 RouteMatch::Found { .. } => "Found",
1850 RouteMatch::NotFound => "NotFound",
1851 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1852 },
1853 match router_result {
1854 RouteMatch::Found { .. } => "Found",
1855 RouteMatch::NotFound => "NotFound",
1856 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1857 }
1858 );
1859 }
1860 }
1861 }
1862 }
1863
1864 #[test]
1866 fn test_openapi_operations_propagated_during_nesting() {
1867 async fn list_users() -> &'static str {
1868 "list users"
1869 }
1870 async fn get_user() -> &'static str {
1871 "get user"
1872 }
1873 async fn create_user() -> &'static str {
1874 "create user"
1875 }
1876
1877 let users_router = Router::new()
1880 .route("/", get(list_users))
1881 .route("/create", post(create_user))
1882 .route("/{id}", get(get_user));
1883
1884 let app = RustApi::new().nest("/api/v1/users", users_router);
1886
1887 let spec = app.openapi_spec();
1888
1889 assert!(
1891 spec.paths.contains_key("/api/v1/users"),
1892 "Should have /api/v1/users path"
1893 );
1894 let users_path = spec.paths.get("/api/v1/users").unwrap();
1895 assert!(users_path.get.is_some(), "Should have GET operation");
1896
1897 assert!(
1899 spec.paths.contains_key("/api/v1/users/create"),
1900 "Should have /api/v1/users/create path"
1901 );
1902 let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1903 assert!(create_path.post.is_some(), "Should have POST operation");
1904
1905 assert!(
1907 spec.paths.contains_key("/api/v1/users/{id}"),
1908 "Should have /api/v1/users/{{id}} path"
1909 );
1910 let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1911 assert!(
1912 user_path.get.is_some(),
1913 "Should have GET operation for user by id"
1914 );
1915
1916 let get_user_op = user_path.get.as_ref().unwrap();
1918 assert!(get_user_op.parameters.is_some(), "Should have parameters");
1919 let params = get_user_op.parameters.as_ref().unwrap();
1920 assert!(
1921 params
1922 .iter()
1923 .any(|p| p.name == "id" && p.location == "path"),
1924 "Should have 'id' path parameter"
1925 );
1926 }
1927
1928 #[test]
1930 fn test_openapi_spec_empty_without_routes() {
1931 let app = RustApi::new();
1932 let spec = app.openapi_spec();
1933
1934 assert!(
1936 spec.paths.is_empty(),
1937 "OpenAPI spec should have no paths without routes"
1938 );
1939 }
1940
1941 #[test]
1946 fn test_rustapi_nest_delegates_to_router_nest() {
1947 use crate::router::RouteMatch;
1948
1949 async fn list_users() -> &'static str {
1950 "list users"
1951 }
1952 async fn get_user() -> &'static str {
1953 "get user"
1954 }
1955 async fn create_user() -> &'static str {
1956 "create user"
1957 }
1958
1959 let users_router = Router::new()
1961 .route("/", get(list_users))
1962 .route("/create", post(create_user))
1963 .route("/{id}", get(get_user));
1964
1965 let app = RustApi::new().nest("/api/v1/users", users_router);
1967 let router = app.into_router();
1968
1969 let routes = router.registered_routes();
1971 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1972
1973 assert!(
1975 routes.contains_key("/api/v1/users"),
1976 "Should have /api/v1/users route"
1977 );
1978 assert!(
1979 routes.contains_key("/api/v1/users/create"),
1980 "Should have /api/v1/users/create route"
1981 );
1982 assert!(
1983 routes.contains_key("/api/v1/users/:id"),
1984 "Should have /api/v1/users/:id route"
1985 );
1986
1987 match router.match_route("/api/v1/users", &Method::GET) {
1989 RouteMatch::Found { params, .. } => {
1990 assert!(params.is_empty(), "Root route should have no params");
1991 }
1992 _ => panic!("GET /api/v1/users should be found"),
1993 }
1994
1995 match router.match_route("/api/v1/users/create", &Method::POST) {
1996 RouteMatch::Found { params, .. } => {
1997 assert!(params.is_empty(), "Create route should have no params");
1998 }
1999 _ => panic!("POST /api/v1/users/create should be found"),
2000 }
2001
2002 match router.match_route("/api/v1/users/123", &Method::GET) {
2003 RouteMatch::Found { params, .. } => {
2004 assert_eq!(
2005 params.get("id"),
2006 Some(&"123".to_string()),
2007 "Should extract id param"
2008 );
2009 }
2010 _ => panic!("GET /api/v1/users/123 should be found"),
2011 }
2012
2013 match router.match_route("/api/v1/users", &Method::DELETE) {
2015 RouteMatch::MethodNotAllowed { allowed } => {
2016 assert!(allowed.contains(&Method::GET), "Should allow GET");
2017 }
2018 _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
2019 }
2020 }
2021
2022 #[test]
2027 fn test_rustapi_nest_includes_routes_in_openapi_spec() {
2028 async fn list_items() -> &'static str {
2029 "list items"
2030 }
2031 async fn get_item() -> &'static str {
2032 "get item"
2033 }
2034
2035 let items_router = Router::new()
2037 .route("/", get(list_items))
2038 .route("/{item_id}", get(get_item));
2039
2040 let app = RustApi::new().nest("/api/items", items_router);
2042
2043 let spec = app.openapi_spec();
2045
2046 assert!(
2048 spec.paths.contains_key("/api/items"),
2049 "Should have /api/items in OpenAPI"
2050 );
2051 assert!(
2052 spec.paths.contains_key("/api/items/{item_id}"),
2053 "Should have /api/items/{{item_id}} in OpenAPI"
2054 );
2055
2056 let list_path = spec.paths.get("/api/items").unwrap();
2058 assert!(
2059 list_path.get.is_some(),
2060 "Should have GET operation for /api/items"
2061 );
2062
2063 let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2064 assert!(
2065 get_path.get.is_some(),
2066 "Should have GET operation for /api/items/{{item_id}}"
2067 );
2068
2069 let get_op = get_path.get.as_ref().unwrap();
2071 assert!(get_op.parameters.is_some(), "Should have parameters");
2072 let params = get_op.parameters.as_ref().unwrap();
2073 assert!(
2074 params
2075 .iter()
2076 .any(|p| p.name == "item_id" && p.location == "path"),
2077 "Should have 'item_id' path parameter"
2078 );
2079 }
2080}
2081
2082#[cfg(feature = "swagger-ui")]
2084fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2085 req.headers()
2086 .get(http::header::AUTHORIZATION)
2087 .and_then(|v| v.to_str().ok())
2088 .map(|auth| auth == expected)
2089 .unwrap_or(false)
2090}
2091
2092#[cfg(feature = "swagger-ui")]
2094fn unauthorized_response() -> crate::Response {
2095 http::Response::builder()
2096 .status(http::StatusCode::UNAUTHORIZED)
2097 .header(
2098 http::header::WWW_AUTHENTICATE,
2099 "Basic realm=\"API Documentation\"",
2100 )
2101 .header(http::header::CONTENT_TYPE, "text/plain")
2102 .body(crate::response::Body::from("Unauthorized"))
2103 .unwrap()
2104}
2105
2106pub struct RustApiConfig {
2108 docs_path: Option<String>,
2109 docs_enabled: bool,
2110 api_title: String,
2111 api_version: String,
2112 api_description: Option<String>,
2113 body_limit: Option<usize>,
2114 layers: LayerStack,
2115}
2116
2117impl Default for RustApiConfig {
2118 fn default() -> Self {
2119 Self::new()
2120 }
2121}
2122
2123impl RustApiConfig {
2124 pub fn new() -> Self {
2125 Self {
2126 docs_path: Some("/docs".to_string()),
2127 docs_enabled: true,
2128 api_title: "RustAPI".to_string(),
2129 api_version: "1.0.0".to_string(),
2130 api_description: None,
2131 body_limit: None,
2132 layers: LayerStack::new(),
2133 }
2134 }
2135
2136 pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2138 self.docs_path = Some(path.into());
2139 self
2140 }
2141
2142 pub fn docs_enabled(mut self, enabled: bool) -> Self {
2144 self.docs_enabled = enabled;
2145 self
2146 }
2147
2148 pub fn openapi_info(
2150 mut self,
2151 title: impl Into<String>,
2152 version: impl Into<String>,
2153 description: Option<impl Into<String>>,
2154 ) -> Self {
2155 self.api_title = title.into();
2156 self.api_version = version.into();
2157 self.api_description = description.map(|d| d.into());
2158 self
2159 }
2160
2161 pub fn body_limit(mut self, limit: usize) -> Self {
2163 self.body_limit = Some(limit);
2164 self
2165 }
2166
2167 pub fn layer<L>(mut self, layer: L) -> Self
2169 where
2170 L: MiddlewareLayer,
2171 {
2172 self.layers.push(Box::new(layer));
2173 self
2174 }
2175
2176 pub fn build(self) -> RustApi {
2178 let mut app = RustApi::new().mount_auto_routes_grouped();
2179
2180 if let Some(limit) = self.body_limit {
2182 app = app.body_limit(limit);
2183 }
2184
2185 app = app.openapi_info(
2186 &self.api_title,
2187 &self.api_version,
2188 self.api_description.as_deref(),
2189 );
2190
2191 #[cfg(feature = "swagger-ui")]
2192 if self.docs_enabled {
2193 if let Some(path) = self.docs_path {
2194 app = app.docs(&path);
2195 }
2196 }
2197
2198 app.layers.extend(self.layers);
2201
2202 app
2203 }
2204
2205 pub async fn run(
2207 self,
2208 addr: impl AsRef<str>,
2209 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2210 self.build().run(addr.as_ref()).await
2211 }
2212}