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")]
100 pub components: Option<Components>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
108 pub security: Option<Vec<SecurityRequirement>>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
114 pub tags: Option<Vec<Tag>>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
120 pub external_docs: Option<ExternalDocs>,
121
122 #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
127 pub schema: String,
128
129 #[serde(skip_serializing_if = "Option::is_none", flatten)]
131 pub extensions: Option<Extensions>,
132 }
133}
134
135impl OpenApi {
136 pub fn new<P: Into<Paths>>(info: Info, paths: P) -> Self {
149 Self {
150 info,
151 paths: paths.into(),
152 ..Default::default()
153 }
154 }
155
156 pub fn to_json(&self) -> Result<String, serde_json::Error> {
158 serde_json::to_string(self)
159 }
160
161 pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
163 serde_json::to_string_pretty(self)
164 }
165
166 #[cfg(feature = "yaml")]
168 #[cfg_attr(doc_cfg, doc(cfg(feature = "yaml")))]
169 pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
170 serde_norway::to_string(self)
171 }
172
173 pub fn merge_from(mut self, other: OpenApi) -> OpenApi {
178 self.merge(other);
179 self
180 }
181
182 pub fn merge(&mut self, mut other: OpenApi) {
197 if let Some(other_servers) = &mut other.servers {
198 let servers = self.servers.get_or_insert(Vec::new());
199 other_servers.retain(|server| !servers.contains(server));
200 servers.append(other_servers);
201 }
202
203 if !other.paths.paths.is_empty() {
204 self.paths.merge(other.paths);
205 };
206
207 if let Some(other_components) = &mut other.components {
208 let components = self.components.get_or_insert(Components::default());
209
210 other_components
211 .schemas
212 .retain(|name, _| !components.schemas.contains_key(name));
213 components.schemas.append(&mut other_components.schemas);
214
215 other_components
216 .responses
217 .retain(|name, _| !components.responses.contains_key(name));
218 components.responses.append(&mut other_components.responses);
219
220 other_components
221 .security_schemes
222 .retain(|name, _| !components.security_schemes.contains_key(name));
223 components
224 .security_schemes
225 .append(&mut other_components.security_schemes);
226 }
227
228 if let Some(other_security) = &mut other.security {
229 let security = self.security.get_or_insert(Vec::new());
230 other_security.retain(|requirement| !security.contains(requirement));
231 security.append(other_security);
232 }
233
234 if let Some(other_tags) = &mut other.tags {
235 let tags = self.tags.get_or_insert(Vec::new());
236 other_tags.retain(|tag| !tags.contains(tag));
237 tags.append(other_tags);
238 }
239 }
240
241 pub fn nest<P: Into<String>, O: Into<OpenApi>>(self, path: P, other: O) -> Self {
283 self.nest_with_path_composer(path, other, |base, path| format!("{base}{path}"))
284 }
285
286 pub fn nest_with_path_composer<
293 P: Into<String>,
294 O: Into<OpenApi>,
295 F: Fn(&str, &str) -> String,
296 >(
297 mut self,
298 path: P,
299 other: O,
300 composer: F,
301 ) -> Self {
302 let path: String = path.into();
303 let mut other_api: OpenApi = other.into();
304
305 let nested_paths = other_api
306 .paths
307 .paths
308 .into_iter()
309 .map(|(item_path, item)| {
310 let path = composer(&path, &item_path);
311 (path, item)
312 })
313 .collect::<PathsMap<_, _>>();
314
315 self.paths.paths.extend(nested_paths);
316
317 other_api.paths.paths = PathsMap::new();
319 self.merge_from(other_api)
320 }
321}
322
323impl OpenApiBuilder {
324 pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
326 set_value!(self info info.into())
327 }
328
329 pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: Option<I>) -> Self {
331 set_value!(self servers servers.map(|servers| servers.into_iter().collect()))
332 }
333
334 pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
336 set_value!(self paths paths.into())
337 }
338
339 pub fn components(mut self, components: Option<Components>) -> Self {
341 set_value!(self components components)
342 }
343
344 pub fn security<I: IntoIterator<Item = SecurityRequirement>>(
346 mut self,
347 security: Option<I>,
348 ) -> Self {
349 set_value!(self security security.map(|security| security.into_iter().collect()))
350 }
351
352 pub fn tags<I: IntoIterator<Item = Tag>>(mut self, tags: Option<I>) -> Self {
354 set_value!(self tags tags.map(|tags| tags.into_iter().collect()))
355 }
356
357 pub fn external_docs(mut self, external_docs: Option<ExternalDocs>) -> Self {
359 set_value!(self external_docs external_docs)
360 }
361
362 pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
374 set_value!(self schema schema.into())
375 }
376}
377
378#[derive(Serialize, Clone, PartialEq, Eq, Default)]
382#[cfg_attr(feature = "debug", derive(Debug))]
383pub enum OpenApiVersion {
384 #[serde(rename = "3.1.0")]
386 #[default]
387 Version31,
388}
389
390impl<'de> Deserialize<'de> for OpenApiVersion {
391 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
392 where
393 D: Deserializer<'de>,
394 {
395 struct VersionVisitor;
396
397 impl<'v> Visitor<'v> for VersionVisitor {
398 type Value = OpenApiVersion;
399
400 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
401 formatter.write_str("a version string in 3.1.x format")
402 }
403
404 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
405 where
406 E: Error,
407 {
408 self.visit_string(v.to_string())
409 }
410
411 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
412 where
413 E: Error,
414 {
415 let version = v
416 .split('.')
417 .flat_map(|digit| digit.parse::<i8>())
418 .collect::<Vec<_>>();
419
420 if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
421 Ok(OpenApiVersion::Version31)
422 } else {
423 let expected: &dyn Expected = &"3.1.0";
424 Err(Error::invalid_value(
425 serde::de::Unexpected::Str(&v),
426 expected,
427 ))
428 }
429 }
430 }
431
432 deserializer.deserialize_string(VersionVisitor)
433 }
434}
435
436#[derive(PartialEq, Eq, Clone, Default)]
440#[cfg_attr(feature = "debug", derive(Debug))]
441#[allow(missing_docs)]
442pub enum Deprecated {
443 True,
444 #[default]
445 False,
446}
447
448impl Serialize for Deprecated {
449 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
450 where
451 S: Serializer,
452 {
453 serializer.serialize_bool(matches!(self, Self::True))
454 }
455}
456
457impl<'de> Deserialize<'de> for Deprecated {
458 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
459 where
460 D: serde::Deserializer<'de>,
461 {
462 struct BoolVisitor;
463 impl<'de> Visitor<'de> for BoolVisitor {
464 type Value = Deprecated;
465
466 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
467 formatter.write_str("a bool true or false")
468 }
469
470 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
471 where
472 E: serde::de::Error,
473 {
474 match v {
475 true => Ok(Deprecated::True),
476 false => Ok(Deprecated::False),
477 }
478 }
479 }
480 deserializer.deserialize_bool(BoolVisitor)
481 }
482}
483
484#[derive(PartialEq, Eq, Clone, Default)]
488#[allow(missing_docs)]
489#[cfg_attr(feature = "debug", derive(Debug))]
490pub enum Required {
491 True,
492 #[default]
493 False,
494}
495
496impl Serialize for Required {
497 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
498 where
499 S: Serializer,
500 {
501 serializer.serialize_bool(matches!(self, Self::True))
502 }
503}
504
505impl<'de> Deserialize<'de> for Required {
506 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
507 where
508 D: serde::Deserializer<'de>,
509 {
510 struct BoolVisitor;
511 impl<'de> Visitor<'de> for BoolVisitor {
512 type Value = Required;
513
514 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
515 formatter.write_str("a bool true or false")
516 }
517
518 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
519 where
520 E: serde::de::Error,
521 {
522 match v {
523 true => Ok(Required::True),
524 false => Ok(Required::False),
525 }
526 }
527 }
528 deserializer.deserialize_bool(BoolVisitor)
529 }
530}
531
532#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
537#[cfg_attr(feature = "debug", derive(Debug))]
538#[serde(untagged)]
539pub enum RefOr<T> {
540 Ref(Ref),
543 T(T),
546}
547
548macro_rules! build_fn {
549 ( $vis:vis $name:ident $( $field:ident ),+ ) => {
550 #[doc = concat!("Constructs a new [`", stringify!($name),"`] taking all fields values from this object.")]
551 $vis fn build(self) -> $name {
552 $name {
553 $(
554 $field: self.$field,
555 )*
556 }
557 }
558 };
559}
560pub(crate) use build_fn;
561
562macro_rules! set_value {
563 ( $self:ident $field:ident $value:expr ) => {{
564 $self.$field = $value;
565
566 $self
567 }};
568}
569pub(crate) use set_value;
570
571macro_rules! new {
572 ( $vis:vis $name:ident ) => {
573 #[doc = concat!("Constructs a new [`", stringify!($name),"`].")]
574 $vis fn new() -> $name {
575 $name {
576 ..Default::default()
577 }
578 }
579 };
580}
581pub(crate) use new;
582
583macro_rules! from {
584 ( $name:ident $to:ident $( $field:ident ),+ ) => {
585 impl From<$name> for $to {
586 fn from(value: $name) -> Self {
587 Self {
588 $( $field: value.$field, )*
589 }
590 }
591 }
592
593 impl From<$to> for $name {
594 fn from(value: $to) -> Self {
595 value.build()
596 }
597 }
598 };
599}
600pub(crate) use from;
601
602macro_rules! builder {
603 ( $( #[$builder_meta:meta] )* $builder_name:ident; $(#[$meta:meta])* $vis:vis $key:ident $name:ident $( $tt:tt )* ) => {
604 builder!( @type_impl $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
605 builder!( @builder_impl $( #[$builder_meta] )* $builder_name $( #[$meta] )* $vis $key $name $( $tt )* );
606 };
607
608 ( @type_impl $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
609 { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
610 ) => {
611 $( #[$meta] )*
612 $vis $key $name {
613 $( $( #[$field_meta] )* $field_vis $field: $field_ty, )*
614 }
615
616 impl $name {
617 #[doc = concat!("Construct a new ", stringify!($builder_name), ".")]
618 #[doc = ""]
619 #[doc = concat!("This is effectively same as calling [`", stringify!($builder_name), "::new`]")]
620 $vis fn builder() -> $builder_name {
621 $builder_name::new()
622 }
623 }
624 };
625
626 ( @builder_impl $( #[$builder_meta:meta] )* $builder_name:ident $( #[$meta:meta] )* $vis:vis $key:ident $name:ident
627 { $( $( #[$field_meta:meta] )* $field_vis:vis $field:ident: $field_ty:ty, )* }
628 ) => {
629 #[doc = concat!("Builder for [`", stringify!($name),
630 "`] with chainable configuration methods to create a new [`", stringify!($name) , "`].")]
631 $( #[$builder_meta] )*
632 #[cfg_attr(feature = "debug", derive(Debug))]
633 $vis $key $builder_name {
634 $( $field: $field_ty, )*
635 }
636
637 impl Default for $builder_name {
638 fn default() -> Self {
639 let meta_default: $name = $name::default();
640 Self {
641 $( $field: meta_default.$field, )*
642 }
643 }
644 }
645
646 impl $builder_name {
647 crate::openapi::new!($vis $builder_name);
648 crate::openapi::build_fn!($vis $name $( $field ),* );
649 }
650
651 crate::openapi::from!($name $builder_name $( $field ),* );
652 };
653}
654use crate::openapi::extensions::Extensions;
655pub(crate) use builder;
656
657#[cfg(test)]
658mod tests {
659 use crate::openapi::{
660 info::InfoBuilder,
661 path::{OperationBuilder, PathsBuilder},
662 };
663 use insta::assert_json_snapshot;
664
665 use super::{response::Response, *};
666
667 #[test]
668 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
669 assert_eq!(serde_json::to_value(&OpenApiVersion::Version31)?, "3.1.0");
670 Ok(())
671 }
672
673 #[test]
674 fn serialize_openapi_json_minimal_success() {
675 let openapi = OpenApi::new(
676 InfoBuilder::new()
677 .title("My api")
678 .version("1.0.0")
679 .description(Some("My api description"))
680 .license(Some(
681 LicenseBuilder::new()
682 .name("MIT")
683 .url(Some("http://mit.licence"))
684 .build(),
685 ))
686 .build(),
687 Paths::new(),
688 );
689
690 assert_json_snapshot!(openapi);
691 }
692
693 #[test]
694 fn serialize_openapi_json_with_paths_success() {
695 let openapi = OpenApi::new(
696 Info::new("My big api", "1.1.0"),
697 PathsBuilder::new()
698 .path(
699 "/api/v1/users",
700 PathItem::new(
701 HttpMethod::Get,
702 OperationBuilder::new().response("200", Response::new("Get users list")),
703 ),
704 )
705 .path(
706 "/api/v1/users",
707 PathItem::new(
708 HttpMethod::Post,
709 OperationBuilder::new().response("200", Response::new("Post new user")),
710 ),
711 )
712 .path(
713 "/api/v1/users/{id}",
714 PathItem::new(
715 HttpMethod::Get,
716 OperationBuilder::new().response("200", Response::new("Get user by id")),
717 ),
718 ),
719 );
720
721 assert_json_snapshot!(openapi);
722 }
723
724 #[test]
725 fn merge_2_openapi_documents() {
726 let mut api_1 = OpenApi::new(
727 Info::new("Api", "v1"),
728 PathsBuilder::new()
729 .path(
730 "/api/v1/user",
731 PathItem::new(
732 HttpMethod::Get,
733 OperationBuilder::new().response("200", Response::new("Get user success")),
734 ),
735 )
736 .build(),
737 );
738
739 let api_2 = OpenApiBuilder::new()
740 .info(Info::new("Api", "v2"))
741 .paths(
742 PathsBuilder::new()
743 .path(
744 "/api/v1/user",
745 PathItem::new(
746 HttpMethod::Get,
747 OperationBuilder::new()
748 .response("200", Response::new("This will not get added")),
749 ),
750 )
751 .path(
752 "/ap/v2/user",
753 PathItem::new(
754 HttpMethod::Get,
755 OperationBuilder::new()
756 .response("200", Response::new("Get user success 2")),
757 ),
758 )
759 .path(
760 "/api/v2/user",
761 PathItem::new(
762 HttpMethod::Post,
763 OperationBuilder::new()
764 .response("200", Response::new("Get user success")),
765 ),
766 )
767 .build(),
768 )
769 .components(Some(
770 ComponentsBuilder::new()
771 .schema(
772 "User2",
773 ObjectBuilder::new().schema_type(Type::Object).property(
774 "name",
775 ObjectBuilder::new().schema_type(Type::String).build(),
776 ),
777 )
778 .build(),
779 ))
780 .build();
781
782 api_1.merge(api_2);
783
784 assert_json_snapshot!(api_1, {
785 ".paths" => insta::sorted_redaction()
786 });
787 }
788
789 #[test]
790 fn merge_same_path_diff_methods() {
791 let mut api_1 = OpenApi::new(
792 Info::new("Api", "v1"),
793 PathsBuilder::new()
794 .path(
795 "/api/v1/user",
796 PathItem::new(
797 HttpMethod::Get,
798 OperationBuilder::new()
799 .response("200", Response::new("Get user success 1")),
800 ),
801 )
802 .extensions(Some(Extensions::from_iter([("x-v1-api", true)])))
803 .build(),
804 );
805
806 let api_2 = OpenApiBuilder::new()
807 .info(Info::new("Api", "v2"))
808 .paths(
809 PathsBuilder::new()
810 .path(
811 "/api/v1/user",
812 PathItem::new(
813 HttpMethod::Get,
814 OperationBuilder::new()
815 .response("200", Response::new("This will not get added")),
816 ),
817 )
818 .path(
819 "/api/v1/user",
820 PathItem::new(
821 HttpMethod::Post,
822 OperationBuilder::new()
823 .response("200", Response::new("Post user success 1")),
824 ),
825 )
826 .path(
827 "/api/v2/user",
828 PathItem::new(
829 HttpMethod::Get,
830 OperationBuilder::new()
831 .response("200", Response::new("Get user success 2")),
832 ),
833 )
834 .path(
835 "/api/v2/user",
836 PathItem::new(
837 HttpMethod::Post,
838 OperationBuilder::new()
839 .response("200", Response::new("Post user success 2")),
840 ),
841 )
842 .extensions(Some(Extensions::from_iter([("x-random", "Value")])))
843 .build(),
844 )
845 .components(Some(
846 ComponentsBuilder::new()
847 .schema(
848 "User2",
849 ObjectBuilder::new().schema_type(Type::Object).property(
850 "name",
851 ObjectBuilder::new().schema_type(Type::String).build(),
852 ),
853 )
854 .build(),
855 ))
856 .build();
857
858 api_1.merge(api_2);
859
860 assert_json_snapshot!(api_1, {
861 ".paths" => insta::sorted_redaction()
862 });
863 }
864
865 #[test]
866 fn test_nest_open_apis() {
867 let api = OpenApiBuilder::new()
868 .paths(
869 PathsBuilder::new().path(
870 "/api/v1/status",
871 PathItem::new(
872 HttpMethod::Get,
873 OperationBuilder::new()
874 .description(Some("Get status"))
875 .build(),
876 ),
877 ),
878 )
879 .build();
880
881 let user_api = OpenApiBuilder::new()
882 .paths(
883 PathsBuilder::new()
884 .path(
885 "/",
886 PathItem::new(
887 HttpMethod::Get,
888 OperationBuilder::new()
889 .description(Some("Get user details"))
890 .build(),
891 ),
892 )
893 .path(
894 "/foo",
895 PathItem::new(HttpMethod::Post, OperationBuilder::new().build()),
896 ),
897 )
898 .build();
899
900 let nest_merged = api.nest("/api/v1/user", user_api);
901 let value = serde_json::to_value(nest_merged).expect("should serialize as json");
902 let paths = value
903 .pointer("/paths")
904 .expect("paths should exits in openapi");
905
906 assert_json_snapshot!(paths);
907 }
908
909 #[test]
910 fn openapi_custom_extension() {
911 let mut api = OpenApiBuilder::new().build();
912 let extensions = api.extensions.get_or_insert(Default::default());
913 extensions.insert(
914 String::from("x-tagGroup"),
915 String::from("anything that serializes to Json").into(),
916 );
917
918 assert_json_snapshot!(api);
919 }
920}