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")]
1086 pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
1087 self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
1088 self
1089 }
1090
1091 #[cfg(feature = "http3")]
1106 pub async fn run_dual_stack(
1107 mut self,
1108 http_addr: &str,
1109 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1110 use std::sync::Arc;
1111
1112 let mut config = self
1113 .http3_config
1114 .take()
1115 .ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
1116
1117 let http_socket: std::net::SocketAddr = http_addr.parse()?;
1118 config.bind_addr = if http_socket.ip().is_ipv6() {
1119 format!("[{}]", http_socket.ip())
1120 } else {
1121 http_socket.ip().to_string()
1122 };
1123 config.port = http_socket.port();
1124 let http_addr = http_socket.to_string();
1125
1126 self.apply_status_page();
1128
1129 if let Some(limit) = self.body_limit {
1131 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
1132 }
1133
1134 let router = Arc::new(self.router);
1135 let layers = Arc::new(self.layers);
1136 let interceptors = Arc::new(self.interceptors);
1137
1138 let http1_server =
1139 Server::from_shared(router.clone(), layers.clone(), interceptors.clone());
1140 let http3_server =
1141 crate::http3::Http3Server::new(&config, router, layers, interceptors).await?;
1142
1143 tracing::info!(
1144 http1_addr = %http_addr,
1145 http3_addr = %config.socket_addr(),
1146 "Starting dual-stack HTTP/1.1 + HTTP/3 servers"
1147 );
1148
1149 tokio::try_join!(
1150 http1_server.run_with_shutdown(&http_addr, std::future::pending::<()>()),
1151 http3_server.run_with_shutdown(std::future::pending::<()>()),
1152 )?;
1153
1154 Ok(())
1155 }
1156}
1157
1158fn add_path_params_to_operation(
1159 path: &str,
1160 op: &mut rustapi_openapi::Operation,
1161 param_schemas: &BTreeMap<String, String>,
1162) {
1163 let mut params: Vec<String> = Vec::new();
1164 let mut in_brace = false;
1165 let mut current = String::new();
1166
1167 for ch in path.chars() {
1168 match ch {
1169 '{' => {
1170 in_brace = true;
1171 current.clear();
1172 }
1173 '}' => {
1174 if in_brace {
1175 in_brace = false;
1176 if !current.is_empty() {
1177 params.push(current.clone());
1178 }
1179 }
1180 }
1181 _ => {
1182 if in_brace {
1183 current.push(ch);
1184 }
1185 }
1186 }
1187 }
1188
1189 if params.is_empty() {
1190 return;
1191 }
1192
1193 let op_params = &mut op.parameters;
1194
1195 for name in params {
1196 let already = op_params
1197 .iter()
1198 .any(|p| p.location == "path" && p.name == name);
1199 if already {
1200 continue;
1201 }
1202
1203 let schema = if let Some(schema_type) = param_schemas.get(&name) {
1205 schema_type_to_openapi_schema(schema_type)
1206 } else {
1207 infer_path_param_schema(&name)
1208 };
1209
1210 op_params.push(rustapi_openapi::Parameter {
1211 name,
1212 location: "path".to_string(),
1213 required: true,
1214 description: None,
1215 deprecated: None,
1216 schema: Some(schema),
1217 });
1218 }
1219}
1220
1221fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef {
1223 match schema_type.to_lowercase().as_str() {
1224 "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1225 "type": "string",
1226 "format": "uuid"
1227 })),
1228 "integer" | "int" | "int64" | "i64" => {
1229 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1230 "type": "integer",
1231 "format": "int64"
1232 }))
1233 }
1234 "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1235 "type": "integer",
1236 "format": "int32"
1237 })),
1238 "number" | "float" | "f64" | "f32" => {
1239 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1240 "type": "number"
1241 }))
1242 }
1243 "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1244 "type": "boolean"
1245 })),
1246 _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1247 "type": "string"
1248 })),
1249 }
1250}
1251
1252fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef {
1261 let lower = name.to_lowercase();
1262
1263 let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid");
1265
1266 if is_uuid {
1267 return rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1268 "type": "string",
1269 "format": "uuid"
1270 }));
1271 }
1272
1273 let is_integer = lower == "page"
1276 || lower == "limit"
1277 || lower == "offset"
1278 || lower == "count"
1279 || lower.ends_with("_count")
1280 || lower.ends_with("_num")
1281 || lower == "year"
1282 || lower == "month"
1283 || lower == "day"
1284 || lower == "index"
1285 || lower == "position";
1286
1287 if is_integer {
1288 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1289 "type": "integer",
1290 "format": "int64"
1291 }))
1292 } else {
1293 rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1294 }
1295}
1296
1297fn normalize_prefix_for_openapi(prefix: &str) -> String {
1304 if prefix.is_empty() {
1306 return "/".to_string();
1307 }
1308
1309 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
1311
1312 if segments.is_empty() {
1314 return "/".to_string();
1315 }
1316
1317 let mut result = String::with_capacity(prefix.len() + 1);
1319 for segment in segments {
1320 result.push('/');
1321 result.push_str(segment);
1322 }
1323
1324 result
1325}
1326
1327impl Default for RustApi {
1328 fn default() -> Self {
1329 Self::new()
1330 }
1331}
1332
1333#[cfg(test)]
1334mod tests {
1335 use super::RustApi;
1336 use crate::extract::{FromRequestParts, State};
1337 use crate::path_params::PathParams;
1338 use crate::request::Request;
1339 use crate::router::{get, post, Router};
1340 use bytes::Bytes;
1341 use http::Method;
1342 use proptest::prelude::*;
1343
1344 #[test]
1345 fn state_is_available_via_extractor() {
1346 let app = RustApi::new().state(123u32);
1347 let router = app.into_router();
1348
1349 let req = http::Request::builder()
1350 .method(Method::GET)
1351 .uri("/test")
1352 .body(())
1353 .unwrap();
1354 let (parts, _) = req.into_parts();
1355
1356 let request = Request::new(
1357 parts,
1358 crate::request::BodyVariant::Buffered(Bytes::new()),
1359 router.state_ref(),
1360 PathParams::new(),
1361 );
1362 let State(value) = State::<u32>::from_request_parts(&request).unwrap();
1363 assert_eq!(value, 123u32);
1364 }
1365
1366 #[test]
1367 fn test_path_param_type_inference_integer() {
1368 use super::infer_path_param_schema;
1369
1370 let int_params = [
1372 "page",
1373 "limit",
1374 "offset",
1375 "count",
1376 "item_count",
1377 "year",
1378 "month",
1379 "day",
1380 "index",
1381 "position",
1382 ];
1383
1384 for name in int_params {
1385 let schema = infer_path_param_schema(name);
1386 match schema {
1387 rustapi_openapi::SchemaRef::Inline(v) => {
1388 assert_eq!(
1389 v.get("type").and_then(|v| v.as_str()),
1390 Some("integer"),
1391 "Expected '{}' to be inferred as integer",
1392 name
1393 );
1394 }
1395 _ => panic!("Expected inline schema for '{}'", name),
1396 }
1397 }
1398 }
1399
1400 #[test]
1401 fn test_path_param_type_inference_uuid() {
1402 use super::infer_path_param_schema;
1403
1404 let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
1406
1407 for name in uuid_params {
1408 let schema = infer_path_param_schema(name);
1409 match schema {
1410 rustapi_openapi::SchemaRef::Inline(v) => {
1411 assert_eq!(
1412 v.get("type").and_then(|v| v.as_str()),
1413 Some("string"),
1414 "Expected '{}' to be inferred as string",
1415 name
1416 );
1417 assert_eq!(
1418 v.get("format").and_then(|v| v.as_str()),
1419 Some("uuid"),
1420 "Expected '{}' to have uuid format",
1421 name
1422 );
1423 }
1424 _ => panic!("Expected inline schema for '{}'", name),
1425 }
1426 }
1427 }
1428
1429 #[test]
1430 fn test_path_param_type_inference_string() {
1431 use super::infer_path_param_schema;
1432
1433 let string_params = [
1435 "name", "slug", "code", "token", "username", "id", "user_id", "userId", "postId",
1436 ];
1437
1438 for name in string_params {
1439 let schema = infer_path_param_schema(name);
1440 match schema {
1441 rustapi_openapi::SchemaRef::Inline(v) => {
1442 assert_eq!(
1443 v.get("type").and_then(|v| v.as_str()),
1444 Some("string"),
1445 "Expected '{}' to be inferred as string",
1446 name
1447 );
1448 assert!(
1449 v.get("format").is_none()
1450 || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
1451 "Expected '{}' to NOT have uuid format",
1452 name
1453 );
1454 }
1455 _ => panic!("Expected inline schema for '{}'", name),
1456 }
1457 }
1458 }
1459
1460 #[test]
1461 fn test_schema_type_to_openapi_schema() {
1462 use super::schema_type_to_openapi_schema;
1463
1464 let uuid_schema = schema_type_to_openapi_schema("uuid");
1466 match uuid_schema {
1467 rustapi_openapi::SchemaRef::Inline(v) => {
1468 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1469 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
1470 }
1471 _ => panic!("Expected inline schema for uuid"),
1472 }
1473
1474 for schema_type in ["integer", "int", "int64", "i64"] {
1476 let schema = schema_type_to_openapi_schema(schema_type);
1477 match schema {
1478 rustapi_openapi::SchemaRef::Inline(v) => {
1479 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1480 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
1481 }
1482 _ => panic!("Expected inline schema for {}", schema_type),
1483 }
1484 }
1485
1486 let int32_schema = schema_type_to_openapi_schema("int32");
1488 match int32_schema {
1489 rustapi_openapi::SchemaRef::Inline(v) => {
1490 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1491 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
1492 }
1493 _ => panic!("Expected inline schema for int32"),
1494 }
1495
1496 for schema_type in ["number", "float"] {
1498 let schema = schema_type_to_openapi_schema(schema_type);
1499 match schema {
1500 rustapi_openapi::SchemaRef::Inline(v) => {
1501 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
1502 }
1503 _ => panic!("Expected inline schema for {}", schema_type),
1504 }
1505 }
1506
1507 for schema_type in ["boolean", "bool"] {
1509 let schema = schema_type_to_openapi_schema(schema_type);
1510 match schema {
1511 rustapi_openapi::SchemaRef::Inline(v) => {
1512 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
1513 }
1514 _ => panic!("Expected inline schema for {}", schema_type),
1515 }
1516 }
1517
1518 let string_schema = schema_type_to_openapi_schema("string");
1520 match string_schema {
1521 rustapi_openapi::SchemaRef::Inline(v) => {
1522 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1523 }
1524 _ => panic!("Expected inline schema for string"),
1525 }
1526 }
1527
1528 proptest! {
1535 #![proptest_config(ProptestConfig::with_cases(100))]
1536
1537 #[test]
1542 fn prop_nested_routes_in_openapi_spec(
1543 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1545 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1547 has_param in any::<bool>(),
1548 ) {
1549 async fn handler() -> &'static str { "handler" }
1550
1551 let prefix = format!("/{}", prefix_segments.join("/"));
1553
1554 let mut route_path = format!("/{}", route_segments.join("/"));
1556 if has_param {
1557 route_path.push_str("/{id}");
1558 }
1559
1560 let nested_router = Router::new().route(&route_path, get(handler));
1562 let app = RustApi::new().nest(&prefix, nested_router);
1563
1564 let expected_openapi_path = format!("{}{}", prefix, route_path);
1566
1567 let spec = app.openapi_spec();
1569
1570 prop_assert!(
1572 spec.paths.contains_key(&expected_openapi_path),
1573 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1574 expected_openapi_path,
1575 spec.paths.keys().collect::<Vec<_>>()
1576 );
1577
1578 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1580 prop_assert!(
1581 path_item.get.is_some(),
1582 "GET operation should exist for path '{}'",
1583 expected_openapi_path
1584 );
1585 }
1586
1587 #[test]
1592 fn prop_multiple_methods_preserved_in_openapi(
1593 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1594 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1595 ) {
1596 async fn get_handler() -> &'static str { "get" }
1597 async fn post_handler() -> &'static str { "post" }
1598
1599 let prefix = format!("/{}", prefix_segments.join("/"));
1601 let route_path = format!("/{}", route_segments.join("/"));
1602
1603 let get_route_path = format!("{}/get", route_path);
1606 let post_route_path = format!("{}/post", route_path);
1607 let nested_router = Router::new()
1608 .route(&get_route_path, get(get_handler))
1609 .route(&post_route_path, post(post_handler));
1610 let app = RustApi::new().nest(&prefix, nested_router);
1611
1612 let expected_get_path = format!("{}{}", prefix, get_route_path);
1614 let expected_post_path = format!("{}{}", prefix, post_route_path);
1615
1616 let spec = app.openapi_spec();
1618
1619 prop_assert!(
1621 spec.paths.contains_key(&expected_get_path),
1622 "Expected OpenAPI path '{}' not found",
1623 expected_get_path
1624 );
1625 prop_assert!(
1626 spec.paths.contains_key(&expected_post_path),
1627 "Expected OpenAPI path '{}' not found",
1628 expected_post_path
1629 );
1630
1631 let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1633 prop_assert!(
1634 get_path_item.get.is_some(),
1635 "GET operation should exist for path '{}'",
1636 expected_get_path
1637 );
1638
1639 let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1641 prop_assert!(
1642 post_path_item.post.is_some(),
1643 "POST operation should exist for path '{}'",
1644 expected_post_path
1645 );
1646 }
1647
1648 #[test]
1653 fn prop_path_params_in_openapi_after_nesting(
1654 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1655 param_name in "[a-z][a-z0-9]{0,5}",
1656 ) {
1657 async fn handler() -> &'static str { "handler" }
1658
1659 let prefix = format!("/{}", prefix_segments.join("/"));
1661 let route_path = format!("/{{{}}}", param_name);
1662
1663 let nested_router = Router::new().route(&route_path, get(handler));
1665 let app = RustApi::new().nest(&prefix, nested_router);
1666
1667 let expected_openapi_path = format!("{}{}", prefix, route_path);
1669
1670 let spec = app.openapi_spec();
1672
1673 prop_assert!(
1675 spec.paths.contains_key(&expected_openapi_path),
1676 "Expected OpenAPI path '{}' not found",
1677 expected_openapi_path
1678 );
1679
1680 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1682 let get_op = path_item.get.as_ref().unwrap();
1683
1684 prop_assert!(
1685 !get_op.parameters.is_empty(),
1686 "Operation should have parameters for path '{}'",
1687 expected_openapi_path
1688 );
1689
1690 let params = &get_op.parameters;
1691 let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1692 prop_assert!(
1693 has_param,
1694 "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1695 param_name,
1696 params.iter().map(|p| &p.name).collect::<Vec<_>>()
1697 );
1698 }
1699 }
1700
1701 proptest! {
1709 #![proptest_config(ProptestConfig::with_cases(100))]
1710
1711 #[test]
1716 fn prop_rustapi_nest_delegates_to_router_nest(
1717 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1718 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1719 has_param in any::<bool>(),
1720 ) {
1721 async fn handler() -> &'static str { "handler" }
1722
1723 let prefix = format!("/{}", prefix_segments.join("/"));
1725
1726 let mut route_path = format!("/{}", route_segments.join("/"));
1728 if has_param {
1729 route_path.push_str("/{id}");
1730 }
1731
1732 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1734 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1735
1736 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1738 let rustapi_router = rustapi_app.into_router();
1739
1740 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1742
1743 let rustapi_routes = rustapi_router.registered_routes();
1745 let router_routes = router_app.registered_routes();
1746
1747 prop_assert_eq!(
1748 rustapi_routes.len(),
1749 router_routes.len(),
1750 "RustApi and Router should have same number of routes"
1751 );
1752
1753 for (path, info) in router_routes {
1755 prop_assert!(
1756 rustapi_routes.contains_key(path),
1757 "Route '{}' from Router should exist in RustApi routes",
1758 path
1759 );
1760
1761 let rustapi_info = rustapi_routes.get(path).unwrap();
1762 prop_assert_eq!(
1763 &info.path, &rustapi_info.path,
1764 "Display paths should match for route '{}'",
1765 path
1766 );
1767 prop_assert_eq!(
1768 info.methods.len(), rustapi_info.methods.len(),
1769 "Method count should match for route '{}'",
1770 path
1771 );
1772 }
1773 }
1774
1775 #[test]
1780 fn prop_rustapi_nest_includes_routes_in_openapi(
1781 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1782 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1783 has_param in any::<bool>(),
1784 ) {
1785 async fn handler() -> &'static str { "handler" }
1786
1787 let prefix = format!("/{}", prefix_segments.join("/"));
1789
1790 let mut route_path = format!("/{}", route_segments.join("/"));
1792 if has_param {
1793 route_path.push_str("/{id}");
1794 }
1795
1796 let nested_router = Router::new().route(&route_path, get(handler));
1798 let app = RustApi::new().nest(&prefix, nested_router);
1799
1800 let expected_openapi_path = format!("{}{}", prefix, route_path);
1802
1803 let spec = app.openapi_spec();
1805
1806 prop_assert!(
1808 spec.paths.contains_key(&expected_openapi_path),
1809 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1810 expected_openapi_path,
1811 spec.paths.keys().collect::<Vec<_>>()
1812 );
1813
1814 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1816 prop_assert!(
1817 path_item.get.is_some(),
1818 "GET operation should exist for path '{}'",
1819 expected_openapi_path
1820 );
1821 }
1822
1823 #[test]
1828 fn prop_rustapi_nest_route_matching_identical(
1829 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1830 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1831 param_value in "[a-z0-9]{1,10}",
1832 ) {
1833 use crate::router::RouteMatch;
1834
1835 async fn handler() -> &'static str { "handler" }
1836
1837 let prefix = format!("/{}", prefix_segments.join("/"));
1839 let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1840
1841 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1843 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1844
1845 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1847 let rustapi_router = rustapi_app.into_router();
1848 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1849
1850 let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1852
1853 let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1855 let router_match = router_app.match_route(&full_path, &Method::GET);
1856
1857 match (rustapi_match, router_match) {
1859 (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1860 prop_assert_eq!(
1861 rustapi_params.len(),
1862 router_params.len(),
1863 "Parameter count should match"
1864 );
1865 for (key, value) in &router_params {
1866 prop_assert!(
1867 rustapi_params.contains_key(key),
1868 "RustApi should have parameter '{}'",
1869 key
1870 );
1871 prop_assert_eq!(
1872 rustapi_params.get(key).unwrap(),
1873 value,
1874 "Parameter '{}' value should match",
1875 key
1876 );
1877 }
1878 }
1879 (rustapi_result, router_result) => {
1880 prop_assert!(
1881 false,
1882 "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1883 match rustapi_result {
1884 RouteMatch::Found { .. } => "Found",
1885 RouteMatch::NotFound => "NotFound",
1886 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1887 },
1888 match router_result {
1889 RouteMatch::Found { .. } => "Found",
1890 RouteMatch::NotFound => "NotFound",
1891 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1892 }
1893 );
1894 }
1895 }
1896 }
1897 }
1898
1899 #[test]
1901 fn test_openapi_operations_propagated_during_nesting() {
1902 async fn list_users() -> &'static str {
1903 "list users"
1904 }
1905 async fn get_user() -> &'static str {
1906 "get user"
1907 }
1908 async fn create_user() -> &'static str {
1909 "create user"
1910 }
1911
1912 let users_router = Router::new()
1915 .route("/", get(list_users))
1916 .route("/create", post(create_user))
1917 .route("/{id}", get(get_user));
1918
1919 let app = RustApi::new().nest("/api/v1/users", users_router);
1921
1922 let spec = app.openapi_spec();
1923
1924 assert!(
1926 spec.paths.contains_key("/api/v1/users"),
1927 "Should have /api/v1/users path"
1928 );
1929 let users_path = spec.paths.get("/api/v1/users").unwrap();
1930 assert!(users_path.get.is_some(), "Should have GET operation");
1931
1932 assert!(
1934 spec.paths.contains_key("/api/v1/users/create"),
1935 "Should have /api/v1/users/create path"
1936 );
1937 let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1938 assert!(create_path.post.is_some(), "Should have POST operation");
1939
1940 assert!(
1942 spec.paths.contains_key("/api/v1/users/{id}"),
1943 "Should have /api/v1/users/{{id}} path"
1944 );
1945 let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1946 assert!(
1947 user_path.get.is_some(),
1948 "Should have GET operation for user by id"
1949 );
1950
1951 let get_user_op = user_path.get.as_ref().unwrap();
1953 assert!(!get_user_op.parameters.is_empty(), "Should have parameters");
1954 let params = &get_user_op.parameters;
1955 assert!(
1956 params
1957 .iter()
1958 .any(|p| p.name == "id" && p.location == "path"),
1959 "Should have 'id' path parameter"
1960 );
1961 }
1962
1963 #[test]
1965 fn test_openapi_spec_empty_without_routes() {
1966 let app = RustApi::new();
1967 let spec = app.openapi_spec();
1968
1969 assert!(
1971 spec.paths.is_empty(),
1972 "OpenAPI spec should have no paths without routes"
1973 );
1974 }
1975
1976 #[test]
1981 fn test_rustapi_nest_delegates_to_router_nest() {
1982 use crate::router::RouteMatch;
1983
1984 async fn list_users() -> &'static str {
1985 "list users"
1986 }
1987 async fn get_user() -> &'static str {
1988 "get user"
1989 }
1990 async fn create_user() -> &'static str {
1991 "create user"
1992 }
1993
1994 let users_router = Router::new()
1996 .route("/", get(list_users))
1997 .route("/create", post(create_user))
1998 .route("/{id}", get(get_user));
1999
2000 let app = RustApi::new().nest("/api/v1/users", users_router);
2002 let router = app.into_router();
2003
2004 let routes = router.registered_routes();
2006 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
2007
2008 assert!(
2010 routes.contains_key("/api/v1/users"),
2011 "Should have /api/v1/users route"
2012 );
2013 assert!(
2014 routes.contains_key("/api/v1/users/create"),
2015 "Should have /api/v1/users/create route"
2016 );
2017 assert!(
2018 routes.contains_key("/api/v1/users/:id"),
2019 "Should have /api/v1/users/:id route"
2020 );
2021
2022 match router.match_route("/api/v1/users", &Method::GET) {
2024 RouteMatch::Found { params, .. } => {
2025 assert!(params.is_empty(), "Root route should have no params");
2026 }
2027 _ => panic!("GET /api/v1/users should be found"),
2028 }
2029
2030 match router.match_route("/api/v1/users/create", &Method::POST) {
2031 RouteMatch::Found { params, .. } => {
2032 assert!(params.is_empty(), "Create route should have no params");
2033 }
2034 _ => panic!("POST /api/v1/users/create should be found"),
2035 }
2036
2037 match router.match_route("/api/v1/users/123", &Method::GET) {
2038 RouteMatch::Found { params, .. } => {
2039 assert_eq!(
2040 params.get("id"),
2041 Some(&"123".to_string()),
2042 "Should extract id param"
2043 );
2044 }
2045 _ => panic!("GET /api/v1/users/123 should be found"),
2046 }
2047
2048 match router.match_route("/api/v1/users", &Method::DELETE) {
2050 RouteMatch::MethodNotAllowed { allowed } => {
2051 assert!(allowed.contains(&Method::GET), "Should allow GET");
2052 }
2053 _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
2054 }
2055 }
2056
2057 #[test]
2062 fn test_rustapi_nest_includes_routes_in_openapi_spec() {
2063 async fn list_items() -> &'static str {
2064 "list items"
2065 }
2066 async fn get_item() -> &'static str {
2067 "get item"
2068 }
2069
2070 let items_router = Router::new()
2072 .route("/", get(list_items))
2073 .route("/{item_id}", get(get_item));
2074
2075 let app = RustApi::new().nest("/api/items", items_router);
2077
2078 let spec = app.openapi_spec();
2080
2081 assert!(
2083 spec.paths.contains_key("/api/items"),
2084 "Should have /api/items in OpenAPI"
2085 );
2086 assert!(
2087 spec.paths.contains_key("/api/items/{item_id}"),
2088 "Should have /api/items/{{item_id}} in OpenAPI"
2089 );
2090
2091 let list_path = spec.paths.get("/api/items").unwrap();
2093 assert!(
2094 list_path.get.is_some(),
2095 "Should have GET operation for /api/items"
2096 );
2097
2098 let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2099 assert!(
2100 get_path.get.is_some(),
2101 "Should have GET operation for /api/items/{{item_id}}"
2102 );
2103
2104 let get_op = get_path.get.as_ref().unwrap();
2106 assert!(!get_op.parameters.is_empty(), "Should have parameters");
2107 let params = &get_op.parameters;
2108 assert!(
2109 params
2110 .iter()
2111 .any(|p| p.name == "item_id" && p.location == "path"),
2112 "Should have 'item_id' path parameter"
2113 );
2114 }
2115}
2116
2117#[cfg(feature = "swagger-ui")]
2119fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2120 req.headers()
2121 .get(http::header::AUTHORIZATION)
2122 .and_then(|v| v.to_str().ok())
2123 .map(|auth| auth == expected)
2124 .unwrap_or(false)
2125}
2126
2127#[cfg(feature = "swagger-ui")]
2129fn unauthorized_response() -> crate::Response {
2130 http::Response::builder()
2131 .status(http::StatusCode::UNAUTHORIZED)
2132 .header(
2133 http::header::WWW_AUTHENTICATE,
2134 "Basic realm=\"API Documentation\"",
2135 )
2136 .header(http::header::CONTENT_TYPE, "text/plain")
2137 .body(crate::response::Body::from("Unauthorized"))
2138 .unwrap()
2139}
2140
2141pub struct RustApiConfig {
2143 docs_path: Option<String>,
2144 docs_enabled: bool,
2145 api_title: String,
2146 api_version: String,
2147 api_description: Option<String>,
2148 body_limit: Option<usize>,
2149 layers: LayerStack,
2150}
2151
2152impl Default for RustApiConfig {
2153 fn default() -> Self {
2154 Self::new()
2155 }
2156}
2157
2158impl RustApiConfig {
2159 pub fn new() -> Self {
2160 Self {
2161 docs_path: Some("/docs".to_string()),
2162 docs_enabled: true,
2163 api_title: "RustAPI".to_string(),
2164 api_version: "1.0.0".to_string(),
2165 api_description: None,
2166 body_limit: None,
2167 layers: LayerStack::new(),
2168 }
2169 }
2170
2171 pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2173 self.docs_path = Some(path.into());
2174 self
2175 }
2176
2177 pub fn docs_enabled(mut self, enabled: bool) -> Self {
2179 self.docs_enabled = enabled;
2180 self
2181 }
2182
2183 pub fn openapi_info(
2185 mut self,
2186 title: impl Into<String>,
2187 version: impl Into<String>,
2188 description: Option<impl Into<String>>,
2189 ) -> Self {
2190 self.api_title = title.into();
2191 self.api_version = version.into();
2192 self.api_description = description.map(|d| d.into());
2193 self
2194 }
2195
2196 pub fn body_limit(mut self, limit: usize) -> Self {
2198 self.body_limit = Some(limit);
2199 self
2200 }
2201
2202 pub fn layer<L>(mut self, layer: L) -> Self
2204 where
2205 L: MiddlewareLayer,
2206 {
2207 self.layers.push(Box::new(layer));
2208 self
2209 }
2210
2211 pub fn build(self) -> RustApi {
2213 let mut app = RustApi::new().mount_auto_routes_grouped();
2214
2215 if let Some(limit) = self.body_limit {
2217 app = app.body_limit(limit);
2218 }
2219
2220 app = app.openapi_info(
2221 &self.api_title,
2222 &self.api_version,
2223 self.api_description.as_deref(),
2224 );
2225
2226 #[cfg(feature = "swagger-ui")]
2227 if self.docs_enabled {
2228 if let Some(path) = self.docs_path {
2229 app = app.docs(&path);
2230 }
2231 }
2232
2233 app.layers.extend(self.layers);
2236
2237 app
2238 }
2239
2240 pub async fn run(
2242 self,
2243 addr: impl AsRef<str>,
2244 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2245 self.build().run(addr.as_ref()).await
2246 }
2247}