1use serde::{
4 de::{Error, Expected, Visitor},
5 Deserialize, Deserializer, Serialize, Serializer,
6};
7use std::fmt::Formatter;
8
9use self::path::PathsMap;
10pub use self::{
11 content::{Content, ContentBuilder},
12 external_docs::ExternalDocs,
13 header::{Header, HeaderBuilder},
14 info::{Contact, ContactBuilder, Info, InfoBuilder, License, LicenseBuilder},
15 path::{HttpMethod, PathItem, Paths, PathsBuilder},
16 response::{Response, ResponseBuilder, Responses, ResponsesBuilder},
17 schema::{
18 AllOf, AllOfBuilder, Array, ArrayBuilder, Components, ComponentsBuilder, Discriminator,
19 KnownFormat, Object, ObjectBuilder, OneOf, OneOfBuilder, Ref, Schema, SchemaFormat,
20 ToArray, Type,
21 },
22 security::SecurityRequirement,
23 server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder},
24 tag::Tag,
25};
26
27pub mod content;
28pub mod encoding;
29pub mod example;
30pub mod extensions;
31pub mod external_docs;
32pub mod header;
33pub mod info;
34pub mod link;
35pub mod path;
36pub mod request_body;
37pub mod response;
38pub mod schema;
39pub mod security;
40pub mod server;
41pub mod tag;
42pub mod xml;
43
44builder! {
45 OpenApiBuilder;
59
60 #[non_exhaustive]
69 #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
70 #[cfg_attr(feature = "debug", derive(Debug))]
71 #[serde(rename_all = "camelCase")]
72 pub struct OpenApi {
73 pub openapi: OpenApiVersion,
75
76 pub info: Info,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
87 pub servers: Option<Vec<Server>>,
88
89 pub paths: Paths,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
103 pub webhooks: Option<Paths>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
111 pub components: Option<Components>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
119 pub security: Option<Vec<SecurityRequirement>>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
125 pub tags: Option<Vec<Tag>>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
131 pub external_docs: Option<ExternalDocs>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
139 pub json_schema_dialect: Option<String>,
140
141 #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
146 pub schema: String,
147
148 #[serde(rename = "$self", skip_serializing_if = "Option::is_none")]
153 pub self_uri: Option<String>,
154
155 #[serde(skip_serializing_if = "Option::is_none", flatten)]
157 pub extensions: Option<Extensions>,
158 }
159}
160
161impl OpenApi {
162 pub fn new<P: Into<Paths>>(info: Info, paths: P) -> Self {
175 Self {
176 info,
177 paths: paths.into(),
178 ..Default::default()
179 }
180 }
181
182 pub fn to_json(&self) -> Result<String, serde_json::Error> {
184 serde_json::to_string(self)
185 }
186
187 pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
189 serde_json::to_string_pretty(self)
190 }
191
192 #[cfg(feature = "yaml")]
194 #[cfg_attr(doc_cfg, doc(cfg(feature = "yaml")))]
195 pub fn to_yaml(&self) -> Result<String, yaml_serde::Error> {
196 yaml_serde::to_string(self)
197 }
198
199 pub fn merge_from(mut self, other: OpenApi) -> OpenApi {
204 self.merge(other);
205 self
206 }
207
208 pub fn merge(&mut self, mut other: OpenApi) {
223 if let Some(other_servers) = &mut other.servers {
224 let servers = self.servers.get_or_insert(Vec::new());
225 other_servers.retain(|server| !servers.contains(server));
226 servers.append(other_servers);
227 }
228
229 if !other.paths.paths.is_empty() {
230 self.paths.merge(other.paths);
231 };
232
233 if let Some(other_components) = &mut other.components {
234 let components = self.components.get_or_insert(Components::default());
235
236 other_components
237 .schemas
238 .retain(|name, _| !components.schemas.contains_key(name));
239 components.schemas.append(&mut other_components.schemas);
240
241 other_components
242 .responses
243 .retain(|name, _| !components.responses.contains_key(name));
244 components.responses.append(&mut other_components.responses);
245
246 other_components
247 .security_schemes
248 .retain(|name, _| !components.security_schemes.contains_key(name));
249 components
250 .security_schemes
251 .append(&mut other_components.security_schemes);
252 }
253
254 if let Some(other_security) = &mut other.security {
255 let security = self.security.get_or_insert(Vec::new());
256 other_security.retain(|requirement| !security.contains(requirement));
257 security.append(other_security);
258 }
259
260 if let Some(other_tags) = &mut other.tags {
261 let tags = self.tags.get_or_insert(Vec::new());
262 other_tags.retain(|tag| !tags.contains(tag));
263 tags.append(other_tags);
264 }
265 }
266
267 pub fn nest<P: Into<String>, O: Into<OpenApi>>(self, path: P, other: O) -> Self {
309 self.nest_with_path_composer(path, other, |base, path| format!("{base}{path}"))
310 }
311
312 pub fn nest_with_path_composer<
319 P: Into<String>,
320 O: Into<OpenApi>,
321 F: Fn(&str, &str) -> String,
322 >(
323 mut self,
324 path: P,
325 other: O,
326 composer: F,
327 ) -> Self {
328 let path: String = path.into();
329 let mut other_api: OpenApi = other.into();
330
331 let nested_paths = other_api
332 .paths
333 .paths
334 .into_iter()
335 .map(|(item_path, item)| {
336 let path = composer(&path, &item_path);
337 (path, item)
338 })
339 .collect::<PathsMap<_, _>>();
340
341 self.paths.paths.extend(nested_paths);
342
343 other_api.paths.paths = PathsMap::new();
345 self.merge_from(other_api)
346 }
347}
348
349impl OpenApiBuilder {
350 pub fn openapi(mut self, openapi: OpenApiVersion) -> Self {
366 set_value!(self openapi openapi)
367 }
368
369 pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
371 set_value!(self info info.into())
372 }
373
374 pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: Option<I>) -> Self {
376 set_value!(self servers servers.map(|servers| servers.into_iter().collect()))
377 }
378
379 pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
381 set_value!(self paths paths.into())
382 }
383
384 pub fn webhooks<P: Into<Paths>>(mut self, webhooks: Option<P>) -> Self {
407 set_value!(self webhooks webhooks.map(Into::into))
408 }
409
410 pub fn components(mut self, components: Option<Components>) -> Self {
412 set_value!(self components components)
413 }
414
415 pub fn security<I: IntoIterator<Item = SecurityRequirement>>(
417 mut self,
418 security: Option<I>,
419 ) -> Self {
420 set_value!(self security security.map(|security| security.into_iter().collect()))
421 }
422
423 pub fn tags<I: IntoIterator<Item = Tag>>(mut self, tags: Option<I>) -> Self {
425 set_value!(self tags tags.map(|tags| tags.into_iter().collect()))
426 }
427
428 pub fn external_docs(mut self, external_docs: Option<ExternalDocs>) -> Self {
430 set_value!(self external_docs external_docs)
431 }
432
433 pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
445 set_value!(self schema schema.into())
446 }
447
448 pub fn json_schema_dialect<S: Into<String>>(mut self, json_schema_dialect: Option<S>) -> Self {
459 set_value!(self json_schema_dialect json_schema_dialect.map(Into::into))
460 }
461
462 pub fn self_uri<S: Into<String>>(mut self, self_uri: Option<S>) -> Self {
473 set_value!(self self_uri self_uri.map(Into::into))
474 }
475}
476
477#[derive(Serialize, Clone, PartialEq, Eq, Default)]
481#[cfg_attr(feature = "debug", derive(Debug))]
482pub enum OpenApiVersion {
483 #[serde(rename = "3.1.0")]
485 #[default]
486 Version31,
487 #[serde(rename = "3.2.0")]
489 Version32,
490}
491
492impl<'de> Deserialize<'de> for OpenApiVersion {
493 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
494 where
495 D: Deserializer<'de>,
496 {
497 struct VersionVisitor;
498
499 impl<'v> Visitor<'v> for VersionVisitor {
500 type Value = OpenApiVersion;
501
502 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
503 formatter.write_str("a version string in 3.1.x or 3.2.x format")
504 }
505
506 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
507 where
508 E: Error,
509 {
510 self.visit_string(v.to_string())
511 }
512
513 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
514 where
515 E: Error,
516 {
517 let version = v
518 .split('.')
519 .flat_map(|digit| digit.parse::<i8>())
520 .collect::<Vec<_>>();
521
522 if matches!(version.as_slice(), [3, 1, _]) {
523 Ok(OpenApiVersion::Version31)
524 } else if matches!(version.as_slice(), [3, 2, _]) {
525 Ok(OpenApiVersion::Version32)
526 } else {
527 let expected: &dyn Expected = &"3.1.0 or 3.2.0";
528 Err(Error::invalid_value(
529 serde::de::Unexpected::Str(&v),
530 expected,
531 ))
532 }
533 }
534 }
535
536 deserializer.deserialize_string(VersionVisitor)
537 }
538}
539
540#[derive(PartialEq, Eq, Clone, Default)]
544#[cfg_attr(feature = "debug", derive(Debug))]
545#[allow(missing_docs)]
546pub enum Deprecated {
547 True,
548 #[default]
549 False,
550}
551
552impl Serialize for Deprecated {
553 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
554 where
555 S: Serializer,
556 {
557 serializer.serialize_bool(matches!(self, Self::True))
558 }
559}
560
561impl<'de> Deserialize<'de> for Deprecated {
562 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
563 where
564 D: serde::Deserializer<'de>,
565 {
566 struct BoolVisitor;
567 impl<'de> Visitor<'de> for BoolVisitor {
568 type Value = Deprecated;
569
570 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
571 formatter.write_str("a bool true or false")
572 }
573
574 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
575 where
576 E: serde::de::Error,
577 {
578 match v {
579 true => Ok(Deprecated::True),
580 false => Ok(Deprecated::False),
581 }
582 }
583 }
584 deserializer.deserialize_bool(BoolVisitor)
585 }
586}
587
588#[derive(PartialEq, Eq, Clone, Default)]
592#[allow(missing_docs)]
593#[cfg_attr(feature = "debug", derive(Debug))]
594pub enum Required {
595 True,
596 #[default]
597 False,
598}
599
600impl Serialize for Required {
601 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
602 where
603 S: Serializer,
604 {
605 serializer.serialize_bool(matches!(self, Self::True))
606 }
607}
608
609impl<'de> Deserialize<'de> for Required {
610 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
611 where
612 D: serde::Deserializer<'de>,
613 {
614 struct BoolVisitor;
615 impl<'de> Visitor<'de> for BoolVisitor {
616 type Value = Required;
617
618 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
619 formatter.write_str("a bool true or false")
620 }
621
622 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
623 where
624 E: serde::de::Error,
625 {
626 match v {
627 true => Ok(Required::True),
628 false => Ok(Required::False),
629 }
630 }
631 }
632 deserializer.deserialize_bool(BoolVisitor)
633 }
634}
635
636#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
641#[cfg_attr(feature = "debug", derive(Debug))]
642#[serde(untagged)]
643pub enum RefOr<T> {
644 Ref(Ref),
647 T(T),
650}
651
652macro_rules! build_fn {
653 ( $vis:vis $name:ident $( $field:ident ),+ ) => {
654 #[doc = concat!("Constructs a new [`", stringify!($name),"`] taking all fields values from this object.")]
655 $vis fn build(self) -> $name {
656 $name {
657 $(
658 $field: self.$field,
659 )*
660 }
661 }
662 };
663}
664pub(crate) use build_fn;
665
666macro_rules! set_value {
667 ( $self:ident $field:ident $value:expr ) => {{
668 $self.$field = $value;
669
670 $self
671 }};
672}
673pub(crate) use set_value;
674
675macro_rules! new {
676 ( $vis:vis $name:ident ) => {
677 #[doc = concat!("Constructs a new [`", stringify!($name),"`].")]
678 $vis fn new() -> $name {
679 $name {
680 ..Default::default()
681 }
682 }
683 };
684}
685pub(crate) use new;
686
687macro_rules! from {
688 ( $name:ident $to:ident $( $field:ident ),+ ) => {
689 impl From<$name> for $to {
690 fn from(value: $name) -> Self {
691 Self {
692 $( $field: value.$field, )*
693 }
694 }
695 }
696
697 impl From<$to> for $name {
698 fn from(value: $to) -> Self {
699 value.build()
700 }
701 }
702 };
703}
704pub(crate) use from;
705
706macro_rules! builder {
707 ( $( #[$builder_meta:meta] )* $builder_name:ident; $(#[$meta:meta])* $vis:vis $key:ident $name:ident $( $tt:tt )* ) => {
708 builder!( @type_impl $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
709 builder!( @builder_impl $( #[$builder_meta] )* $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
710 };
711
712 ( @type_impl $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
713 { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
714 ) => {
715 $( #[$meta] )*
716 $vis $key $name {
717 $( $( #[$field_meta] )* $field_vis $field: $field_ty, )*
718 }
719
720 impl $name {
721 #[doc = concat!("Construct a new ", stringify!($builder_name), ".")]
722 #[doc = ""]
723 #[doc = concat!("This is effectively same as calling [`", stringify!($builder_name), "::new`]")]
724 $vis fn builder() -> $builder_name {
725 $builder_name::new()
726 }
727 }
728 };
729
730 ( @builder_impl $( #[$builder_meta:meta] )* $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
731 { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
732 ) => {
733 #[doc = concat!("Builder for [`", stringify!($name),
734 "`] with chainable configuration methods to create a new [`", stringify!($name) , "`].")]
735 $( #[$builder_meta] )*
736 #[cfg_attr(feature = "debug", derive(Debug))]
737 $vis $key $builder_name {
738 $( $field: $field_ty, )*
739 }
740
741 impl Default for $builder_name {
742 fn default() -> Self {
743 let meta_default: $name = $name::default();
744 Self {
745 $( $field: meta_default.$field, )*
746 }
747 }
748 }
749
750 impl $builder_name {
751 crate::openapi::new!($vis $builder_name);
752 crate::openapi::build_fn!($vis $name $( $field ),* );
753 }
754
755 crate::openapi::from!($name $builder_name $( $field ),* );
756 };
757}
758use crate::openapi::extensions::Extensions;
759pub(crate) use builder;
760
761#[cfg(test)]
762mod tests {
763 use crate::openapi::{
764 encoding::EncodingBuilder,
765 example::ExampleBuilder,
766 info::InfoBuilder,
767 link::LinkBuilder,
768 path::{
769 OperationBuilder, Parameter, ParameterBuilder, ParameterIn, ParameterStyle,
770 PathItemBuilder, PathsBuilder,
771 },
772 request_body::RequestBodyBuilder,
773 security::{DeviceAuthorization, Flow, OAuth2, Scopes, SecurityScheme},
774 tag::TagBuilder,
775 xml::XmlBuilder,
776 };
777 use insta::assert_json_snapshot;
778 use serde_json::json;
779 use std::collections::BTreeMap;
780
781 use super::{response::Response, *};
782
783 #[test]
784 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
785 assert_eq!(serde_json::to_value(&OpenApiVersion::Version32)?, "3.2.0");
786 assert_eq!(serde_json::to_value(&OpenApiVersion::Version31)?, "3.1.0");
787 assert_eq!(
788 serde_json::from_str::<OpenApiVersion>("\"3.1.1\"")?,
789 OpenApiVersion::Version31
790 );
791 assert_eq!(
792 serde_json::from_str::<OpenApiVersion>("\"3.2.0\"")?,
793 OpenApiVersion::Version32
794 );
795 Ok(())
796 }
797
798 #[test]
799 fn openapi_32_root_self_and_webhooks_serialize() {
800 let openapi = OpenApiBuilder::new()
801 .openapi(OpenApiVersion::Version32)
802 .info(Info::new("Events API", "1.0.0"))
803 .json_schema_dialect(Some("https://spec.openapis.org/oas/3.2/dialect/2025-09-17"))
804 .self_uri(Some("https://example.com/openapi.json"))
805 .paths(Paths::new())
806 .webhooks(Some(PathsBuilder::new().path(
807 "newPet",
808 PathItem::new(
809 HttpMethod::Post,
810 OperationBuilder::new().response("200", Response::new("Webhook received")),
811 ),
812 )))
813 .build();
814
815 assert_eq!(
816 serde_json::to_value(openapi).unwrap(),
817 json!({
818 "openapi": "3.2.0",
819 "$self": "https://example.com/openapi.json",
820 "jsonSchemaDialect": "https://spec.openapis.org/oas/3.2/dialect/2025-09-17",
821 "info": {
822 "title": "Events API",
823 "version": "1.0.0"
824 },
825 "paths": {},
826 "webhooks": {
827 "newPet": {
828 "post": {
829 "responses": {
830 "200": {
831 "description": "Webhook received"
832 }
833 }
834 }
835 }
836 }
837 })
838 );
839 }
840
841 #[test]
842 fn openapi_32_components_and_callbacks_use_spec_data_model() {
843 let callback_path = PathItemBuilder::new()
844 .query(Some(
845 OperationBuilder::new().response("200", ResponseBuilder::new()),
846 ))
847 .build();
848 let operation = OperationBuilder::new()
849 .callback(
850 "onEvent",
851 BTreeMap::from([(
852 "{$request.body#/callbackUrl}".to_string(),
853 callback_path.clone().into(),
854 )]),
855 )
856 .response("202", ResponseBuilder::new().summary(Some("Accepted")))
857 .build();
858 let components = ComponentsBuilder::new()
859 .parameter(
860 "Limit",
861 ParameterBuilder::new()
862 .name("limit")
863 .parameter_in(ParameterIn::Query)
864 .schema(Some(ObjectBuilder::new().schema_type(Type::Integer)))
865 .build(),
866 )
867 .example(
868 "Accepted",
869 ExampleBuilder::new().summary("Accepted").build(),
870 )
871 .request_body(
872 "EventRequest",
873 RequestBodyBuilder::new()
874 .content(
875 "application/json",
876 ContentBuilder::new()
877 .schema(Some(Ref::from_schema_name("EventPayload")))
878 .build(),
879 )
880 .build(),
881 )
882 .header(
883 "RateLimit",
884 HeaderBuilder::new()
885 .schema(Some(ObjectBuilder::new().schema_type(Type::Integer)))
886 .build(),
887 )
888 .link(
889 "GetEvent",
890 LinkBuilder::new().operation_id("getEvent").build(),
891 )
892 .callback(
893 "EventCallback",
894 BTreeMap::from([(
895 "{$request.body#/callbackUrl}".to_string(),
896 callback_path.clone().into(),
897 )]),
898 )
899 .path_item("EventPath", callback_path)
900 .media_type(
901 "SseEvent",
902 ContentBuilder::new()
903 .description(Some("Server-sent event item"))
904 .item_schema(Some(Ref::from_schema_name("ServerEvent")))
905 .item_encoding(Some(
906 EncodingBuilder::new().content_type(Some("application/json")),
907 ))
908 .prefix_encoding([EncodingBuilder::new().content_type(Some("text/plain"))])
909 .build(),
910 )
911 .security_scheme("ApiKey", RefOr::Ref(Ref::from_schema_name("ApiKey")))
912 .build();
913
914 assert_eq!(
915 serde_json::to_value(operation).unwrap(),
916 json!({
917 "responses": {
918 "202": {
919 "summary": "Accepted"
920 }
921 },
922 "callbacks": {
923 "onEvent": {
924 "{$request.body#/callbackUrl}": {
925 "query": {
926 "responses": {
927 "200": {}
928 }
929 }
930 }
931 }
932 }
933 })
934 );
935 assert_eq!(
936 serde_json::to_value(components).unwrap(),
937 json!({
938 "parameters": {
939 "Limit": {
940 "name": "limit",
941 "in": "query",
942 "required": false,
943 "schema": {
944 "type": "integer"
945 }
946 }
947 },
948 "examples": {
949 "Accepted": {
950 "summary": "Accepted"
951 }
952 },
953 "requestBodies": {
954 "EventRequest": {
955 "content": {
956 "application/json": {
957 "schema": {
958 "$ref": "#/components/schemas/EventPayload"
959 }
960 }
961 }
962 }
963 },
964 "headers": {
965 "RateLimit": {
966 "schema": {
967 "type": "integer"
968 }
969 }
970 },
971 "links": {
972 "GetEvent": {
973 "operationId": "getEvent"
974 }
975 },
976 "callbacks": {
977 "EventCallback": {
978 "{$request.body#/callbackUrl}": {
979 "query": {
980 "responses": {
981 "200": {}
982 }
983 }
984 }
985 }
986 },
987 "pathItems": {
988 "EventPath": {
989 "query": {
990 "responses": {
991 "200": {}
992 }
993 }
994 }
995 },
996 "mediaTypes": {
997 "SseEvent": {
998 "description": "Server-sent event item",
999 "itemSchema": {
1000 "$ref": "#/components/schemas/ServerEvent"
1001 },
1002 "prefixEncoding": [
1003 {
1004 "contentType": "text/plain"
1005 }
1006 ],
1007 "itemEncoding": {
1008 "contentType": "application/json"
1009 }
1010 }
1011 },
1012 "securitySchemes": {
1013 "ApiKey": {
1014 "$ref": "#/components/schemas/ApiKey"
1015 }
1016 }
1017 })
1018 );
1019 }
1020
1021 #[test]
1022 fn openapi_32_tags_server_xml_and_examples_serialize() {
1023 let tag = TagBuilder::new()
1024 .name("partner")
1025 .summary(Some("Partner"))
1026 .description(Some("Operations available to partners"))
1027 .parent(Some("external"))
1028 .kind(Some("audience"))
1029 .build();
1030 let server = ServerBuilder::new()
1031 .url("https://api.example.com")
1032 .name(Some("production"))
1033 .build();
1034 let xml = XmlBuilder::new()
1035 .name(Some("animal"))
1036 .node_type(Some("element"))
1037 .build();
1038 let example = ExampleBuilder::new()
1039 .summary("Serialized query")
1040 .data_value(Some(json!({"flag": true})))
1041 .serialized_value(Some("flag=true"))
1042 .build();
1043
1044 assert_eq!(
1045 serde_json::to_value(tag).unwrap(),
1046 json!({
1047 "name": "partner",
1048 "summary": "Partner",
1049 "description": "Operations available to partners",
1050 "parent": "external",
1051 "kind": "audience"
1052 })
1053 );
1054 assert_eq!(
1055 serde_json::to_value(server).unwrap(),
1056 json!({
1057 "url": "https://api.example.com",
1058 "name": "production"
1059 })
1060 );
1061 assert_eq!(
1062 serde_json::to_value(xml).unwrap(),
1063 json!({
1064 "name": "animal",
1065 "nodeType": "element"
1066 })
1067 );
1068 assert_eq!(
1069 serde_json::to_value(example).unwrap(),
1070 json!({
1071 "summary": "Serialized query",
1072 "dataValue": {
1073 "flag": true
1074 },
1075 "serializedValue": "flag=true"
1076 })
1077 );
1078 }
1079
1080 #[test]
1081 fn openapi_32_query_and_additional_operations_serialize() {
1082 let path_item = PathItemBuilder::new()
1083 .query(Some(
1084 OperationBuilder::new()
1085 .operation_id(Some("searchProducts"))
1086 .request_body(Some(
1087 request_body::RequestBodyBuilder::new()
1088 .content(
1089 "application/json",
1090 ContentBuilder::new()
1091 .schema(Some(Ref::from_schema_name("SearchCriteria")))
1092 .build(),
1093 )
1094 .build(),
1095 ))
1096 .response("200", Response::new("Search results")),
1097 ))
1098 .additional_operation(
1099 "COPY",
1100 OperationBuilder::new()
1101 .operation_id(Some("copyPet"))
1102 .response("200", Response::new("Copied")),
1103 )
1104 .build();
1105
1106 assert_eq!(
1107 serde_json::to_value(path_item).unwrap(),
1108 json!({
1109 "query": {
1110 "operationId": "searchProducts",
1111 "requestBody": {
1112 "content": {
1113 "application/json": {
1114 "schema": {
1115 "$ref": "#/components/schemas/SearchCriteria"
1116 }
1117 }
1118 }
1119 },
1120 "responses": {
1121 "200": {
1122 "description": "Search results"
1123 }
1124 }
1125 },
1126 "additionalOperations": {
1127 "COPY": {
1128 "operationId": "copyPet",
1129 "responses": {
1130 "200": {
1131 "description": "Copied"
1132 }
1133 }
1134 }
1135 }
1136 })
1137 );
1138 }
1139
1140 #[test]
1141 fn openapi_32_querystring_parameter_and_cookie_style_serialize() {
1142 let querystring = ParameterBuilder::from(Parameter::new("advancedQuery"))
1143 .parameter_in(ParameterIn::QueryString)
1144 .required(Required::False)
1145 .content(
1146 "application/x-www-form-urlencoded",
1147 ContentBuilder::new()
1148 .schema(Some(
1149 ObjectBuilder::new()
1150 .schema_type(Type::Object)
1151 .property("foo", ObjectBuilder::new().schema_type(Type::String))
1152 .property("bar", ObjectBuilder::new().schema_type(Type::Boolean)),
1153 ))
1154 .examples_from_iter([(
1155 "spacesAndPluses",
1156 ExampleBuilder::new()
1157 .description("Form-encoded query string")
1158 .data_value(Some(json!({
1159 "foo": "a + b",
1160 "bar": true
1161 })))
1162 .serialized_value(Some("foo=a+%2B+b&bar=true")),
1163 )])
1164 .build(),
1165 )
1166 .build();
1167 let cookie = ParameterBuilder::from(Parameter::new("greeting"))
1168 .parameter_in(ParameterIn::Cookie)
1169 .style(Some(ParameterStyle::Cookie))
1170 .example(Some(json!("Hello, world!")))
1171 .build();
1172
1173 assert_eq!(
1174 serde_json::to_value(querystring).unwrap(),
1175 json!({
1176 "name": "advancedQuery",
1177 "in": "querystring",
1178 "required": false,
1179 "content": {
1180 "application/x-www-form-urlencoded": {
1181 "schema": {
1182 "type": "object",
1183 "properties": {
1184 "bar": {
1185 "type": "boolean"
1186 },
1187 "foo": {
1188 "type": "string"
1189 }
1190 }
1191 },
1192 "examples": {
1193 "spacesAndPluses": {
1194 "description": "Form-encoded query string",
1195 "dataValue": {
1196 "foo": "a + b",
1197 "bar": true
1198 },
1199 "serializedValue": "foo=a+%2B+b&bar=true"
1200 }
1201 }
1202 }
1203 }
1204 })
1205 );
1206 assert_eq!(
1207 serde_json::to_value(cookie).unwrap(),
1208 json!({
1209 "name": "greeting",
1210 "in": "cookie",
1211 "required": true,
1212 "style": "cookie",
1213 "example": "Hello, world!"
1214 })
1215 );
1216 }
1217
1218 #[test]
1219 fn openapi_32_components_media_types_streaming_and_response_summary_serialize() {
1220 let event_payload = ObjectBuilder::new()
1221 .schema_type(Type::Object)
1222 .property("pet_id", ObjectBuilder::new().schema_type(Type::Integer))
1223 .property("status", ObjectBuilder::new().schema_type(Type::String));
1224 let server_event = ObjectBuilder::new()
1225 .schema_type(Type::Object)
1226 .property("event", ObjectBuilder::new().schema_type(Type::String))
1227 .property(
1228 "data",
1229 ObjectBuilder::new()
1230 .schema_type(Type::String)
1231 .content_media_type("application/json")
1232 .content_schema(Some(Ref::from_schema_name("PetEventPayload"))),
1233 )
1234 .property("id", ObjectBuilder::new().schema_type(Type::String))
1235 .property("retry", ObjectBuilder::new().schema_type(Type::Integer));
1236 let components = ComponentsBuilder::new()
1237 .schema("PetEventPayload", event_payload)
1238 .schema("ServerEvent", server_event)
1239 .media_type(
1240 "ServerSentEvents",
1241 ContentBuilder::new()
1242 .item_schema(Some(Ref::from_schema_name("ServerEvent")))
1243 .build(),
1244 )
1245 .media_type(
1246 "JsonLines",
1247 ContentBuilder::new()
1248 .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1249 .build(),
1250 )
1251 .media_type(
1252 "JsonTextSequences",
1253 ContentBuilder::new()
1254 .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1255 .build(),
1256 )
1257 .media_type(
1258 "MultipartMixed",
1259 ContentBuilder::new()
1260 .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1261 .build(),
1262 )
1263 .build();
1264 let response = ResponseBuilder::new()
1265 .summary(Some("Streaming response"))
1266 .content(
1267 "text/event-stream",
1268 ContentBuilder::new()
1269 .item_schema(Some(Ref::from_schema_name("ServerEvent")))
1270 .build(),
1271 )
1272 .content(
1273 "application/jsonl",
1274 ContentBuilder::new()
1275 .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1276 .build(),
1277 )
1278 .content(
1279 "application/json-seq",
1280 ContentBuilder::new()
1281 .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1282 .build(),
1283 )
1284 .content(
1285 "multipart/mixed",
1286 ContentBuilder::new()
1287 .item_schema(Some(Ref::from_schema_name("PetEventPayload")))
1288 .build(),
1289 )
1290 .build();
1291
1292 assert_eq!(
1293 serde_json::to_value(components).unwrap(),
1294 json!({
1295 "schemas": {
1296 "PetEventPayload": {
1297 "type": "object",
1298 "properties": {
1299 "pet_id": {
1300 "type": "integer"
1301 },
1302 "status": {
1303 "type": "string"
1304 }
1305 }
1306 },
1307 "ServerEvent": {
1308 "type": "object",
1309 "properties": {
1310 "data": {
1311 "type": "string",
1312 "contentMediaType": "application/json",
1313 "contentSchema": {
1314 "$ref": "#/components/schemas/PetEventPayload"
1315 }
1316 },
1317 "event": {
1318 "type": "string"
1319 },
1320 "id": {
1321 "type": "string"
1322 },
1323 "retry": {
1324 "type": "integer"
1325 }
1326 }
1327 }
1328 },
1329 "mediaTypes": {
1330 "JsonLines": {
1331 "itemSchema": {
1332 "$ref": "#/components/schemas/PetEventPayload"
1333 }
1334 },
1335 "JsonTextSequences": {
1336 "itemSchema": {
1337 "$ref": "#/components/schemas/PetEventPayload"
1338 }
1339 },
1340 "MultipartMixed": {
1341 "itemSchema": {
1342 "$ref": "#/components/schemas/PetEventPayload"
1343 }
1344 },
1345 "ServerSentEvents": {
1346 "itemSchema": {
1347 "$ref": "#/components/schemas/ServerEvent"
1348 }
1349 }
1350 }
1351 })
1352 );
1353 assert_eq!(
1354 serde_json::to_value(response).unwrap(),
1355 json!({
1356 "summary": "Streaming response",
1357 "content": {
1358 "application/json-seq": {
1359 "itemSchema": {
1360 "$ref": "#/components/schemas/PetEventPayload"
1361 }
1362 },
1363 "application/jsonl": {
1364 "itemSchema": {
1365 "$ref": "#/components/schemas/PetEventPayload"
1366 }
1367 },
1368 "multipart/mixed": {
1369 "itemSchema": {
1370 "$ref": "#/components/schemas/PetEventPayload"
1371 }
1372 },
1373 "text/event-stream": {
1374 "itemSchema": {
1375 "$ref": "#/components/schemas/ServerEvent"
1376 }
1377 }
1378 }
1379 })
1380 );
1381 }
1382
1383 #[test]
1384 fn openapi_32_oauth_device_authorization_and_deprecated_security_serialize() {
1385 let oauth = SecurityScheme::OAuth2(
1386 OAuth2::new([Flow::DeviceAuthorization(DeviceAuthorization::new(
1387 "https://example.com/device",
1388 "https://example.com/token",
1389 Scopes::from_iter([("read:pets", "read pets")]),
1390 ))])
1391 .with_metadata_url("https://example.com/.well-known/oauth-authorization-server")
1392 .deprecated(Some(Deprecated::True)),
1393 );
1394
1395 assert_eq!(
1396 serde_json::to_value(oauth).unwrap(),
1397 json!({
1398 "type": "oauth2",
1399 "flows": {
1400 "deviceAuthorization": {
1401 "deviceAuthorizationUrl": "https://example.com/device",
1402 "tokenUrl": "https://example.com/token",
1403 "scopes": {
1404 "read:pets": "read pets"
1405 }
1406 }
1407 },
1408 "oauth2MetadataUrl": "https://example.com/.well-known/oauth-authorization-server",
1409 "deprecated": true
1410 })
1411 );
1412 }
1413
1414 #[test]
1415 fn serialize_openapi_json_minimal_success() {
1416 let openapi = OpenApi::new(
1417 InfoBuilder::new()
1418 .title("My api")
1419 .version("1.0.0")
1420 .description(Some("My api description"))
1421 .license(Some(
1422 LicenseBuilder::new()
1423 .name("MIT")
1424 .url(Some("http://mit.licence"))
1425 .build(),
1426 ))
1427 .build(),
1428 Paths::new(),
1429 );
1430
1431 assert_json_snapshot!(openapi);
1432 }
1433
1434 #[test]
1435 fn serialize_openapi_json_with_paths_success() {
1436 let openapi = OpenApi::new(
1437 Info::new("My big api", "1.1.0"),
1438 PathsBuilder::new()
1439 .path(
1440 "/api/v1/users",
1441 PathItem::new(
1442 HttpMethod::Get,
1443 OperationBuilder::new().response("200", Response::new("Get users list")),
1444 ),
1445 )
1446 .path(
1447 "/api/v1/users",
1448 PathItem::new(
1449 HttpMethod::Post,
1450 OperationBuilder::new().response("200", Response::new("Post new user")),
1451 ),
1452 )
1453 .path(
1454 "/api/v1/users/{id}",
1455 PathItem::new(
1456 HttpMethod::Get,
1457 OperationBuilder::new().response("200", Response::new("Get user by id")),
1458 ),
1459 ),
1460 );
1461
1462 assert_json_snapshot!(openapi);
1463 }
1464
1465 #[test]
1466 fn merge_2_openapi_documents() {
1467 let mut api_1 = OpenApi::new(
1468 Info::new("Api", "v1"),
1469 PathsBuilder::new()
1470 .path(
1471 "/api/v1/user",
1472 PathItem::new(
1473 HttpMethod::Get,
1474 OperationBuilder::new().response("200", Response::new("Get user success")),
1475 ),
1476 )
1477 .build(),
1478 );
1479
1480 let api_2 = OpenApiBuilder::new()
1481 .info(Info::new("Api", "v2"))
1482 .paths(
1483 PathsBuilder::new()
1484 .path(
1485 "/api/v1/user",
1486 PathItem::new(
1487 HttpMethod::Get,
1488 OperationBuilder::new()
1489 .response("200", Response::new("This will not get added")),
1490 ),
1491 )
1492 .path(
1493 "/ap/v2/user",
1494 PathItem::new(
1495 HttpMethod::Get,
1496 OperationBuilder::new()
1497 .response("200", Response::new("Get user success 2")),
1498 ),
1499 )
1500 .path(
1501 "/api/v2/user",
1502 PathItem::new(
1503 HttpMethod::Post,
1504 OperationBuilder::new()
1505 .response("200", Response::new("Get user success")),
1506 ),
1507 )
1508 .build(),
1509 )
1510 .components(Some(
1511 ComponentsBuilder::new()
1512 .schema(
1513 "User2",
1514 ObjectBuilder::new().schema_type(Type::Object).property(
1515 "name",
1516 ObjectBuilder::new().schema_type(Type::String).build(),
1517 ),
1518 )
1519 .build(),
1520 ))
1521 .build();
1522
1523 api_1.merge(api_2);
1524
1525 assert_json_snapshot!(api_1, {
1526 ".paths" => insta::sorted_redaction()
1527 });
1528 }
1529
1530 #[test]
1531 fn merge_same_path_diff_methods() {
1532 let mut api_1 = OpenApi::new(
1533 Info::new("Api", "v1"),
1534 PathsBuilder::new()
1535 .path(
1536 "/api/v1/user",
1537 PathItem::new(
1538 HttpMethod::Get,
1539 OperationBuilder::new()
1540 .response("200", Response::new("Get user success 1")),
1541 ),
1542 )
1543 .extensions(Some(Extensions::from_iter([("x-v1-api", true)])))
1544 .build(),
1545 );
1546
1547 let api_2 = OpenApiBuilder::new()
1548 .info(Info::new("Api", "v2"))
1549 .paths(
1550 PathsBuilder::new()
1551 .path(
1552 "/api/v1/user",
1553 PathItem::new(
1554 HttpMethod::Get,
1555 OperationBuilder::new()
1556 .response("200", Response::new("This will not get added")),
1557 ),
1558 )
1559 .path(
1560 "/api/v1/user",
1561 PathItem::new(
1562 HttpMethod::Post,
1563 OperationBuilder::new()
1564 .response("200", Response::new("Post user success 1")),
1565 ),
1566 )
1567 .path(
1568 "/api/v2/user",
1569 PathItem::new(
1570 HttpMethod::Get,
1571 OperationBuilder::new()
1572 .response("200", Response::new("Get user success 2")),
1573 ),
1574 )
1575 .path(
1576 "/api/v2/user",
1577 PathItem::new(
1578 HttpMethod::Post,
1579 OperationBuilder::new()
1580 .response("200", Response::new("Post user success 2")),
1581 ),
1582 )
1583 .extensions(Some(Extensions::from_iter([("x-random", "Value")])))
1584 .build(),
1585 )
1586 .components(Some(
1587 ComponentsBuilder::new()
1588 .schema(
1589 "User2",
1590 ObjectBuilder::new().schema_type(Type::Object).property(
1591 "name",
1592 ObjectBuilder::new().schema_type(Type::String).build(),
1593 ),
1594 )
1595 .build(),
1596 ))
1597 .build();
1598
1599 api_1.merge(api_2);
1600
1601 assert_json_snapshot!(api_1, {
1602 ".paths" => insta::sorted_redaction()
1603 });
1604 }
1605
1606 #[test]
1607 fn test_nest_open_apis() {
1608 let api = OpenApiBuilder::new()
1609 .paths(
1610 PathsBuilder::new().path(
1611 "/api/v1/status",
1612 PathItem::new(
1613 HttpMethod::Get,
1614 OperationBuilder::new()
1615 .description(Some("Get status"))
1616 .build(),
1617 ),
1618 ),
1619 )
1620 .build();
1621
1622 let user_api = OpenApiBuilder::new()
1623 .paths(
1624 PathsBuilder::new()
1625 .path(
1626 "/",
1627 PathItem::new(
1628 HttpMethod::Get,
1629 OperationBuilder::new()
1630 .description(Some("Get user details"))
1631 .build(),
1632 ),
1633 )
1634 .path(
1635 "/foo",
1636 PathItem::new(HttpMethod::Post, OperationBuilder::new().build()),
1637 ),
1638 )
1639 .build();
1640
1641 let nest_merged = api.nest("/api/v1/user", user_api);
1642 let value = serde_json::to_value(nest_merged).expect("should serialize as json");
1643 let paths = value
1644 .pointer("/paths")
1645 .expect("paths should exits in openapi");
1646
1647 assert_json_snapshot!(paths);
1648 }
1649
1650 #[test]
1651 fn openapi_custom_extension() {
1652 let mut api = OpenApiBuilder::new().build();
1653 let extensions = api.extensions.get_or_insert(Default::default());
1654 extensions.insert(
1655 String::from("x-tagGroup"),
1656 String::from("anything that serializes to Json").into(),
1657 );
1658
1659 assert_json_snapshot!(api);
1660 }
1661}