1use crate::schema::Schema;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct OpenApi {
10 pub openapi: String,
12 pub info: Info,
14 #[serde(default, skip_serializing_if = "Vec::is_empty")]
16 pub servers: Vec<Server>,
17 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
19 pub paths: HashMap<String, PathItem>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub components: Option<Components>,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 pub tags: Vec<Tag>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Info {
31 pub title: String,
33 pub version: String,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub description: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub terms_of_service: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub contact: Option<Contact>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub license: Option<License>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Contact {
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub name: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub url: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub email: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct License {
66 pub name: String,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub url: Option<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Server {
76 pub url: String,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub description: Option<String>,
81}
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct PathItem {
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub get: Option<Operation>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub post: Option<Operation>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub put: Option<Operation>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub delete: Option<Operation>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub patch: Option<Operation>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub options: Option<Operation>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub head: Option<Operation>,
107}
108
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
111pub struct Operation {
112 #[serde(
114 rename = "operationId",
115 default,
116 skip_serializing_if = "Option::is_none"
117 )]
118 pub operation_id: Option<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub summary: Option<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub description: Option<String>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 pub tags: Vec<String>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub parameters: Vec<Parameter>,
131 #[serde(
133 rename = "requestBody",
134 default,
135 skip_serializing_if = "Option::is_none"
136 )]
137 pub request_body: Option<RequestBody>,
138 pub responses: HashMap<String, Response>,
140 #[serde(default, skip_serializing_if = "is_false")]
142 pub deprecated: bool,
143}
144
145fn is_false(b: &bool) -> bool {
146 !*b
147}
148
149fn default_responses() -> HashMap<String, Response> {
151 let mut responses = HashMap::new();
152 responses.insert(
153 "200".to_string(),
154 Response {
155 description: "Successful response".to_string(),
156 content: HashMap::new(),
157 },
158 );
159 responses
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct Parameter {
165 pub name: String,
167 #[serde(rename = "in")]
169 pub location: ParameterLocation,
170 #[serde(default)]
172 pub required: bool,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub schema: Option<Schema>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub title: Option<String>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub description: Option<String>,
182 #[serde(default, skip_serializing_if = "is_false")]
184 pub deprecated: bool,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub example: Option<serde_json::Value>,
188 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
190 pub examples: HashMap<String, Example>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct Example {
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub summary: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub description: Option<String>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub value: Option<serde_json::Value>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub external_value: Option<String>,
208}
209
210#[derive(Debug, Clone, Default)]
228pub struct ParamMeta {
229 pub title: Option<String>,
231 pub description: Option<String>,
233 pub deprecated: bool,
235 pub include_in_schema: bool,
237 pub example: Option<serde_json::Value>,
239 pub examples: HashMap<String, Example>,
241 pub ge: Option<f64>,
243 pub le: Option<f64>,
245 pub gt: Option<f64>,
247 pub lt: Option<f64>,
249 pub min_length: Option<usize>,
251 pub max_length: Option<usize>,
253 pub pattern: Option<String>,
255}
256
257impl ParamMeta {
258 #[must_use]
260 pub fn new() -> Self {
261 Self {
262 include_in_schema: true,
263 ..Default::default()
264 }
265 }
266
267 #[must_use]
269 pub fn title(mut self, title: impl Into<String>) -> Self {
270 self.title = Some(title.into());
271 self
272 }
273
274 #[must_use]
276 pub fn description(mut self, description: impl Into<String>) -> Self {
277 self.description = Some(description.into());
278 self
279 }
280
281 #[must_use]
283 pub fn deprecated(mut self) -> Self {
284 self.deprecated = true;
285 self
286 }
287
288 #[must_use]
290 pub fn exclude_from_schema(mut self) -> Self {
291 self.include_in_schema = false;
292 self
293 }
294
295 #[must_use]
297 pub fn example(mut self, example: serde_json::Value) -> Self {
298 self.example = Some(example);
299 self
300 }
301
302 #[must_use]
304 pub fn ge(mut self, value: f64) -> Self {
305 self.ge = Some(value);
306 self
307 }
308
309 #[must_use]
311 pub fn le(mut self, value: f64) -> Self {
312 self.le = Some(value);
313 self
314 }
315
316 #[must_use]
318 pub fn gt(mut self, value: f64) -> Self {
319 self.gt = Some(value);
320 self
321 }
322
323 #[must_use]
325 pub fn lt(mut self, value: f64) -> Self {
326 self.lt = Some(value);
327 self
328 }
329
330 #[must_use]
332 pub fn min_length(mut self, len: usize) -> Self {
333 self.min_length = Some(len);
334 self
335 }
336
337 #[must_use]
339 pub fn max_length(mut self, len: usize) -> Self {
340 self.max_length = Some(len);
341 self
342 }
343
344 #[must_use]
346 pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
347 self.pattern = Some(pattern.into());
348 self
349 }
350
351 #[must_use]
353 pub fn to_parameter(
354 &self,
355 name: impl Into<String>,
356 location: ParameterLocation,
357 required: bool,
358 schema: Option<Schema>,
359 ) -> Parameter {
360 Parameter {
361 name: name.into(),
362 location,
363 required,
364 schema,
365 title: self.title.clone(),
366 description: self.description.clone(),
367 deprecated: self.deprecated,
368 example: self.example.clone(),
369 examples: self.examples.clone(),
370 }
371 }
372}
373
374pub trait HasParamMeta {
378 fn param_meta() -> ParamMeta {
380 ParamMeta::new()
381 }
382}
383
384#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
386#[serde(rename_all = "lowercase")]
387pub enum ParameterLocation {
388 Path,
390 Query,
392 Header,
394 Cookie,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct RequestBody {
401 #[serde(default)]
403 pub required: bool,
404 pub content: HashMap<String, MediaType>,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub description: Option<String>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct MediaType {
414 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub schema: Option<Schema>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct Response {
422 pub description: String,
424 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
426 pub content: HashMap<String, MediaType>,
427}
428
429#[derive(Debug, Clone, Default, Serialize, Deserialize)]
431pub struct Components {
432 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
434 pub schemas: HashMap<String, Schema>,
435}
436
437#[derive(Debug, Default)]
441pub struct SchemaRegistry {
442 schemas: HashMap<String, Schema>,
443}
444
445impl SchemaRegistry {
446 #[must_use]
448 pub fn new() -> Self {
449 Self {
450 schemas: HashMap::new(),
451 }
452 }
453
454 pub fn register(&mut self, name: impl Into<String>, schema: Schema) -> Schema {
458 let name = name.into();
459 self.schemas.entry(name.clone()).or_insert(schema);
460 Schema::reference(&name)
461 }
462
463 #[must_use]
465 pub fn into_schemas(self) -> HashMap<String, Schema> {
466 self.schemas
467 }
468}
469
470pub struct SchemaRegistryMut<'a> {
472 schemas: &'a mut HashMap<String, Schema>,
473}
474
475impl SchemaRegistryMut<'_> {
476 pub fn register(&mut self, name: impl Into<String>, schema: Schema) -> Schema {
478 let name = name.into();
479 self.schemas.entry(name.clone()).or_insert(schema);
480 Schema::reference(&name)
481 }
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct Tag {
487 pub name: String,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub description: Option<String>,
492}
493
494#[cfg(test)]
499mod param_meta_tests {
500 use super::*;
501
502 #[test]
503 fn new_creates_default_with_include_in_schema_true() {
504 let meta = ParamMeta::new();
505 assert!(meta.include_in_schema);
506 assert!(meta.title.is_none());
507 assert!(meta.description.is_none());
508 assert!(!meta.deprecated);
509 }
510
511 #[test]
512 fn title_sets_title() {
513 let meta = ParamMeta::new().title("User ID");
514 assert_eq!(meta.title.as_deref(), Some("User ID"));
515 }
516
517 #[test]
518 fn description_sets_description() {
519 let meta = ParamMeta::new().description("The unique identifier");
520 assert_eq!(meta.description.as_deref(), Some("The unique identifier"));
521 }
522
523 #[test]
524 fn deprecated_marks_as_deprecated() {
525 let meta = ParamMeta::new().deprecated();
526 assert!(meta.deprecated);
527 }
528
529 #[test]
530 fn exclude_from_schema_sets_include_false() {
531 let meta = ParamMeta::new().exclude_from_schema();
532 assert!(!meta.include_in_schema);
533 }
534
535 #[test]
536 fn example_sets_example_value() {
537 let meta = ParamMeta::new().example(serde_json::json!(42));
538 assert_eq!(meta.example, Some(serde_json::json!(42)));
539 }
540
541 #[test]
542 fn ge_sets_minimum_constraint() {
543 let meta = ParamMeta::new().ge(1.0);
544 assert_eq!(meta.ge, Some(1.0));
545 }
546
547 #[test]
548 fn le_sets_maximum_constraint() {
549 let meta = ParamMeta::new().le(100.0);
550 assert_eq!(meta.le, Some(100.0));
551 }
552
553 #[test]
554 fn gt_sets_exclusive_minimum() {
555 let meta = ParamMeta::new().gt(0.0);
556 assert_eq!(meta.gt, Some(0.0));
557 }
558
559 #[test]
560 fn lt_sets_exclusive_maximum() {
561 let meta = ParamMeta::new().lt(1000.0);
562 assert_eq!(meta.lt, Some(1000.0));
563 }
564
565 #[test]
566 fn min_length_sets_minimum_string_length() {
567 let meta = ParamMeta::new().min_length(3);
568 assert_eq!(meta.min_length, Some(3));
569 }
570
571 #[test]
572 fn max_length_sets_maximum_string_length() {
573 let meta = ParamMeta::new().max_length(255);
574 assert_eq!(meta.max_length, Some(255));
575 }
576
577 #[test]
578 fn pattern_sets_regex_constraint() {
579 let meta = ParamMeta::new().pattern(r"^\d{4}-\d{2}-\d{2}$");
580 assert_eq!(meta.pattern.as_deref(), Some(r"^\d{4}-\d{2}-\d{2}$"));
581 }
582
583 #[test]
584 fn builder_methods_chain() {
585 let meta = ParamMeta::new()
586 .title("Page")
587 .description("Page number for pagination")
588 .ge(1.0)
589 .le(1000.0)
590 .example(serde_json::json!(1));
591
592 assert_eq!(meta.title.as_deref(), Some("Page"));
593 assert_eq!(
594 meta.description.as_deref(),
595 Some("Page number for pagination")
596 );
597 assert_eq!(meta.ge, Some(1.0));
598 assert_eq!(meta.le, Some(1000.0));
599 assert_eq!(meta.example, Some(serde_json::json!(1)));
600 }
601
602 #[test]
603 fn to_parameter_creates_parameter_with_metadata() {
604 let meta = ParamMeta::new()
605 .title("User ID")
606 .description("Unique user identifier")
607 .deprecated()
608 .example(serde_json::json!(42));
609
610 let param = meta.to_parameter("user_id", ParameterLocation::Path, true, None);
611
612 assert_eq!(param.name, "user_id");
613 assert!(matches!(param.location, ParameterLocation::Path));
614 assert!(param.required);
615 assert_eq!(param.title.as_deref(), Some("User ID"));
616 assert_eq!(param.description.as_deref(), Some("Unique user identifier"));
617 assert!(param.deprecated);
618 assert_eq!(param.example, Some(serde_json::json!(42)));
619 }
620
621 #[test]
622 fn to_parameter_with_query_location() {
623 let meta = ParamMeta::new().description("Search query");
624 let param = meta.to_parameter("q", ParameterLocation::Query, false, None);
625
626 assert_eq!(param.name, "q");
627 assert!(matches!(param.location, ParameterLocation::Query));
628 assert!(!param.required);
629 }
630
631 #[test]
632 fn to_parameter_with_header_location() {
633 let meta = ParamMeta::new().description("API key");
634 let param = meta.to_parameter("X-API-Key", ParameterLocation::Header, true, None);
635
636 assert_eq!(param.name, "X-API-Key");
637 assert!(matches!(param.location, ParameterLocation::Header));
638 }
639
640 #[test]
641 fn to_parameter_with_cookie_location() {
642 let meta = ParamMeta::new().description("Session cookie");
643 let param = meta.to_parameter("session", ParameterLocation::Cookie, false, None);
644
645 assert_eq!(param.name, "session");
646 assert!(matches!(param.location, ParameterLocation::Cookie));
647 }
648
649 #[test]
650 fn default_param_meta_is_empty() {
651 let meta = ParamMeta::default();
652 assert!(meta.title.is_none());
653 assert!(meta.description.is_none());
654 assert!(!meta.deprecated);
655 assert!(!meta.include_in_schema); assert!(meta.example.is_none());
657 assert!(meta.ge.is_none());
658 assert!(meta.le.is_none());
659 assert!(meta.gt.is_none());
660 assert!(meta.lt.is_none());
661 assert!(meta.min_length.is_none());
662 assert!(meta.max_length.is_none());
663 assert!(meta.pattern.is_none());
664 }
665
666 #[test]
667 fn string_constraints_together() {
668 let meta = ParamMeta::new()
669 .min_length(1)
670 .max_length(100)
671 .pattern(r"^[a-zA-Z]+$");
672
673 assert_eq!(meta.min_length, Some(1));
674 assert_eq!(meta.max_length, Some(100));
675 assert_eq!(meta.pattern.as_deref(), Some(r"^[a-zA-Z]+$"));
676 }
677
678 #[test]
679 fn numeric_constraints_together() {
680 let meta = ParamMeta::new().gt(0.0).lt(100.0).ge(1.0).le(99.0);
681
682 assert_eq!(meta.gt, Some(0.0));
683 assert_eq!(meta.lt, Some(100.0));
684 assert_eq!(meta.ge, Some(1.0));
685 assert_eq!(meta.le, Some(99.0));
686 }
687}
688
689#[cfg(test)]
694mod serialization_tests {
695 use super::*;
696
697 #[test]
698 fn parameter_serializes_location_as_in() {
699 let param = Parameter {
700 name: "id".to_string(),
701 location: ParameterLocation::Path,
702 required: true,
703 schema: None,
704 title: None,
705 description: None,
706 deprecated: false,
707 example: None,
708 examples: HashMap::new(),
709 };
710
711 let json = serde_json::to_string(¶m).unwrap();
712 assert!(json.contains(r#""in":"path""#));
713 }
714
715 #[test]
716 fn parameter_location_serializes_lowercase() {
717 let path_json = serde_json::to_string(&ParameterLocation::Path).unwrap();
718 assert_eq!(path_json, r#""path""#);
719
720 let query_json = serde_json::to_string(&ParameterLocation::Query).unwrap();
721 assert_eq!(query_json, r#""query""#);
722
723 let header_json = serde_json::to_string(&ParameterLocation::Header).unwrap();
724 assert_eq!(header_json, r#""header""#);
725
726 let cookie_json = serde_json::to_string(&ParameterLocation::Cookie).unwrap();
727 assert_eq!(cookie_json, r#""cookie""#);
728 }
729
730 #[test]
731 fn parameter_skips_false_deprecated() {
732 let param = Parameter {
733 name: "id".to_string(),
734 location: ParameterLocation::Path,
735 required: true,
736 schema: None,
737 title: None,
738 description: None,
739 deprecated: false,
740 example: None,
741 examples: HashMap::new(),
742 };
743
744 let json = serde_json::to_string(¶m).unwrap();
745 assert!(!json.contains("deprecated"));
746 }
747
748 #[test]
749 fn parameter_includes_true_deprecated() {
750 let param = Parameter {
751 name: "old_id".to_string(),
752 location: ParameterLocation::Path,
753 required: true,
754 schema: None,
755 title: None,
756 description: Some("Deprecated, use new_id instead".to_string()),
757 deprecated: true,
758 example: None,
759 examples: HashMap::new(),
760 };
761
762 let json = serde_json::to_string(¶m).unwrap();
763 assert!(json.contains(r#""deprecated":true"#));
764 }
765
766 #[test]
767 fn openapi_builder_creates_valid_document() {
768 let doc = OpenApiBuilder::new("Test API", "1.0.0")
769 .description("A test API")
770 .server("https://api.example.com", Some("Production".to_string()))
771 .tag("users", Some("User operations".to_string()))
772 .build();
773
774 assert_eq!(doc.openapi, "3.1.0");
775 assert_eq!(doc.info.title, "Test API");
776 assert_eq!(doc.info.version, "1.0.0");
777 assert_eq!(doc.info.description.as_deref(), Some("A test API"));
778 assert_eq!(doc.servers.len(), 1);
779 assert_eq!(doc.servers[0].url, "https://api.example.com");
780 assert_eq!(doc.tags.len(), 1);
781 assert_eq!(doc.tags[0].name, "users");
782 }
783
784 #[test]
785 fn openapi_serializes_to_valid_json() {
786 let doc = OpenApiBuilder::new("Test API", "1.0.0").build();
787 let json = serde_json::to_string_pretty(&doc).unwrap();
788
789 assert!(json.contains(r#""openapi": "3.1.0""#));
790 assert!(json.contains(r#""title": "Test API""#));
791 assert!(json.contains(r#""version": "1.0.0""#));
792 }
793
794 #[test]
795 fn example_serializes_all_fields() {
796 let example = Example {
797 summary: Some("Example summary".to_string()),
798 description: Some("Example description".to_string()),
799 value: Some(serde_json::json!({"key": "value"})),
800 external_value: None,
801 };
802
803 let json = serde_json::to_string(&example).unwrap();
804 assert!(json.contains(r#""summary":"Example summary""#));
805 assert!(json.contains(r#""description":"Example description""#));
806 assert!(json.contains(r#""value""#));
807 }
808}
809
810pub struct OpenApiBuilder {
812 info: Info,
813 servers: Vec<Server>,
814 paths: HashMap<String, PathItem>,
815 components: Components,
816 tags: Vec<Tag>,
817}
818
819impl OpenApiBuilder {
820 #[must_use]
822 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
823 Self {
824 info: Info {
825 title: title.into(),
826 version: version.into(),
827 description: None,
828 terms_of_service: None,
829 contact: None,
830 license: None,
831 },
832 servers: Vec::new(),
833 paths: HashMap::new(),
834 components: Components::default(),
835 tags: Vec::new(),
836 }
837 }
838
839 #[must_use]
841 pub fn description(mut self, description: impl Into<String>) -> Self {
842 self.info.description = Some(description.into());
843 self
844 }
845
846 #[must_use]
848 pub fn server(mut self, url: impl Into<String>, description: Option<String>) -> Self {
849 self.servers.push(Server {
850 url: url.into(),
851 description,
852 });
853 self
854 }
855
856 #[must_use]
858 pub fn tag(mut self, name: impl Into<String>, description: Option<String>) -> Self {
859 self.tags.push(Tag {
860 name: name.into(),
861 description,
862 });
863 self
864 }
865
866 #[must_use]
868 pub fn schema(mut self, name: impl Into<String>, schema: Schema) -> Self {
869 self.components.schemas.insert(name.into(), schema);
870 self
871 }
872
873 pub fn registry(&mut self) -> SchemaRegistryMut<'_> {
875 SchemaRegistryMut {
876 schemas: &mut self.components.schemas,
877 }
878 }
879
880 #[allow(clippy::too_many_lines)]
884 pub fn add_route(&mut self, route: &fastapi_router::Route) {
885 use fastapi_router::Converter as RouteConverter;
886
887 fn param_schema(conv: RouteConverter) -> Schema {
888 match conv {
889 RouteConverter::Str | RouteConverter::Path => Schema::string(),
890 RouteConverter::Int => Schema::integer(Some("int64")),
891 RouteConverter::Float => Schema::number(Some("double")),
892 RouteConverter::Uuid => Schema::Primitive(crate::schema::PrimitiveSchema {
893 schema_type: crate::schema::SchemaType::String,
894 format: Some("uuid".to_string()),
895 nullable: false,
896 }),
897 }
898 }
899
900 let mut op = Operation {
901 operation_id: if route.operation_id.is_empty() {
902 None
903 } else {
904 Some(route.operation_id.clone())
905 },
906 summary: route.summary.clone(),
907 description: route.description.clone(),
908 tags: route.tags.clone(),
909 deprecated: route.deprecated,
910 ..Default::default()
911 };
912
913 for p in &route.path_params {
915 let mut examples = HashMap::new();
916 for (name, value) in &p.examples {
917 examples.insert(
918 name.clone(),
919 Example {
920 summary: None,
921 description: None,
922 value: Some(value.clone()),
923 external_value: None,
924 },
925 );
926 }
927
928 op.parameters.push(Parameter {
929 name: p.name.clone(),
930 location: ParameterLocation::Path,
931 required: true,
932 schema: Some(param_schema(p.converter)),
933 title: p.title.clone(),
934 description: p.description.clone(),
935 deprecated: p.deprecated,
936 example: p.example.clone(),
937 examples,
938 });
939 }
940
941 if let Some(schema_name) = &route.request_body_schema {
943 let content_type = route
944 .request_body_content_type
945 .clone()
946 .unwrap_or_else(|| "application/json".to_string());
947 let mut content = HashMap::new();
948 content.insert(
949 content_type,
950 MediaType {
951 schema: Some(Schema::reference(schema_name)),
952 },
953 );
954 op.request_body = Some(RequestBody {
955 required: route.request_body_required,
956 content,
957 description: None,
958 });
959 }
960
961 let mut responses = HashMap::new();
963 if route.responses.is_empty() {
964 responses = default_responses();
965 } else {
966 for r in &route.responses {
967 let mut content = HashMap::new();
968 content.insert(
969 "application/json".to_string(),
970 MediaType {
971 schema: Some(Schema::reference(&r.schema_name)),
972 },
973 );
974 responses.insert(
975 r.status.to_string(),
976 Response {
977 description: r.description.clone(),
978 content,
979 },
980 );
981 }
982 }
983 op.responses = responses;
984
985 let path_item = self.paths.entry(route.path.clone()).or_default();
986 match route.method.as_str() {
987 "GET" => path_item.get = Some(op),
988 "POST" => path_item.post = Some(op),
989 "PUT" => path_item.put = Some(op),
990 "DELETE" => path_item.delete = Some(op),
991 "PATCH" => path_item.patch = Some(op),
992 "OPTIONS" => path_item.options = Some(op),
993 "HEAD" => path_item.head = Some(op),
994 _ => {}
995 }
996 }
997
998 pub fn add_routes<'a, I>(&mut self, routes: I)
1000 where
1001 I: IntoIterator<Item = &'a fastapi_router::Route>,
1002 {
1003 for r in routes {
1004 self.add_route(r);
1005 }
1006 }
1007
1008 #[must_use]
1029 pub fn operation(
1030 mut self,
1031 method: &str,
1032 path: impl Into<String>,
1033 operation: Operation,
1034 ) -> Self {
1035 let path = path.into();
1036 let path_item = self.paths.entry(path).or_default();
1037
1038 match method.to_uppercase().as_str() {
1039 "GET" => path_item.get = Some(operation),
1040 "POST" => path_item.post = Some(operation),
1041 "PUT" => path_item.put = Some(operation),
1042 "DELETE" => path_item.delete = Some(operation),
1043 "PATCH" => path_item.patch = Some(operation),
1044 "OPTIONS" => path_item.options = Some(operation),
1045 "HEAD" => path_item.head = Some(operation),
1046 _ => {} }
1048
1049 self
1050 }
1051
1052 #[must_use]
1054 pub fn get(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
1055 let operation_id = operation_id.into();
1056 self.operation(
1057 "GET",
1058 path,
1059 Operation {
1060 operation_id: if operation_id.is_empty() {
1061 None
1062 } else {
1063 Some(operation_id)
1064 },
1065 responses: default_responses(),
1066 ..Default::default()
1067 },
1068 )
1069 }
1070
1071 #[must_use]
1073 pub fn post(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
1074 let operation_id = operation_id.into();
1075 self.operation(
1076 "POST",
1077 path,
1078 Operation {
1079 operation_id: if operation_id.is_empty() {
1080 None
1081 } else {
1082 Some(operation_id)
1083 },
1084 responses: default_responses(),
1085 ..Default::default()
1086 },
1087 )
1088 }
1089
1090 #[must_use]
1092 pub fn put(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
1093 let operation_id = operation_id.into();
1094 self.operation(
1095 "PUT",
1096 path,
1097 Operation {
1098 operation_id: if operation_id.is_empty() {
1099 None
1100 } else {
1101 Some(operation_id)
1102 },
1103 responses: default_responses(),
1104 ..Default::default()
1105 },
1106 )
1107 }
1108
1109 #[must_use]
1111 pub fn delete(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
1112 let operation_id = operation_id.into();
1113 self.operation(
1114 "DELETE",
1115 path,
1116 Operation {
1117 operation_id: if operation_id.is_empty() {
1118 None
1119 } else {
1120 Some(operation_id)
1121 },
1122 responses: default_responses(),
1123 ..Default::default()
1124 },
1125 )
1126 }
1127
1128 #[must_use]
1130 pub fn patch(self, path: impl Into<String>, operation_id: impl Into<String>) -> Self {
1131 let operation_id = operation_id.into();
1132 self.operation(
1133 "PATCH",
1134 path,
1135 Operation {
1136 operation_id: if operation_id.is_empty() {
1137 None
1138 } else {
1139 Some(operation_id)
1140 },
1141 responses: default_responses(),
1142 ..Default::default()
1143 },
1144 )
1145 }
1146
1147 #[must_use]
1149 pub fn build(self) -> OpenApi {
1150 OpenApi {
1151 openapi: "3.1.0".to_string(),
1152 info: self.info,
1153 servers: self.servers,
1154 paths: self.paths,
1155 components: if self.components.schemas.is_empty() {
1156 None
1157 } else {
1158 Some(self.components)
1159 },
1160 tags: self.tags,
1161 }
1162 }
1163}