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