1use crate::error::Result;
4use crate::interceptor::{InterceptorChain, RequestInterceptor, ResponseInterceptor};
5use crate::middleware::{BodyLimitLayer, LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT};
6use crate::response::IntoResponse;
7use crate::router::{MethodRouter, Router};
8use crate::server::Server;
9use std::collections::HashMap;
10use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
11
12pub struct RustApi {
30 router: Router,
31 openapi_spec: rustapi_openapi::OpenApiSpec,
32 layers: LayerStack,
33 body_limit: Option<usize>,
34 interceptors: InterceptorChain,
35 #[cfg(feature = "http3")]
36 http3_config: Option<crate::http3::Http3Config>,
37}
38
39impl RustApi {
40 pub fn new() -> Self {
42 let _ = tracing_subscriber::registry()
44 .with(
45 EnvFilter::try_from_default_env()
46 .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
47 )
48 .with(tracing_subscriber::fmt::layer())
49 .try_init();
50
51 Self {
52 router: Router::new(),
53 openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
54 .register::<rustapi_openapi::ErrorSchema>()
55 .register::<rustapi_openapi::ErrorBodySchema>()
56 .register::<rustapi_openapi::ValidationErrorSchema>()
57 .register::<rustapi_openapi::ValidationErrorBodySchema>()
58 .register::<rustapi_openapi::FieldErrorSchema>(),
59 layers: LayerStack::new(),
60 body_limit: Some(DEFAULT_BODY_LIMIT), interceptors: InterceptorChain::new(),
62 #[cfg(feature = "http3")]
63 http3_config: None,
64 }
65 }
66
67 #[cfg(feature = "swagger-ui")]
91 pub fn auto() -> Self {
92 Self::new().mount_auto_routes_grouped().docs("/docs")
94 }
95
96 #[cfg(not(feature = "swagger-ui"))]
101 pub fn auto() -> Self {
102 Self::new().mount_auto_routes_grouped()
103 }
104
105 pub fn config() -> RustApiConfig {
123 RustApiConfig::new()
124 }
125
126 pub fn body_limit(mut self, limit: usize) -> Self {
147 self.body_limit = Some(limit);
148 self
149 }
150
151 pub fn no_body_limit(mut self) -> Self {
164 self.body_limit = None;
165 self
166 }
167
168 pub fn layer<L>(mut self, layer: L) -> Self
188 where
189 L: MiddlewareLayer,
190 {
191 self.layers.push(Box::new(layer));
192 self
193 }
194
195 pub fn request_interceptor<I>(mut self, interceptor: I) -> Self
227 where
228 I: RequestInterceptor,
229 {
230 self.interceptors.add_request_interceptor(interceptor);
231 self
232 }
233
234 pub fn response_interceptor<I>(mut self, interceptor: I) -> Self
266 where
267 I: ResponseInterceptor,
268 {
269 self.interceptors.add_response_interceptor(interceptor);
270 self
271 }
272
273 pub fn state<S>(self, _state: S) -> Self
289 where
290 S: Clone + Send + Sync + 'static,
291 {
292 let state = _state;
294 let mut app = self;
295 app.router = app.router.state(state);
296 app
297 }
298
299 pub fn register_schema<T: for<'a> rustapi_openapi::Schema<'a>>(mut self) -> Self {
311 self.openapi_spec = self.openapi_spec.register::<T>();
312 self
313 }
314
315 pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
317 self.openapi_spec.info.title = title.to_string();
320 self.openapi_spec.info.version = version.to_string();
321 self.openapi_spec.info.description = description.map(|d| d.to_string());
322 self
323 }
324
325 pub fn openapi_spec(&self) -> &rustapi_openapi::OpenApiSpec {
327 &self.openapi_spec
328 }
329
330 fn mount_auto_routes_grouped(mut self) -> Self {
331 let routes = crate::auto_route::collect_auto_routes();
332 let mut by_path: HashMap<String, MethodRouter> = HashMap::new();
333
334 for route in routes {
335 let method_enum = match route.method {
336 "GET" => http::Method::GET,
337 "POST" => http::Method::POST,
338 "PUT" => http::Method::PUT,
339 "DELETE" => http::Method::DELETE,
340 "PATCH" => http::Method::PATCH,
341 _ => http::Method::GET,
342 };
343
344 let path = if route.path.starts_with('/') {
345 route.path.to_string()
346 } else {
347 format!("/{}", route.path)
348 };
349
350 let entry = by_path.entry(path).or_default();
351 entry.insert_boxed_with_operation(method_enum, route.handler, route.operation);
352 }
353
354 #[cfg(feature = "tracing")]
355 let route_count: usize = by_path.values().map(|mr| mr.allowed_methods().len()).sum();
356 #[cfg(feature = "tracing")]
357 let path_count = by_path.len();
358
359 for (path, method_router) in by_path {
360 self = self.route(&path, method_router);
361 }
362
363 crate::trace_info!(
364 paths = path_count,
365 routes = route_count,
366 "Auto-registered routes"
367 );
368
369 crate::auto_schema::apply_auto_schemas(&mut self.openapi_spec);
371
372 self
373 }
374
375 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
386 for (method, op) in &method_router.operations {
388 let mut op = op.clone();
389 add_path_params_to_operation(path, &mut op, &std::collections::HashMap::new());
390 self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op);
391 }
392
393 self.router = self.router.route(path, method_router);
394 self
395 }
396
397 pub fn typed<P: crate::typed_path::TypedPath>(self, method_router: MethodRouter) -> Self {
399 self.route(P::PATH, method_router)
400 }
401
402 #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
406 pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
407 self.route(path, method_router)
408 }
409
410 pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
428 let method_enum = match route.method {
429 "GET" => http::Method::GET,
430 "POST" => http::Method::POST,
431 "PUT" => http::Method::PUT,
432 "DELETE" => http::Method::DELETE,
433 "PATCH" => http::Method::PATCH,
434 _ => http::Method::GET,
435 };
436
437 let mut op = route.operation;
439 add_path_params_to_operation(route.path, &mut op, &route.param_schemas);
440 self.openapi_spec = self.openapi_spec.path(route.path, route.method, op);
441
442 self.route_with_method(route.path, method_enum, route.handler)
443 }
444
445 fn route_with_method(
447 self,
448 path: &str,
449 method: http::Method,
450 handler: crate::handler::BoxedHandler,
451 ) -> Self {
452 use crate::router::MethodRouter;
453 let path = if !path.starts_with('/') {
462 format!("/{}", path)
463 } else {
464 path.to_string()
465 };
466
467 let mut handlers = std::collections::HashMap::new();
476 handlers.insert(method, handler);
477
478 let method_router = MethodRouter::from_boxed(handlers);
479 self.route(&path, method_router)
480 }
481
482 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
498 let normalized_prefix = normalize_prefix_for_openapi(prefix);
500
501 for (matchit_path, method_router) in router.method_routers() {
504 let display_path = router
506 .registered_routes()
507 .get(matchit_path)
508 .map(|info| info.path.clone())
509 .unwrap_or_else(|| matchit_path.clone());
510
511 let prefixed_path = if display_path == "/" {
513 normalized_prefix.clone()
514 } else {
515 format!("{}{}", normalized_prefix, display_path)
516 };
517
518 for (method, op) in &method_router.operations {
520 let mut op = op.clone();
521 add_path_params_to_operation(
522 &prefixed_path,
523 &mut op,
524 &std::collections::HashMap::new(),
525 );
526 self.openapi_spec = self.openapi_spec.path(&prefixed_path, method.as_str(), op);
527 }
528 }
529
530 self.router = self.router.nest(prefix, router);
532 self
533 }
534
535 pub fn serve_static(self, prefix: &str, root: impl Into<std::path::PathBuf>) -> Self {
564 self.serve_static_with_config(crate::static_files::StaticFileConfig::new(root, prefix))
565 }
566
567 pub fn serve_static_with_config(self, config: crate::static_files::StaticFileConfig) -> Self {
584 use crate::router::MethodRouter;
585 use std::collections::HashMap;
586
587 let prefix = config.prefix.clone();
588 let catch_all_path = format!("{}/*path", prefix.trim_end_matches('/'));
589
590 let handler: crate::handler::BoxedHandler =
592 std::sync::Arc::new(move |req: crate::Request| {
593 let config = config.clone();
594 let path = req.uri().path().to_string();
595
596 Box::pin(async move {
597 let relative_path = path.strip_prefix(&config.prefix).unwrap_or(&path);
598
599 match crate::static_files::StaticFile::serve(relative_path, &config).await {
600 Ok(response) => response,
601 Err(err) => err.into_response(),
602 }
603 })
604 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
605 });
606
607 let mut handlers = HashMap::new();
608 handlers.insert(http::Method::GET, handler);
609 let method_router = MethodRouter::from_boxed(handlers);
610
611 self.route(&catch_all_path, method_router)
612 }
613
614 #[cfg(feature = "compression")]
631 pub fn compression(self) -> Self {
632 self.layer(crate::middleware::CompressionLayer::new())
633 }
634
635 #[cfg(feature = "compression")]
651 pub fn compression_with_config(self, config: crate::middleware::CompressionConfig) -> Self {
652 self.layer(crate::middleware::CompressionLayer::with_config(config))
653 }
654
655 #[cfg(feature = "swagger-ui")]
679 pub fn docs(self, path: &str) -> Self {
680 let title = self.openapi_spec.info.title.clone();
681 let version = self.openapi_spec.info.version.clone();
682 let description = self.openapi_spec.info.description.clone();
683
684 self.docs_with_info(path, &title, &version, description.as_deref())
685 }
686
687 #[cfg(feature = "swagger-ui")]
696 pub fn docs_with_info(
697 mut self,
698 path: &str,
699 title: &str,
700 version: &str,
701 description: Option<&str>,
702 ) -> Self {
703 use crate::router::get;
704 self.openapi_spec.info.title = title.to_string();
706 self.openapi_spec.info.version = version.to_string();
707 if let Some(desc) = description {
708 self.openapi_spec.info.description = Some(desc.to_string());
709 }
710
711 let path = path.trim_end_matches('/');
712 let openapi_path = format!("{}/openapi.json", path);
713
714 let spec_json =
716 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
717 let openapi_url = openapi_path.clone();
718
719 let spec_handler = move || {
721 let json = spec_json.clone();
722 async move {
723 http::Response::builder()
724 .status(http::StatusCode::OK)
725 .header(http::header::CONTENT_TYPE, "application/json")
726 .body(crate::response::Body::from(json))
727 .unwrap()
728 }
729 };
730
731 let docs_handler = move || {
733 let url = openapi_url.clone();
734 async move {
735 let response = rustapi_openapi::swagger_ui_html(&url);
736 response.map(crate::response::Body::Full)
737 }
738 };
739
740 self.route(&openapi_path, get(spec_handler))
741 .route(path, get(docs_handler))
742 }
743
744 #[cfg(feature = "swagger-ui")]
760 pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
761 let title = self.openapi_spec.info.title.clone();
762 let version = self.openapi_spec.info.version.clone();
763 let description = self.openapi_spec.info.description.clone();
764
765 self.docs_with_auth_and_info(
766 path,
767 username,
768 password,
769 &title,
770 &version,
771 description.as_deref(),
772 )
773 }
774
775 #[cfg(feature = "swagger-ui")]
791 pub fn docs_with_auth_and_info(
792 mut self,
793 path: &str,
794 username: &str,
795 password: &str,
796 title: &str,
797 version: &str,
798 description: Option<&str>,
799 ) -> Self {
800 use crate::router::MethodRouter;
801 use base64::{engine::general_purpose::STANDARD, Engine};
802 use std::collections::HashMap;
803
804 self.openapi_spec.info.title = title.to_string();
806 self.openapi_spec.info.version = version.to_string();
807 if let Some(desc) = description {
808 self.openapi_spec.info.description = Some(desc.to_string());
809 }
810
811 let path = path.trim_end_matches('/');
812 let openapi_path = format!("{}/openapi.json", path);
813
814 let credentials = format!("{}:{}", username, password);
816 let encoded = STANDARD.encode(credentials.as_bytes());
817 let expected_auth = format!("Basic {}", encoded);
818
819 let spec_json =
821 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
822 let openapi_url = openapi_path.clone();
823 let expected_auth_spec = expected_auth.clone();
824 let expected_auth_docs = expected_auth;
825
826 let spec_handler: crate::handler::BoxedHandler =
828 std::sync::Arc::new(move |req: crate::Request| {
829 let json = spec_json.clone();
830 let expected = expected_auth_spec.clone();
831 Box::pin(async move {
832 if !check_basic_auth(&req, &expected) {
833 return unauthorized_response();
834 }
835 http::Response::builder()
836 .status(http::StatusCode::OK)
837 .header(http::header::CONTENT_TYPE, "application/json")
838 .body(crate::response::Body::from(json))
839 .unwrap()
840 })
841 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
842 });
843
844 let docs_handler: crate::handler::BoxedHandler =
846 std::sync::Arc::new(move |req: crate::Request| {
847 let url = openapi_url.clone();
848 let expected = expected_auth_docs.clone();
849 Box::pin(async move {
850 if !check_basic_auth(&req, &expected) {
851 return unauthorized_response();
852 }
853 let response = rustapi_openapi::swagger_ui_html(&url);
854 response.map(crate::response::Body::Full)
855 })
856 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
857 });
858
859 let mut spec_handlers = HashMap::new();
861 spec_handlers.insert(http::Method::GET, spec_handler);
862 let spec_router = MethodRouter::from_boxed(spec_handlers);
863
864 let mut docs_handlers = HashMap::new();
865 docs_handlers.insert(http::Method::GET, docs_handler);
866 let docs_router = MethodRouter::from_boxed(docs_handlers);
867
868 self.route(&openapi_path, spec_router)
869 .route(path, docs_router)
870 }
871
872 pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
883 if let Some(limit) = self.body_limit {
885 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
887 }
888
889 let server = Server::new(self.router, self.layers, self.interceptors);
890 server.run(addr).await
891 }
892
893 pub async fn run_with_shutdown<F>(
895 mut self,
896 addr: impl AsRef<str>,
897 signal: F,
898 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
899 where
900 F: std::future::Future<Output = ()> + Send + 'static,
901 {
902 if let Some(limit) = self.body_limit {
904 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
906 }
907
908 let server = Server::new(self.router, self.layers, self.interceptors);
909 server.run_with_shutdown(addr.as_ref(), signal).await
910 }
911
912 pub fn into_router(self) -> Router {
914 self.router
915 }
916
917 pub fn layers(&self) -> &LayerStack {
919 &self.layers
920 }
921
922 pub fn interceptors(&self) -> &InterceptorChain {
924 &self.interceptors
925 }
926
927 #[cfg(feature = "http3")]
941 pub async fn run_http3(
942 mut self,
943 config: crate::http3::Http3Config,
944 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
945 use std::sync::Arc;
946
947 if let Some(limit) = self.body_limit {
949 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
950 }
951
952 let server = crate::http3::Http3Server::new(
953 &config,
954 Arc::new(self.router),
955 Arc::new(self.layers),
956 Arc::new(self.interceptors),
957 )
958 .await?;
959
960 server.run().await
961 }
962
963 #[cfg(feature = "http3-dev")]
977 pub async fn run_http3_dev(
978 mut self,
979 addr: &str,
980 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
981 use std::sync::Arc;
982
983 if let Some(limit) = self.body_limit {
985 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
986 }
987
988 let server = crate::http3::Http3Server::new_with_self_signed(
989 addr,
990 Arc::new(self.router),
991 Arc::new(self.layers),
992 Arc::new(self.interceptors),
993 )
994 .await?;
995
996 server.run().await
997 }
998
999 #[cfg(feature = "http3")]
1023 pub fn with_http3(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
1024 self.http3_config = Some(crate::http3::Http3Config::new(cert_path, key_path));
1025 self
1026 }
1027
1028 #[cfg(feature = "http3")]
1043 pub async fn run_dual_stack(
1044 mut self,
1045 _http_addr: &str,
1046 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1047 let config = self
1055 .http3_config
1056 .take()
1057 .ok_or("HTTP/3 config not set. Use .with_http3(...)")?;
1058
1059 tracing::warn!("run_dual_stack currently only runs HTTP/3. HTTP/1.1 support coming soon.");
1060 self.run_http3(config).await
1061 }
1062}
1063
1064fn add_path_params_to_operation(
1065 path: &str,
1066 op: &mut rustapi_openapi::Operation,
1067 param_schemas: &std::collections::HashMap<String, String>,
1068) {
1069 let mut params: Vec<String> = Vec::new();
1070 let mut in_brace = false;
1071 let mut current = String::new();
1072
1073 for ch in path.chars() {
1074 match ch {
1075 '{' => {
1076 in_brace = true;
1077 current.clear();
1078 }
1079 '}' => {
1080 if in_brace {
1081 in_brace = false;
1082 if !current.is_empty() {
1083 params.push(current.clone());
1084 }
1085 }
1086 }
1087 _ => {
1088 if in_brace {
1089 current.push(ch);
1090 }
1091 }
1092 }
1093 }
1094
1095 if params.is_empty() {
1096 return;
1097 }
1098
1099 let op_params = op.parameters.get_or_insert_with(Vec::new);
1100
1101 for name in params {
1102 let already = op_params
1103 .iter()
1104 .any(|p| p.location == "path" && p.name == name);
1105 if already {
1106 continue;
1107 }
1108
1109 let schema = if let Some(schema_type) = param_schemas.get(&name) {
1111 schema_type_to_openapi_schema(schema_type)
1112 } else {
1113 infer_path_param_schema(&name)
1114 };
1115
1116 op_params.push(rustapi_openapi::Parameter {
1117 name,
1118 location: "path".to_string(),
1119 required: true,
1120 description: None,
1121 schema,
1122 });
1123 }
1124}
1125
1126fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef {
1128 match schema_type.to_lowercase().as_str() {
1129 "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1130 "type": "string",
1131 "format": "uuid"
1132 })),
1133 "integer" | "int" | "int64" | "i64" => {
1134 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1135 "type": "integer",
1136 "format": "int64"
1137 }))
1138 }
1139 "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1140 "type": "integer",
1141 "format": "int32"
1142 })),
1143 "number" | "float" | "f64" | "f32" => {
1144 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1145 "type": "number"
1146 }))
1147 }
1148 "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1149 "type": "boolean"
1150 })),
1151 _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1152 "type": "string"
1153 })),
1154 }
1155}
1156
1157fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef {
1166 let lower = name.to_lowercase();
1167
1168 let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid");
1170
1171 if is_uuid {
1172 return rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1173 "type": "string",
1174 "format": "uuid"
1175 }));
1176 }
1177
1178 let is_integer = lower == "page"
1181 || lower == "limit"
1182 || lower == "offset"
1183 || lower == "count"
1184 || lower.ends_with("_count")
1185 || lower.ends_with("_num")
1186 || lower == "year"
1187 || lower == "month"
1188 || lower == "day"
1189 || lower == "index"
1190 || lower == "position";
1191
1192 if is_integer {
1193 rustapi_openapi::SchemaRef::Inline(serde_json::json!({
1194 "type": "integer",
1195 "format": "int64"
1196 }))
1197 } else {
1198 rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" }))
1199 }
1200}
1201
1202fn normalize_prefix_for_openapi(prefix: &str) -> String {
1209 if prefix.is_empty() {
1211 return "/".to_string();
1212 }
1213
1214 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
1216
1217 if segments.is_empty() {
1219 return "/".to_string();
1220 }
1221
1222 let mut result = String::with_capacity(prefix.len() + 1);
1224 for segment in segments {
1225 result.push('/');
1226 result.push_str(segment);
1227 }
1228
1229 result
1230}
1231
1232impl Default for RustApi {
1233 fn default() -> Self {
1234 Self::new()
1235 }
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::RustApi;
1241 use crate::extract::{FromRequestParts, State};
1242 use crate::path_params::PathParams;
1243 use crate::request::Request;
1244 use crate::router::{get, post, Router};
1245 use bytes::Bytes;
1246 use http::Method;
1247 use proptest::prelude::*;
1248
1249 #[test]
1250 fn state_is_available_via_extractor() {
1251 let app = RustApi::new().state(123u32);
1252 let router = app.into_router();
1253
1254 let req = http::Request::builder()
1255 .method(Method::GET)
1256 .uri("/test")
1257 .body(())
1258 .unwrap();
1259 let (parts, _) = req.into_parts();
1260
1261 let request = Request::new(
1262 parts,
1263 crate::request::BodyVariant::Buffered(Bytes::new()),
1264 router.state_ref(),
1265 PathParams::new(),
1266 );
1267 let State(value) = State::<u32>::from_request_parts(&request).unwrap();
1268 assert_eq!(value, 123u32);
1269 }
1270
1271 #[test]
1272 fn test_path_param_type_inference_integer() {
1273 use super::infer_path_param_schema;
1274
1275 let int_params = [
1277 "page",
1278 "limit",
1279 "offset",
1280 "count",
1281 "item_count",
1282 "year",
1283 "month",
1284 "day",
1285 "index",
1286 "position",
1287 ];
1288
1289 for name in int_params {
1290 let schema = infer_path_param_schema(name);
1291 match schema {
1292 rustapi_openapi::SchemaRef::Inline(v) => {
1293 assert_eq!(
1294 v.get("type").and_then(|v| v.as_str()),
1295 Some("integer"),
1296 "Expected '{}' to be inferred as integer",
1297 name
1298 );
1299 }
1300 _ => panic!("Expected inline schema for '{}'", name),
1301 }
1302 }
1303 }
1304
1305 #[test]
1306 fn test_path_param_type_inference_uuid() {
1307 use super::infer_path_param_schema;
1308
1309 let uuid_params = ["uuid", "user_uuid", "sessionUuid"];
1311
1312 for name in uuid_params {
1313 let schema = infer_path_param_schema(name);
1314 match schema {
1315 rustapi_openapi::SchemaRef::Inline(v) => {
1316 assert_eq!(
1317 v.get("type").and_then(|v| v.as_str()),
1318 Some("string"),
1319 "Expected '{}' to be inferred as string",
1320 name
1321 );
1322 assert_eq!(
1323 v.get("format").and_then(|v| v.as_str()),
1324 Some("uuid"),
1325 "Expected '{}' to have uuid format",
1326 name
1327 );
1328 }
1329 _ => panic!("Expected inline schema for '{}'", name),
1330 }
1331 }
1332 }
1333
1334 #[test]
1335 fn test_path_param_type_inference_string() {
1336 use super::infer_path_param_schema;
1337
1338 let string_params = [
1340 "name", "slug", "code", "token", "username", "id", "user_id", "userId", "postId",
1341 ];
1342
1343 for name in string_params {
1344 let schema = infer_path_param_schema(name);
1345 match schema {
1346 rustapi_openapi::SchemaRef::Inline(v) => {
1347 assert_eq!(
1348 v.get("type").and_then(|v| v.as_str()),
1349 Some("string"),
1350 "Expected '{}' to be inferred as string",
1351 name
1352 );
1353 assert!(
1354 v.get("format").is_none()
1355 || v.get("format").and_then(|v| v.as_str()) != Some("uuid"),
1356 "Expected '{}' to NOT have uuid format",
1357 name
1358 );
1359 }
1360 _ => panic!("Expected inline schema for '{}'", name),
1361 }
1362 }
1363 }
1364
1365 #[test]
1366 fn test_schema_type_to_openapi_schema() {
1367 use super::schema_type_to_openapi_schema;
1368
1369 let uuid_schema = schema_type_to_openapi_schema("uuid");
1371 match uuid_schema {
1372 rustapi_openapi::SchemaRef::Inline(v) => {
1373 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1374 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid"));
1375 }
1376 _ => panic!("Expected inline schema for uuid"),
1377 }
1378
1379 for schema_type in ["integer", "int", "int64", "i64"] {
1381 let schema = schema_type_to_openapi_schema(schema_type);
1382 match schema {
1383 rustapi_openapi::SchemaRef::Inline(v) => {
1384 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1385 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64"));
1386 }
1387 _ => panic!("Expected inline schema for {}", schema_type),
1388 }
1389 }
1390
1391 let int32_schema = schema_type_to_openapi_schema("int32");
1393 match int32_schema {
1394 rustapi_openapi::SchemaRef::Inline(v) => {
1395 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer"));
1396 assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32"));
1397 }
1398 _ => panic!("Expected inline schema for int32"),
1399 }
1400
1401 for schema_type in ["number", "float"] {
1403 let schema = schema_type_to_openapi_schema(schema_type);
1404 match schema {
1405 rustapi_openapi::SchemaRef::Inline(v) => {
1406 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number"));
1407 }
1408 _ => panic!("Expected inline schema for {}", schema_type),
1409 }
1410 }
1411
1412 for schema_type in ["boolean", "bool"] {
1414 let schema = schema_type_to_openapi_schema(schema_type);
1415 match schema {
1416 rustapi_openapi::SchemaRef::Inline(v) => {
1417 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean"));
1418 }
1419 _ => panic!("Expected inline schema for {}", schema_type),
1420 }
1421 }
1422
1423 let string_schema = schema_type_to_openapi_schema("string");
1425 match string_schema {
1426 rustapi_openapi::SchemaRef::Inline(v) => {
1427 assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string"));
1428 }
1429 _ => panic!("Expected inline schema for string"),
1430 }
1431 }
1432
1433 proptest! {
1440 #![proptest_config(ProptestConfig::with_cases(100))]
1441
1442 #[test]
1447 fn prop_nested_routes_in_openapi_spec(
1448 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1450 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1452 has_param in any::<bool>(),
1453 ) {
1454 async fn handler() -> &'static str { "handler" }
1455
1456 let prefix = format!("/{}", prefix_segments.join("/"));
1458
1459 let mut route_path = format!("/{}", route_segments.join("/"));
1461 if has_param {
1462 route_path.push_str("/{id}");
1463 }
1464
1465 let nested_router = Router::new().route(&route_path, get(handler));
1467 let app = RustApi::new().nest(&prefix, nested_router);
1468
1469 let expected_openapi_path = format!("{}{}", prefix, route_path);
1471
1472 let spec = app.openapi_spec();
1474
1475 prop_assert!(
1477 spec.paths.contains_key(&expected_openapi_path),
1478 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1479 expected_openapi_path,
1480 spec.paths.keys().collect::<Vec<_>>()
1481 );
1482
1483 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1485 prop_assert!(
1486 path_item.get.is_some(),
1487 "GET operation should exist for path '{}'",
1488 expected_openapi_path
1489 );
1490 }
1491
1492 #[test]
1497 fn prop_multiple_methods_preserved_in_openapi(
1498 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1499 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1500 ) {
1501 async fn get_handler() -> &'static str { "get" }
1502 async fn post_handler() -> &'static str { "post" }
1503
1504 let prefix = format!("/{}", prefix_segments.join("/"));
1506 let route_path = format!("/{}", route_segments.join("/"));
1507
1508 let get_route_path = format!("{}/get", route_path);
1511 let post_route_path = format!("{}/post", route_path);
1512 let nested_router = Router::new()
1513 .route(&get_route_path, get(get_handler))
1514 .route(&post_route_path, post(post_handler));
1515 let app = RustApi::new().nest(&prefix, nested_router);
1516
1517 let expected_get_path = format!("{}{}", prefix, get_route_path);
1519 let expected_post_path = format!("{}{}", prefix, post_route_path);
1520
1521 let spec = app.openapi_spec();
1523
1524 prop_assert!(
1526 spec.paths.contains_key(&expected_get_path),
1527 "Expected OpenAPI path '{}' not found",
1528 expected_get_path
1529 );
1530 prop_assert!(
1531 spec.paths.contains_key(&expected_post_path),
1532 "Expected OpenAPI path '{}' not found",
1533 expected_post_path
1534 );
1535
1536 let get_path_item = spec.paths.get(&expected_get_path).unwrap();
1538 prop_assert!(
1539 get_path_item.get.is_some(),
1540 "GET operation should exist for path '{}'",
1541 expected_get_path
1542 );
1543
1544 let post_path_item = spec.paths.get(&expected_post_path).unwrap();
1546 prop_assert!(
1547 post_path_item.post.is_some(),
1548 "POST operation should exist for path '{}'",
1549 expected_post_path
1550 );
1551 }
1552
1553 #[test]
1558 fn prop_path_params_in_openapi_after_nesting(
1559 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1560 param_name in "[a-z][a-z0-9]{0,5}",
1561 ) {
1562 async fn handler() -> &'static str { "handler" }
1563
1564 let prefix = format!("/{}", prefix_segments.join("/"));
1566 let route_path = format!("/{{{}}}", param_name);
1567
1568 let nested_router = Router::new().route(&route_path, get(handler));
1570 let app = RustApi::new().nest(&prefix, nested_router);
1571
1572 let expected_openapi_path = format!("{}{}", prefix, route_path);
1574
1575 let spec = app.openapi_spec();
1577
1578 prop_assert!(
1580 spec.paths.contains_key(&expected_openapi_path),
1581 "Expected OpenAPI path '{}' not found",
1582 expected_openapi_path
1583 );
1584
1585 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1587 let get_op = path_item.get.as_ref().unwrap();
1588
1589 prop_assert!(
1590 get_op.parameters.is_some(),
1591 "Operation should have parameters for path '{}'",
1592 expected_openapi_path
1593 );
1594
1595 let params = get_op.parameters.as_ref().unwrap();
1596 let has_param = params.iter().any(|p| p.name == param_name && p.location == "path");
1597 prop_assert!(
1598 has_param,
1599 "Path parameter '{}' should exist in operation parameters. Found: {:?}",
1600 param_name,
1601 params.iter().map(|p| &p.name).collect::<Vec<_>>()
1602 );
1603 }
1604 }
1605
1606 proptest! {
1614 #![proptest_config(ProptestConfig::with_cases(100))]
1615
1616 #[test]
1621 fn prop_rustapi_nest_delegates_to_router_nest(
1622 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1623 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1624 has_param in any::<bool>(),
1625 ) {
1626 async fn handler() -> &'static str { "handler" }
1627
1628 let prefix = format!("/{}", prefix_segments.join("/"));
1630
1631 let mut route_path = format!("/{}", route_segments.join("/"));
1633 if has_param {
1634 route_path.push_str("/{id}");
1635 }
1636
1637 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1639 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1640
1641 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1643 let rustapi_router = rustapi_app.into_router();
1644
1645 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1647
1648 let rustapi_routes = rustapi_router.registered_routes();
1650 let router_routes = router_app.registered_routes();
1651
1652 prop_assert_eq!(
1653 rustapi_routes.len(),
1654 router_routes.len(),
1655 "RustApi and Router should have same number of routes"
1656 );
1657
1658 for (path, info) in router_routes {
1660 prop_assert!(
1661 rustapi_routes.contains_key(path),
1662 "Route '{}' from Router should exist in RustApi routes",
1663 path
1664 );
1665
1666 let rustapi_info = rustapi_routes.get(path).unwrap();
1667 prop_assert_eq!(
1668 &info.path, &rustapi_info.path,
1669 "Display paths should match for route '{}'",
1670 path
1671 );
1672 prop_assert_eq!(
1673 info.methods.len(), rustapi_info.methods.len(),
1674 "Method count should match for route '{}'",
1675 path
1676 );
1677 }
1678 }
1679
1680 #[test]
1685 fn prop_rustapi_nest_includes_routes_in_openapi(
1686 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1687 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1688 has_param in any::<bool>(),
1689 ) {
1690 async fn handler() -> &'static str { "handler" }
1691
1692 let prefix = format!("/{}", prefix_segments.join("/"));
1694
1695 let mut route_path = format!("/{}", route_segments.join("/"));
1697 if has_param {
1698 route_path.push_str("/{id}");
1699 }
1700
1701 let nested_router = Router::new().route(&route_path, get(handler));
1703 let app = RustApi::new().nest(&prefix, nested_router);
1704
1705 let expected_openapi_path = format!("{}{}", prefix, route_path);
1707
1708 let spec = app.openapi_spec();
1710
1711 prop_assert!(
1713 spec.paths.contains_key(&expected_openapi_path),
1714 "Expected OpenAPI path '{}' not found. Available paths: {:?}",
1715 expected_openapi_path,
1716 spec.paths.keys().collect::<Vec<_>>()
1717 );
1718
1719 let path_item = spec.paths.get(&expected_openapi_path).unwrap();
1721 prop_assert!(
1722 path_item.get.is_some(),
1723 "GET operation should exist for path '{}'",
1724 expected_openapi_path
1725 );
1726 }
1727
1728 #[test]
1733 fn prop_rustapi_nest_route_matching_identical(
1734 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1735 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
1736 param_value in "[a-z0-9]{1,10}",
1737 ) {
1738 use crate::router::RouteMatch;
1739
1740 async fn handler() -> &'static str { "handler" }
1741
1742 let prefix = format!("/{}", prefix_segments.join("/"));
1744 let route_path = format!("/{}/{{id}}", route_segments.join("/"));
1745
1746 let nested_router_for_rustapi = Router::new().route(&route_path, get(handler));
1748 let nested_router_for_router = Router::new().route(&route_path, get(handler));
1749
1750 let rustapi_app = RustApi::new().nest(&prefix, nested_router_for_rustapi);
1752 let rustapi_router = rustapi_app.into_router();
1753 let router_app = Router::new().nest(&prefix, nested_router_for_router);
1754
1755 let full_path = format!("{}/{}/{}", prefix, route_segments.join("/"), param_value);
1757
1758 let rustapi_match = rustapi_router.match_route(&full_path, &Method::GET);
1760 let router_match = router_app.match_route(&full_path, &Method::GET);
1761
1762 match (rustapi_match, router_match) {
1764 (RouteMatch::Found { params: rustapi_params, .. }, RouteMatch::Found { params: router_params, .. }) => {
1765 prop_assert_eq!(
1766 rustapi_params.len(),
1767 router_params.len(),
1768 "Parameter count should match"
1769 );
1770 for (key, value) in &router_params {
1771 prop_assert!(
1772 rustapi_params.contains_key(key),
1773 "RustApi should have parameter '{}'",
1774 key
1775 );
1776 prop_assert_eq!(
1777 rustapi_params.get(key).unwrap(),
1778 value,
1779 "Parameter '{}' value should match",
1780 key
1781 );
1782 }
1783 }
1784 (rustapi_result, router_result) => {
1785 prop_assert!(
1786 false,
1787 "Both should return Found, but RustApi returned {:?} and Router returned {:?}",
1788 match rustapi_result {
1789 RouteMatch::Found { .. } => "Found",
1790 RouteMatch::NotFound => "NotFound",
1791 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1792 },
1793 match router_result {
1794 RouteMatch::Found { .. } => "Found",
1795 RouteMatch::NotFound => "NotFound",
1796 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
1797 }
1798 );
1799 }
1800 }
1801 }
1802 }
1803
1804 #[test]
1806 fn test_openapi_operations_propagated_during_nesting() {
1807 async fn list_users() -> &'static str {
1808 "list users"
1809 }
1810 async fn get_user() -> &'static str {
1811 "get user"
1812 }
1813 async fn create_user() -> &'static str {
1814 "create user"
1815 }
1816
1817 let users_router = Router::new()
1820 .route("/", get(list_users))
1821 .route("/create", post(create_user))
1822 .route("/{id}", get(get_user));
1823
1824 let app = RustApi::new().nest("/api/v1/users", users_router);
1826
1827 let spec = app.openapi_spec();
1828
1829 assert!(
1831 spec.paths.contains_key("/api/v1/users"),
1832 "Should have /api/v1/users path"
1833 );
1834 let users_path = spec.paths.get("/api/v1/users").unwrap();
1835 assert!(users_path.get.is_some(), "Should have GET operation");
1836
1837 assert!(
1839 spec.paths.contains_key("/api/v1/users/create"),
1840 "Should have /api/v1/users/create path"
1841 );
1842 let create_path = spec.paths.get("/api/v1/users/create").unwrap();
1843 assert!(create_path.post.is_some(), "Should have POST operation");
1844
1845 assert!(
1847 spec.paths.contains_key("/api/v1/users/{id}"),
1848 "Should have /api/v1/users/{{id}} path"
1849 );
1850 let user_path = spec.paths.get("/api/v1/users/{id}").unwrap();
1851 assert!(
1852 user_path.get.is_some(),
1853 "Should have GET operation for user by id"
1854 );
1855
1856 let get_user_op = user_path.get.as_ref().unwrap();
1858 assert!(get_user_op.parameters.is_some(), "Should have parameters");
1859 let params = get_user_op.parameters.as_ref().unwrap();
1860 assert!(
1861 params
1862 .iter()
1863 .any(|p| p.name == "id" && p.location == "path"),
1864 "Should have 'id' path parameter"
1865 );
1866 }
1867
1868 #[test]
1870 fn test_openapi_spec_empty_without_routes() {
1871 let app = RustApi::new();
1872 let spec = app.openapi_spec();
1873
1874 assert!(
1876 spec.paths.is_empty(),
1877 "OpenAPI spec should have no paths without routes"
1878 );
1879 }
1880
1881 #[test]
1886 fn test_rustapi_nest_delegates_to_router_nest() {
1887 use crate::router::RouteMatch;
1888
1889 async fn list_users() -> &'static str {
1890 "list users"
1891 }
1892 async fn get_user() -> &'static str {
1893 "get user"
1894 }
1895 async fn create_user() -> &'static str {
1896 "create user"
1897 }
1898
1899 let users_router = Router::new()
1901 .route("/", get(list_users))
1902 .route("/create", post(create_user))
1903 .route("/{id}", get(get_user));
1904
1905 let app = RustApi::new().nest("/api/v1/users", users_router);
1907 let router = app.into_router();
1908
1909 let routes = router.registered_routes();
1911 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1912
1913 assert!(
1915 routes.contains_key("/api/v1/users"),
1916 "Should have /api/v1/users route"
1917 );
1918 assert!(
1919 routes.contains_key("/api/v1/users/create"),
1920 "Should have /api/v1/users/create route"
1921 );
1922 assert!(
1923 routes.contains_key("/api/v1/users/:id"),
1924 "Should have /api/v1/users/:id route"
1925 );
1926
1927 match router.match_route("/api/v1/users", &Method::GET) {
1929 RouteMatch::Found { params, .. } => {
1930 assert!(params.is_empty(), "Root route should have no params");
1931 }
1932 _ => panic!("GET /api/v1/users should be found"),
1933 }
1934
1935 match router.match_route("/api/v1/users/create", &Method::POST) {
1936 RouteMatch::Found { params, .. } => {
1937 assert!(params.is_empty(), "Create route should have no params");
1938 }
1939 _ => panic!("POST /api/v1/users/create should be found"),
1940 }
1941
1942 match router.match_route("/api/v1/users/123", &Method::GET) {
1943 RouteMatch::Found { params, .. } => {
1944 assert_eq!(
1945 params.get("id"),
1946 Some(&"123".to_string()),
1947 "Should extract id param"
1948 );
1949 }
1950 _ => panic!("GET /api/v1/users/123 should be found"),
1951 }
1952
1953 match router.match_route("/api/v1/users", &Method::DELETE) {
1955 RouteMatch::MethodNotAllowed { allowed } => {
1956 assert!(allowed.contains(&Method::GET), "Should allow GET");
1957 }
1958 _ => panic!("DELETE /api/v1/users should return MethodNotAllowed"),
1959 }
1960 }
1961
1962 #[test]
1967 fn test_rustapi_nest_includes_routes_in_openapi_spec() {
1968 async fn list_items() -> &'static str {
1969 "list items"
1970 }
1971 async fn get_item() -> &'static str {
1972 "get item"
1973 }
1974
1975 let items_router = Router::new()
1977 .route("/", get(list_items))
1978 .route("/{item_id}", get(get_item));
1979
1980 let app = RustApi::new().nest("/api/items", items_router);
1982
1983 let spec = app.openapi_spec();
1985
1986 assert!(
1988 spec.paths.contains_key("/api/items"),
1989 "Should have /api/items in OpenAPI"
1990 );
1991 assert!(
1992 spec.paths.contains_key("/api/items/{item_id}"),
1993 "Should have /api/items/{{item_id}} in OpenAPI"
1994 );
1995
1996 let list_path = spec.paths.get("/api/items").unwrap();
1998 assert!(
1999 list_path.get.is_some(),
2000 "Should have GET operation for /api/items"
2001 );
2002
2003 let get_path = spec.paths.get("/api/items/{item_id}").unwrap();
2004 assert!(
2005 get_path.get.is_some(),
2006 "Should have GET operation for /api/items/{{item_id}}"
2007 );
2008
2009 let get_op = get_path.get.as_ref().unwrap();
2011 assert!(get_op.parameters.is_some(), "Should have parameters");
2012 let params = get_op.parameters.as_ref().unwrap();
2013 assert!(
2014 params
2015 .iter()
2016 .any(|p| p.name == "item_id" && p.location == "path"),
2017 "Should have 'item_id' path parameter"
2018 );
2019 }
2020}
2021
2022#[cfg(feature = "swagger-ui")]
2024fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
2025 req.headers()
2026 .get(http::header::AUTHORIZATION)
2027 .and_then(|v| v.to_str().ok())
2028 .map(|auth| auth == expected)
2029 .unwrap_or(false)
2030}
2031
2032#[cfg(feature = "swagger-ui")]
2034fn unauthorized_response() -> crate::Response {
2035 http::Response::builder()
2036 .status(http::StatusCode::UNAUTHORIZED)
2037 .header(
2038 http::header::WWW_AUTHENTICATE,
2039 "Basic realm=\"API Documentation\"",
2040 )
2041 .header(http::header::CONTENT_TYPE, "text/plain")
2042 .body(crate::response::Body::from("Unauthorized"))
2043 .unwrap()
2044}
2045
2046pub struct RustApiConfig {
2048 docs_path: Option<String>,
2049 docs_enabled: bool,
2050 api_title: String,
2051 api_version: String,
2052 api_description: Option<String>,
2053 body_limit: Option<usize>,
2054 layers: LayerStack,
2055}
2056
2057impl Default for RustApiConfig {
2058 fn default() -> Self {
2059 Self::new()
2060 }
2061}
2062
2063impl RustApiConfig {
2064 pub fn new() -> Self {
2065 Self {
2066 docs_path: Some("/docs".to_string()),
2067 docs_enabled: true,
2068 api_title: "RustAPI".to_string(),
2069 api_version: "1.0.0".to_string(),
2070 api_description: None,
2071 body_limit: None,
2072 layers: LayerStack::new(),
2073 }
2074 }
2075
2076 pub fn docs_path(mut self, path: impl Into<String>) -> Self {
2078 self.docs_path = Some(path.into());
2079 self
2080 }
2081
2082 pub fn docs_enabled(mut self, enabled: bool) -> Self {
2084 self.docs_enabled = enabled;
2085 self
2086 }
2087
2088 pub fn openapi_info(
2090 mut self,
2091 title: impl Into<String>,
2092 version: impl Into<String>,
2093 description: Option<impl Into<String>>,
2094 ) -> Self {
2095 self.api_title = title.into();
2096 self.api_version = version.into();
2097 self.api_description = description.map(|d| d.into());
2098 self
2099 }
2100
2101 pub fn body_limit(mut self, limit: usize) -> Self {
2103 self.body_limit = Some(limit);
2104 self
2105 }
2106
2107 pub fn layer<L>(mut self, layer: L) -> Self
2109 where
2110 L: MiddlewareLayer,
2111 {
2112 self.layers.push(Box::new(layer));
2113 self
2114 }
2115
2116 pub fn build(self) -> RustApi {
2118 let mut app = RustApi::new().mount_auto_routes_grouped();
2119
2120 if let Some(limit) = self.body_limit {
2122 app = app.body_limit(limit);
2123 }
2124
2125 app = app.openapi_info(
2126 &self.api_title,
2127 &self.api_version,
2128 self.api_description.as_deref(),
2129 );
2130
2131 #[cfg(feature = "swagger-ui")]
2132 if self.docs_enabled {
2133 if let Some(path) = self.docs_path {
2134 app = app.docs(&path);
2135 }
2136 }
2137
2138 app.layers.extend(self.layers);
2141
2142 app
2143 }
2144
2145 pub async fn run(
2147 self,
2148 addr: impl AsRef<str>,
2149 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
2150 self.build().run(addr.as_ref()).await
2151 }
2152}