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, 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, 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>>(mut self, content_type: S, content: Content) -> Self {
59        self.content.insert(content_type.into(), content);
60
61        self
62    }
63
64    /// Add openapi extensions (x-something) of the API.
65    pub fn extensions(mut self, extensions: Option<Extensions>) -> Self {
66        set_value!(self extensions extensions)
67    }
68}
69
70/// Trait with convenience functions for documenting request bodies.
71///
72/// With a single method call we can add [`Content`] to our [`RequestBodyBuilder`] and
73/// [`RequestBody`] that references a [schema][schema] using
74/// content-type `"application/json"`.
75///
76/// _**Add json request body from schema ref.**_
77/// ```rust
78/// use utoipa::openapi::request_body::{RequestBodyBuilder, RequestBodyExt};
79///
80/// let request = RequestBodyBuilder::new().json_schema_ref("EmailPayload").build();
81/// ```
82///
83/// If serialized to JSON, the above will result in a requestBody schema like this.
84/// ```json
85/// {
86///   "content": {
87///     "application/json": {
88///       "schema": {
89///         "$ref": "#/components/schemas/EmailPayload"
90///       }
91///     }
92///   }
93/// }
94/// ```
95///
96/// [schema]: crate::ToSchema
97///
98#[cfg(feature = "openapi_extensions")]
99#[cfg_attr(doc_cfg, doc(cfg(feature = "openapi_extensions")))]
100pub trait RequestBodyExt {
101    /// Add [`Content`] to [`RequestBody`] referring to a _`schema`_
102    /// with Content-Type `application/json`.
103    fn json_schema_ref(self, ref_name: &str) -> Self;
104}
105
106#[cfg(feature = "openapi_extensions")]
107impl RequestBodyExt for RequestBody {
108    fn json_schema_ref(mut self, ref_name: &str) -> RequestBody {
109        self.content.insert(
110            "application/json".to_string(),
111            crate::openapi::Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name))),
112        );
113        self
114    }
115}
116
117#[cfg(feature = "openapi_extensions")]
118impl RequestBodyExt for RequestBodyBuilder {
119    fn json_schema_ref(self, ref_name: &str) -> RequestBodyBuilder {
120        self.content(
121            "application/json",
122            crate::openapi::Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name))),
123        )
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::{Content, RequestBody, RequestBodyBuilder, Required};
130    use insta::assert_json_snapshot;
131
132    #[test]
133    fn request_body_new() {
134        let request_body = RequestBody::new();
135
136        assert!(request_body.content.is_empty());
137        assert_eq!(request_body.description, None);
138        assert!(request_body.required.is_none());
139    }
140
141    #[test]
142    fn request_body_builder() {
143        let request_body = RequestBodyBuilder::new()
144            .description(Some("A sample requestBody"))
145            .required(Some(Required::True))
146            .content(
147                "application/json",
148                Content::new(Some(crate::openapi::Ref::from_schema_name("EmailPayload"))),
149            )
150            .build();
151        assert_json_snapshot!(request_body);
152    }
153}
154
155#[cfg(all(test, feature = "openapi_extensions"))]
156#[cfg_attr(doc_cfg, doc(cfg(feature = "openapi_extensions")))]
157mod openapi_extensions_tests {
158    use crate::openapi::request_body::RequestBodyBuilder;
159    use insta::assert_json_snapshot;
160
161    use super::RequestBodyExt;
162
163    #[test]
164    fn request_body_ext() {
165        let request_body = RequestBodyBuilder::new()
166            .build()
167            // build a RequestBody first to test the method
168            .json_schema_ref("EmailPayload");
169        assert_json_snapshot!(request_body);
170    }
171
172    #[test]
173    fn request_body_builder_ext() {
174        let request_body = RequestBodyBuilder::new()
175            .json_schema_ref("EmailPayload")
176            .build();
177        assert_json_snapshot!(request_body);
178    }
179}