Skip to main content

salvo_oapi/openapi/
content.rs

1//! Implements content object for request body and response.
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use super::encoding::Encoding;
6use super::example::Example;
7use super::{PropMap, RefOr, Schema};
8
9/// Content holds request body content or response content.
10#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
11#[serde(rename_all = "camelCase")]
12#[non_exhaustive]
13pub struct Content {
14    /// Reference to a reusable Media Type Object, e.g. one defined under
15    /// `components.mediaTypes`. Added in OpenAPI 3.2.
16    ///
17    /// `content` maps are typed as [`Content`] rather than `RefOr<Content>` so that existing
18    /// callers keep compiling; a [`Content`] carrying only this field serializes exactly as a
19    /// Reference Object. As with any Reference Object, sibling fields are ignored by consumers.
20    ///
21    /// ```
22    /// # use salvo_oapi::Content;
23    /// let content = Content::from_ref("#/components/mediaTypes/FramePayload");
24    /// ```
25    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none", default)]
26    pub ref_location: Option<String>,
27
28    /// Schema used in response body or request body.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub schema: Option<RefOr<Schema>>,
31
32    /// Schema describing each item within a sequential media type, e.g. `text/event-stream` or
33    /// `application/jsonl`. Added in OpenAPI 3.2.
34    ///
35    /// Unlike [`Content::schema`], which applies to the complete content, `item_schema` applies
36    /// to each item in the stream independently. Both may be used together.
37    ///
38    /// See <https://spec.openapis.org/oas/v3.2.0.html#media-type-object>.
39    #[serde(skip_serializing_if = "Option::is_none", default)]
40    pub item_schema: Option<RefOr<Schema>>,
41
42    /// Example for request body or response body.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub example: Option<Value>,
45
46    /// Examples of the request body or response body. [`Content::examples`] should match to
47    /// media type and specified schema if present. [`Content::examples`] and
48    /// [`Content::example`] are mutually exclusive. If both are defined `examples` will
49    /// override value in `example`.
50    #[serde(default, skip_serializing_if = "PropMap::is_empty")]
51    pub examples: PropMap<String, RefOr<Example>>,
52
53    /// A map between a property name and its encoding information.
54    ///
55    /// The key, being the property name, MUST exist in the [`Content::schema`] as a property, with
56    /// `schema` being a [`Schema::Object`] and this object containing the same property key in
57    /// [`Object::properties`](crate::schema::Object::properties).
58    ///
59    /// The encoding object SHALL only apply to `request_body` objects when the media type is
60    /// multipart or `application/x-www-form-urlencoded`.
61    ///
62    /// Must not be combined with [`Content::prefix_encoding`] or [`Content::item_encoding`].
63    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
64    pub encoding: PropMap<String, Encoding>,
65
66    /// Positional encoding information, applied to the array item at the same index. Added in
67    /// OpenAPI 3.2 and only applicable to `multipart` media types.
68    ///
69    /// Requires either [`Content::item_schema`] or an array [`Content::schema`] to be present,
70    /// and must not be combined with [`Content::encoding`].
71    #[serde(skip_serializing_if = "Vec::is_empty", default)]
72    pub prefix_encoding: Vec<Encoding>,
73
74    /// A single encoding applied to all array items not covered by
75    /// [`Content::prefix_encoding`]. Added in OpenAPI 3.2 and only applicable to `multipart`
76    /// media types.
77    ///
78    /// Requires either [`Content::item_schema`] or an array [`Content::schema`] to be present,
79    /// and must not be combined with [`Content::encoding`].
80    #[serde(skip_serializing_if = "Option::is_none", default)]
81    pub item_encoding: Option<Encoding>,
82
83    /// Optional extensions "x-something"
84    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
85    pub extensions: PropMap<String, serde_json::Value>,
86}
87
88impl Content {
89    /// Construct a new [`Content`].
90    #[must_use]
91    pub fn new<I: Into<RefOr<Schema>>>(schema: I) -> Self {
92        Self {
93            schema: Some(schema.into()),
94            ..Self::default()
95        }
96    }
97
98    /// Construct a [`Content`] that is purely a reference to a reusable Media Type Object.
99    /// Requires OpenAPI 3.2.
100    #[must_use]
101    pub fn from_ref<S: Into<String>>(ref_location: S) -> Self {
102        Self {
103            ref_location: Some(ref_location.into()),
104            ..Self::default()
105        }
106    }
107
108    /// Set the `$ref` location for this [`Content`] and return `self`. Requires OpenAPI 3.2.
109    #[must_use]
110    pub fn ref_location<S: Into<String>>(mut self, ref_location: S) -> Self {
111        self.ref_location = Some(ref_location.into());
112        self
113    }
114
115    /// Add schema.
116    #[must_use]
117    pub fn schema<I: Into<RefOr<Schema>>>(mut self, component: I) -> Self {
118        self.schema = Some(component.into());
119        self
120    }
121
122    /// Add the schema describing each item of a sequential media type.
123    /// See [`Content::item_schema`]. Requires OpenAPI 3.2.
124    #[must_use]
125    pub fn item_schema<I: Into<RefOr<Schema>>>(mut self, component: I) -> Self {
126        self.item_schema = Some(component.into());
127        self
128    }
129
130    /// Add example of schema.
131    #[must_use]
132    pub fn example(mut self, example: Value) -> Self {
133        self.example = Some(example);
134        self
135    }
136
137    /// Add iterator of _`(N, V)`_ where `N` is name of example and `V` is [`Example`][example] to
138    /// [`Content`] of a request body or response body.
139    ///
140    /// [`Content::examples`] and [`Content::example`] are mutually exclusive. If both are defined
141    /// `examples` will override value in `example`.
142    ///
143    /// [example]: ../example/Example.html
144    #[must_use]
145    pub fn extend_examples<
146        E: IntoIterator<Item = (N, V)>,
147        N: Into<String>,
148        V: Into<RefOr<Example>>,
149    >(
150        mut self,
151        examples: E,
152    ) -> Self {
153        self.examples.extend(
154            examples
155                .into_iter()
156                .map(|(name, example)| (name.into(), example.into())),
157        );
158
159        self
160    }
161
162    /// Add openapi extensions (`x-something`) for [`Content`].
163    #[must_use]
164    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
165        self.extensions = extensions;
166        self
167    }
168
169    /// Add an encoding.
170    ///
171    /// The `property_name` MUST exist in the [`Content::schema`] as a property,
172    /// with `schema` being a [`Schema::Object`] and this object containing the same property
173    /// key in [`Object::properties`](crate::openapi::schema::Object::properties).
174    ///
175    /// The encoding object SHALL only apply to `request_body` objects when the media type is
176    /// multipart or `application/x-www-form-urlencoded`.
177    #[must_use]
178    pub fn encoding<S: Into<String>, E: Into<Encoding>>(
179        mut self,
180        property_name: S,
181        encoding: E,
182    ) -> Self {
183        self.encoding.insert(property_name.into(), encoding.into());
184        self
185    }
186
187    /// Set the positional encodings. See [`Content::prefix_encoding`]. Requires OpenAPI 3.2.
188    #[must_use]
189    pub fn prefix_encoding<I: IntoIterator<Item = Encoding>>(mut self, prefix_encoding: I) -> Self {
190        self.prefix_encoding = prefix_encoding.into_iter().collect();
191        self
192    }
193
194    /// Set the encoding applied to remaining array items. See [`Content::item_encoding`].
195    /// Requires OpenAPI 3.2.
196    #[must_use]
197    pub fn item_encoding<E: Into<Encoding>>(mut self, item_encoding: E) -> Self {
198        self.item_encoding = Some(item_encoding.into());
199        self
200    }
201}
202
203impl From<RefOr<Schema>> for Content {
204    fn from(schema: RefOr<Schema>) -> Self {
205        Self {
206            schema: Some(schema),
207            ..Self::default()
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use assert_json_diff::assert_json_eq;
215    use serde_json::{Map, json};
216
217    use super::*;
218
219    #[test]
220    fn test_build_content() {
221        let content = Content::new(RefOr::Ref(crate::Ref::from_schema_name("MySchema")))
222            .example(Value::Object(Map::from_iter([(
223                "schema".into(),
224                Value::String("MySchema".to_owned()),
225            )])))
226            .encoding(
227                "schema".to_owned(),
228                Encoding::default().content_type("text/plain"),
229            );
230        assert_json_eq!(
231            content,
232            json!({
233              "schema": {
234                "$ref": "#/components/schemas/MySchema"
235              },
236              "example": {
237                "schema": "MySchema"
238              },
239              "encoding": {
240                  "schema": {
241                    "contentType": "text/plain"
242                  }
243              }
244            })
245        );
246
247        let content = content
248            .schema(RefOr::Ref(crate::Ref::from_schema_name("NewSchema")))
249            .extend_examples([(
250                "example1".to_owned(),
251                Example::new().value(Value::Object(Map::from_iter([(
252                    "schema".into(),
253                    Value::String("MySchema".to_owned()),
254                )]))),
255            )]);
256        assert_json_eq!(
257            content,
258            json!({
259              "schema": {
260                "$ref": "#/components/schemas/NewSchema"
261              },
262              "example": {
263                "schema": "MySchema"
264              },
265              "examples": {
266                "example1": {
267                  "value": {
268                    "schema": "MySchema"
269                  }
270                }
271              },
272              "encoding": {
273                  "schema": {
274                    "contentType": "text/plain"
275                  }
276              }
277            })
278        );
279    }
280
281    #[test]
282    fn content_ref_serializes_as_a_reference_object() {
283        let content = Content::from_ref("#/components/mediaTypes/FramePayload");
284        let value = serde_json::to_value(&content).expect("serialize");
285        assert_json_eq!(
286            &value,
287            json!({ "$ref": "#/components/mediaTypes/FramePayload" })
288        );
289
290        let parsed: Content = serde_json::from_value(value).expect("deserialize");
291        assert_eq!(parsed, content);
292        assert!(parsed.schema.is_none());
293    }
294
295    #[test]
296    fn test_content_openapi_3_2_streaming_fields() {
297        let content = Content::default()
298            .item_schema(crate::Ref::from_schema_name("Frame"))
299            .prefix_encoding([Encoding::default().content_type("text/html")])
300            .item_encoding(Encoding::default().content_type("image/*"));
301
302        let value = serde_json::to_value(&content).expect("serialize");
303        assert_json_eq!(
304            &value,
305            json!({
306              "itemSchema": { "$ref": "#/components/schemas/Frame" },
307              "prefixEncoding": [ { "contentType": "text/html" } ],
308              "itemEncoding": { "contentType": "image/*" }
309            })
310        );
311
312        let parsed: Content = serde_json::from_value(value).expect("deserialize");
313        assert_eq!(parsed, content);
314    }
315}