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;
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: BTreeMap<String, MethodRouter> = BTreeMap::new();
336
337 for route in routes {
338 let method_enum = match route.method {
339 "GET" => http::Method::GET,
340 "POST" => http::Method::POST,
341 "PUT" => http::Method::PUT,
342 "DELETE" => http::Method::DELETE,
343 "PATCH" => http::Method::PATCH,
344 _ => http::Method::GET,
345 };
346
347 let path = if route.path.starts_with('/') {
348 route.path.to_string()
349 } else {
350 format!("/{}", route.path)
351 };
352
353 let entry = by_path.entry(path).or_default();
354 entry.insert_boxed_with_operation(method_enum, route.handler, route.operation);
355 }
356
357 #[cfg(feature = "tracing")]
358 let route_count: usize = by_path.values().map(|mr| mr.allowed_methods().len()).sum();
359 #[cfg(feature = "tracing")]
360 let path_count = by_path.len();
361
362 for (path, method_router) in by_path {
363 self = self.route(&path, method_router);
364 }
365
366 crate::trace_info!(
367 paths = path_count,
368 routes = route_count,
369 "Auto-registered routes"
370 );
371
372 crate::auto_schema::apply_auto_schemas(&mut self.openapi_spec);
374
375 self
376 }
377
378 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
389 for (method, op) in &method_router.operations {
391 let mut op = op.clone();
392 add_path_params_to_operation(path, &mut op, &BTreeMap::new());
393 self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op);
394 }
395
396 self.router = self.router.route(path, method_router);
397 self
398 }
399
400 pub fn typed<P: crate::typed_path::TypedPath>(self, method_router: MethodRouter) -> Self {
402 self.route(P::PATH, method_router)
403 }
404
405 #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
409 pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
410 self.route(path, method_router)
411 }
412
413 pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
431 let method_enum = match route.method {
432 "GET" => http::Method::GET,
433 "POST" => http::Method::POST,
434 "PUT" => http::Method::PUT,
435 "DELETE" => http::Method::DELETE,
436 "PATCH" => http::Method::PATCH,
437 _ => http::Method::GET,
438 };
439
440 let mut op = route.operation;
442 add_path_params_to_operation(route.path, &mut op, &route.param_schemas);
443 self.openapi_spec = self.openapi_spec.path(route.path, route.method, op);
444
445 self.route_with_method(route.path, method_enum, route.handler)
446 }
447
448 fn route_with_method(
450 self,
451 path: &str,
452 method: http::Method,
453 handler: crate::handler::BoxedHandler,
454 ) -> Self {
455 use crate::router::MethodRouter;
456 let path = if !path.starts_with('/') {
465 format!("/{}", path)
466 } else {
467 path.to_string()
468 };
469
470 let mut handlers = std::collections::HashMap::new();
479 handlers.insert(method, handler);
480
481 let method_router = MethodRouter::from_boxed(handlers);
482 self.route(&path, method_router)
483 }
484
485 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
501 let normalized_prefix = normalize_prefix_for_openapi(prefix);
503
504 for (matchit_path, method_router) in router.method_routers() {
507 let display_path = router
509 .registered_routes()
510 .get(matchit_path)
511 .map(|info| info.path.clone())
512 .unwrap_or_else(|| matchit_path.clone());
513
514 let prefixed_path = if display_path == "/" {
516 normalized_prefix.clone()
517 } else {
518 format!("{}{}", normalized_prefix, display_path)
519 };
520
521 for (method, op) in &method_router.operations {
523 let mut op = op.clone();
524 add_path_params_to_operation(&prefixed_path, &mut op, &BTreeMap::new());
525 self.openapi_spec = self.openapi_spec.path(&prefixed_path, method.as_str(), op);
526 }
527 }
528
529 self.router = self.router.nest(prefix, router);
531 self
532 }
533
534 pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
563 self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
564 }
565
566 pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
583 use crate::router::MethodRouter;
584 use std::collections::HashMap;
585
586 let prefix = config.prefix.clone();
587 let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
588
589 let handler: crate::handler::BoxedHandler =
591 std::sync::Arc::new(move |req: crate::Request| {
592 let config = config.clone();
593 let path = req.uri().path().to_string();
594
595 Box::pin(async move {
596 let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
597
598 match crate::static_files::StaticFile::serve(relative_path, &config).await {
599 Ok(response) => response,
600 Err(err) => err.into_response(),
601 }
602 })
603 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
604 });
605
606 let mut handlers = HashMap::new();
607 handlers.insert(http::Method::GET, handler);
608 let method_router = MethodRouter::from_boxed(handlers);
609
610 self.route(&catch_all_path, method_router)
611 }
612
613 #[cfg(feature = "compression")]
630 pub fn compression(self) -> Self {
631 self.layer(crate::middleware::CompressionLayer::new())
632 }
633
634 #[cfg(feature = "compression")]
650 pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
651 self.layer(crate::middleware::CompressionLayer::with_config(config))
652 }
653
654 #[cfg(feature = "swagger-ui")]
678 pub fn docs(self, path: &str) -> Self {
679 let title = self.openapi_spec.info.title.clone();
680 let version = self.openapi_spec.info.version.clone();
681 let description = self.openapi_spec.info.description.clone();
682
683 self.docs_with_info(path, &title, &version, description.as_deref())
684 }
685
686 #[cfg(feature = "swagger-ui")]
695 pub fn docs_with_info(
696 mut self,
697 path: &str,
698 title: &str,
699 version: &str,
700 description: Option<&str>,
701 ) -> Self {
702 use crate::router::get;
703 self.openapi_spec.info.title = title.to_string();
705 self.openapi_spec.info.version = version.to_string();
706 if let Some(desc) = description {
707 self.openapi_spec.info.description = Some(desc.to_string());
708 }
709
710 let path = path.trim_end_matches('/');
711 let openapi_path = format!("{}/openapi.json", path);
712
713 let spec_value = self.openapi_spec.to_json();
715 let spec_json = serde_json::to_string_pretty(&spec_value).unwrap_or_else(|e| {
716 tracing::error!("Failed to serialize OpenAPI spec: {}", e);
718 "{}".to_string()
719 });
720 let openapi_url = openapi_path.clone();
721
722 let spec_handler = move || {
724 let json = spec_json.clone();
725 async move {
726 http::Response::builder()
727 .status(http::StatusCode::OK)
728 .header(http::header::CONTENT_TYPE, "application/json")
729 .body(crate::response::Body::from(json))
730 .unwrap_or_else(|e| {
731 tracing::error!("Failed to build response: {}", e);
732 http::Response::builder()
733 .status(http::StatusCode::INTERNAL_SERVER_ERROR)
734 .body(crate::response::Body::from("Internal Server Error"))
735 .unwrap()
736 })
737 }
738 };
739
740 let docs_handler = move || {
742 let url = openapi_url.clone();
743 async move {
744 let response = rustapi_openapi::swagger_ui_html(&url);
745 response.map(crate::response::Body::Full)
746 }
747 };
748
749 self.route(&openapi_path, get(spec_handler))
750 .route(path, get(docs_handler))
751 }
752
753 #[cfg(feature = "swagger-ui")]
769 pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
770 let title = self.openapi_spec.info.title.clone();
771 let version = self.openapi_spec.info.version.clone();
772 let description = self.openapi_spec.info.description.clone();
773
774 self.docs_with_auth_and_info(
775 path,
776 username,
777 password,
778 &title,
779 &version,
780 description.as_deref(),
781 )
782 }
783
784 #[cfg(feature = "swagger-ui")]
800 pub fn docs_with_auth_and_info(
801 mut self,
802 path: &str,
803 username: &str,
804 password: &str,
805 title: &str,
806 version: &str,
807 description: Option<&str>,
808 ) -> Self {
809 use crate::router::MethodRouter;
810 use base64::{engine::general_purpose::STANDARD, Engine};
811 use std::collections::HashMap;
812
813 self.openapi_spec.info.title = title.to_string();
815 self.openapi_spec.info.version = version.to_string();
816 if let Some(desc) = description {
817 self.openapi_spec.info.description = Some(desc.to_string());
818 }
819
820 let path = path.trim_end_matches('/');
821 let openapi_path = format!("{}/openapi.json", path);
822
823 let credentials = format!("{}:{}", username, password);
825 let encoded = STANDARD.encode(credentials.as_bytes());
826 let expected_auth = format!("Basic {}", encoded);
827
828 let spec_value = self.openapi_spec.to_json();
830 let spec_json = serde_json::to_string_pretty(&spec_value).unwrap_or_else(|e| {
831 tracing::error!("Failed to serialize OpenAPI spec: {}", e);
832 "{}".to_string()
833 });
834 let openapi_url = openapi_path.clone();
835 let expected_auth_spec = expected_auth.clone();
836 let expected_auth_docs = expected_auth;
837
838 let spec_handler: crate::handler::BoxedHandler =
840 std::sync::Arc::new(move |req: crate::Request| {
841 let json = spec_json.clone();
842 let expected = expected_auth_spec.clone();
843 Box::pin(async move {
844 if !check_basic_auth(&req, &expected) {
845 return unauthorized_response();
846 }
847 http::Response::builder()
848 .status(http::StatusCode::OK)
849 .header(http::header::CONTENT_TYPE, "application/json")
850 .body(crate::response::Body::from(json))
851 .unwrap_or_else(|e| {
852 tracing::error!("Failed to build response: {}", e);
853 http::Response::builder()
854 .status(http::StatusCode::INTERNAL_SERVER_ERROR)
855 .body(crate::response::Body::from("Internal Server Error"))
856 .unwrap()
857 })
858 })
859 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
860 });
861
862 let docs_handler: crate::handler::BoxedHandler =
864 std::sync::Arc::new(move |req: crate::Request| {
865 let url = openapi_url.clone();
866 let expected = expected_auth_docs.clone();
867 Box::pin(async move {
868 if !check_basic_auth(&req, &expected) {
869 return unauthorized_response();
870 }
871 let response = rustapi_openapi::swagger_ui_html(&url);
872 response.map(crate::response::Body::Full)
873 })
874 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
875 });
876
877 let mut spec_handlers = HashMap::new();
879 spec_handlers.insert(http::Method::GET, spec_handler);
880 let spec_router = MethodRouter::from_boxed(spec_handlers);
881
882 let mut docs_handlers = HashMap::new();
883 docs_handlers.insert(http::Method::GET, docs_handler);
884 let docs_router = MethodRouter::from_boxed(docs_handlers);
885
886 self.route(&openapi_path, spec_router)
887 .route(path, docs_router)
888 }
889
890 pub fn status_page(self) -> Self {
892 self.status_page_with_config(crate::status::StatusConfig::default())
893 }
894
895 pub fn status_page_with_config(mut self, config: crate::status::StatusConfig) -> Self {
897 self.status_config = Some(config);
898 self
899 }
900
901 fn apply_status_page(&mut self) {
903 if let Some(config) = &self.status_config {
904 let monitor = std::sync::Arc::new(crate::status::StatusMonitor::new());
905
906 self.layers
908 .push(Box::new(crate::status::StatusLayer::new(monitor.clone())));
909
910 use crate::router::MethodRouter;
912 use std::collections::HashMap;
913
914 let monitor = monitor.clone();
915 let config = config.clone();
916 let path = config.path.clone(); let handler: crate::handler::BoxedHandler = std::sync::Arc::new(move |_| {
919 let monitor = monitor.clone();
920 let config = config.clone();
921 Box::pin(async move {
922 crate::status::status_handler(monitor, config)
923 .await
924 .into_response()
925 })
926 });
927
928 let mut handlers = HashMap::new();
929 handlers.insert(http::Method::GET, handler);
930 let method_router = MethodRouter::from_boxed(handlers);
931
932 let router = std::mem::take(&mut self.router);
934 self.router = router.route(&path, method_router);
935 }
936 }
937
938 pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
949 self.apply_status_page();
951
952 if let Some(limit) = self.body_limit {
954 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
956 }
957
958 let server = Server::new(self.router, self.layers, self.interceptors);
959 server.run(addr).await
960 }
961
962 pub async fn run_with_shutdown<F>(
964 mut self,
965 addr: impl AsRef<str>,
966 signal: F,
967 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
968 where
969 F: std::future::Future<Output = ()> + Send + 'static,
970 {
971 self.apply_status_page();
973
974 if let Some(limit) = self.body_limit {
975 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
976 }
977
978 let server = Server::new(self.router, self.layers, self.interceptors);
979 server.run_with_shutdown(addr.as_ref(), signal).await
980 }
981
982 pub fn into_router(self) -> Router {
984 self.router
985 }
986
987 pub fn layers(&self) -> &LayerStack {
989 &self.layers
990 }
991
992 pub fn interceptors(&self) -> &InterceptorChain {
994 &self.interceptors
995 }
996
997 #[cfg(feature = "http3")]
1011 pub async fn run_http3(
1012 mut self,
1013 config: crate::http3::Http3Config,
1014 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1015 use std::sync::Arc;
1016
1017 self.apply_status_page();
1019
1020 if let Some(limit) = self.body_limit {
1022 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1023 }
1024
1025 let server = crate::http3::Http3Server::new(
1026 &config,
1027 Arc::new(self.router),
1028 Arc::new(self.layers),
1029 Arc::new(self.interceptors),
1030 )
1031 .await?;
1032
1033 server.run().await
1034 }
1035
1036 #[cfg(feature = "http3-dev")]
1050 pub async fn run_http3_dev(
1051 mut self,
1052 addr: &str,
1053 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1054 use std::sync::Arc;
1055
1056 self.apply_status_page();
1058
1059 if let Some(limit) = self.body_limit {
1061 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1062 }
1063
1064 let server = crate::http3::Http3Server::new_with_self_signed(
1065 addr,
1066 Arc::new(self.router),
1067 Arc::new(self.layers),
1068 Arc::new(self.interceptors),
1069 )
1070 .await?;
1071
1072 server.run().await
1073 }
1074
1075 #[cfg(feature = "http3")]
1099 pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
1100 self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
1101 self
1102 }
1103
1104 #[cfg(feature = "http3")]
1119 pub async fn run_dual_stack(
1120 mut self,
1121 _http_addr: &str,
1122 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1123 let config = self
1131 .http3_config
1132 .take()
1133 .ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
1134
1135 tracing::warn!("run_dual_stack currently only runs HTTP/3. HTTP/1.1 support coming soon.");
1136 self.run_http3(config).await
1137 }
1138}
1139
1140fn add_path_params_to_operation(
1141 path: &str,
1142 op: &mut rustapi_openapi::Operation,
1143 param_schemas: &BTreeMap<String, String>,
1144) {
1145 let mut params: Vec<String> = Vec::new();
1146 let mut in_brace = false;
1147 let mut current = String::new();
1148
1149 for ch in path.chars() {
1150 match ch {
1151 '{' => {
1152 in_brace = true;
1153 current.clear();
1154 }
1155 '}' => {
1156 if in_brace {
1157 in_brace = false;
1158 if !current.is_empty() {
1159 params.push(current.clone());
1160 }
1161 }
1162 }
1163 _ => {
1164 if in_brace {
1165 current.push(ch);
1166 }
1167 }
1168 }
1169 }
1170
1171 if params.is_empty() {
1172 return;
1173 }
1174
1175 let op_params = &mut op.parameters;
1176
1177 for name in params {
1178 let already = op_params
1179 .iter()
1180 .any(|p| p.location == "path" && p.name == name);
1181 if already {
1182 continue;
1183 }
1184
1185 let schema = if let Some(schema_type) = param_schemas.get(&name) {
1187 schema_type_to_openapi_schema(schema_type)
1188 } else {
1189 infer_path_param_schema(&name)
1190 };
1191
1192 op_params.push(rustapi_openapi::Parameter {
1193 name,
1194 location: "path".to_string(),
1195 required: true,
1196 description: None,
1197 deprecated: None,
1198 schema: Some(schema),
1199 });
1200 }
1201}
1202
1203fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef {
1205 match schema_type.to_lowercase().as_str() {
1206 "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1207 "type": "string",
1208 "format": "uuid"
1209 })),
1210 "integer" | "int" | "int64" | "i64" => {
1211 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1212 "type": "integer",
1213 "format": "int64"
1214 }))
1215 }
1216 "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1217 "type": "integer",
1218 "format": "int32"
1219 })),
1220 "number" | "float" | "f64" | "f32" => {
1221 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1222 "type": "number"
1223 }))
1224 }
1225 "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1226 "type": "boolean"
1227 })),
1228 _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1229 "type": "string"
1230 })),
1231 }
1232}
1233
1234fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef {
1243 let lower = name.to_lowercase();
1244
1245 let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid");
1247
1248 if is_uuid {
1249 return rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1250 "type": "string",
1251 "format": "uuid"
1252 }));
1253 }
1254
1255 let is_integer = lower == "page"
1258 || lower == "limit"
1259 || lower == "offset"
1260 || lower == "count"
1261 || lower.ends_with("_count")
1262 || lower.ends_with("_num")
1263 || lower == "year"
1264 || lower == "month"
1265 || lower == "day"
1266 || lower == "index"
1267 || lower == "position";
1268
1269 if is_integer {
1270 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1271 "type": "integer",
1272 "format": "int64"
1273 }))
1274 } else {
1275 rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1276 }
1277}
1278
1279fn normalize_prefix_for_openapi(prefix: &str) -> String {
1286 if prefix.is_empty() {
1288 return "/".to_string();
1289 }
1290
1291 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
1293
1294 if segments.is_empty() {
1296 return "/".to_string();
1297 }
1298
1299 let mut result = String::with_capacity(prefix.len() + 1);
1301 for segment in segments {
1302 result.push('/');
1303 result.push_str(segment);
1304 }
1305
1306 result
1307}
1308
1309impl Default for RustApi {
1310 fn default() -> Self {
1311 Self::new()
1312 }
1313}
1314
1315#[cfg(test)]
1316mod tests {
1317 use super::RustApi;
1318 use crate::extract::{FromRequestParts, State};
1319 use crate::path_params::PathParams;
1320 use crate::request::Request;
1321 use crate::router::{get, post, Router};
1322 use bytes::Bytes;
1323 use http::Method;
1324 use proptest::prelude::*;
1325
1326 #[test]
1327 fn state_is_available_via_extractor() {
1328 let app = RustApi::new().state(123u32);
1329 let router = app.into_router();
1330
1331 let req = http::Request::builder()
1332 .method(Method::GET)
1333 .uri("/test")
1334 .body(())
1335 .unwrap();
1336 let (parts, _) = req.into_parts();
1337
1338 let request = Request::new(
1339 parts,
1340 crate::request::BodyVariant::Buffered(Bytes::new()),
1341 router.state_ref(),
1342 PathParams::new(),
1343 );
1344 let State(value) = State::<u32>::from_request_parts(&request).unwrap();
1345 assert_eq!(value, 123u32);
1346 }
1347
1348 #[test]
1349 fn test_path_param_type_inference_integer() {
1350 use super::infer_path_param_schema;
1351
1352 let int_params = [
1354 "page",
1355 "limit",
1356 "offset",
1357 "count",
1358 "item_count",
1359 "year",
1360 "month",
1361 "day",
1362 "index",
1363 "position",
1364 ];
1365
1366 for name in int_params {
1367 let schema = infer_path_param_schema(name);
1368 match schema {
1369 rustapi_openapi::SchemaRef::Inline(v) => {
1370 assert_eq!(
1371 v.get("type").and_then(|v| v.as_str()),
1372 Some("integer"),
1373 "Expected '{}' to be inferred as integer",
1374 name
1375 );
1376 }
1377 _ => panic!("Expected inline schema for '{}'", name),
1378 }
1379 }
1380 }
1381
1382 #[test]
1383 fn test_path_param_type_inference_uuid() {
1384 use super::infer_path_param_schema;
1385
1386 let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
1388
1389 for name in uuid_params {
1390 let schema = infer_path_param_schema(name);
1391 match schema {
1392 rustapi_openapi::SchemaRef::Inline(v) => {
1393 assert_eq!(
1394 v.get("type").and_then(|v| v.as_str()),
1395 Some("string"),
1396 "Expected '{}' to be inferred as string",
1397 name
1398 );
1399 assert_eq!(
1400 v.get("format").and_then(|v| v.as_str()),
1401 Some("uuid"),
1402 "Expected '{}' to have uuid format",
1403 name
1404 );
1405 }
1406 _ => panic!("Expected inline schema for '{}'", name),
1407 }
1408 }
1409 }
1410
1411 #[test]
1412 fn test_path_param_type_inference_string() {
1413 use super::infer_path_param_schema;
1414
1415 let string_params = [
1417 "name", "slug", "code", "token", "username", "id", "user_id", "userId", "postId",
1418 ];
1419
1420 for name in string_params {
1421 let schema = infer_path_param_schema(name);
1422 match schema {
1423 rustapi_openapi::SchemaRef::Inline(v) => {
1424 assert_eq!(
1425 v.get("type").and_then(|v| v.as_str()),
1426 Some("string"),
1427 "Expected '{}' to be inferred as string",
1428 name
1429 );
1430 assert!(
1431 v.get("format").is_none()
1432 || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
1433 "Expected '{}' to NOT have uuid format",
1434 name
1435 );
1436 }
1437 _ => panic!("Expected inline schema for '{}'", name),
1438 }
1439 }
1440 }
1441
1442 #[test]
1443 fn test_schema_type_to_openapi_schema() {
1444 use super::schema_type_to_openapi_schema;
1445
1446 let uuid_schema = schema_type_to_openapi_schema("uuid");
1448 match uuid_schema {
1449 rustapi_openapi::SchemaRef::Inline(v) => {
1450 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1451 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
1452 }
1453 _ => panic!("Expected inline schema for uuid"),
1454 }
1455
1456 for schema_type in ["integer", "int", "int64", "i64"] {
1458 let schema = schema_type_to_openapi_schema(schema_type);
1459 match schema {
1460 rustapi_openapi::SchemaRef::Inline(v) => {
1461 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1462 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
1463 }
1464 _ => panic!("Expected inline schema for {}", schema_type),
1465 }
1466 }
1467
1468 let int32_schema = schema_type_to_openapi_schema("int32");
1470 match int32_schema {
1471 rustapi_openapi::SchemaRef::Inline(v) => {
1472 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1473 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
1474 }
1475 _ => panic!("Expected inline schema for int32"),
1476 }
1477
1478 for schema_type in ["number", "float"] {
1480 let schema = schema_type_to_openapi_schema(schema_type);
1481 match schema {
1482 rustapi_openapi::SchemaRef::Inline(v) => {
1483 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
1484 }
1485 _ => panic!("Expected inline schema for {}", schema_type),
1486 }
1487 }
1488
1489 for schema_type in ["boolean", "bool"] {
1491 let schema = schema_type_to_openapi_schema(schema_type);
1492 match schema {
1493 rustapi_openapi::SchemaRef::Inline(v) => {
1494 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
1495 }
1496 _ => panic!("Expected inline schema for {}", schema_type),
1497 }
1498 }
1499
1500 let string_schema = schema_type_to_openapi_schema("string");
1502 match string_schema {
1503 rustapi_openapi::SchemaRef::Inline(v) => {
1504 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1505 }
1506 _ => panic!("Expected inline schema for string"),
1507 }
1508 }
1509
1510 proptest! {
1517 #![proptest_config(ProptestConfig::with_cases(100))]
1518
1519 #[test]
1524 fn prop_nested_routes_in_openapi_spec(
1525 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1527 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1529 has_param in any::<bool>(),
1530 ) {
1531 async fn handler() -> &'static str { "handler" }
1532
1533 let prefix = format!("/{}", prefix_segments.join("/"));
1535
1536 let mut route_path = format!("/{}", route_segments.join("/"));
1538 if has_param {
1539 route_path.push_str("/{id}");
1540 }
1541
1542 let nested_router = Router::new().route(&route_path, get(handler));
1544 let app = RustApi::new().nest(&prefix, nested_router);
1545
1546 let expected_openapi_path = format!("{}{}", prefix, route_path);
1548
1549 let spec = app.openapi_spec();
1551
1552 prop_assert!(
1554 spec.paths.contains_key(&expected_openapi_path),
1555 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1556 expected_openapi_path,
1557 spec.paths.keys().collect::<Vec<_>>()
1558 );
1559
1560 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1562 prop_assert!(
1563 path_item.get.is_some(),
1564 "GET operation should exist for path '{}'",
1565 expected_openapi_path
1566 );
1567 }
1568
1569 #[test]
1574 fn prop_multiple_methods_preserved_in_openapi(
1575 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1576 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1577 ) {
1578 async fn get_handler() -> &'static str { "get" }
1579 async fn post_handler() -> &'static str { "post" }
1580
1581 let prefix = format!("/{}", prefix_segments.join("/"));
1583 let route_path = format!("/{}", route_segments.join("/"));
1584
1585 let get_route_path = format!("{}/get", route_path);
1588 let post_route_path = format!("{}/post", route_path);
1589 let nested_router = Router::new()
1590 .route(&get_route_path, get(get_handler))
1591 .route(&post_route_path, post(post_handler));
1592 let app = RustApi::new().nest(&prefix, nested_router);
1593
1594 let expected_get_path = format!("{}{}", prefix, get_route_path);
1596 let expected_post_path = format!("{}{}", prefix, post_route_path);
1597
1598 let spec = app.openapi_spec();
1600
1601 prop_assert!(
1603 spec.paths.contains_key(&expected_get_path),
1604 "Expected OpenAPI path '{}' not found",
1605 expected_get_path
1606 );
1607 prop_assert!(
1608 spec.paths.contains_key(&expected_post_path),
1609 "Expected OpenAPI path '{}' not found",
1610 expected_post_path
1611 );
1612
1613 let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1615 prop_assert!(
1616 get_path_item.get.is_some(),
1617 "GET operation should exist for path '{}'",
1618 expected_get_path
1619 );
1620
1621 let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1623 prop_assert!(
1624 post_path_item.post.is_some(),
1625 "POST operation should exist for path '{}'",
1626 expected_post_path
1627 );
1628 }
1629
1630 #[test]
1635 fn prop_path_params_in_openapi_after_nesting(
1636 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1637 param_name in "[a-z][a-z0-9]{0,5}",
1638 ) {
1639 async fn handler() -> &'static str { "handler" }
1640
1641 let prefix = format!("/{}", prefix_segments.join("/"));
1643 let route_path = format!("/{{{}}}", param_name);
1644
1645 let nested_router = Router::new().route(&route_path, get(handler));
1647 let app = RustApi::new().nest(&prefix, nested_router);
1648
1649 let expected_openapi_path = format!("{}{}", prefix, route_path);
1651
1652 let spec = app.openapi_spec();
1654
1655 prop_assert!(
1657 spec.paths.contains_key(&expected_openapi_path),
1658 "Expected OpenAPI path '{}' not found",
1659 expected_openapi_path
1660 );
1661
1662 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1664 let get_op = path_item.get.as_ref().unwrap();
1665
1666 prop_assert!(
1667 !get_op.parameters.is_empty(),
1668 "Operation should have parameters for path '{}'",
1669 expected_openapi_path
1670 );
1671
1672 let params = &get_op.parameters;
1673 let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1674 prop_assert!(
1675 has_param,
1676 "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1677 param_name,
1678 params.iter().map(|p| &p.name).collect::<Vec<_>>()
1679 );
1680 }
1681 }
1682
1683 proptest! {
1691 #![proptest_config(ProptestConfig::with_cases(100))]
1692
1693 #[test]
1698 fn prop_rustapi_nest_delegates_to_router_nest(
1699 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1700 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1701 has_param in any::<bool>(),
1702 ) {
1703 async fn handler() -> &'static str { "handler" }
1704
1705 let prefix = format!("/{}", prefix_segments.join("/"));
1707
1708 let mut route_path = format!("/{}", route_segments.join("/"));
1710 if has_param {
1711 route_path.push_str("/{id}");
1712 }
1713
1714 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1716 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1717
1718 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1720 let rustapi_router = rustapi_app.into_router();
1721
1722 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1724
1725 let rustapi_routes = rustapi_router.registered_routes();
1727 let router_routes = router_app.registered_routes();
1728
1729 prop_assert_eq!(
1730 rustapi_routes.len(),
1731 router_routes.len(),
1732 "RustApi and Router should have same number of routes"
1733 );
1734
1735 for (path, info) in router_routes {
1737 prop_assert!(
1738 rustapi_routes.contains_key(path),
1739 "Route '{}' from Router should exist in RustApi routes",
1740 path
1741 );
1742
1743 let rustapi_info = rustapi_routes.get(path).unwrap();
1744 prop_assert_eq!(
1745 &info.path, &rustapi_info.path,
1746 "Display paths should match for route '{}'",
1747 path
1748 );
1749 prop_assert_eq!(
1750 info.methods.len(), rustapi_info.methods.len(),
1751 "Method count should match for route '{}'",
1752 path
1753 );
1754 }
1755 }
1756
1757 #[test]
1762 fn prop_rustapi_nest_includes_routes_in_openapi(
1763 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1764 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1765 has_param in any::<bool>(),
1766 ) {
1767 async fn handler() -> &'static str { "handler" }
1768
1769 let prefix = format!("/{}", prefix_segments.join("/"));
1771
1772 let mut route_path = format!("/{}", route_segments.join("/"));
1774 if has_param {
1775 route_path.push_str("/{id}");
1776 }
1777
1778 let nested_router = Router::new().route(&route_path, get(handler));
1780 let app = RustApi::new().nest(&prefix, nested_router);
1781
1782 let expected_openapi_path = format!("{}{}", prefix, route_path);
1784
1785 let spec = app.openapi_spec();
1787
1788 prop_assert!(
1790 spec.paths.contains_key(&expected_openapi_path),
1791 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1792 expected_openapi_path,
1793 spec.paths.keys().collect::<Vec<_>>()
1794 );
1795
1796 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1798 prop_assert!(
1799 path_item.get.is_some(),
1800 "GET operation should exist for path '{}'",
1801 expected_openapi_path
1802 );
1803 }
1804
1805 #[test]
1810 fn prop_rustapi_nest_route_matching_identical(
1811 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1812 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1813 param_value in "[a-z0-9]{1,10}",
1814 ) {
1815 use crate::router::RouteMatch;
1816
1817 async fn handler() -> &'static str { "handler" }
1818
1819 let prefix = format!("/{}", prefix_segments.join("/"));
1821 let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1822
1823 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1825 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1826
1827 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1829 let rustapi_router = rustapi_app.into_router();
1830 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1831
1832 let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1834
1835 let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1837 let router_match = router_app.match_route(&full_path, &Method::GET);
1838
1839 match (rustapi_match, router_match) {
1841 (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1842 prop_assert_eq!(
1843 rustapi_params.len(),
1844 router_params.len(),
1845 "Parameter count should match"
1846 );
1847 for (key, value) in &router_params {
1848 prop_assert!(
1849 rustapi_params.contains_key(key),
1850 "RustApi should have parameter '{}'",
1851 key
1852 );
1853 prop_assert_eq!(
1854 rustapi_params.get(key).unwrap(),
1855 value,
1856 "Parameter '{}' value should match",
1857 key
1858 );
1859 }
1860 }
1861 (rustapi_result, router_result) => {
1862 prop_assert!(
1863 false,
1864 "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1865 match rustapi_result {
1866 RouteMatch::Found { .. } => "Found",
1867 RouteMatch::NotFound => "NotFound",
1868 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1869 },
1870 match router_result {
1871 RouteMatch::Found { .. } => "Found",
1872 RouteMatch::NotFound => "NotFound",
1873 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1874 }
1875 );
1876 }
1877 }
1878 }
1879 }
1880
1881 #[test]
1883 fn test_openapi_operations_propagated_during_nesting() {
1884 async fn list_users() -> &'static str {
1885 "list users"
1886 }
1887 async fn get_user() -> &'static str {
1888 "get user"
1889 }
1890 async fn create_user() -> &'static str {
1891 "create user"
1892 }
1893
1894 let users_router = Router::new()
1897 .route("/", get(list_users))
1898 .route("/create", post(create_user))
1899 .route("/{id}", get(get_user));
1900
1901 let app = RustApi::new().nest("/api/v1/users", users_router);
1903
1904 let spec = app.openapi_spec();
1905
1906 assert!(
1908 spec.paths.contains_key("/api/v1/users"),
1909 "Should have /api/v1/users path"
1910 );
1911 let users_path = spec.paths.get("/api/v1/users").unwrap();
1912 assert!(users_path.get.is_some(), "Should have GET operation");
1913
1914 assert!(
1916 spec.paths.contains_key("/api/v1/users/create"),
1917 "Should have /api/v1/users/create path"
1918 );
1919 let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1920 assert!(create_path.post.is_some(), "Should have POST operation");
1921
1922 assert!(
1924 spec.paths.contains_key("/api/v1/users/{id}"),
1925 "Should have /api/v1/users/{{id}} path"
1926 );
1927 let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1928 assert!(
1929 user_path.get.is_some(),
1930 "Should have GET operation for user by id"
1931 );
1932
1933 let get_user_op = user_path.get.as_ref().unwrap();
1935 assert!(!get_user_op.parameters.is_empty(), "Should have parameters");
1936 let params = &get_user_op.parameters;
1937 assert!(
1938 params
1939 .iter()
1940 .any(|p| p.name == "id" && p.location == "path"),
1941 "Should have 'id' path parameter"
1942 );
1943 }
1944
1945 #[test]
1947 fn test_openapi_spec_empty_without_routes() {
1948 let app = RustApi::new();
1949 let spec = app.openapi_spec();
1950
1951 assert!(
1953 spec.paths.is_empty(),
1954 "OpenAPI spec should have no paths without routes"
1955 );
1956 }
1957
1958 #[test]
1963 fn test_rustapi_nest_delegates_to_router_nest() {
1964 use crate::router::RouteMatch;
1965
1966 async fn list_users() -> &'static str {
1967 "list users"
1968 }
1969 async fn get_user() -> &'static str {
1970 "get user"
1971 }
1972 async fn create_user() -> &'static str {
1973 "create user"
1974 }
1975
1976 let users_router = Router::new()
1978 .route("/", get(list_users))
1979 .route("/create", post(create_user))
1980 .route("/{id}", get(get_user));
1981
1982 let app = RustApi::new().nest("/api/v1/users", users_router);
1984 let router = app.into_router();
1985
1986 let routes = router.registered_routes();
1988 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1989
1990 assert!(
1992 routes.contains_key("/api/v1/users"),
1993 "Should have /api/v1/users route"
1994 );
1995 assert!(
1996 routes.contains_key("/api/v1/users/create"),
1997 "Should have /api/v1/users/create route"
1998 );
1999 assert!(
2000 routes.contains_key("/api/v1/users/:id"),
2001 "Should have /api/v1/users/:id route"
2002 );
2003
2004 match router.match_route("/api/v1/users", &Method::GET) {
2006 RouteMatch::Found { params, .. } => {
2007 assert!(params.is_empty(), "Root route should have no params");
2008 }
2009 _ => panic!("GET /api/v1/users should be found"),
2010 }
2011
2012 match router.match_route("/api/v1/users/create", &Method::POST) {
2013 RouteMatch::Found { params, .. } => {
2014 assert!(params.is_empty(), "Create route should have no params");
2015 }
2016 _ => panic!("POST /api/v1/users/create should be found"),
2017 }
2018
2019 match router.match_route("/api/v1/users/123", &Method::GET) {
2020 RouteMatch::Found { params, .. } => {
2021 assert_eq!(
2022 params.get("id"),
2023 Some(&"123".to_string()),
2024 "Should extract id param"
2025 );
2026 }
2027 _ => panic!("GET /api/v1/users/123 should be found"),
2028 }
2029
2030 match router.match_route("/api/v1/users", &Method::DELETE) {
2032 RouteMatch::MethodNotAllowed { allowed } => {
2033 assert!(allowed.contains(&Method::GET), "Should allow GET");
2034 }
2035 _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
2036 }
2037 }
2038
2039 #[test]
2044 fn test_rustapi_nest_includes_routes_in_openapi_spec() {
2045 async fn list_items() -> &'static str {
2046 "list items"
2047 }
2048 async fn get_item() -> &'static str {
2049 "get item"
2050 }
2051
2052 let items_router = Router::new()
2054 .route("/", get(list_items))
2055 .route("/{item_id}", get(get_item));
2056
2057 let app = RustApi::new().nest("/api/items", items_router);
2059
2060 let spec = app.openapi_spec();
2062
2063 assert!(
2065 spec.paths.contains_key("/api/items"),
2066 "Should have /api/items in OpenAPI"
2067 );
2068 assert!(
2069 spec.paths.contains_key("/api/items/{item_id}"),
2070 "Should have /api/items/{{item_id}} in OpenAPI"
2071 );
2072
2073 let list_path = spec.paths.get("/api/items").unwrap();
2075 assert!(
2076 list_path.get.is_some(),
2077 "Should have GET operation for /api/items"
2078 );
2079
2080 let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2081 assert!(
2082 get_path.get.is_some(),
2083 "Should have GET operation for /api/items/{{item_id}}"
2084 );
2085
2086 let get_op = get_path.get.as_ref().unwrap();
2088 assert!(!get_op.parameters.is_empty(), "Should have parameters");
2089 let params = &get_op.parameters;
2090 assert!(
2091 params
2092 .iter()
2093 .any(|p| p.name == "item_id" && p.location == "path"),
2094 "Should have 'item_id' path parameter"
2095 );
2096 }
2097}
2098
2099#[cfg(feature = "swagger-ui")]
2101fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2102 req.headers()
2103 .get(http::header::AUTHORIZATION)
2104 .and_then(|v| v.to_str().ok())
2105 .map(|auth| auth == expected)
2106 .unwrap_or(false)
2107}
2108
2109#[cfg(feature = "swagger-ui")]
2111fn unauthorized_response() -> crate::Response {
2112 http::Response::builder()
2113 .status(http::StatusCode::UNAUTHORIZED)
2114 .header(
2115 http::header::WWW_AUTHENTICATE,
2116 "Basic realm=\"API Documentation\"",
2117 )
2118 .header(http::header::CONTENT_TYPE, "text/plain")
2119 .body(crate::response::Body::from("Unauthorized"))
2120 .unwrap()
2121}
2122
2123pub struct RustApiConfig {
2125 docs_path: Option<String>,
2126 docs_enabled: bool,
2127 api_title: String,
2128 api_version: String,
2129 api_description: Option<String>,
2130 body_limit: Option<usize>,
2131 layers: LayerStack,
2132}
2133
2134impl Default for RustApiConfig {
2135 fn default() -> Self {
2136 Self::new()
2137 }
2138}
2139
2140impl RustApiConfig {
2141 pub fn new() -> Self {
2142 Self {
2143 docs_path: Some("/docs".to_string()),
2144 docs_enabled: true,
2145 api_title: "RustAPI".to_string(),
2146 api_version: "1.0.0".to_string(),
2147 api_description: None,
2148 body_limit: None,
2149 layers: LayerStack::new(),
2150 }
2151 }
2152
2153 pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2155 self.docs_path = Some(path.into());
2156 self
2157 }
2158
2159 pub fn docs_enabled(mut self, enabled: bool) -> Self {
2161 self.docs_enabled = enabled;
2162 self
2163 }
2164
2165 pub fn openapi_info(
2167 mut self,
2168 title: impl Into<String>,
2169 version: impl Into<String>,
2170 description: Option<impl Into<String>>,
2171 ) -> Self {
2172 self.api_title = title.into();
2173 self.api_version = version.into();
2174 self.api_description = description.map(|d| d.into());
2175 self
2176 }
2177
2178 pub fn body_limit(mut self, limit: usize) -> Self {
2180 self.body_limit = Some(limit);
2181 self
2182 }
2183
2184 pub fn layer<L>(mut self, layer: L) -> Self
2186 where
2187 L: MiddlewareLayer,
2188 {
2189 self.layers.push(Box::new(layer));
2190 self
2191 }
2192
2193 pub fn build(self) -> RustApi {
2195 let mut app = RustApi::new().mount_auto_routes_grouped();
2196
2197 if let Some(limit) = self.body_limit {
2199 app = app.body_limit(limit);
2200 }
2201
2202 app = app.openapi_info(
2203 &self.api_title,
2204 &self.api_version,
2205 self.api_description.as_deref(),
2206 );
2207
2208 #[cfg(feature = "swagger-ui")]
2209 if self.docs_enabled {
2210 if let Some(path) = self.docs_path {
2211 app = app.docs(&path);
2212 }
2213 }
2214
2215 app.layers.extend(self.layers);
2218
2219 app
2220 }
2221
2222 pub async fn run(
2224 self,
2225 addr: impl AsRef<str>,
2226 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2227 self.build().run(addr.as_ref()).await
2228 }
2229}