1pub mod openapi;
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use axum::Router;
6use serde::Serialize;
7use std::any::{Any, TypeId};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11pub use axum;
13pub use inventory;
14pub use regex;
15pub use schemars;
16pub use serde;
17pub use serde_json;
18pub use ultraapi_macros::{api_model, delete, get, post, put};
19
20pub mod prelude {
21 pub use crate::axum::extract::Query;
22 pub use crate::{api_model, delete, get, post, put};
23 pub use crate::{ApiError, Dep, State, UltraApiApp, UltraApiRouter, Validate};
24}
25
26pub trait Validate {
28 fn validate(&self) -> Result<(), Vec<String>>;
29}
30
31impl Validate for () {
32 fn validate(&self) -> Result<(), Vec<String>> {
33 Ok(())
34 }
35}
36
37#[async_trait::async_trait]
38pub trait AsyncValidate: Send + Sync {
39 async fn validate_async(&self, _state: &AppState) -> Result<(), Vec<String>> {
40 Ok(())
41 }
42}
43
44#[async_trait::async_trait]
45impl AsyncValidate for () {
46 async fn validate_async(&self, _state: &AppState) -> Result<(), Vec<String>> {
47 Ok(())
48 }
49}
50
51#[macro_export]
52macro_rules! impl_async_validate {
53 ($ty:ty, $sync_fn:ident) => {
54 #[async_trait::async_trait]
55 impl $crate::AsyncValidate for $ty {
56 async fn validate_async(&self, _state: &$crate::AppState) -> Result<(), Vec<String>> {
57 $sync_fn(self)
58 }
59 }
60 };
61}
62
63#[doc(hidden)]
65pub trait HasSchemaPatches {
66 fn patch_schema(props: &mut HashMap<String, openapi::PropertyPatch>);
67}
68
69#[derive(Clone)]
71pub struct AppState {
72 deps: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
73}
74
75impl Default for AppState {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl AppState {
82 pub fn new() -> Self {
83 Self {
84 deps: Arc::new(HashMap::new()),
85 }
86 }
87
88 pub fn get<T: 'static + Send + Sync>(&self) -> Option<Arc<T>> {
89 self.deps
90 .get(&TypeId::of::<T>())
91 .and_then(|v| v.clone().downcast::<T>().ok())
92 }
93}
94
95pub struct Dep<T: 'static + Send + Sync>(Arc<T>);
97
98impl<T: 'static + Send + Sync> Dep<T> {
99 pub fn from_app_state(state: &AppState) -> Result<Self, ApiError> {
100 state.get::<T>().map(Dep).ok_or_else(|| {
101 ApiError::internal(format!(
102 "Dependency not registered: {}",
103 std::any::type_name::<T>()
104 ))
105 })
106 }
107}
108
109impl<T: 'static + Send + Sync> std::ops::Deref for Dep<T> {
110 type Target = T;
111 fn deref(&self) -> &T {
112 &self.0
113 }
114}
115
116pub struct State<T: 'static + Send + Sync>(Arc<T>);
119
120impl<T: 'static + Send + Sync> State<T> {
121 pub fn from_app_state(state: &AppState) -> Result<Self, ApiError> {
122 state.get::<T>().map(State).ok_or_else(|| {
123 ApiError::internal(format!(
124 "State not registered: {}",
125 std::any::type_name::<T>()
126 ))
127 })
128 }
129}
130
131impl<T: 'static + Send + Sync> std::ops::Deref for State<T> {
132 type Target = T;
133 fn deref(&self) -> &T {
134 &self.0
135 }
136}
137
138#[derive(Debug, Serialize)]
140pub struct ApiError {
141 #[serde(skip)]
142 pub status: StatusCode,
143 pub error: String,
144 #[serde(skip_serializing_if = "Vec::is_empty")]
145 pub details: Vec<String>,
146}
147
148impl ApiError {
149 pub fn unauthorized(msg: impl Into<String>) -> Self {
150 Self {
151 status: StatusCode::UNAUTHORIZED,
152 error: msg.into(),
153 details: vec![],
154 }
155 }
156
157 pub fn bad_request(msg: String) -> Self {
158 Self {
159 status: StatusCode::BAD_REQUEST,
160 error: msg,
161 details: vec![],
162 }
163 }
164
165 pub fn not_found(msg: String) -> Self {
166 Self {
167 status: StatusCode::NOT_FOUND,
168 error: msg,
169 details: vec![],
170 }
171 }
172
173 pub fn internal(msg: String) -> Self {
174 Self {
175 status: StatusCode::INTERNAL_SERVER_ERROR,
176 error: msg,
177 details: vec![],
178 }
179 }
180
181 pub fn validation_error(errors: Vec<String>) -> Self {
182 Self {
183 status: StatusCode::UNPROCESSABLE_ENTITY,
184 error: "Validation failed".into(),
185 details: errors,
186 }
187 }
188}
189
190impl IntoResponse for ApiError {
191 fn into_response(self) -> Response {
192 let body = serde_json::to_string(&self)
193 .unwrap_or_else(|_| r#"{"error":"Internal server error"}"#.to_string());
194 (self.status, [("content-type", "application/json")], body).into_response()
195 }
196}
197
198pub struct RouteInfo {
200 pub path: &'static str,
201 pub axum_path: &'static str,
202 pub method: &'static str,
203 pub handler_name: &'static str,
204 pub response_type_name: &'static str,
205 pub is_result_return: bool,
206 pub is_vec_response: bool,
207 pub vec_inner_type_name: &'static str,
208 pub parameters: &'static [openapi::Parameter],
209 pub has_body: bool,
210 pub body_type_name: &'static str,
211 pub success_status: u16,
212 pub description: &'static str,
213 pub tags: &'static [&'static str],
214 pub security: &'static [&'static str],
215 pub query_params_fn: Option<fn() -> Vec<openapi::DynParameter>>,
216 pub register_fn: fn(Router<AppState>) -> Router<AppState>,
217 pub method_router_fn: fn() -> axum::routing::MethodRouter<AppState>,
218}
219
220inventory::collect!(&'static RouteInfo);
221
222pub struct SchemaInfo {
224 pub name: &'static str,
225 pub schema_fn: fn() -> openapi::Schema,
226 pub nested_fn: fn() -> std::collections::HashMap<String, openapi::Schema>,
227}
228
229inventory::collect!(SchemaInfo);
230
231pub struct ResolvedRoute {
233 pub route_info: &'static RouteInfo,
234 pub prefix: String,
235 pub extra_tags: Vec<String>,
236 pub extra_security: Vec<String>,
237}
238
239impl ResolvedRoute {
240 pub fn full_path(&self) -> String {
242 let base = self.route_info.path;
243 if self.prefix.is_empty() {
244 base.to_string()
245 } else {
246 format!("{}{}", self.prefix, base)
247 }
248 }
249
250 pub fn full_axum_path(&self) -> String {
252 let base = self.route_info.axum_path;
253 if self.prefix.is_empty() {
254 base.to_string()
255 } else {
256 format!("{}{}", self.prefix, base)
257 }
258 }
259
260 pub fn merged_tags(&self) -> Vec<String> {
262 let mut tags: Vec<String> = self.extra_tags.clone();
263 for t in self.route_info.tags {
264 if !tags.contains(&t.to_string()) {
265 tags.push(t.to_string());
266 }
267 }
268 tags
269 }
270
271 pub fn merged_security(&self) -> Vec<&str> {
273 let mut sec: Vec<&str> = self.extra_security.iter().map(|s| s.as_str()).collect();
274 for s in self.route_info.security {
275 if !sec.contains(s) {
276 sec.push(s);
277 }
278 }
279 sec
280 }
281}
282
283pub struct UltraApiRouter {
285 prefix: String,
286 routes: Vec<&'static RouteInfo>,
287 tags: Vec<String>,
288 security: Vec<String>,
289 deps: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
290 children: Vec<UltraApiRouter>,
291}
292
293impl UltraApiRouter {
294 pub fn new(prefix: &str) -> Self {
295 Self {
296 prefix: prefix.to_string(),
297 routes: Vec::new(),
298 tags: Vec::new(),
299 security: Vec::new(),
300 deps: HashMap::new(),
301 children: Vec::new(),
302 }
303 }
304
305 pub fn route(mut self, route: &'static RouteInfo) -> Self {
306 self.routes.push(route);
307 self
308 }
309
310 pub fn tag(mut self, tag: &str) -> Self {
311 self.tags.push(tag.to_string());
312 self
313 }
314
315 pub fn security(mut self, scheme: &str) -> Self {
316 self.security.push(scheme.to_string());
317 self
318 }
319
320 pub fn dep<T: 'static + Send + Sync>(mut self, dep: T) -> Self {
321 self.deps.insert(TypeId::of::<T>(), Arc::new(dep));
322 self
323 }
324
325 pub fn include(mut self, child: UltraApiRouter) -> Self {
326 self.children.push(child);
327 self
328 }
329
330 pub fn resolve(
332 &self,
333 parent_prefix: &str,
334 parent_tags: &[String],
335 parent_security: &[String],
336 ) -> Vec<ResolvedRoute> {
337 let full_prefix = format!("{}{}", parent_prefix, self.prefix);
338 let mut merged_tags: Vec<String> = parent_tags.to_vec();
339 for t in &self.tags {
340 if !merged_tags.contains(t) {
341 merged_tags.push(t.clone());
342 }
343 }
344 let mut merged_security: Vec<String> = parent_security.to_vec();
345 for s in &self.security {
346 if !merged_security.contains(s) {
347 merged_security.push(s.clone());
348 }
349 }
350
351 let mut resolved = Vec::new();
352 for route in &self.routes {
353 resolved.push(ResolvedRoute {
354 route_info: route,
355 prefix: full_prefix.clone(),
356 extra_tags: merged_tags.clone(),
357 extra_security: merged_security.clone(),
358 });
359 }
360 for child in &self.children {
361 resolved.extend(child.resolve(&full_prefix, &merged_tags, &merged_security));
362 }
363 resolved
364 }
365
366 pub fn collect_deps(&self) -> HashMap<TypeId, Arc<dyn Any + Send + Sync>> {
368 let mut all = self.deps.clone();
369 for child in &self.children {
370 all.extend(child.collect_deps());
371 }
372 all
373 }
374}
375
376#[derive(Debug, Clone)]
378pub enum SwaggerMode {
379 Cdn(String),
381 Embedded,
383}
384
385pub struct UltraApiApp {
387 deps: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
388 dep_overrides: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
390 title: String,
391 version: String,
392 description: Option<String>,
393 contact: Option<openapi::Contact>,
394 license: Option<openapi::License>,
395 swagger_mode: SwaggerMode,
396 servers: Vec<openapi::Server>,
397 security_schemes: HashMap<String, openapi::SecurityScheme>,
398 routers: Vec<UltraApiRouter>,
399}
400
401impl Default for UltraApiApp {
402 fn default() -> Self {
403 Self::new()
404 }
405}
406
407impl UltraApiApp {
408 pub fn new() -> Self {
409 Self {
410 deps: HashMap::new(),
411 dep_overrides: HashMap::new(),
412 title: env!("CARGO_PKG_NAME").to_string(),
413 version: env!("CARGO_PKG_VERSION").to_string(),
414 description: None,
415 contact: None,
416 license: None,
417 swagger_mode: SwaggerMode::Embedded,
418 servers: Vec::new(),
419 security_schemes: HashMap::new(),
420 routers: Vec::new(),
421 }
422 }
423
424 pub fn title(mut self, title: &str) -> Self {
425 self.title = title.to_string();
426 self
427 }
428
429 pub fn version(mut self, version: &str) -> Self {
430 self.version = version.to_string();
431 self
432 }
433
434 pub fn description(mut self, desc: &str) -> Self {
435 self.description = Some(desc.to_string());
436 self
437 }
438
439 pub fn contact(mut self, name: &str, email: &str, url: &str) -> Self {
440 self.contact = Some(openapi::Contact {
441 name: Some(name.to_string()),
442 email: Some(email.to_string()),
443 url: Some(url.to_string()),
444 });
445 self
446 }
447
448 pub fn license(mut self, name: &str, url: &str) -> Self {
449 self.license = Some(openapi::License {
450 name: name.to_string(),
451 url: Some(url.to_string()),
452 });
453 self
454 }
455
456 pub fn swagger_mode(mut self, mode: SwaggerMode) -> Self {
457 self.swagger_mode = mode;
458 self
459 }
460
461 pub fn swagger_cdn(mut self, url: &str) -> Self {
462 self.swagger_mode = SwaggerMode::Cdn(url.to_string());
463 self
464 }
465
466 pub fn dep<T: 'static + Send + Sync>(mut self, dep: T) -> Self {
467 self.deps.insert(TypeId::of::<T>(), Arc::new(dep));
468 self
469 }
470
471 pub fn override_dep<T: 'static + Send + Sync>(mut self, dep: T) -> Self {
503 self.dep_overrides.insert(TypeId::of::<T>(), Arc::new(dep));
504 self
505 }
506
507 pub fn has_override<T: 'static + Send + Sync>(&self) -> bool {
509 self.dep_overrides.contains_key(&TypeId::of::<T>())
510 }
511
512 pub fn clear_overrides(mut self) -> Self {
514 self.dep_overrides.clear();
515 self
516 }
517
518 pub fn server(mut self, url: &str) -> Self {
519 self.servers.push(openapi::Server {
520 url: url.to_string(),
521 });
522 self
523 }
524
525 pub fn security_scheme(mut self, name: &str, scheme: openapi::SecurityScheme) -> Self {
526 self.security_schemes.insert(name.to_string(), scheme);
527 self
528 }
529
530 pub fn bearer_auth(self) -> Self {
531 self.security_scheme(
532 "bearerAuth",
533 openapi::SecurityScheme {
534 scheme_type: "http".to_string(),
535 scheme: Some("bearer".to_string()),
536 bearer_format: None,
537 name: None,
538 location: None,
539 },
540 )
541 }
542
543 pub fn include(mut self, router: UltraApiRouter) -> Self {
544 self.routers.push(router);
545 self
546 }
547
548 pub fn has_explicit_routes(&self) -> bool {
550 !self.routers.is_empty()
551 }
552
553 pub fn resolve_routes(&self) -> Vec<ResolvedRoute> {
555 let mut resolved = Vec::new();
556 for router in &self.routers {
557 resolved.extend(router.resolve("", &[], &[]));
558 }
559 resolved
560 }
561
562 pub fn into_router(self) -> Router {
563 let spec = self.generate_openapi_spec();
564 let swagger_html = self.generate_swagger_html();
565 let has_explicit = self.has_explicit_routes();
566 let resolved = if has_explicit {
567 self.resolve_routes()
568 } else {
569 Vec::new()
570 };
571 let spec_json =
572 serde_json::to_string_pretty(&spec.to_json_with_query_params(&self.routers))
573 .expect("Failed to serialize OpenAPI spec");
574
575 let mut all_deps = self.deps;
577 for router in &self.routers {
578 all_deps.extend(router.collect_deps());
579 }
580
581 let state = AppState {
582 deps: Arc::new(all_deps),
583 };
584
585 let mut app = Router::new();
586
587 if has_explicit {
588 for r in &resolved {
589 let axum_path = r.full_axum_path();
590 let method_router = (r.route_info.method_router_fn)();
591 app = app.route(&axum_path, method_router);
592 }
593 } else {
594 for route in inventory::iter::<&RouteInfo> {
595 app = (route.register_fn)(app);
596 }
597 }
598
599 let spec_json_clone = spec_json.clone();
600 app = app.route(
601 "/openapi.json",
602 axum::routing::get(move || {
603 let spec = spec_json_clone.clone();
604 async move { (StatusCode::OK, [("content-type", "application/json")], spec) }
605 }),
606 );
607
608 app = app.route(
609 "/docs",
610 axum::routing::get(move || {
611 let html = swagger_html.clone();
612 async move { (StatusCode::OK, [("content-type", "text/html")], html) }
613 }),
614 );
615
616 app.with_state(state)
617 }
618
619 pub async fn serve(self, addr: &str) {
620 let app = self.into_router();
621
622 let listener = tokio::net::TcpListener::bind(addr)
623 .await
624 .expect("Failed to bind to address");
625 println!("🚀 Hayai server running at http://{}", addr);
626 println!("📖 Swagger UI available at http://{}/docs", addr);
627 axum::serve(listener, app).await.expect("Server error");
628 }
629
630 fn build_operation(
631 route: &RouteInfo,
632 tags: Vec<String>,
633 security_list: &[&str],
634 ) -> openapi::Operation {
635 let description = if route.description.is_empty() {
636 None
637 } else {
638 Some(route.description.to_string())
639 };
640
641 let status_code = route.success_status.to_string();
642
643 let security: Vec<HashMap<String, Vec<String>>> = security_list
644 .iter()
645 .map(|s| {
646 let scheme_name = match *s {
647 "bearer" => "bearerAuth",
648 other => other,
649 };
650 let mut map = HashMap::new();
651 map.insert(scheme_name.to_string(), vec![]);
652 map
653 })
654 .collect();
655
656 let schema_ref_value = if route.success_status == 204 {
657 None
658 } else if route.is_vec_response {
659 Some(serde_json::json!({
660 "type": "array",
661 "items": { "$ref": format!("#/components/schemas/{}", route.vec_inner_type_name) }
662 }))
663 } else {
664 Some(
665 serde_json::json!({ "$ref": format!("#/components/schemas/{}", route.response_type_name) }),
666 )
667 };
668
669 let success_desc = openapi::status_description(route.success_status).to_string();
670
671 openapi::Operation {
672 summary: Some(route.handler_name.replace('_', " ")),
673 description,
674 operation_id: Some(route.handler_name.to_string()),
675 tags,
676 parameters: route.parameters.to_vec(),
677 request_body: if route.has_body {
678 Some(openapi::RequestBody {
679 required: true,
680 content_type: "application/json".to_string(),
681 schema_ref: format!("#/components/schemas/{}", route.body_type_name),
682 })
683 } else {
684 None
685 },
686 responses: {
687 let mut map = HashMap::new();
688 map.insert(
689 status_code,
690 openapi::ResponseDef {
691 description: success_desc,
692 schema_ref: schema_ref_value,
693 },
694 );
695 map.insert(
696 "400".to_string(),
697 openapi::ResponseDef {
698 description: "Bad Request".to_string(),
699 schema_ref: Some(
700 serde_json::json!({ "$ref": "#/components/schemas/ApiError" }),
701 ),
702 },
703 );
704 if route.is_result_return {
705 map.insert(
706 "404".to_string(),
707 openapi::ResponseDef {
708 description: "Not Found".to_string(),
709 schema_ref: Some(
710 serde_json::json!({ "$ref": "#/components/schemas/ApiError" }),
711 ),
712 },
713 );
714 }
715 if route.has_body {
716 map.insert(
717 "422".to_string(),
718 openapi::ResponseDef {
719 description: "Validation Failed".to_string(),
720 schema_ref: Some(
721 serde_json::json!({ "$ref": "#/components/schemas/ApiError" }),
722 ),
723 },
724 );
725 }
726 map.insert(
727 "500".to_string(),
728 openapi::ResponseDef {
729 description: "Internal Server Error".to_string(),
730 schema_ref: Some(
731 serde_json::json!({ "$ref": "#/components/schemas/ApiError" }),
732 ),
733 },
734 );
735 map
736 },
737 security,
738 }
739 }
740
741 fn generate_swagger_html(&self) -> String {
742 match &self.swagger_mode {
743 SwaggerMode::Cdn(cdn_base) => {
744 format!(
745 r#"<!DOCTYPE html>
746<html>
747<head>
748 <title>{title} - Swagger UI</title>
749 <link rel="stylesheet" type="text/css" href="{cdn}/swagger-ui.css" >
750</head>
751<body>
752 <div id="swagger-ui"></div>
753 <script src="{cdn}/swagger-ui-bundle.js"> </script>
754 <script>
755 SwaggerUIBundle({{
756 url: "/openapi.json",
757 dom_id: '#swagger-ui',
758 presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
759 layout: "BaseLayout"
760 }})
761 </script>
762</body>
763</html>"#,
764 title = self.title,
765 cdn = cdn_base,
766 )
767 }
768 SwaggerMode::Embedded => {
769 format!(
770 r#"<!DOCTYPE html>
771<html>
772<head>
773 <title>{title} - API Reference</title>
774 <meta charset="utf-8" />
775 <meta name="viewport" content="width=device-width, initial-scale=1" />
776 <style>{css}</style>
777</head>
778<body>
779 <script id="api-reference" data-url="/openapi.json"></script>
780 <script>{js}</script>
781</body>
782</html>"#,
783 title = self.title,
784 css = include_str!("../assets/scalar.min.css"),
785 js = include_str!("../assets/scalar.min.js"),
786 )
787 }
788 }
789 }
790
791 fn generate_openapi_spec(&self) -> openapi::OpenApiSpec {
792 let mut schemas = HashMap::new();
793
794 schemas.insert("ApiError".to_string(), openapi::api_error_schema());
796
797 for info in inventory::iter::<SchemaInfo> {
798 schemas.insert(info.name.to_string(), (info.schema_fn)());
799 for (nested_name, nested_schema) in (info.nested_fn)() {
800 schemas.entry(nested_name).or_insert(nested_schema);
801 }
802 }
803
804 let mut paths = HashMap::new();
805
806 if self.has_explicit_routes() {
807 let resolved = self.resolve_routes();
808 for r in &resolved {
809 let route = r.route_info;
810 let full_path = r.full_path();
811 let tags = r.merged_tags();
812 let sec = r.merged_security();
813 let operation = Self::build_operation(route, tags, &sec);
814 let path_item = paths.entry(full_path).or_insert_with(HashMap::new);
815 path_item.insert(route.method.to_lowercase(), operation);
816 }
817 } else {
818 for route in inventory::iter::<&RouteInfo> {
819 let tags: Vec<String> = route.tags.iter().map(|s| s.to_string()).collect();
820 let sec: Vec<&str> = route.security.to_vec();
821 let operation = Self::build_operation(route, tags, &sec);
822 let path_item = paths
823 .entry(route.path.to_string())
824 .or_insert_with(HashMap::new);
825 path_item.insert(route.method.to_lowercase(), operation);
826 }
827 }
828
829 openapi::OpenApiSpec {
830 openapi: "3.1.0".to_string(),
831 info: openapi::Info {
832 title: self.title.clone(),
833 version: self.version.clone(),
834 description: self.description.clone(),
835 contact: self.contact.clone(),
836 license: self.license.clone(),
837 },
838 servers: self.servers.clone(),
839 paths,
840 schemas,
841 security_schemes: self.security_schemes.clone(),
842 }
843 }
844}
845
846impl openapi::OpenApiSpec {
847 pub fn to_json_with_query_params(&self, routers: &[UltraApiRouter]) -> serde_json::Value {
849 let mut val = self.to_json();
850
851 let use_routers = !routers.is_empty();
853
854 struct RoutePathInfo {
855 spec_path: String,
856 method: String,
857 query_params_fn: Option<fn() -> Vec<openapi::DynParameter>>,
858 }
859
860 let mut route_paths = Vec::new();
861
862 if use_routers {
863 for router in routers {
864 let resolved = router.resolve("", &[], &[]);
865 for r in &resolved {
866 if let Some(qfn) = r.route_info.query_params_fn {
867 route_paths.push(RoutePathInfo {
868 spec_path: r.full_path(),
869 method: r.route_info.method.to_lowercase(),
870 query_params_fn: Some(qfn),
871 });
872 }
873 }
874 }
875 } else {
876 for route in inventory::iter::<&RouteInfo> {
877 if let Some(qfn) = route.query_params_fn {
878 route_paths.push(RoutePathInfo {
879 spec_path: route.path.to_string(),
880 method: route.method.to_lowercase(),
881 query_params_fn: Some(qfn),
882 });
883 }
884 }
885 }
886
887 for rp in &route_paths {
888 if let Some(qfn) = rp.query_params_fn {
889 let dyn_params = qfn();
890 if !dyn_params.is_empty() {
891 let escaped = rp.spec_path.replace('~', "~0").replace('/', "~1");
892 let pointer = format!("/paths/{}/{}", escaped, rp.method);
893 if let Some(op) = val.pointer_mut(&pointer) {
894 let params = op
895 .get("parameters")
896 .and_then(|v| v.as_array())
897 .cloned()
898 .unwrap_or_default();
899 let mut all_params = params;
900 for dp in &dyn_params {
901 let mut schema = serde_json::json!({ "type": dp.schema_type });
902 if let Some(v) = dp.minimum {
903 schema["minimum"] = serde_json::json!(v);
904 }
905 if let Some(v) = dp.maximum {
906 schema["maximum"] = serde_json::json!(v);
907 }
908 if let Some(v) = dp.min_length {
909 schema["minLength"] = serde_json::json!(v);
910 }
911 if let Some(v) = dp.max_length {
912 schema["maxLength"] = serde_json::json!(v);
913 }
914 if let Some(v) = &dp.pattern {
915 schema["pattern"] = serde_json::json!(v);
916 }
917 let mut param = serde_json::json!({
918 "name": dp.name,
919 "in": dp.location,
920 "required": dp.required,
921 "schema": schema
922 });
923 if let Some(desc) = &dp.description {
924 param["description"] = serde_json::Value::String(desc.clone());
925 }
926 all_params.push(param);
927 }
928 op["parameters"] = serde_json::Value::Array(all_params);
929 }
930 }
931 }
932 }
933
934 val
935 }
936}
937
938#[allow(deprecated)]
940pub use UltraApiApp as HayaiApp;
941#[allow(deprecated)]
942pub use UltraApiRouter as HayaiRouter;