Skip to main content

utoipa/openapi/
request_body.rs

1//! Implements [OpenAPI Request Body][request_body] types.
2//!
3//! [request_body]: https://spec.openapis.org/oas/latest.html#request-body-object
4use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8use super::extensions::Extensions;
9use super::{builder, set_value, Content, RefOr, Required};
10
11builder! {
12    RequestBodyBuilder;
13
14    /// Implements [OpenAPI Request Body][request_body].
15    ///
16    /// [request_body]: https://spec.openapis.org/oas/latest.html#request-body-object
17    #[non_exhaustive]
18    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
19    #[cfg_attr(feature = "debug", derive(Debug))]
20    #[serde(rename_all = "camelCase")]
21    pub struct RequestBody {
22        /// Additional description of [`RequestBody`] supporting markdown syntax.
23        #[serde(skip_serializing_if = "Option::is_none")]
24        pub description: Option<String>,
25
26        /// Map of request body contents mapped by content type e.g. `application/json`.
27        pub content: BTreeMap<String, RefOr<Content>>,
28
29        /// Determines whether request body is required in the request or not.
30        #[serde(skip_serializing_if = "Option::is_none")]
31        pub required: Option<Required>,
32
33        /// Optional extensions "x-something".
34        #[serde(skip_serializing_if = "Option::is_none", flatten)]
35        pub extensions: Option<Extensions>,
36    }
37}
38
39impl RequestBody {
40    /// Construct a new [`RequestBody`].
41    pub fn new() -> Self {
42        Default::default()
43    }
44}
45
46impl RequestBodyBuilder {
47    /// Add description for [`RequestBody`].
48    pub fn description<S: Into<String>>(mut self, description: Option<S>) -> Self {
49        set_value!(self description description.map(|description| description.into()))
50    }
51
52    /// Define [`RequestBody`] required.
53    pub fn required(mut self, required: Option<Required>) -> Self {
54        set_value!(self required required)
55    }
56
57    /// Add [`Content`] by content type e.g `application/json` to [`RequestBody`].
58    pub fn content<S: Into<String>, C: Into<Content>>(
59        mut self,
60        content_type: S,
61        content: C,
62    ) -> Self {
63        self.content
64            .insert(content_type.into(), content.into().into());
65
66        self
67    }
68
69    /// Add reusable [`Content`] reference by content type e.g `application/json`.
70    pub fn content_ref<S: Into<String>>(
71        mut self,
72        content_type: S,
73        content_ref: super::Ref,
74    ) -> Self {
75        self.content
76            .insert(content_type.into(), RefOr::Ref(content_ref));
77
78        self
79    }
80
81    /// Add openapi extensions (x-something) of the API.
82    pub fn extensions(mut self, extensions: Option<Extensions>) -> Self {
83        set_value!(self extensions extensions)
84    }
85}
86
87/// Trait with convenience functions for documenting request bodies.
88///
89/// With a single method call we can add [`Content`] to our [`RequestBodyBuilder`] and
90/// [`RequestBody`] that references a [schema][schema] using
91/// content-type `"application/json"`.
92///
93/// _**Add json request body from schema ref.**_
94/// ```rust
95/// use utoipa::openapi::request_body::{RequestBodyBuilder, RequestBodyExt};
96///
97/// let request = RequestBodyBuilder::new().json_schema_ref("EmailPayload").build();
98/// ```
99///
100/// If serialized to JSON, the above will result in a requestBody schema like this.
101/// ```json
102/// {
103///   "content": {
104///     "application/json": {
105///       "schema": {
106///         "$ref": "#/components/schemas/EmailPayload"
107///       }
108///     }
109///   }
110/// }
111/// ```
112///
113/// [schema]: crate::ToSchema
114///
115#[cfg(feature = "openapi_extensions")]
116#[cfg_attr(doc_cfg, doc(cfg(feature = "openapi_extensions")))]
117pub trait RequestBodyExt {
118    /// Add [`Content`] to [`RequestBody`] referring to a _`schema`_
119    /// with Content-Type `application/json`.
120    fn json_schema_ref(self, ref_name: &str) -> Self;
121}
122
123#[cfg(feature = "openapi_extensions")]
124impl RequestBodyExt for RequestBody {
125    fn json_schema_ref(mut self, ref_name: &str) -> RequestBody {
126        self.content.insert(
127            "application/json".to_string(),
128            crate::openapi::Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name)))
129                .into(),
130        );
131        self
132    }
133}
134
135#[cfg(feature = "openapi_extensions")]
136impl RequestBodyExt for RequestBodyBuilder {
137    fn json_schema_ref(self, ref_name: &str) -> RequestBodyBuilder {
138        self.content(
139            "application/json",
140            crate::openapi::Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name))),
141        )
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::{Content, RequestBody, RequestBodyBuilder, Required};
148    use insta::assert_json_snapshot;
149
150    #[test]
151    fn request_body_new() {
152        let request_body = RequestBody::new();
153
154        assert!(request_body.content.is_empty());
155        assert_eq!(request_body.description, None);
156        assert!(request_body.required.is_none());
157    }
158
159    #[test]
160    fn request_body_builder() {
161        let request_body = RequestBodyBuilder::new()
162            .description(Some("A sample requestBody"))
163            .required(Some(Required::True))
164            .content(
165                "application/json",
166                Content::new(Some(crate::openapi::Ref::from_schema_name("EmailPayload"))),
167            )
168            .build();
169        assert_json_snapshot!(request_body);
170    }
171}
172
173#[cfg(all(test, feature = "openapi_extensions"))]
174#[cfg_attr(doc_cfg, doc(cfg(feature = "openapi_extensions")))]
175mod openapi_extensions_tests {
176    use crate::openapi::request_body::RequestBodyBuilder;
177    use insta::assert_json_snapshot;
178
179    use super::RequestBodyExt;
180
181    #[test]
182    fn request_body_ext() {
183        let request_body = RequestBodyBuilder::new()
184            .build()
185            // build a RequestBody first to test the method
186            .json_schema_ref("EmailPayload");
187        assert_json_snapshot!(request_body);
188    }
189
190    #[test]
191    fn request_body_builder_ext() {
192        let request_body = RequestBodyBuilder::new()
193            .json_schema_ref("EmailPayload")
194            .build();
195        assert_json_snapshot!(request_body);
196    }
197}