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