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::{BTreeMap, 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: rustapi_openapi::schema::RustApiSchema>(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, &BTreeMap::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(&prefixed_path, &mut op, &BTreeMap::new());
524 self.openapi_spec = self.openapi_spec.path(&prefixed_path, method.as_str(), op);
525 }
526 }
527
528 self.router = self.router.nest(prefix, router);
530 self
531 }
532
533 pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
562 self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
563 }
564
565 pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
582 use crate::router::MethodRouter;
583 use std::collections::HashMap;
584
585 let prefix = config.prefix.clone();
586 let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
587
588 let handler: crate::handler::BoxedHandler =
590 std::sync::Arc::new(move |req: crate::Request| {
591 let config = config.clone();
592 let path = req.uri().path().to_string();
593
594 Box::pin(async move {
595 let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
596
597 match crate::static_files::StaticFile::serve(relative_path, &config).await {
598 Ok(response) => response,
599 Err(err) => err.into_response(),
600 }
601 })
602 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
603 });
604
605 let mut handlers = HashMap::new();
606 handlers.insert(http::Method::GET, handler);
607 let method_router = MethodRouter::from_boxed(handlers);
608
609 self.route(&catch_all_path, method_router)
610 }
611
612 #[cfg(feature = "compression")]
629 pub fn compression(self) -> Self {
630 self.layer(crate::middleware::CompressionLayer::new())
631 }
632
633 #[cfg(feature = "compression")]
649 pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
650 self.layer(crate::middleware::CompressionLayer::with_config(config))
651 }
652
653 #[cfg(feature = "swagger-ui")]
677 pub fn docs(self, path: &str) -> Self {
678 let title = self.openapi_spec.info.title.clone();
679 let version = self.openapi_spec.info.version.clone();
680 let description = self.openapi_spec.info.description.clone();
681
682 self.docs_with_info(path, &title, &version, description.as_deref())
683 }
684
685 #[cfg(feature = "swagger-ui")]
694 pub fn docs_with_info(
695 mut self,
696 path: &str,
697 title: &str,
698 version: &str,
699 description: Option<&str>,
700 ) -> Self {
701 use crate::router::get;
702 self.openapi_spec.info.title = title.to_string();
704 self.openapi_spec.info.version = version.to_string();
705 if let Some(desc) = description {
706 self.openapi_spec.info.description = Some(desc.to_string());
707 }
708
709 let path = path.trim_end_matches('/');
710 let openapi_path = format!("{}/openapi.json", path);
711
712 let spec_json =
714 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
715 let openapi_url = openapi_path.clone();
716
717 let spec_handler = move || {
719 let json = spec_json.clone();
720 async move {
721 http::Response::builder()
722 .status(http::StatusCode::OK)
723 .header(http::header::CONTENT_TYPE, "application/json")
724 .body(crate::response::Body::from(json))
725 .unwrap()
726 }
727 };
728
729 let docs_handler = move || {
731 let url = openapi_url.clone();
732 async move {
733 let response = rustapi_openapi::swagger_ui_html(&url);
734 response.map(crate::response::Body::Full)
735 }
736 };
737
738 self.route(&openapi_path, get(spec_handler))
739 .route(path, get(docs_handler))
740 }
741
742 #[cfg(feature = "swagger-ui")]
758 pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
759 let title = self.openapi_spec.info.title.clone();
760 let version = self.openapi_spec.info.version.clone();
761 let description = self.openapi_spec.info.description.clone();
762
763 self.docs_with_auth_and_info(
764 path,
765 username,
766 password,
767 &title,
768 &version,
769 description.as_deref(),
770 )
771 }
772
773 #[cfg(feature = "swagger-ui")]
789 pub fn docs_with_auth_and_info(
790 mut self,
791 path: &str,
792 username: &str,
793 password: &str,
794 title: &str,
795 version: &str,
796 description: Option<&str>,
797 ) -> Self {
798 use crate::router::MethodRouter;
799 use base64::{engine::general_purpose::STANDARD, Engine};
800 use std::collections::HashMap;
801
802 self.openapi_spec.info.title = title.to_string();
804 self.openapi_spec.info.version = version.to_string();
805 if let Some(desc) = description {
806 self.openapi_spec.info.description = Some(desc.to_string());
807 }
808
809 let path = path.trim_end_matches('/');
810 let openapi_path = format!("{}/openapi.json", path);
811
812 let credentials = format!("{}:{}", username, password);
814 let encoded = STANDARD.encode(credentials.as_bytes());
815 let expected_auth = format!("Basic {}", encoded);
816
817 let spec_json =
819 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
820 let openapi_url = openapi_path.clone();
821 let expected_auth_spec = expected_auth.clone();
822 let expected_auth_docs = expected_auth;
823
824 let spec_handler: crate::handler::BoxedHandler =
826 std::sync::Arc::new(move |req: crate::Request| {
827 let json = spec_json.clone();
828 let expected = expected_auth_spec.clone();
829 Box::pin(async move {
830 if !check_basic_auth(&req, &expected) {
831 return unauthorized_response();
832 }
833 http::Response::builder()
834 .status(http::StatusCode::OK)
835 .header(http::header::CONTENT_TYPE, "application/json")
836 .body(crate::response::Body::from(json))
837 .unwrap()
838 })
839 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
840 });
841
842 let docs_handler: crate::handler::BoxedHandler =
844 std::sync::Arc::new(move |req: crate::Request| {
845 let url = openapi_url.clone();
846 let expected = expected_auth_docs.clone();
847 Box::pin(async move {
848 if !check_basic_auth(&req, &expected) {
849 return unauthorized_response();
850 }
851 let response = rustapi_openapi::swagger_ui_html(&url);
852 response.map(crate::response::Body::Full)
853 })
854 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
855 });
856
857 let mut spec_handlers = HashMap::new();
859 spec_handlers.insert(http::Method::GET, spec_handler);
860 let spec_router = MethodRouter::from_boxed(spec_handlers);
861
862 let mut docs_handlers = HashMap::new();
863 docs_handlers.insert(http::Method::GET, docs_handler);
864 let docs_router = MethodRouter::from_boxed(docs_handlers);
865
866 self.route(&openapi_path, spec_router)
867 .route(path, docs_router)
868 }
869
870 pub fn status_page(self) -> Self {
872 self.status_page_with_config(crate::status::StatusConfig::default())
873 }
874
875 pub fn status_page_with_config(mut self, config: crate::status::StatusConfig) -> Self {
877 self.status_config = Some(config);
878 self
879 }
880
881 fn apply_status_page(&mut self) {
883 if let Some(config) = &self.status_config {
884 let monitor = std::sync::Arc::new(crate::status::StatusMonitor::new());
885
886 self.layers
888 .push(Box::new(crate::status::StatusLayer::new(monitor.clone())));
889
890 use crate::router::MethodRouter;
892 use std::collections::HashMap;
893
894 let monitor = monitor.clone();
895 let config = config.clone();
896 let path = config.path.clone(); let handler: crate::handler::BoxedHandler = std::sync::Arc::new(move |_| {
899 let monitor = monitor.clone();
900 let config = config.clone();
901 Box::pin(async move {
902 crate::status::status_handler(monitor, config)
903 .await
904 .into_response()
905 })
906 });
907
908 let mut handlers = HashMap::new();
909 handlers.insert(http::Method::GET, handler);
910 let method_router = MethodRouter::from_boxed(handlers);
911
912 let router = std::mem::take(&mut self.router);
914 self.router = router.route(&path, method_router);
915 }
916 }
917
918 pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
929 self.apply_status_page();
931
932 if let Some(limit) = self.body_limit {
934 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
936 }
937
938 let server = Server::new(self.router, self.layers, self.interceptors);
939 server.run(addr).await
940 }
941
942 pub async fn run_with_shutdown<F>(
944 mut self,
945 addr: impl AsRef<str>,
946 signal: F,
947 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
948 where
949 F: std::future::Future<Output = ()> + Send + 'static,
950 {
951 self.apply_status_page();
953
954 if let Some(limit) = self.body_limit {
955 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
956 }
957
958 let server = Server::new(self.router, self.layers, self.interceptors);
959 server.run_with_shutdown(addr.as_ref(), signal).await
960 }
961
962 pub fn into_router(self) -> Router {
964 self.router
965 }
966
967 pub fn layers(&self) -> &LayerStack {
969 &self.layers
970 }
971
972 pub fn interceptors(&self) -> &InterceptorChain {
974 &self.interceptors
975 }
976
977 #[cfg(feature = "http3")]
991 pub async fn run_http3(
992 mut self,
993 config: crate::http3::Http3Config,
994 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
995 use std::sync::Arc;
996
997 self.apply_status_page();
999
1000 if let Some(limit) = self.body_limit {
1002 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1003 }
1004
1005 let server = crate::http3::Http3Server::new(
1006 &config,
1007 Arc::new(self.router),
1008 Arc::new(self.layers),
1009 Arc::new(self.interceptors),
1010 )
1011 .await?;
1012
1013 server.run().await
1014 }
1015
1016 #[cfg(feature = "http3-dev")]
1030 pub async fn run_http3_dev(
1031 mut self,
1032 addr: &str,
1033 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1034 use std::sync::Arc;
1035
1036 self.apply_status_page();
1038
1039 if let Some(limit) = self.body_limit {
1041 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1042 }
1043
1044 let server = crate::http3::Http3Server::new_with_self_signed(
1045 addr,
1046 Arc::new(self.router),
1047 Arc::new(self.layers),
1048 Arc::new(self.interceptors),
1049 )
1050 .await?;
1051
1052 server.run().await
1053 }
1054
1055 #[cfg(feature = "http3")]
1079 pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
1080 self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
1081 self
1082 }
1083
1084 #[cfg(feature = "http3")]
1099 pub async fn run_dual_stack(
1100 mut self,
1101 _http_addr: &str,
1102 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1103 let config = self
1111 .http3_config
1112 .take()
1113 .ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
1114
1115 tracing::warn!("run_dual_stack currently only runs HTTP/3. HTTP/1.1 support coming soon.");
1116 self.run_http3(config).await
1117 }
1118}
1119
1120fn add_path_params_to_operation(
1121 path: &str,
1122 op: &mut rustapi_openapi::Operation,
1123 param_schemas: &BTreeMap<String, String>,
1124) {
1125 let mut params: Vec<String> = Vec::new();
1126 let mut in_brace = false;
1127 let mut current = String::new();
1128
1129 for ch in path.chars() {
1130 match ch {
1131 '{' => {
1132 in_brace = true;
1133 current.clear();
1134 }
1135 '}' => {
1136 if in_brace {
1137 in_brace = false;
1138 if !current.is_empty() {
1139 params.push(current.clone());
1140 }
1141 }
1142 }
1143 _ => {
1144 if in_brace {
1145 current.push(ch);
1146 }
1147 }
1148 }
1149 }
1150
1151 if params.is_empty() {
1152 return;
1153 }
1154
1155 let op_params = &mut op.parameters;
1156
1157 for name in params {
1158 let already = op_params
1159 .iter()
1160 .any(|p| p.location == "path" && p.name == name);
1161 if already {
1162 continue;
1163 }
1164
1165 let schema = if let Some(schema_type) = param_schemas.get(&name) {
1167 schema_type_to_openapi_schema(schema_type)
1168 } else {
1169 infer_path_param_schema(&name)
1170 };
1171
1172 op_params.push(rustapi_openapi::Parameter {
1173 name,
1174 location: "path".to_string(),
1175 required: true,
1176 description: None,
1177 deprecated: None,
1178 schema: Some(schema),
1179 });
1180 }
1181}
1182
1183fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef {
1185 match schema_type.to_lowercase().as_str() {
1186 "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1187 "type": "string",
1188 "format": "uuid"
1189 })),
1190 "integer" | "int" | "int64" | "i64" => {
1191 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1192 "type": "integer",
1193 "format": "int64"
1194 }))
1195 }
1196 "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1197 "type": "integer",
1198 "format": "int32"
1199 })),
1200 "number" | "float" | "f64" | "f32" => {
1201 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1202 "type": "number"
1203 }))
1204 }
1205 "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1206 "type": "boolean"
1207 })),
1208 _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1209 "type": "string"
1210 })),
1211 }
1212}
1213
1214fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef {
1223 let lower = name.to_lowercase();
1224
1225 let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid");
1227
1228 if is_uuid {
1229 return rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1230 "type": "string",
1231 "format": "uuid"
1232 }));
1233 }
1234
1235 let is_integer = lower == "page"
1238 || lower == "limit"
1239 || lower == "offset"
1240 || lower == "count"
1241 || lower.ends_with("_count")
1242 || lower.ends_with("_num")
1243 || lower == "year"
1244 || lower == "month"
1245 || lower == "day"
1246 || lower == "index"
1247 || lower == "position";
1248
1249 if is_integer {
1250 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1251 "type": "integer",
1252 "format": "int64"
1253 }))
1254 } else {
1255 rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1256 }
1257}
1258
1259fn normalize_prefix_for_openapi(prefix: &str) -> String {
1266 if prefix.is_empty() {
1268 return "/".to_string();
1269 }
1270
1271 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
1273
1274 if segments.is_empty() {
1276 return "/".to_string();
1277 }
1278
1279 let mut result = String::with_capacity(prefix.len() + 1);
1281 for segment in segments {
1282 result.push('/');
1283 result.push_str(segment);
1284 }
1285
1286 result
1287}
1288
1289impl Default for RustApi {
1290 fn default() -> Self {
1291 Self::new()
1292 }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::RustApi;
1298 use crate::extract::{FromRequestParts, State};
1299 use crate::path_params::PathParams;
1300 use crate::request::Request;
1301 use crate::router::{get, post, Router};
1302 use bytes::Bytes;
1303 use http::Method;
1304 use proptest::prelude::*;
1305
1306 #[test]
1307 fn state_is_available_via_extractor() {
1308 let app = RustApi::new().state(123u32);
1309 let router = app.into_router();
1310
1311 let req = http::Request::builder()
1312 .method(Method::GET)
1313 .uri("/test")
1314 .body(())
1315 .unwrap();
1316 let (parts, _) = req.into_parts();
1317
1318 let request = Request::new(
1319 parts,
1320 crate::request::BodyVariant::Buffered(Bytes::new()),
1321 router.state_ref(),
1322 PathParams::new(),
1323 );
1324 let State(value) = State::<u32>::from_request_parts(&request).unwrap();
1325 assert_eq!(value, 123u32);
1326 }
1327
1328 #[test]
1329 fn test_path_param_type_inference_integer() {
1330 use super::infer_path_param_schema;
1331
1332 let int_params = [
1334 "page",
1335 "limit",
1336 "offset",
1337 "count",
1338 "item_count",
1339 "year",
1340 "month",
1341 "day",
1342 "index",
1343 "position",
1344 ];
1345
1346 for name in int_params {
1347 let schema = infer_path_param_schema(name);
1348 match schema {
1349 rustapi_openapi::SchemaRef::Inline(v) => {
1350 assert_eq!(
1351 v.get("type").and_then(|v| v.as_str()),
1352 Some("integer"),
1353 "Expected '{}' to be inferred as integer",
1354 name
1355 );
1356 }
1357 _ => panic!("Expected inline schema for '{}'", name),
1358 }
1359 }
1360 }
1361
1362 #[test]
1363 fn test_path_param_type_inference_uuid() {
1364 use super::infer_path_param_schema;
1365
1366 let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
1368
1369 for name in uuid_params {
1370 let schema = infer_path_param_schema(name);
1371 match schema {
1372 rustapi_openapi::SchemaRef::Inline(v) => {
1373 assert_eq!(
1374 v.get("type").and_then(|v| v.as_str()),
1375 Some("string"),
1376 "Expected '{}' to be inferred as string",
1377 name
1378 );
1379 assert_eq!(
1380 v.get("format").and_then(|v| v.as_str()),
1381 Some("uuid"),
1382 "Expected '{}' to have uuid format",
1383 name
1384 );
1385 }
1386 _ => panic!("Expected inline schema for '{}'", name),
1387 }
1388 }
1389 }
1390
1391 #[test]
1392 fn test_path_param_type_inference_string() {
1393 use super::infer_path_param_schema;
1394
1395 let string_params = [
1397 "name", "slug", "code", "token", "username", "id", "user_id", "userId", "postId",
1398 ];
1399
1400 for name in string_params {
1401 let schema = infer_path_param_schema(name);
1402 match schema {
1403 rustapi_openapi::SchemaRef::Inline(v) => {
1404 assert_eq!(
1405 v.get("type").and_then(|v| v.as_str()),
1406 Some("string"),
1407 "Expected '{}' to be inferred as string",
1408 name
1409 );
1410 assert!(
1411 v.get("format").is_none()
1412 || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
1413 "Expected '{}' to NOT have uuid format",
1414 name
1415 );
1416 }
1417 _ => panic!("Expected inline schema for '{}'", name),
1418 }
1419 }
1420 }
1421
1422 #[test]
1423 fn test_schema_type_to_openapi_schema() {
1424 use super::schema_type_to_openapi_schema;
1425
1426 let uuid_schema = schema_type_to_openapi_schema("uuid");
1428 match uuid_schema {
1429 rustapi_openapi::SchemaRef::Inline(v) => {
1430 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1431 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
1432 }
1433 _ => panic!("Expected inline schema for uuid"),
1434 }
1435
1436 for schema_type in ["integer", "int", "int64", "i64"] {
1438 let schema = schema_type_to_openapi_schema(schema_type);
1439 match schema {
1440 rustapi_openapi::SchemaRef::Inline(v) => {
1441 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1442 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
1443 }
1444 _ => panic!("Expected inline schema for {}", schema_type),
1445 }
1446 }
1447
1448 let int32_schema = schema_type_to_openapi_schema("int32");
1450 match int32_schema {
1451 rustapi_openapi::SchemaRef::Inline(v) => {
1452 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1453 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
1454 }
1455 _ => panic!("Expected inline schema for int32"),
1456 }
1457
1458 for schema_type in ["number", "float"] {
1460 let schema = schema_type_to_openapi_schema(schema_type);
1461 match schema {
1462 rustapi_openapi::SchemaRef::Inline(v) => {
1463 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
1464 }
1465 _ => panic!("Expected inline schema for {}", schema_type),
1466 }
1467 }
1468
1469 for schema_type in ["boolean", "bool"] {
1471 let schema = schema_type_to_openapi_schema(schema_type);
1472 match schema {
1473 rustapi_openapi::SchemaRef::Inline(v) => {
1474 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
1475 }
1476 _ => panic!("Expected inline schema for {}", schema_type),
1477 }
1478 }
1479
1480 let string_schema = schema_type_to_openapi_schema("string");
1482 match string_schema {
1483 rustapi_openapi::SchemaRef::Inline(v) => {
1484 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1485 }
1486 _ => panic!("Expected inline schema for string"),
1487 }
1488 }
1489
1490 proptest! {
1497 #![proptest_config(ProptestConfig::with_cases(100))]
1498
1499 #[test]
1504 fn prop_nested_routes_in_openapi_spec(
1505 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1507 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1509 has_param in any::<bool>(),
1510 ) {
1511 async fn handler() -> &'static str { "handler" }
1512
1513 let prefix = format!("/{}", prefix_segments.join("/"));
1515
1516 let mut route_path = format!("/{}", route_segments.join("/"));
1518 if has_param {
1519 route_path.push_str("/{id}");
1520 }
1521
1522 let nested_router = Router::new().route(&route_path, get(handler));
1524 let app = RustApi::new().nest(&prefix, nested_router);
1525
1526 let expected_openapi_path = format!("{}{}", prefix, route_path);
1528
1529 let spec = app.openapi_spec();
1531
1532 prop_assert!(
1534 spec.paths.contains_key(&expected_openapi_path),
1535 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1536 expected_openapi_path,
1537 spec.paths.keys().collect::<Vec<_>>()
1538 );
1539
1540 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1542 prop_assert!(
1543 path_item.get.is_some(),
1544 "GET operation should exist for path '{}'",
1545 expected_openapi_path
1546 );
1547 }
1548
1549 #[test]
1554 fn prop_multiple_methods_preserved_in_openapi(
1555 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1556 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1557 ) {
1558 async fn get_handler() -> &'static str { "get" }
1559 async fn post_handler() -> &'static str { "post" }
1560
1561 let prefix = format!("/{}", prefix_segments.join("/"));
1563 let route_path = format!("/{}", route_segments.join("/"));
1564
1565 let get_route_path = format!("{}/get", route_path);
1568 let post_route_path = format!("{}/post", route_path);
1569 let nested_router = Router::new()
1570 .route(&get_route_path, get(get_handler))
1571 .route(&post_route_path, post(post_handler));
1572 let app = RustApi::new().nest(&prefix, nested_router);
1573
1574 let expected_get_path = format!("{}{}", prefix, get_route_path);
1576 let expected_post_path = format!("{}{}", prefix, post_route_path);
1577
1578 let spec = app.openapi_spec();
1580
1581 prop_assert!(
1583 spec.paths.contains_key(&expected_get_path),
1584 "Expected OpenAPI path '{}' not found",
1585 expected_get_path
1586 );
1587 prop_assert!(
1588 spec.paths.contains_key(&expected_post_path),
1589 "Expected OpenAPI path '{}' not found",
1590 expected_post_path
1591 );
1592
1593 let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1595 prop_assert!(
1596 get_path_item.get.is_some(),
1597 "GET operation should exist for path '{}'",
1598 expected_get_path
1599 );
1600
1601 let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1603 prop_assert!(
1604 post_path_item.post.is_some(),
1605 "POST operation should exist for path '{}'",
1606 expected_post_path
1607 );
1608 }
1609
1610 #[test]
1615 fn prop_path_params_in_openapi_after_nesting(
1616 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1617 param_name in "[a-z][a-z0-9]{0,5}",
1618 ) {
1619 async fn handler() -> &'static str { "handler" }
1620
1621 let prefix = format!("/{}", prefix_segments.join("/"));
1623 let route_path = format!("/{{{}}}", param_name);
1624
1625 let nested_router = Router::new().route(&route_path, get(handler));
1627 let app = RustApi::new().nest(&prefix, nested_router);
1628
1629 let expected_openapi_path = format!("{}{}", prefix, route_path);
1631
1632 let spec = app.openapi_spec();
1634
1635 prop_assert!(
1637 spec.paths.contains_key(&expected_openapi_path),
1638 "Expected OpenAPI path '{}' not found",
1639 expected_openapi_path
1640 );
1641
1642 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1644 let get_op = path_item.get.as_ref().unwrap();
1645
1646 prop_assert!(
1647 !get_op.parameters.is_empty(),
1648 "Operation should have parameters for path '{}'",
1649 expected_openapi_path
1650 );
1651
1652 let params = &get_op.parameters;
1653 let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1654 prop_assert!(
1655 has_param,
1656 "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1657 param_name,
1658 params.iter().map(|p| &p.name).collect::<Vec<_>>()
1659 );
1660 }
1661 }
1662
1663 proptest! {
1671 #![proptest_config(ProptestConfig::with_cases(100))]
1672
1673 #[test]
1678 fn prop_rustapi_nest_delegates_to_router_nest(
1679 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1680 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1681 has_param in any::<bool>(),
1682 ) {
1683 async fn handler() -> &'static str { "handler" }
1684
1685 let prefix = format!("/{}", prefix_segments.join("/"));
1687
1688 let mut route_path = format!("/{}", route_segments.join("/"));
1690 if has_param {
1691 route_path.push_str("/{id}");
1692 }
1693
1694 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1696 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1697
1698 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1700 let rustapi_router = rustapi_app.into_router();
1701
1702 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1704
1705 let rustapi_routes = rustapi_router.registered_routes();
1707 let router_routes = router_app.registered_routes();
1708
1709 prop_assert_eq!(
1710 rustapi_routes.len(),
1711 router_routes.len(),
1712 "RustApi and Router should have same number of routes"
1713 );
1714
1715 for (path, info) in router_routes {
1717 prop_assert!(
1718 rustapi_routes.contains_key(path),
1719 "Route '{}' from Router should exist in RustApi routes",
1720 path
1721 );
1722
1723 let rustapi_info = rustapi_routes.get(path).unwrap();
1724 prop_assert_eq!(
1725 &info.path, &rustapi_info.path,
1726 "Display paths should match for route '{}'",
1727 path
1728 );
1729 prop_assert_eq!(
1730 info.methods.len(), rustapi_info.methods.len(),
1731 "Method count should match for route '{}'",
1732 path
1733 );
1734 }
1735 }
1736
1737 #[test]
1742 fn prop_rustapi_nest_includes_routes_in_openapi(
1743 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1744 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1745 has_param in any::<bool>(),
1746 ) {
1747 async fn handler() -> &'static str { "handler" }
1748
1749 let prefix = format!("/{}", prefix_segments.join("/"));
1751
1752 let mut route_path = format!("/{}", route_segments.join("/"));
1754 if has_param {
1755 route_path.push_str("/{id}");
1756 }
1757
1758 let nested_router = Router::new().route(&route_path, get(handler));
1760 let app = RustApi::new().nest(&prefix, nested_router);
1761
1762 let expected_openapi_path = format!("{}{}", prefix, route_path);
1764
1765 let spec = app.openapi_spec();
1767
1768 prop_assert!(
1770 spec.paths.contains_key(&expected_openapi_path),
1771 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1772 expected_openapi_path,
1773 spec.paths.keys().collect::<Vec<_>>()
1774 );
1775
1776 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1778 prop_assert!(
1779 path_item.get.is_some(),
1780 "GET operation should exist for path '{}'",
1781 expected_openapi_path
1782 );
1783 }
1784
1785 #[test]
1790 fn prop_rustapi_nest_route_matching_identical(
1791 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1792 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1793 param_value in "[a-z0-9]{1,10}",
1794 ) {
1795 use crate::router::RouteMatch;
1796
1797 async fn handler() -> &'static str { "handler" }
1798
1799 let prefix = format!("/{}", prefix_segments.join("/"));
1801 let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1802
1803 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1805 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1806
1807 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1809 let rustapi_router = rustapi_app.into_router();
1810 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1811
1812 let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1814
1815 let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1817 let router_match = router_app.match_route(&full_path, &Method::GET);
1818
1819 match (rustapi_match, router_match) {
1821 (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1822 prop_assert_eq!(
1823 rustapi_params.len(),
1824 router_params.len(),
1825 "Parameter count should match"
1826 );
1827 for (key, value) in &router_params {
1828 prop_assert!(
1829 rustapi_params.contains_key(key),
1830 "RustApi should have parameter '{}'",
1831 key
1832 );
1833 prop_assert_eq!(
1834 rustapi_params.get(key).unwrap(),
1835 value,
1836 "Parameter '{}' value should match",
1837 key
1838 );
1839 }
1840 }
1841 (rustapi_result, router_result) => {
1842 prop_assert!(
1843 false,
1844 "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1845 match rustapi_result {
1846 RouteMatch::Found { .. } => "Found",
1847 RouteMatch::NotFound => "NotFound",
1848 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1849 },
1850 match router_result {
1851 RouteMatch::Found { .. } => "Found",
1852 RouteMatch::NotFound => "NotFound",
1853 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1854 }
1855 );
1856 }
1857 }
1858 }
1859 }
1860
1861 #[test]
1863 fn test_openapi_operations_propagated_during_nesting() {
1864 async fn list_users() -> &'static str {
1865 "list users"
1866 }
1867 async fn get_user() -> &'static str {
1868 "get user"
1869 }
1870 async fn create_user() -> &'static str {
1871 "create user"
1872 }
1873
1874 let users_router = Router::new()
1877 .route("/", get(list_users))
1878 .route("/create", post(create_user))
1879 .route("/{id}", get(get_user));
1880
1881 let app = RustApi::new().nest("/api/v1/users", users_router);
1883
1884 let spec = app.openapi_spec();
1885
1886 assert!(
1888 spec.paths.contains_key("/api/v1/users"),
1889 "Should have /api/v1/users path"
1890 );
1891 let users_path = spec.paths.get("/api/v1/users").unwrap();
1892 assert!(users_path.get.is_some(), "Should have GET operation");
1893
1894 assert!(
1896 spec.paths.contains_key("/api/v1/users/create"),
1897 "Should have /api/v1/users/create path"
1898 );
1899 let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1900 assert!(create_path.post.is_some(), "Should have POST operation");
1901
1902 assert!(
1904 spec.paths.contains_key("/api/v1/users/{id}"),
1905 "Should have /api/v1/users/{{id}} path"
1906 );
1907 let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1908 assert!(
1909 user_path.get.is_some(),
1910 "Should have GET operation for user by id"
1911 );
1912
1913 let get_user_op = user_path.get.as_ref().unwrap();
1915 assert!(!get_user_op.parameters.is_empty(), "Should have parameters");
1916 let params = &get_user_op.parameters;
1917 assert!(
1918 params
1919 .iter()
1920 .any(|p| p.name == "id" && p.location == "path"),
1921 "Should have 'id' path parameter"
1922 );
1923 }
1924
1925 #[test]
1927 fn test_openapi_spec_empty_without_routes() {
1928 let app = RustApi::new();
1929 let spec = app.openapi_spec();
1930
1931 assert!(
1933 spec.paths.is_empty(),
1934 "OpenAPI spec should have no paths without routes"
1935 );
1936 }
1937
1938 #[test]
1943 fn test_rustapi_nest_delegates_to_router_nest() {
1944 use crate::router::RouteMatch;
1945
1946 async fn list_users() -> &'static str {
1947 "list users"
1948 }
1949 async fn get_user() -> &'static str {
1950 "get user"
1951 }
1952 async fn create_user() -> &'static str {
1953 "create user"
1954 }
1955
1956 let users_router = Router::new()
1958 .route("/", get(list_users))
1959 .route("/create", post(create_user))
1960 .route("/{id}", get(get_user));
1961
1962 let app = RustApi::new().nest("/api/v1/users", users_router);
1964 let router = app.into_router();
1965
1966 let routes = router.registered_routes();
1968 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1969
1970 assert!(
1972 routes.contains_key("/api/v1/users"),
1973 "Should have /api/v1/users route"
1974 );
1975 assert!(
1976 routes.contains_key("/api/v1/users/create"),
1977 "Should have /api/v1/users/create route"
1978 );
1979 assert!(
1980 routes.contains_key("/api/v1/users/:id"),
1981 "Should have /api/v1/users/:id route"
1982 );
1983
1984 match router.match_route("/api/v1/users", &Method::GET) {
1986 RouteMatch::Found { params, .. } => {
1987 assert!(params.is_empty(), "Root route should have no params");
1988 }
1989 _ => panic!("GET /api/v1/users should be found"),
1990 }
1991
1992 match router.match_route("/api/v1/users/create", &Method::POST) {
1993 RouteMatch::Found { params, .. } => {
1994 assert!(params.is_empty(), "Create route should have no params");
1995 }
1996 _ => panic!("POST /api/v1/users/create should be found"),
1997 }
1998
1999 match router.match_route("/api/v1/users/123", &Method::GET) {
2000 RouteMatch::Found { params, .. } => {
2001 assert_eq!(
2002 params.get("id"),
2003 Some(&"123".to_string()),
2004 "Should extract id param"
2005 );
2006 }
2007 _ => panic!("GET /api/v1/users/123 should be found"),
2008 }
2009
2010 match router.match_route("/api/v1/users", &Method::DELETE) {
2012 RouteMatch::MethodNotAllowed { allowed } => {
2013 assert!(allowed.contains(&Method::GET), "Should allow GET");
2014 }
2015 _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
2016 }
2017 }
2018
2019 #[test]
2024 fn test_rustapi_nest_includes_routes_in_openapi_spec() {
2025 async fn list_items() -> &'static str {
2026 "list items"
2027 }
2028 async fn get_item() -> &'static str {
2029 "get item"
2030 }
2031
2032 let items_router = Router::new()
2034 .route("/", get(list_items))
2035 .route("/{item_id}", get(get_item));
2036
2037 let app = RustApi::new().nest("/api/items", items_router);
2039
2040 let spec = app.openapi_spec();
2042
2043 assert!(
2045 spec.paths.contains_key("/api/items"),
2046 "Should have /api/items in OpenAPI"
2047 );
2048 assert!(
2049 spec.paths.contains_key("/api/items/{item_id}"),
2050 "Should have /api/items/{{item_id}} in OpenAPI"
2051 );
2052
2053 let list_path = spec.paths.get("/api/items").unwrap();
2055 assert!(
2056 list_path.get.is_some(),
2057 "Should have GET operation for /api/items"
2058 );
2059
2060 let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2061 assert!(
2062 get_path.get.is_some(),
2063 "Should have GET operation for /api/items/{{item_id}}"
2064 );
2065
2066 let get_op = get_path.get.as_ref().unwrap();
2068 assert!(!get_op.parameters.is_empty(), "Should have parameters");
2069 let params = &get_op.parameters;
2070 assert!(
2071 params
2072 .iter()
2073 .any(|p| p.name == "item_id" && p.location == "path"),
2074 "Should have 'item_id' path parameter"
2075 );
2076 }
2077}
2078
2079#[cfg(feature = "swagger-ui")]
2081fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2082 req.headers()
2083 .get(http::header::AUTHORIZATION)
2084 .and_then(|v| v.to_str().ok())
2085 .map(|auth| auth == expected)
2086 .unwrap_or(false)
2087}
2088
2089#[cfg(feature = "swagger-ui")]
2091fn unauthorized_response() -> crate::Response {
2092 http::Response::builder()
2093 .status(http::StatusCode::UNAUTHORIZED)
2094 .header(
2095 http::header::WWW_AUTHENTICATE,
2096 "Basic realm=\"API Documentation\"",
2097 )
2098 .header(http::header::CONTENT_TYPE, "text/plain")
2099 .body(crate::response::Body::from("Unauthorized"))
2100 .unwrap()
2101}
2102
2103pub struct RustApiConfig {
2105 docs_path: Option<String>,
2106 docs_enabled: bool,
2107 api_title: String,
2108 api_version: String,
2109 api_description: Option<String>,
2110 body_limit: Option<usize>,
2111 layers: LayerStack,
2112}
2113
2114impl Default for RustApiConfig {
2115 fn default() -> Self {
2116 Self::new()
2117 }
2118}
2119
2120impl RustApiConfig {
2121 pub fn new() -> Self {
2122 Self {
2123 docs_path: Some("/docs".to_string()),
2124 docs_enabled: true,
2125 api_title: "RustAPI".to_string(),
2126 api_version: "1.0.0".to_string(),
2127 api_description: None,
2128 body_limit: None,
2129 layers: LayerStack::new(),
2130 }
2131 }
2132
2133 pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2135 self.docs_path = Some(path.into());
2136 self
2137 }
2138
2139 pub fn docs_enabled(mut self, enabled: bool) -> Self {
2141 self.docs_enabled = enabled;
2142 self
2143 }
2144
2145 pub fn openapi_info(
2147 mut self,
2148 title: impl Into<String>,
2149 version: impl Into<String>,
2150 description: Option<impl Into<String>>,
2151 ) -> Self {
2152 self.api_title = title.into();
2153 self.api_version = version.into();
2154 self.api_description = description.map(|d| d.into());
2155 self
2156 }
2157
2158 pub fn body_limit(mut self, limit: usize) -> Self {
2160 self.body_limit = Some(limit);
2161 self
2162 }
2163
2164 pub fn layer<L>(mut self, layer: L) -> Self
2166 where
2167 L: MiddlewareLayer,
2168 {
2169 self.layers.push(Box::new(layer));
2170 self
2171 }
2172
2173 pub fn build(self) -> RustApi {
2175 let mut app = RustApi::new().mount_auto_routes_grouped();
2176
2177 if let Some(limit) = self.body_limit {
2179 app = app.body_limit(limit);
2180 }
2181
2182 app = app.openapi_info(
2183 &self.api_title,
2184 &self.api_version,
2185 self.api_description.as_deref(),
2186 );
2187
2188 #[cfg(feature = "swagger-ui")]
2189 if self.docs_enabled {
2190 if let Some(path) = self.docs_path {
2191 app = app.docs(&path);
2192 }
2193 }
2194
2195 app.layers.extend(self.layers);
2198
2199 app
2200 }
2201
2202 pub async fn run(
2204 self,
2205 addr: impl AsRef<str>,
2206 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2207 self.build().run(addr.as_ref()).await
2208 }
2209}