1use crate::schema::{Schema, SchemaRegistry};
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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub security: Vec<SecurityRequirement>,
29}
30
31pub type SecurityRequirement = HashMap<String, Vec<String>>;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Info {
40 pub title: String,
42 pub version: String,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub description: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub terms_of_service: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub contact: Option<Contact>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub license: Option<License>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Contact {
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub name: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub url: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub email: Option<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct License {
75 pub name: String,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub url: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Server {
85 pub url: String,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub description: Option<String>,
90}
91
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct PathItem {
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub get: Option<Operation>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub post: Option<Operation>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub put: Option<Operation>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub delete: Option<Operation>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub patch: Option<Operation>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub options: Option<Operation>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub head: Option<Operation>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct Operation {
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub operation_id: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub summary: Option<String>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub description: Option<String>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub tags: Vec<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub parameters: Vec<Parameter>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub request_body: Option<RequestBody>,
140 pub responses: HashMap<String, Response>,
142 #[serde(default, skip_serializing_if = "is_false")]
144 pub deprecated: bool,
145 #[serde(default, skip_serializing_if = "Vec::is_empty")]
149 pub security: Vec<SecurityRequirement>,
150}
151
152fn is_false(b: &bool) -> bool {
153 !*b
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Parameter {
159 pub name: String,
161 #[serde(rename = "in")]
163 pub location: ParameterLocation,
164 #[serde(default)]
166 pub required: bool,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub schema: Option<Schema>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub title: Option<String>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub description: Option<String>,
176 #[serde(default, skip_serializing_if = "is_false")]
178 pub deprecated: bool,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub example: Option<serde_json::Value>,
182 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
184 pub examples: HashMap<String, Example>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct Example {
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub summary: Option<String>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub description: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub value: Option<serde_json::Value>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub external_value: Option<String>,
202}
203
204#[derive(Debug, Clone, Default)]
222pub struct ParamMeta {
223 pub title: Option<String>,
225 pub description: Option<String>,
227 pub deprecated: bool,
229 pub include_in_schema: bool,
231 pub example: Option<serde_json::Value>,
233 pub examples: HashMap<String, Example>,
235 pub ge: Option<f64>,
237 pub le: Option<f64>,
239 pub gt: Option<f64>,
241 pub lt: Option<f64>,
243 pub min_length: Option<usize>,
245 pub max_length: Option<usize>,
247 pub pattern: Option<String>,
249 pub alias: Option<String>,
253 pub validation_alias: Option<String>,
255 pub serialization_alias: Option<String>,
257}
258
259impl ParamMeta {
260 #[must_use]
262 pub fn new() -> Self {
263 Self {
264 include_in_schema: true,
265 ..Default::default()
266 }
267 }
268
269 #[must_use]
271 pub fn title(mut self, title: impl Into<String>) -> Self {
272 self.title = Some(title.into());
273 self
274 }
275
276 #[must_use]
278 pub fn description(mut self, description: impl Into<String>) -> Self {
279 self.description = Some(description.into());
280 self
281 }
282
283 #[must_use]
285 pub fn deprecated(mut self) -> Self {
286 self.deprecated = true;
287 self
288 }
289
290 #[must_use]
292 pub fn exclude_from_schema(mut self) -> Self {
293 self.include_in_schema = false;
294 self
295 }
296
297 #[must_use]
299 pub fn example(mut self, example: serde_json::Value) -> Self {
300 self.example = Some(example);
301 self
302 }
303
304 #[must_use]
306 pub fn ge(mut self, value: f64) -> Self {
307 self.ge = Some(value);
308 self
309 }
310
311 #[must_use]
313 pub fn le(mut self, value: f64) -> Self {
314 self.le = Some(value);
315 self
316 }
317
318 #[must_use]
320 pub fn gt(mut self, value: f64) -> Self {
321 self.gt = Some(value);
322 self
323 }
324
325 #[must_use]
327 pub fn lt(mut self, value: f64) -> Self {
328 self.lt = Some(value);
329 self
330 }
331
332 #[must_use]
334 pub fn min_length(mut self, len: usize) -> Self {
335 self.min_length = Some(len);
336 self
337 }
338
339 #[must_use]
341 pub fn max_length(mut self, len: usize) -> Self {
342 self.max_length = Some(len);
343 self
344 }
345
346 #[must_use]
348 pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
349 self.pattern = Some(pattern.into());
350 self
351 }
352
353 #[must_use]
366 pub fn alias(mut self, alias: impl Into<String>) -> Self {
367 self.alias = Some(alias.into());
368 self
369 }
370
371 #[must_use]
376 pub fn validation_alias(mut self, alias: impl Into<String>) -> Self {
377 self.validation_alias = Some(alias.into());
378 self
379 }
380
381 #[must_use]
386 pub fn serialization_alias(mut self, alias: impl Into<String>) -> Self {
387 self.serialization_alias = Some(alias.into());
388 self
389 }
390
391 #[must_use]
395 pub fn effective_validation_name(&self) -> Option<&str> {
396 self.validation_alias.as_deref().or(self.alias.as_deref())
397 }
398
399 #[must_use]
403 pub fn effective_serialization_name(&self) -> Option<&str> {
404 self.serialization_alias
405 .as_deref()
406 .or(self.alias.as_deref())
407 }
408
409 #[must_use]
414 pub fn to_parameter(
415 &self,
416 name: impl Into<String>,
417 location: ParameterLocation,
418 required: bool,
419 schema: Option<Schema>,
420 ) -> Parameter {
421 let effective_name = self
423 .effective_serialization_name()
424 .map_or_else(|| name.into(), String::from);
425 Parameter {
426 name: effective_name,
427 location,
428 required,
429 schema,
430 title: self.title.clone(),
431 description: self.description.clone(),
432 deprecated: self.deprecated,
433 example: self.example.clone(),
434 examples: self.examples.clone(),
435 }
436 }
437}
438
439pub trait HasParamMeta {
443 fn param_meta() -> ParamMeta {
445 ParamMeta::new()
446 }
447}
448
449#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
451#[serde(rename_all = "lowercase")]
452pub enum ParameterLocation {
453 Path,
455 Query,
457 Header,
459 Cookie,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct RequestBody {
466 #[serde(default)]
468 pub required: bool,
469 pub content: HashMap<String, MediaType>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub description: Option<String>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct MediaType {
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub schema: Option<Schema>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub example: Option<serde_json::Value>,
485 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
487 pub examples: HashMap<String, Example>,
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct Response {
493 pub description: String,
495 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
497 pub content: HashMap<String, MediaType>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
502#[serde(tag = "type", rename_all = "camelCase")]
503#[allow(clippy::large_enum_variant)] pub enum SecurityScheme {
505 #[serde(rename = "apiKey")]
507 ApiKey {
508 name: String,
510 #[serde(rename = "in")]
512 location: ApiKeyLocation,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
515 description: Option<String>,
516 },
517 #[serde(rename = "http")]
519 Http {
520 scheme: String,
522 #[serde(
524 default,
525 skip_serializing_if = "Option::is_none",
526 rename = "bearerFormat"
527 )]
528 bearer_format: Option<String>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
531 description: Option<String>,
532 },
533 #[serde(rename = "oauth2")]
535 OAuth2 {
536 flows: OAuth2Flows,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
540 description: Option<String>,
541 },
542 #[serde(rename = "openIdConnect")]
544 OpenIdConnect {
545 #[serde(rename = "openIdConnectUrl")]
547 open_id_connect_url: String,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 description: Option<String>,
551 },
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(rename_all = "lowercase")]
557pub enum ApiKeyLocation {
558 Query,
560 Header,
562 Cookie,
564}
565
566#[derive(Debug, Clone, Default, Serialize, Deserialize)]
568#[serde(rename_all = "camelCase")]
569pub struct OAuth2Flows {
570 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub implicit: Option<OAuth2Flow>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub authorization_code: Option<OAuth2Flow>,
576 #[serde(default, skip_serializing_if = "Option::is_none")]
578 pub client_credentials: Option<OAuth2Flow>,
579 #[serde(default, skip_serializing_if = "Option::is_none")]
581 pub password: Option<OAuth2Flow>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
586#[serde(rename_all = "camelCase")]
587pub struct OAuth2Flow {
588 #[serde(default, skip_serializing_if = "Option::is_none")]
590 pub authorization_url: Option<String>,
591 #[serde(default, skip_serializing_if = "Option::is_none")]
593 pub token_url: Option<String>,
594 #[serde(default, skip_serializing_if = "Option::is_none")]
596 pub refresh_url: Option<String>,
597 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
599 pub scopes: HashMap<String, String>,
600}
601
602#[derive(Debug, Clone, Default, Serialize, Deserialize)]
604#[serde(rename_all = "camelCase")]
605pub struct Components {
606 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
608 pub schemas: HashMap<String, Schema>,
609 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
611 pub security_schemes: HashMap<String, SecurityScheme>,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
616pub struct Tag {
617 pub name: String,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub description: Option<String>,
622}
623
624#[cfg(test)]
629mod param_meta_tests {
630 use super::*;
631
632 #[test]
633 fn new_creates_default_with_include_in_schema_true() {
634 let meta = ParamMeta::new();
635 assert!(meta.include_in_schema);
636 assert!(meta.title.is_none());
637 assert!(meta.description.is_none());
638 assert!(!meta.deprecated);
639 }
640
641 #[test]
642 fn title_sets_title() {
643 let meta = ParamMeta::new().title("User ID");
644 assert_eq!(meta.title.as_deref(), Some("User ID"));
645 }
646
647 #[test]
648 fn description_sets_description() {
649 let meta = ParamMeta::new().description("The unique identifier");
650 assert_eq!(meta.description.as_deref(), Some("The unique identifier"));
651 }
652
653 #[test]
654 fn deprecated_marks_as_deprecated() {
655 let meta = ParamMeta::new().deprecated();
656 assert!(meta.deprecated);
657 }
658
659 #[test]
660 fn exclude_from_schema_sets_include_false() {
661 let meta = ParamMeta::new().exclude_from_schema();
662 assert!(!meta.include_in_schema);
663 }
664
665 #[test]
666 fn example_sets_example_value() {
667 let meta = ParamMeta::new().example(serde_json::json!(42));
668 assert_eq!(meta.example, Some(serde_json::json!(42)));
669 }
670
671 #[test]
672 fn ge_sets_minimum_constraint() {
673 let meta = ParamMeta::new().ge(1.0);
674 assert_eq!(meta.ge, Some(1.0));
675 }
676
677 #[test]
678 fn le_sets_maximum_constraint() {
679 let meta = ParamMeta::new().le(100.0);
680 assert_eq!(meta.le, Some(100.0));
681 }
682
683 #[test]
684 fn gt_sets_exclusive_minimum() {
685 let meta = ParamMeta::new().gt(0.0);
686 assert_eq!(meta.gt, Some(0.0));
687 }
688
689 #[test]
690 fn lt_sets_exclusive_maximum() {
691 let meta = ParamMeta::new().lt(1000.0);
692 assert_eq!(meta.lt, Some(1000.0));
693 }
694
695 #[test]
696 fn min_length_sets_minimum_string_length() {
697 let meta = ParamMeta::new().min_length(3);
698 assert_eq!(meta.min_length, Some(3));
699 }
700
701 #[test]
702 fn max_length_sets_maximum_string_length() {
703 let meta = ParamMeta::new().max_length(255);
704 assert_eq!(meta.max_length, Some(255));
705 }
706
707 #[test]
708 fn pattern_sets_regex_constraint() {
709 let meta = ParamMeta::new().pattern(r"^\d{4}-\d{2}-\d{2}$");
710 assert_eq!(meta.pattern.as_deref(), Some(r"^\d{4}-\d{2}-\d{2}$"));
711 }
712
713 #[test]
714 fn builder_methods_chain() {
715 let meta = ParamMeta::new()
716 .title("Page")
717 .description("Page number for pagination")
718 .ge(1.0)
719 .le(1000.0)
720 .example(serde_json::json!(1));
721
722 assert_eq!(meta.title.as_deref(), Some("Page"));
723 assert_eq!(
724 meta.description.as_deref(),
725 Some("Page number for pagination")
726 );
727 assert_eq!(meta.ge, Some(1.0));
728 assert_eq!(meta.le, Some(1000.0));
729 assert_eq!(meta.example, Some(serde_json::json!(1)));
730 }
731
732 #[test]
733 fn to_parameter_creates_parameter_with_metadata() {
734 let meta = ParamMeta::new()
735 .title("User ID")
736 .description("Unique user identifier")
737 .deprecated()
738 .example(serde_json::json!(42));
739
740 let param = meta.to_parameter("user_id", ParameterLocation::Path, true, None);
741
742 assert_eq!(param.name, "user_id");
743 assert!(matches!(param.location, ParameterLocation::Path));
744 assert!(param.required);
745 assert_eq!(param.title.as_deref(), Some("User ID"));
746 assert_eq!(param.description.as_deref(), Some("Unique user identifier"));
747 assert!(param.deprecated);
748 assert_eq!(param.example, Some(serde_json::json!(42)));
749 }
750
751 #[test]
752 fn to_parameter_with_query_location() {
753 let meta = ParamMeta::new().description("Search query");
754 let param = meta.to_parameter("q", ParameterLocation::Query, false, None);
755
756 assert_eq!(param.name, "q");
757 assert!(matches!(param.location, ParameterLocation::Query));
758 assert!(!param.required);
759 }
760
761 #[test]
762 fn to_parameter_with_header_location() {
763 let meta = ParamMeta::new().description("API key");
764 let param = meta.to_parameter("X-API-Key", ParameterLocation::Header, true, None);
765
766 assert_eq!(param.name, "X-API-Key");
767 assert!(matches!(param.location, ParameterLocation::Header));
768 }
769
770 #[test]
771 fn to_parameter_with_cookie_location() {
772 let meta = ParamMeta::new().description("Session cookie");
773 let param = meta.to_parameter("session", ParameterLocation::Cookie, false, None);
774
775 assert_eq!(param.name, "session");
776 assert!(matches!(param.location, ParameterLocation::Cookie));
777 }
778
779 #[test]
780 fn default_param_meta_is_empty() {
781 let meta = ParamMeta::default();
782 assert!(meta.title.is_none());
783 assert!(meta.description.is_none());
784 assert!(!meta.deprecated);
785 assert!(!meta.include_in_schema); assert!(meta.example.is_none());
787 assert!(meta.ge.is_none());
788 assert!(meta.le.is_none());
789 assert!(meta.gt.is_none());
790 assert!(meta.lt.is_none());
791 assert!(meta.min_length.is_none());
792 assert!(meta.max_length.is_none());
793 assert!(meta.pattern.is_none());
794 }
795
796 #[test]
797 fn string_constraints_together() {
798 let meta = ParamMeta::new()
799 .min_length(1)
800 .max_length(100)
801 .pattern(r"^[a-zA-Z]+$");
802
803 assert_eq!(meta.min_length, Some(1));
804 assert_eq!(meta.max_length, Some(100));
805 assert_eq!(meta.pattern.as_deref(), Some(r"^[a-zA-Z]+$"));
806 }
807
808 #[test]
809 fn numeric_constraints_together() {
810 let meta = ParamMeta::new().gt(0.0).lt(100.0).ge(1.0).le(99.0);
811
812 assert_eq!(meta.gt, Some(0.0));
813 assert_eq!(meta.lt, Some(100.0));
814 assert_eq!(meta.ge, Some(1.0));
815 assert_eq!(meta.le, Some(99.0));
816 }
817
818 #[test]
821 fn alias_sets_alias() {
822 let meta = ParamMeta::new().alias("q");
823 assert_eq!(meta.alias.as_deref(), Some("q"));
824 }
825
826 #[test]
827 fn validation_alias_sets_validation_alias() {
828 let meta = ParamMeta::new().validation_alias("query_param");
829 assert_eq!(meta.validation_alias.as_deref(), Some("query_param"));
830 }
831
832 #[test]
833 fn serialization_alias_sets_serialization_alias() {
834 let meta = ParamMeta::new().serialization_alias("search_query");
835 assert_eq!(meta.serialization_alias.as_deref(), Some("search_query"));
836 }
837
838 #[test]
839 fn effective_validation_name_uses_validation_alias_first() {
840 let meta = ParamMeta::new().alias("a").validation_alias("v");
841 assert_eq!(meta.effective_validation_name(), Some("v"));
842 }
843
844 #[test]
845 fn effective_validation_name_falls_back_to_alias() {
846 let meta = ParamMeta::new().alias("a");
847 assert_eq!(meta.effective_validation_name(), Some("a"));
848 }
849
850 #[test]
851 fn effective_validation_name_returns_none_when_no_alias() {
852 let meta = ParamMeta::new();
853 assert!(meta.effective_validation_name().is_none());
854 }
855
856 #[test]
857 fn effective_serialization_name_uses_serialization_alias_first() {
858 let meta = ParamMeta::new().alias("a").serialization_alias("s");
859 assert_eq!(meta.effective_serialization_name(), Some("s"));
860 }
861
862 #[test]
863 fn effective_serialization_name_falls_back_to_alias() {
864 let meta = ParamMeta::new().alias("a");
865 assert_eq!(meta.effective_serialization_name(), Some("a"));
866 }
867
868 #[test]
869 fn effective_serialization_name_returns_none_when_no_alias() {
870 let meta = ParamMeta::new();
871 assert!(meta.effective_serialization_name().is_none());
872 }
873
874 #[test]
875 fn to_parameter_uses_alias_for_name() {
876 let meta = ParamMeta::new().alias("q");
877 let param = meta.to_parameter("query", ParameterLocation::Query, false, None);
878 assert_eq!(param.name, "q");
880 }
881
882 #[test]
883 fn to_parameter_uses_serialization_alias_for_name() {
884 let meta = ParamMeta::new().alias("a").serialization_alias("search");
885 let param = meta.to_parameter("query", ParameterLocation::Query, false, None);
886 assert_eq!(param.name, "search");
888 }
889
890 #[test]
891 fn to_parameter_uses_original_name_when_no_alias() {
892 let meta = ParamMeta::new();
893 let param = meta.to_parameter("query", ParameterLocation::Query, false, None);
894 assert_eq!(param.name, "query");
895 }
896
897 #[test]
898 fn alias_propagation_rules() {
899 let meta = ParamMeta::new().alias("q");
901 assert_eq!(meta.effective_validation_name(), Some("q"));
902 assert_eq!(meta.effective_serialization_name(), Some("q"));
903
904 let meta2 = ParamMeta::new()
906 .alias("q")
907 .validation_alias("query_input")
908 .serialization_alias("query_output");
909 assert_eq!(meta2.effective_validation_name(), Some("query_input"));
910 assert_eq!(meta2.effective_serialization_name(), Some("query_output"));
911 }
912
913 #[test]
914 fn header_alias_example() {
915 let meta = ParamMeta::new().alias("X-Custom-Token");
917 let param = meta.to_parameter("token", ParameterLocation::Header, true, None);
918 assert_eq!(param.name, "X-Custom-Token");
919 assert!(matches!(param.location, ParameterLocation::Header));
920 }
921}
922
923#[cfg(test)]
928mod serialization_tests {
929 use super::*;
930
931 #[test]
932 fn parameter_serializes_location_as_in() {
933 let param = Parameter {
934 name: "id".to_string(),
935 location: ParameterLocation::Path,
936 required: true,
937 schema: None,
938 title: None,
939 description: None,
940 deprecated: false,
941 example: None,
942 examples: HashMap::new(),
943 };
944
945 let json = serde_json::to_string(¶m).unwrap();
946 assert!(json.contains(r#""in":"path""#));
947 }
948
949 #[test]
950 fn parameter_location_serializes_lowercase() {
951 let path_json = serde_json::to_string(&ParameterLocation::Path).unwrap();
952 assert_eq!(path_json, r#""path""#);
953
954 let query_json = serde_json::to_string(&ParameterLocation::Query).unwrap();
955 assert_eq!(query_json, r#""query""#);
956
957 let header_json = serde_json::to_string(&ParameterLocation::Header).unwrap();
958 assert_eq!(header_json, r#""header""#);
959
960 let cookie_json = serde_json::to_string(&ParameterLocation::Cookie).unwrap();
961 assert_eq!(cookie_json, r#""cookie""#);
962 }
963
964 #[test]
965 fn parameter_skips_false_deprecated() {
966 let param = Parameter {
967 name: "id".to_string(),
968 location: ParameterLocation::Path,
969 required: true,
970 schema: None,
971 title: None,
972 description: None,
973 deprecated: false,
974 example: None,
975 examples: HashMap::new(),
976 };
977
978 let json = serde_json::to_string(¶m).unwrap();
979 assert!(!json.contains("deprecated"));
980 }
981
982 #[test]
983 fn parameter_includes_true_deprecated() {
984 let param = Parameter {
985 name: "old_id".to_string(),
986 location: ParameterLocation::Path,
987 required: true,
988 schema: None,
989 title: None,
990 description: Some("Deprecated, use new_id instead".to_string()),
991 deprecated: true,
992 example: None,
993 examples: HashMap::new(),
994 };
995
996 let json = serde_json::to_string(¶m).unwrap();
997 assert!(json.contains(r#""deprecated":true"#));
998 }
999
1000 #[test]
1001 fn openapi_builder_creates_valid_document() {
1002 let doc = OpenApiBuilder::new("Test API", "1.0.0")
1003 .description("A test API")
1004 .server("https://api.example.com", Some("Production".to_string()))
1005 .tag("users", Some("User operations".to_string()))
1006 .build();
1007
1008 assert_eq!(doc.openapi, "3.1.0");
1009 assert_eq!(doc.info.title, "Test API");
1010 assert_eq!(doc.info.version, "1.0.0");
1011 assert_eq!(doc.info.description.as_deref(), Some("A test API"));
1012 assert_eq!(doc.servers.len(), 1);
1013 assert_eq!(doc.servers[0].url, "https://api.example.com");
1014 assert_eq!(doc.tags.len(), 1);
1015 assert_eq!(doc.tags[0].name, "users");
1016 }
1017
1018 #[test]
1019 fn openapi_serializes_to_valid_json() {
1020 let doc = OpenApiBuilder::new("Test API", "1.0.0").build();
1021 let json = serde_json::to_string_pretty(&doc).unwrap();
1022
1023 assert!(json.contains(r#""openapi": "3.1.0""#));
1024 assert!(json.contains(r#""title": "Test API""#));
1025 assert!(json.contains(r#""version": "1.0.0""#));
1026 }
1027
1028 #[test]
1029 fn example_serializes_all_fields() {
1030 let example = Example {
1031 summary: Some("Example summary".to_string()),
1032 description: Some("Example description".to_string()),
1033 value: Some(serde_json::json!({"key": "value"})),
1034 external_value: None,
1035 };
1036
1037 let json = serde_json::to_string(&example).unwrap();
1038 assert!(json.contains(r#""summary":"Example summary""#));
1039 assert!(json.contains(r#""description":"Example description""#));
1040 assert!(json.contains(r#""value""#));
1041 }
1042
1043 #[test]
1044 fn openapi_builder_with_registry_includes_schemas() {
1045 use crate::schema::Schema;
1046
1047 let builder = OpenApiBuilder::new("Test API", "1.0.0");
1048
1049 builder.registry().register(
1051 "User",
1052 Schema::object(
1053 [
1054 ("id".to_string(), Schema::integer(Some("int64"))),
1055 ("name".to_string(), Schema::string()),
1056 ]
1057 .into_iter()
1058 .collect(),
1059 vec!["id".to_string(), "name".to_string()],
1060 ),
1061 );
1062
1063 let doc = builder.build();
1064
1065 assert!(doc.components.is_some());
1067 let components = doc.components.unwrap();
1068 assert!(components.schemas.contains_key("User"));
1069 }
1070
1071 #[test]
1072 fn openapi_builder_registry_returns_refs() {
1073 use crate::schema::Schema;
1074
1075 let builder = OpenApiBuilder::new("Test API", "1.0.0");
1076
1077 let user_ref = builder.registry().register("User", Schema::string());
1079
1080 if let Schema::Ref(ref_schema) = user_ref {
1081 assert_eq!(ref_schema.reference, "#/components/schemas/User");
1082 } else {
1083 panic!("Expected Schema::Ref");
1084 }
1085 }
1086
1087 #[test]
1088 fn openapi_builder_merges_registry_and_explicit_schemas() {
1089 use crate::schema::Schema;
1090
1091 let builder =
1092 OpenApiBuilder::new("Test API", "1.0.0").schema("ExplicitSchema", Schema::boolean());
1093
1094 builder
1096 .registry()
1097 .register("RegistrySchema", Schema::string());
1098
1099 let doc = builder.build();
1100
1101 let components = doc.components.unwrap();
1102 assert!(components.schemas.contains_key("ExplicitSchema"));
1103 assert!(components.schemas.contains_key("RegistrySchema"));
1104 }
1105
1106 #[test]
1107 fn openapi_builder_explicit_schemas_override_registry() {
1108 use crate::schema::Schema;
1109
1110 let builder = OpenApiBuilder::new("Test API", "1.0.0");
1111
1112 builder.registry().register("MyType", Schema::string());
1114
1115 let builder = builder.schema("MyType", Schema::boolean());
1117
1118 let doc = builder.build();
1119 let components = doc.components.unwrap();
1120
1121 if let Schema::Primitive(p) = &components.schemas["MyType"] {
1123 assert!(matches!(p.schema_type, crate::schema::SchemaType::Boolean));
1124 } else {
1125 panic!("Expected primitive boolean schema");
1126 }
1127 }
1128
1129 #[test]
1130 fn openapi_builder_with_existing_registry() {
1131 use crate::schema::Schema;
1132
1133 let registry = SchemaRegistry::new();
1135 registry.register("PreRegistered", Schema::string());
1136
1137 let builder = OpenApiBuilder::with_registry("Test API", "1.0.0", registry);
1139
1140 let doc = builder.build();
1141 let components = doc.components.unwrap();
1142 assert!(components.schemas.contains_key("PreRegistered"));
1143 }
1144
1145 #[test]
1146 fn openapi_builder_registry_serializes_refs_correctly() {
1147 use crate::schema::Schema;
1148
1149 let builder = OpenApiBuilder::new("Test API", "1.0.0");
1150
1151 let user_ref = builder.registry().register(
1153 "User",
1154 Schema::object(
1155 [("name".to_string(), Schema::string())]
1156 .into_iter()
1157 .collect(),
1158 vec!["name".to_string()],
1159 ),
1160 );
1161
1162 let doc = builder.build();
1164 let json = serde_json::to_string_pretty(&doc).unwrap();
1165
1166 assert!(json.contains(r#""User""#));
1168
1169 let ref_json = serde_json::to_string(&user_ref).unwrap();
1171 assert!(ref_json.contains(r##""$ref":"#/components/schemas/User""##));
1172 }
1173}
1174
1175pub struct OpenApiBuilder {
1177 info: Info,
1178 servers: Vec<Server>,
1179 paths: HashMap<String, PathItem>,
1180 components: Components,
1181 tags: Vec<Tag>,
1182 security: Vec<SecurityRequirement>,
1184 registry: SchemaRegistry,
1186}
1187
1188impl OpenApiBuilder {
1189 #[must_use]
1191 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
1192 Self {
1193 info: Info {
1194 title: title.into(),
1195 version: version.into(),
1196 description: None,
1197 terms_of_service: None,
1198 contact: None,
1199 license: None,
1200 },
1201 servers: Vec::new(),
1202 paths: HashMap::new(),
1203 components: Components::default(),
1204 tags: Vec::new(),
1205 security: Vec::new(),
1206 registry: SchemaRegistry::new(),
1207 }
1208 }
1209
1210 #[must_use]
1215 pub fn with_registry(
1216 title: impl Into<String>,
1217 version: impl Into<String>,
1218 registry: SchemaRegistry,
1219 ) -> Self {
1220 Self {
1221 info: Info {
1222 title: title.into(),
1223 version: version.into(),
1224 description: None,
1225 terms_of_service: None,
1226 contact: None,
1227 license: None,
1228 },
1229 servers: Vec::new(),
1230 paths: HashMap::new(),
1231 components: Components::default(),
1232 tags: Vec::new(),
1233 security: Vec::new(),
1234 registry,
1235 }
1236 }
1237
1238 #[must_use]
1243 pub fn registry(&self) -> &SchemaRegistry {
1244 &self.registry
1245 }
1246
1247 #[must_use]
1249 pub fn description(mut self, description: impl Into<String>) -> Self {
1250 self.info.description = Some(description.into());
1251 self
1252 }
1253
1254 #[must_use]
1256 pub fn server(mut self, url: impl Into<String>, description: Option<String>) -> Self {
1257 self.servers.push(Server {
1258 url: url.into(),
1259 description,
1260 });
1261 self
1262 }
1263
1264 #[must_use]
1266 pub fn tag(mut self, name: impl Into<String>, description: Option<String>) -> Self {
1267 self.tags.push(Tag {
1268 name: name.into(),
1269 description,
1270 });
1271 self
1272 }
1273
1274 #[must_use]
1276 pub fn schema(mut self, name: impl Into<String>, schema: Schema) -> Self {
1277 self.components.schemas.insert(name.into(), schema);
1278 self
1279 }
1280
1281 #[must_use]
1304 pub fn security_scheme(mut self, name: impl Into<String>, scheme: SecurityScheme) -> Self {
1305 self.components.security_schemes.insert(name.into(), scheme);
1306 self
1307 }
1308
1309 #[must_use]
1325 pub fn security_requirement(mut self, scheme: impl Into<String>, scopes: Vec<String>) -> Self {
1326 let mut req = SecurityRequirement::new();
1327 req.insert(scheme.into(), scopes);
1328 self.security.push(req);
1329 self
1330 }
1331
1332 pub fn add_route(&mut self, route: &Route) {
1354 let operation = self.route_to_operation(route);
1355 let path_item = self.paths.entry(route.path.clone()).or_default();
1356
1357 match route.method {
1358 Method::Get => path_item.get = Some(operation),
1359 Method::Post => path_item.post = Some(operation),
1360 Method::Put => path_item.put = Some(operation),
1361 Method::Delete => path_item.delete = Some(operation),
1362 Method::Patch => path_item.patch = Some(operation),
1363 Method::Options => path_item.options = Some(operation),
1364 Method::Head => path_item.head = Some(operation),
1365 Method::Trace => {
1366 }
1369 }
1370 }
1371
1372 pub fn add_routes(&mut self, routes: &[Route]) {
1376 for route in routes {
1377 self.add_route(route);
1378 }
1379 }
1380
1381 #[allow(clippy::unused_self)] fn route_to_operation(&self, route: &Route) -> Operation {
1384 let parameters: Vec<Parameter> = route
1386 .path_params
1387 .iter()
1388 .map(param_info_to_parameter)
1389 .collect();
1390
1391 let request_body = route.request_body_schema.as_ref().map(|schema_name| {
1393 let content_type = route
1394 .request_body_content_type
1395 .as_deref()
1396 .unwrap_or("application/json");
1397
1398 let mut content = HashMap::new();
1399 content.insert(
1400 content_type.to_string(),
1401 MediaType {
1402 schema: Some(Schema::reference(schema_name)),
1403 example: None,
1404 examples: HashMap::new(),
1405 },
1406 );
1407
1408 RequestBody {
1409 required: route.request_body_required,
1410 content,
1411 description: None,
1412 }
1413 });
1414
1415 let mut responses = HashMap::new();
1417 responses.insert(
1418 "200".to_string(),
1419 Response {
1420 description: "Successful response".to_string(),
1421 content: HashMap::new(),
1422 },
1423 );
1424
1425 let security: Vec<SecurityRequirement> = route
1427 .security
1428 .iter()
1429 .map(|req| {
1430 let mut sec_req = SecurityRequirement::new();
1431 sec_req.insert(req.scheme.clone(), req.scopes.clone());
1432 sec_req
1433 })
1434 .collect();
1435
1436 Operation {
1437 operation_id: if route.operation_id.is_empty() {
1438 None
1439 } else {
1440 Some(route.operation_id.clone())
1441 },
1442 summary: route.summary.clone(),
1443 description: route.description.clone(),
1444 tags: route.tags.clone(),
1445 parameters,
1446 request_body,
1447 responses,
1448 deprecated: route.deprecated,
1449 security,
1450 }
1451 }
1452
1453 #[must_use]
1457 pub fn build(self) -> OpenApi {
1458 let mut all_schemas = self.registry.into_schemas();
1460 for (name, schema) in self.components.schemas {
1461 all_schemas.insert(name, schema);
1463 }
1464
1465 OpenApi {
1466 openapi: "3.1.0".to_string(),
1467 info: self.info,
1468 servers: self.servers,
1469 paths: self.paths,
1470 components: if all_schemas.is_empty() && self.components.security_schemes.is_empty() {
1471 None
1472 } else {
1473 Some(Components {
1474 schemas: all_schemas,
1475 security_schemes: self.components.security_schemes,
1476 })
1477 },
1478 tags: self.tags,
1479 security: self.security,
1480 }
1481 }
1482}
1483
1484use fastapi_core::Method;
1489use fastapi_router::{Converter, ParamInfo, Route, extract_path_params};
1490
1491#[must_use]
1500pub fn converter_to_schema(converter: &Converter) -> Schema {
1501 match converter {
1502 Converter::Int => Schema::integer(Some("int64")),
1503 Converter::Float => Schema::number(Some("double")),
1504 Converter::Uuid => Schema::Primitive(crate::schema::PrimitiveSchema {
1505 schema_type: crate::schema::SchemaType::String,
1506 format: Some("uuid".to_string()),
1507 nullable: false,
1508 minimum: None,
1509 maximum: None,
1510 exclusive_minimum: None,
1511 exclusive_maximum: None,
1512 min_length: None,
1513 max_length: None,
1514 pattern: None,
1515 enum_values: None,
1516 example: None,
1517 }),
1518 Converter::Str | Converter::Path => Schema::string(),
1520 }
1521}
1522
1523#[must_use]
1529pub fn param_info_to_parameter(param: &ParamInfo) -> Parameter {
1530 let examples: HashMap<String, Example> = param
1532 .examples
1533 .iter()
1534 .map(|(name, value)| {
1535 (
1536 name.clone(),
1537 Example {
1538 summary: None,
1539 description: None,
1540 value: Some(value.clone()),
1541 external_value: None,
1542 },
1543 )
1544 })
1545 .collect();
1546
1547 Parameter {
1548 name: param.name.clone(),
1549 location: ParameterLocation::Path,
1550 required: true, schema: Some(converter_to_schema(¶m.converter)),
1552 title: param.title.clone(),
1553 description: param.description.clone(),
1554 deprecated: param.deprecated,
1555 example: param.example.clone(),
1556 examples,
1557 }
1558}
1559
1560#[must_use]
1579pub fn path_params_to_parameters(path: &str) -> Vec<Parameter> {
1580 extract_path_params(path)
1581 .iter()
1582 .map(param_info_to_parameter)
1583 .collect()
1584}
1585
1586#[cfg(test)]
1591mod path_param_tests {
1592 use super::*;
1593 use crate::schema::SchemaType;
1594
1595 #[test]
1596 fn converter_to_schema_str() {
1597 let schema = converter_to_schema(&Converter::Str);
1598 if let Schema::Primitive(p) = schema {
1599 assert!(matches!(p.schema_type, SchemaType::String));
1600 assert!(p.format.is_none());
1601 } else {
1602 panic!("Expected primitive schema");
1603 }
1604 }
1605
1606 #[test]
1607 fn converter_to_schema_int() {
1608 let schema = converter_to_schema(&Converter::Int);
1609 if let Schema::Primitive(p) = schema {
1610 assert!(matches!(p.schema_type, SchemaType::Integer));
1611 assert_eq!(p.format.as_deref(), Some("int64"));
1612 } else {
1613 panic!("Expected primitive schema");
1614 }
1615 }
1616
1617 #[test]
1618 fn converter_to_schema_float() {
1619 let schema = converter_to_schema(&Converter::Float);
1620 if let Schema::Primitive(p) = schema {
1621 assert!(matches!(p.schema_type, SchemaType::Number));
1622 assert_eq!(p.format.as_deref(), Some("double"));
1623 } else {
1624 panic!("Expected primitive schema");
1625 }
1626 }
1627
1628 #[test]
1629 fn converter_to_schema_uuid() {
1630 let schema = converter_to_schema(&Converter::Uuid);
1631 if let Schema::Primitive(p) = schema {
1632 assert!(matches!(p.schema_type, SchemaType::String));
1633 assert_eq!(p.format.as_deref(), Some("uuid"));
1634 } else {
1635 panic!("Expected primitive schema");
1636 }
1637 }
1638
1639 #[test]
1640 fn converter_to_schema_path() {
1641 let schema = converter_to_schema(&Converter::Path);
1642 if let Schema::Primitive(p) = schema {
1643 assert!(matches!(p.schema_type, SchemaType::String));
1644 } else {
1645 panic!("Expected primitive schema");
1646 }
1647 }
1648
1649 #[test]
1650 fn param_info_to_parameter_basic() {
1651 let param = param_info_to_parameter(&ParamInfo::new("id", Converter::Str));
1652
1653 assert_eq!(param.name, "id");
1654 assert!(matches!(param.location, ParameterLocation::Path));
1655 assert!(param.required);
1656 assert!(param.schema.is_some());
1657 }
1658
1659 #[test]
1660 fn param_info_to_parameter_int() {
1661 let param = param_info_to_parameter(&ParamInfo::new("item_id", Converter::Int));
1662
1663 assert_eq!(param.name, "item_id");
1664 assert!(param.required);
1665 if let Some(Schema::Primitive(p)) = ¶m.schema {
1666 assert!(matches!(p.schema_type, SchemaType::Integer));
1667 assert_eq!(p.format.as_deref(), Some("int64"));
1668 } else {
1669 panic!("Expected integer schema");
1670 }
1671 }
1672
1673 #[test]
1674 fn path_params_to_parameters_simple() {
1675 let params = path_params_to_parameters("/users/{id}");
1676 assert_eq!(params.len(), 1);
1677 assert_eq!(params[0].name, "id");
1678 assert!(matches!(params[0].location, ParameterLocation::Path));
1679 assert!(params[0].required);
1680 }
1681
1682 #[test]
1683 fn path_params_to_parameters_multiple() {
1684 let params = path_params_to_parameters("/users/{user_id}/posts/{post_id}");
1685 assert_eq!(params.len(), 2);
1686 assert_eq!(params[0].name, "user_id");
1687 assert_eq!(params[1].name, "post_id");
1688 }
1689
1690 #[test]
1691 fn path_params_to_parameters_typed() {
1692 let params = path_params_to_parameters("/items/{id:int}/price/{value:float}");
1693 assert_eq!(params.len(), 2);
1694
1695 if let Some(Schema::Primitive(p)) = ¶ms[0].schema {
1697 assert!(matches!(p.schema_type, SchemaType::Integer));
1698 } else {
1699 panic!("Expected integer schema for id");
1700 }
1701
1702 if let Some(Schema::Primitive(p)) = ¶ms[1].schema {
1704 assert!(matches!(p.schema_type, SchemaType::Number));
1705 } else {
1706 panic!("Expected number schema for value");
1707 }
1708 }
1709
1710 #[test]
1711 fn path_params_to_parameters_uuid() {
1712 let params = path_params_to_parameters("/resources/{uuid:uuid}");
1713 assert_eq!(params.len(), 1);
1714
1715 if let Some(Schema::Primitive(p)) = ¶ms[0].schema {
1716 assert!(matches!(p.schema_type, SchemaType::String));
1717 assert_eq!(p.format.as_deref(), Some("uuid"));
1718 } else {
1719 panic!("Expected string/uuid schema");
1720 }
1721 }
1722
1723 #[test]
1724 fn path_params_to_parameters_wildcard() {
1725 let params = path_params_to_parameters("/files/{*filepath}");
1726 assert_eq!(params.len(), 1);
1727 assert_eq!(params[0].name, "filepath");
1728
1729 if let Some(Schema::Primitive(p)) = ¶ms[0].schema {
1730 assert!(matches!(p.schema_type, SchemaType::String));
1731 } else {
1732 panic!("Expected string schema for wildcard");
1733 }
1734 }
1735
1736 #[test]
1737 fn path_params_to_parameters_no_params() {
1738 let params = path_params_to_parameters("/static/path");
1739 assert!(params.is_empty());
1740 }
1741
1742 #[test]
1743 fn path_params_to_parameters_serialization() {
1744 let params = path_params_to_parameters("/users/{id:int}");
1745 let json = serde_json::to_string(¶ms[0]).unwrap();
1746
1747 assert!(json.contains(r#""in":"path""#));
1749 assert!(json.contains(r#""required":true"#));
1751 assert!(json.contains(r#""type":"integer""#));
1753 assert!(json.contains(r#""format":"int64""#));
1754 }
1755
1756 #[test]
1757 fn path_params_complex_route() {
1758 let params = path_params_to_parameters("/api/v1/users/{user_id:int}/files/{*path}");
1759 assert_eq!(params.len(), 2);
1760
1761 assert_eq!(params[0].name, "user_id");
1763 if let Some(Schema::Primitive(p)) = ¶ms[0].schema {
1764 assert!(matches!(p.schema_type, SchemaType::Integer));
1765 } else {
1766 panic!("Expected integer schema");
1767 }
1768
1769 assert_eq!(params[1].name, "path");
1771 if let Some(Schema::Primitive(p)) = ¶ms[1].schema {
1772 assert!(matches!(p.schema_type, SchemaType::String));
1773 } else {
1774 panic!("Expected string schema");
1775 }
1776 }
1777
1778 #[test]
1783 fn param_info_with_title() {
1784 let info = ParamInfo::new("user_id", Converter::Int).with_title("User ID");
1785 let param = param_info_to_parameter(&info);
1786
1787 assert_eq!(param.title.as_deref(), Some("User ID"));
1788 }
1789
1790 #[test]
1791 fn param_info_with_description() {
1792 let info =
1793 ParamInfo::new("page", Converter::Int).with_description("Page number for pagination");
1794 let param = param_info_to_parameter(&info);
1795
1796 assert_eq!(
1797 param.description.as_deref(),
1798 Some("Page number for pagination")
1799 );
1800 }
1801
1802 #[test]
1803 fn param_info_deprecated() {
1804 let info = ParamInfo::new("old_id", Converter::Str).deprecated();
1805 let param = param_info_to_parameter(&info);
1806
1807 assert!(param.deprecated);
1808 }
1809
1810 #[test]
1811 fn param_info_with_example() {
1812 let info = ParamInfo::new("user_id", Converter::Int).with_example(serde_json::json!(42));
1813 let param = param_info_to_parameter(&info);
1814
1815 assert_eq!(param.example, Some(serde_json::json!(42)));
1816 }
1817
1818 #[test]
1819 fn param_info_with_named_examples() {
1820 let info = ParamInfo::new("status", Converter::Str)
1821 .with_named_example("active", serde_json::json!("active"))
1822 .with_named_example("inactive", serde_json::json!("inactive"));
1823 let param = param_info_to_parameter(&info);
1824
1825 assert_eq!(param.examples.len(), 2);
1826 assert!(param.examples.contains_key("active"));
1827 assert!(param.examples.contains_key("inactive"));
1828 assert_eq!(
1829 param.examples.get("active").unwrap().value,
1830 Some(serde_json::json!("active"))
1831 );
1832 }
1833
1834 #[test]
1835 fn param_info_all_metadata() {
1836 let info = ParamInfo::new("item_id", Converter::Int)
1837 .with_title("Item ID")
1838 .with_description("The unique identifier for the item")
1839 .deprecated()
1840 .with_example(serde_json::json!(123))
1841 .with_named_example("first", serde_json::json!(1))
1842 .with_named_example("last", serde_json::json!(999));
1843 let param = param_info_to_parameter(&info);
1844
1845 assert_eq!(param.name, "item_id");
1846 assert_eq!(param.title.as_deref(), Some("Item ID"));
1847 assert_eq!(
1848 param.description.as_deref(),
1849 Some("The unique identifier for the item")
1850 );
1851 assert!(param.deprecated);
1852 assert_eq!(param.example, Some(serde_json::json!(123)));
1853 assert_eq!(param.examples.len(), 2);
1854 }
1855
1856 #[test]
1857 fn param_info_metadata_serialization() {
1858 let info = ParamInfo::new("id", Converter::Int)
1859 .with_title("ID")
1860 .with_description("Resource identifier")
1861 .deprecated();
1862 let param = param_info_to_parameter(&info);
1863 let json = serde_json::to_string(¶m).unwrap();
1864
1865 assert!(json.contains(r#""title":"ID""#));
1866 assert!(json.contains(r#""description":"Resource identifier""#));
1867 assert!(json.contains(r#""deprecated":true"#));
1868 }
1869
1870 #[test]
1871 fn param_info_no_metadata_skips_fields() {
1872 let info = ParamInfo::new("id", Converter::Str);
1873 let param = param_info_to_parameter(&info);
1874 let json = serde_json::to_string(¶m).unwrap();
1875
1876 assert!(!json.contains("title"));
1878 assert!(!json.contains("description"));
1879 assert!(!json.contains("deprecated"));
1880 assert!(!json.contains("example"));
1881 }
1882}
1883
1884#[cfg(test)]
1889mod route_conversion_tests {
1890 use super::*;
1891 use crate::schema::SchemaType;
1892 use fastapi_router::Route;
1893
1894 fn make_test_route(path: &str, method: Method) -> Route {
1895 Route::with_placeholder_handler(method, path).operation_id("test_operation")
1896 }
1897
1898 fn make_full_route() -> Route {
1899 Route::with_placeholder_handler(Method::Get, "/users/{id:int}/posts/{post_id:int}")
1900 .operation_id("get_user_post")
1901 .summary("Get a user's post")
1902 .description("Retrieves a specific post by a user")
1903 .tag("users")
1904 .tag("posts")
1905 .deprecated()
1906 }
1907
1908 #[test]
1909 fn add_route_creates_operation_for_get() {
1910 let route = make_test_route("/users", Method::Get);
1911 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
1912 builder.add_route(&route);
1913 let doc = builder.build();
1914
1915 assert!(doc.paths.contains_key("/users"));
1916 let path_item = &doc.paths["/users"];
1917 assert!(path_item.get.is_some());
1918 assert!(path_item.post.is_none());
1919 }
1920
1921 #[test]
1922 fn add_route_creates_operation_for_post() {
1923 let route = make_test_route("/users", Method::Post);
1924 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
1925 builder.add_route(&route);
1926 let doc = builder.build();
1927
1928 let path_item = &doc.paths["/users"];
1929 assert!(path_item.post.is_some());
1930 assert!(path_item.get.is_none());
1931 }
1932
1933 #[test]
1934 fn add_route_merges_methods_on_same_path() {
1935 let get_route = make_test_route("/users", Method::Get);
1936 let post_route = make_test_route("/users", Method::Post);
1937
1938 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
1939 builder.add_route(&get_route);
1940 builder.add_route(&post_route);
1941 let doc = builder.build();
1942
1943 let path_item = &doc.paths["/users"];
1944 assert!(path_item.get.is_some());
1945 assert!(path_item.post.is_some());
1946 }
1947
1948 #[test]
1949 fn add_routes_batch_adds_multiple() {
1950 let routes = vec![
1951 make_test_route("/users", Method::Get),
1952 make_test_route("/users", Method::Post),
1953 make_test_route("/items", Method::Get),
1954 ];
1955
1956 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
1957 builder.add_routes(&routes);
1958 let doc = builder.build();
1959
1960 assert!(doc.paths.contains_key("/users"));
1961 assert!(doc.paths.contains_key("/items"));
1962 assert!(doc.paths["/users"].get.is_some());
1963 assert!(doc.paths["/users"].post.is_some());
1964 assert!(doc.paths["/items"].get.is_some());
1965 }
1966
1967 #[test]
1968 fn route_operation_id_is_preserved() {
1969 let route = Route::with_placeholder_handler(Method::Get, "/test")
1970 .operation_id("my_custom_operation");
1971
1972 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
1973 builder.add_route(&route);
1974 let doc = builder.build();
1975
1976 let op = doc.paths["/test"].get.as_ref().unwrap();
1977 assert_eq!(op.operation_id.as_deref(), Some("my_custom_operation"));
1978 }
1979
1980 #[test]
1981 fn route_summary_and_description_preserved() {
1982 let route = make_full_route();
1983 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
1984 builder.add_route(&route);
1985 let doc = builder.build();
1986
1987 let op = doc.paths["/users/{id:int}/posts/{post_id:int}"]
1988 .get
1989 .as_ref()
1990 .unwrap();
1991 assert_eq!(op.summary.as_deref(), Some("Get a user's post"));
1992 assert_eq!(
1993 op.description.as_deref(),
1994 Some("Retrieves a specific post by a user")
1995 );
1996 }
1997
1998 #[test]
1999 fn route_tags_preserved() {
2000 let route = make_full_route();
2001 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2002 builder.add_route(&route);
2003 let doc = builder.build();
2004
2005 let op = doc.paths["/users/{id:int}/posts/{post_id:int}"]
2006 .get
2007 .as_ref()
2008 .unwrap();
2009 assert!(op.tags.contains(&"users".to_string()));
2010 assert!(op.tags.contains(&"posts".to_string()));
2011 }
2012
2013 #[test]
2014 fn route_deprecated_preserved() {
2015 let route = make_full_route();
2016 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2017 builder.add_route(&route);
2018 let doc = builder.build();
2019
2020 let op = doc.paths["/users/{id:int}/posts/{post_id:int}"]
2021 .get
2022 .as_ref()
2023 .unwrap();
2024 assert!(op.deprecated);
2025 }
2026
2027 #[test]
2028 fn route_path_params_converted_to_parameters() {
2029 let route = make_full_route();
2030 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2031 builder.add_route(&route);
2032 let doc = builder.build();
2033
2034 let op = doc.paths["/users/{id:int}/posts/{post_id:int}"]
2035 .get
2036 .as_ref()
2037 .unwrap();
2038
2039 assert_eq!(op.parameters.len(), 2);
2041 assert_eq!(op.parameters[0].name, "id");
2042 assert_eq!(op.parameters[1].name, "post_id");
2043
2044 assert!(matches!(op.parameters[0].location, ParameterLocation::Path));
2046 assert!(matches!(op.parameters[1].location, ParameterLocation::Path));
2047 assert!(op.parameters[0].required);
2048 assert!(op.parameters[1].required);
2049
2050 if let Some(Schema::Primitive(p)) = &op.parameters[0].schema {
2052 assert!(matches!(p.schema_type, SchemaType::Integer));
2053 } else {
2054 panic!("Expected integer schema for id");
2055 }
2056 }
2057
2058 #[test]
2059 fn route_with_request_body() {
2060 let route = Route::with_placeholder_handler(Method::Post, "/users")
2061 .operation_id("create_user")
2062 .request_body("CreateUserRequest", "application/json", true);
2063
2064 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2065 builder.add_route(&route);
2066 let doc = builder.build();
2067
2068 let op = doc.paths["/users"].post.as_ref().unwrap();
2069 let body = op.request_body.as_ref().expect("Expected request body");
2070
2071 assert!(body.required);
2072 assert!(body.content.contains_key("application/json"));
2073
2074 let media_type = &body.content["application/json"];
2075 if let Some(Schema::Ref(ref_schema)) = &media_type.schema {
2076 assert_eq!(
2077 ref_schema.reference,
2078 "#/components/schemas/CreateUserRequest"
2079 );
2080 } else {
2081 panic!("Expected $ref schema for request body");
2082 }
2083 }
2084
2085 #[test]
2086 fn route_with_custom_content_type() {
2087 let route = Route::with_placeholder_handler(Method::Post, "/upload")
2088 .operation_id("upload_file")
2089 .request_body("FileUpload", "multipart/form-data", false);
2090
2091 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2092 builder.add_route(&route);
2093 let doc = builder.build();
2094
2095 let op = doc.paths["/upload"].post.as_ref().unwrap();
2096 let body = op.request_body.as_ref().unwrap();
2097 assert!(body.content.contains_key("multipart/form-data"));
2098 }
2099
2100 #[test]
2101 fn route_without_request_body() {
2102 let route = make_test_route("/users", Method::Get);
2103 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2104 builder.add_route(&route);
2105 let doc = builder.build();
2106
2107 let op = doc.paths["/users"].get.as_ref().unwrap();
2108 assert!(op.request_body.is_none());
2109 }
2110
2111 #[test]
2112 fn all_http_methods_supported() {
2113 let methods = [
2114 Method::Get,
2115 Method::Post,
2116 Method::Put,
2117 Method::Delete,
2118 Method::Patch,
2119 Method::Options,
2120 Method::Head,
2121 ];
2122
2123 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2124 for method in methods {
2125 builder.add_route(&make_test_route("/test", method));
2126 }
2127 let doc = builder.build();
2128
2129 let path_item = &doc.paths["/test"];
2130 assert!(path_item.get.is_some());
2131 assert!(path_item.post.is_some());
2132 assert!(path_item.put.is_some());
2133 assert!(path_item.delete.is_some());
2134 assert!(path_item.patch.is_some());
2135 assert!(path_item.options.is_some());
2136 assert!(path_item.head.is_some());
2137 }
2138
2139 #[test]
2140 fn default_response_is_added() {
2141 let route = make_test_route("/users", Method::Get);
2142 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2143 builder.add_route(&route);
2144 let doc = builder.build();
2145
2146 let op = doc.paths["/users"].get.as_ref().unwrap();
2147 assert!(op.responses.contains_key("200"));
2148 assert_eq!(op.responses["200"].description, "Successful response");
2149 }
2150
2151 #[test]
2152 fn route_conversion_serializes_to_valid_json() {
2153 let route = make_full_route();
2154 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2155 builder.add_route(&route);
2156 let doc = builder.build();
2157
2158 let json = serde_json::to_string(&doc).unwrap();
2160
2161 assert!(json.contains(r#""operationId":"get_user_post""#));
2163 assert!(json.contains(r#""summary":"Get a user's post""#));
2164 assert!(json.contains(r#""deprecated":true"#));
2165 assert!(json.contains(r#""in":"path""#));
2166 assert!(json.contains(r#""required":true"#));
2167 }
2168
2169 #[test]
2170 fn empty_operation_id_becomes_none() {
2171 let route = Route::with_placeholder_handler(Method::Get, "/test").operation_id("");
2172
2173 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2174 builder.add_route(&route);
2175 let doc = builder.build();
2176
2177 let op = doc.paths["/test"].get.as_ref().unwrap();
2178 assert!(op.operation_id.is_none());
2179
2180 let json = serde_json::to_string(&doc).unwrap();
2182 assert!(!json.contains("operationId"));
2183 }
2184}
2185
2186#[cfg(test)]
2191mod security_tests {
2192 use super::*;
2193
2194 #[test]
2195 fn api_key_header_security_scheme() {
2196 let scheme = SecurityScheme::ApiKey {
2197 name: "X-API-Key".to_string(),
2198 location: ApiKeyLocation::Header,
2199 description: Some("API key for authentication".to_string()),
2200 };
2201
2202 let json = serde_json::to_string(&scheme).unwrap();
2203 assert!(json.contains(r#""type":"apiKey""#));
2204 assert!(json.contains(r#""name":"X-API-Key""#));
2205 assert!(json.contains(r#""in":"header""#));
2206 assert!(json.contains(r#""description":"API key for authentication""#));
2207 }
2208
2209 #[test]
2210 fn api_key_query_security_scheme() {
2211 let scheme = SecurityScheme::ApiKey {
2212 name: "api_key".to_string(),
2213 location: ApiKeyLocation::Query,
2214 description: None,
2215 };
2216
2217 let json = serde_json::to_string(&scheme).unwrap();
2218 assert!(json.contains(r#""type":"apiKey""#));
2219 assert!(json.contains(r#""in":"query""#));
2220 assert!(!json.contains("description"));
2221 }
2222
2223 #[test]
2224 fn http_bearer_security_scheme() {
2225 let scheme = SecurityScheme::Http {
2226 scheme: "bearer".to_string(),
2227 bearer_format: Some("JWT".to_string()),
2228 description: None,
2229 };
2230
2231 let json = serde_json::to_string(&scheme).unwrap();
2232 assert!(json.contains(r#""type":"http""#));
2233 assert!(json.contains(r#""scheme":"bearer""#));
2234 assert!(json.contains(r#""bearerFormat":"JWT""#));
2235 }
2236
2237 #[test]
2238 fn http_basic_security_scheme() {
2239 let scheme = SecurityScheme::Http {
2240 scheme: "basic".to_string(),
2241 bearer_format: None,
2242 description: Some("Basic HTTP authentication".to_string()),
2243 };
2244
2245 let json = serde_json::to_string(&scheme).unwrap();
2246 assert!(json.contains(r#""type":"http""#));
2247 assert!(json.contains(r#""scheme":"basic""#));
2248 assert!(!json.contains("bearerFormat"));
2249 }
2250
2251 #[test]
2252 fn oauth2_security_scheme() {
2253 let mut scopes = HashMap::new();
2254 scopes.insert("read:users".to_string(), "Read user data".to_string());
2255 scopes.insert("write:users".to_string(), "Modify user data".to_string());
2256
2257 let scheme = SecurityScheme::OAuth2 {
2258 flows: OAuth2Flows {
2259 authorization_code: Some(OAuth2Flow {
2260 authorization_url: Some("https://example.com/oauth/authorize".to_string()),
2261 token_url: Some("https://example.com/oauth/token".to_string()),
2262 refresh_url: None,
2263 scopes,
2264 }),
2265 ..Default::default()
2266 },
2267 description: None,
2268 };
2269
2270 let json = serde_json::to_string(&scheme).unwrap();
2271 assert!(json.contains(r#""type":"oauth2""#));
2272 assert!(json.contains(r#""authorizationCode""#));
2273 assert!(json.contains(r#""authorizationUrl""#));
2274 assert!(json.contains(r#""tokenUrl""#));
2275 assert!(json.contains(r#""read:users""#));
2276 }
2277
2278 #[test]
2279 fn openid_connect_security_scheme() {
2280 let scheme = SecurityScheme::OpenIdConnect {
2281 open_id_connect_url: "https://example.com/.well-known/openid-configuration".to_string(),
2282 description: Some("OpenID Connect authentication".to_string()),
2283 };
2284
2285 let json = serde_json::to_string(&scheme).unwrap();
2286 assert!(json.contains(r#""type":"openIdConnect""#));
2287 assert!(json.contains(r#""openIdConnectUrl""#));
2288 }
2289
2290 #[test]
2291 fn builder_adds_security_scheme() {
2292 let doc = OpenApiBuilder::new("Test API", "1.0.0")
2293 .security_scheme(
2294 "api_key",
2295 SecurityScheme::ApiKey {
2296 name: "X-API-Key".to_string(),
2297 location: ApiKeyLocation::Header,
2298 description: None,
2299 },
2300 )
2301 .build();
2302
2303 assert!(doc.components.is_some());
2304 let components = doc.components.as_ref().unwrap();
2305 assert!(components.security_schemes.contains_key("api_key"));
2306 }
2307
2308 #[test]
2309 fn builder_adds_global_security_requirement() {
2310 let doc = OpenApiBuilder::new("Test API", "1.0.0")
2311 .security_scheme(
2312 "bearer",
2313 SecurityScheme::Http {
2314 scheme: "bearer".to_string(),
2315 bearer_format: Some("JWT".to_string()),
2316 description: None,
2317 },
2318 )
2319 .security_requirement("bearer", vec![])
2320 .build();
2321
2322 assert_eq!(doc.security.len(), 1);
2323 assert!(doc.security[0].contains_key("bearer"));
2324 }
2325
2326 #[test]
2327 fn builder_adds_security_with_scopes() {
2328 let doc = OpenApiBuilder::new("Test API", "1.0.0")
2329 .security_requirement(
2330 "oauth2",
2331 vec!["read:users".to_string(), "write:users".to_string()],
2332 )
2333 .build();
2334
2335 assert_eq!(doc.security.len(), 1);
2336 let scopes = doc.security[0].get("oauth2").unwrap();
2337 assert_eq!(scopes.len(), 2);
2338 assert!(scopes.contains(&"read:users".to_string()));
2339 assert!(scopes.contains(&"write:users".to_string()));
2340 }
2341
2342 #[test]
2343 fn full_security_document_serializes() {
2344 let doc = OpenApiBuilder::new("Secure API", "1.0.0")
2345 .security_scheme(
2346 "api_key",
2347 SecurityScheme::ApiKey {
2348 name: "X-API-Key".to_string(),
2349 location: ApiKeyLocation::Header,
2350 description: Some("API key authentication".to_string()),
2351 },
2352 )
2353 .security_scheme(
2354 "bearer",
2355 SecurityScheme::Http {
2356 scheme: "bearer".to_string(),
2357 bearer_format: Some("JWT".to_string()),
2358 description: None,
2359 },
2360 )
2361 .security_requirement("api_key", vec![])
2362 .build();
2363
2364 let json = serde_json::to_string_pretty(&doc).unwrap();
2365
2366 assert!(json.contains(r#""securitySchemes""#));
2368 assert!(json.contains(r#""api_key""#));
2369 assert!(json.contains(r#""bearer""#));
2370 assert!(json.contains(r#""security""#));
2371 }
2372
2373 #[test]
2374 fn route_with_security_scheme() {
2375 let route = Route::with_placeholder_handler(Method::Get, "/protected")
2376 .operation_id("get_protected")
2377 .security_scheme("bearer");
2378
2379 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2380 builder.add_route(&route);
2381 let doc = builder.build();
2382
2383 let op = doc.paths["/protected"].get.as_ref().unwrap();
2384 assert_eq!(op.security.len(), 1);
2385 assert!(op.security[0].contains_key("bearer"));
2386 assert!(op.security[0].get("bearer").unwrap().is_empty());
2387 }
2388
2389 #[test]
2390 fn route_with_security_and_scopes() {
2391 let route = Route::with_placeholder_handler(Method::Post, "/users")
2392 .operation_id("create_user")
2393 .security("oauth2", vec!["write:users"]);
2394
2395 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2396 builder.add_route(&route);
2397 let doc = builder.build();
2398
2399 let op = doc.paths["/users"].post.as_ref().unwrap();
2400 assert_eq!(op.security.len(), 1);
2401 let scopes = op.security[0].get("oauth2").unwrap();
2402 assert_eq!(scopes.len(), 1);
2403 assert_eq!(scopes[0], "write:users");
2404 }
2405
2406 #[test]
2407 fn route_with_multiple_security_options() {
2408 let route = Route::with_placeholder_handler(Method::Get, "/data")
2409 .operation_id("get_data")
2410 .security_scheme("api_key")
2411 .security_scheme("bearer");
2412
2413 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2414 builder.add_route(&route);
2415 let doc = builder.build();
2416
2417 let op = doc.paths["/data"].get.as_ref().unwrap();
2418 assert_eq!(op.security.len(), 2);
2420 assert!(op.security[0].contains_key("api_key"));
2421 assert!(op.security[1].contains_key("bearer"));
2422 }
2423
2424 #[test]
2425 fn route_security_serializes_correctly() {
2426 let route = Route::with_placeholder_handler(Method::Get, "/protected")
2427 .operation_id("protected")
2428 .security("oauth2", vec!["read:data", "write:data"]);
2429
2430 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2431 builder.add_route(&route);
2432 let doc = builder.build();
2433
2434 let json = serde_json::to_string(&doc).unwrap();
2435 assert!(json.contains(r#""security""#));
2436 assert!(json.contains(r#""oauth2""#));
2437 assert!(json.contains(r#""read:data""#));
2438 assert!(json.contains(r#""write:data""#));
2439 }
2440
2441 #[test]
2442 fn route_without_security_has_empty_security() {
2443 let route = Route::with_placeholder_handler(Method::Get, "/public").operation_id("public");
2444
2445 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
2446 builder.add_route(&route);
2447 let doc = builder.build();
2448
2449 let op = doc.paths["/public"].get.as_ref().unwrap();
2450 assert!(op.security.is_empty());
2451 }
2452}
2453
2454#[cfg(test)]
2459mod response_example_tests {
2460 use super::*;
2461 use crate::schema::{ObjectSchema, PrimitiveSchema, Schema, SchemaType};
2462
2463 #[test]
2464 fn media_type_with_example_serializes() {
2465 let mt = MediaType {
2466 schema: Some(Schema::string()),
2467 example: Some(serde_json::json!("hello")),
2468 examples: HashMap::new(),
2469 };
2470 let json = serde_json::to_value(&mt).unwrap();
2471 assert_eq!(json["example"], "hello");
2472 }
2473
2474 #[test]
2475 fn media_type_without_example_omits_field() {
2476 let mt = MediaType {
2477 schema: Some(Schema::string()),
2478 example: None,
2479 examples: HashMap::new(),
2480 };
2481 let json = serde_json::to_value(&mt).unwrap();
2482 assert!(!json.as_object().unwrap().contains_key("example"));
2483 assert!(!json.as_object().unwrap().contains_key("examples"));
2484 }
2485
2486 #[test]
2487 fn media_type_with_named_examples() {
2488 let mut examples = HashMap::new();
2489 examples.insert(
2490 "success".to_string(),
2491 Example {
2492 summary: Some("A success response".to_string()),
2493 description: None,
2494 value: Some(serde_json::json!({"id": 1, "name": "Alice"})),
2495 external_value: None,
2496 },
2497 );
2498 let mt = MediaType {
2499 schema: None,
2500 example: None,
2501 examples,
2502 };
2503 let json = serde_json::to_value(&mt).unwrap();
2504 assert_eq!(json["examples"]["success"]["value"]["name"], "Alice");
2505 }
2506
2507 #[test]
2508 fn object_schema_with_example() {
2509 let schema = ObjectSchema {
2510 title: Some("User".to_string()),
2511 description: None,
2512 properties: HashMap::new(),
2513 required: Vec::new(),
2514 additional_properties: None,
2515 example: Some(serde_json::json!({"name": "Bob", "age": 30})),
2516 };
2517 let json = serde_json::to_value(&schema).unwrap();
2518 assert_eq!(json["example"]["name"], "Bob");
2519 }
2520
2521 #[test]
2522 fn primitive_schema_with_example() {
2523 let schema = PrimitiveSchema {
2524 schema_type: SchemaType::String,
2525 format: Some("email".to_string()),
2526 nullable: false,
2527 minimum: None,
2528 maximum: None,
2529 exclusive_minimum: None,
2530 exclusive_maximum: None,
2531 min_length: None,
2532 max_length: None,
2533 pattern: None,
2534 enum_values: None,
2535 example: Some(serde_json::json!("user@example.com")),
2536 };
2537 let json = serde_json::to_value(&schema).unwrap();
2538 assert_eq!(json["example"], "user@example.com");
2539 assert_eq!(json["format"], "email");
2540 }
2541
2542 #[test]
2543 fn response_with_example_content() {
2544 let mut content = HashMap::new();
2545 content.insert(
2546 "application/json".to_string(),
2547 MediaType {
2548 schema: Some(Schema::reference("User")),
2549 example: Some(serde_json::json!({"id": 1, "name": "Alice"})),
2550 examples: HashMap::new(),
2551 },
2552 );
2553 let response = Response {
2554 description: "Success".to_string(),
2555 content,
2556 };
2557 let json = serde_json::to_value(&response).unwrap();
2558 assert_eq!(
2559 json["content"]["application/json"]["example"]["name"],
2560 "Alice"
2561 );
2562 }
2563
2564 #[test]
2565 fn media_type_roundtrip() {
2566 let mt = MediaType {
2567 schema: Some(Schema::string()),
2568 example: Some(serde_json::json!(42)),
2569 examples: HashMap::new(),
2570 };
2571 let json = serde_json::to_string(&mt).unwrap();
2572 let parsed: MediaType = serde_json::from_str(&json).unwrap();
2573 assert_eq!(parsed.example.unwrap(), 42);
2574 }
2575}