salvo_oapi/openapi/
content.rs

1//! Implements content object for request body and response.
2use serde::{Deserialize, Serialize};
3
4use serde_json::Value;
5
6use super::PropMap;
7use super::example::Example;
8use super::{RefOr, Schema, encoding::Encoding};
9
10/// Content holds request body content or response content.
11#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
12#[non_exhaustive]
13pub struct Content {
14    /// Schema used in response body or request body.
15    pub schema: RefOr<Schema>,
16
17    /// Example for request body or response body.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub example: Option<Value>,
20
21    /// Examples of the request body or response body. [`Content::examples`] should match to
22    /// media type and specified schema if present. [`Content::examples`] and
23    /// [`Content::example`] are mutually exclusive. If both are defined `examples` will
24    /// override value in `example`.
25    #[serde(skip_serializing_if = "PropMap::is_empty")]
26    pub examples: PropMap<String, RefOr<Example>>,
27
28    /// A map between a property name and its encoding information.
29    ///
30    /// The key, being the property name, MUST exist in the [`Content::schema`] as a property, with
31    /// `schema` being a [`Schema::Object`] and this object containing the same property key in
32    /// [`Object::properties`](crate::schema::Object::properties).
33    ///
34    /// The encoding object SHALL only apply to `request_body` objects when the media type is
35    /// multipart or `application/x-www-form-urlencoded`.
36    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
37    pub encoding: PropMap<String, Encoding>,
38}
39
40impl Content {
41    /// Construct a new [`Content`].
42    pub fn new<I: Into<RefOr<Schema>>>(schema: I) -> Self {
43        Self {
44            schema: schema.into(),
45            ..Self::default()
46        }
47    }
48
49    /// Add schema.
50    pub fn schema<I: Into<RefOr<Schema>>>(mut self, component: I) -> Self {
51        self.schema = component.into();
52        self
53    }
54
55    /// Add example of schema.
56    pub fn example(mut self, example: Value) -> Self {
57        self.example = Some(example);
58        self
59    }
60
61    /// Add iterator of _`(N, V)`_ where `N` is name of example and `V` is [`Example`][example] to
62    /// [`Content`] of a request body or response body.
63    ///
64    /// [`Content::examples`] and [`Content::example`] are mutually exclusive. If both are defined
65    /// `examples` will override value in `example`.
66    ///
67    /// [example]: ../example/Example.html
68    pub fn extend_examples<
69        E: IntoIterator<Item = (N, V)>,
70        N: Into<String>,
71        V: Into<RefOr<Example>>,
72    >(
73        mut self,
74        examples: E,
75    ) -> Self {
76        self.examples.extend(
77            examples
78                .into_iter()
79                .map(|(name, example)| (name.into(), example.into())),
80        );
81
82        self
83    }
84
85    /// Add an encoding.
86    ///
87    /// The `property_name` MUST exist in the [`Content::schema`] as a property,
88    /// with `schema` being a [`Schema::Object`] and this object containing the same property
89    /// key in [`Object::properties`](crate::openapi::schema::Object::properties).
90    ///
91    /// The encoding object SHALL only apply to `request_body` objects when the media type is
92    /// multipart or `application/x-www-form-urlencoded`.
93    pub fn encoding<S: Into<String>, E: Into<Encoding>>(
94        mut self,
95        property_name: S,
96        encoding: E,
97    ) -> Self {
98        self.encoding.insert(property_name.into(), encoding.into());
99        self
100    }
101}
102
103impl From<RefOr<Schema>> for Content {
104    fn from(schema: RefOr<Schema>) -> Self {
105        Self {
106            schema,
107            ..Self::default()
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use assert_json_diff::assert_json_eq;
115    use serde_json::{Map, json};
116
117    use super::*;
118
119    #[test]
120    fn test_build_content() {
121        let content = Content::new(RefOr::Ref(crate::Ref::from_schema_name("MySchema")))
122            .example(Value::Object(Map::from_iter([(
123                "schema".into(),
124                Value::String("MySchema".to_string()),
125            )])))
126            .encoding(
127                "schema".to_string(),
128                Encoding::default().content_type("text/plain"),
129            );
130        assert_json_eq!(
131            content,
132            json!({
133              "schema": {
134                "$ref": "#/components/schemas/MySchema"
135              },
136              "example": {
137                "schema": "MySchema"
138              },
139              "encoding": {
140                  "schema": {
141                    "contentType": "text/plain"
142                  }
143              }
144            })
145        );
146
147        let content = content
148            .schema(RefOr::Ref(crate::Ref::from_schema_name("NewSchema")))
149            .extend_examples([(
150                "example1".to_string(),
151                Example::new().value(Value::Object(Map::from_iter([(
152                    "schema".into(),
153                    Value::String("MySchema".to_string()),
154                )]))),
155            )]);
156        assert_json_eq!(
157            content,
158            json!({
159              "schema": {
160                "$ref": "#/components/schemas/NewSchema"
161              },
162              "example": {
163                "schema": "MySchema"
164              },
165              "examples": {
166                "example1": {
167                  "value": {
168                    "schema": "MySchema"
169                  }
170                }
171              },
172              "encoding": {
173                  "schema": {
174                    "contentType": "text/plain"
175                  }
176              }
177            })
178        );
179    }
180}