Skip to main content

salvo_oapi/openapi/
header.rs

1//! Implements [OpenAPI Header Object][header] types.
2//!
3//! [header]: https://spec.openapis.org/oas/latest.html#header-object
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use super::parameter::ParameterStyle;
9use super::{BasicType, Content, Deprecated, Example, Object, PropMap, RefOr, Schema};
10
11/// Implements [OpenAPI Header Object][header] for response headers and for individual parts in
12/// `multipart` representations.
13///
14/// A Header Object follows the structure of the [`Parameter`](crate::Parameter) object minus
15/// `name` and `in`, and describes its value either through [`Header::schema`] or through
16/// [`Header::content`].
17///
18/// [header]: https://spec.openapis.org/oas/latest.html#header-object
19#[non_exhaustive]
20#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
21#[serde(rename_all = "camelCase")]
22pub struct Header {
23    /// Additional description of the header value.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub description: Option<String>,
26
27    /// Determines whether this header is mandatory. Defaults to `false`.
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub required: Option<bool>,
30
31    /// Declares the header deprecated and to be transitioned out of usage.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub deprecated: Option<Deprecated>,
34
35    /// Schema of header type. Mutually exclusive with [`Header::content`].
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub schema: Option<RefOr<Schema>>,
38
39    /// Describes how the header value is serialized. The only legal value for headers is
40    /// [`ParameterStyle::Simple`], which is also the default.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub style: Option<ParameterStyle>,
43
44    /// When `true`, `array` or `object` header values generate a single header whose value is a
45    /// comma-separated list. Defaults to `false`.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub explode: Option<bool>,
48
49    /// Example of the header's potential value.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub example: Option<Value>,
52
53    /// Examples of the header's potential value, indexed by name. Mutually exclusive with
54    /// [`Header::example`].
55    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
56    pub examples: PropMap<String, RefOr<Example>>,
57
58    /// A map containing the representations for the header, keyed by media type. Per spec the
59    /// map must contain exactly one entry. Mutually exclusive with [`Header::schema`].
60    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
61    pub content: PropMap<String, Content>,
62
63    /// Optional extensions "x-something"
64    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
65    pub extensions: PropMap<String, serde_json::Value>,
66}
67
68impl Header {
69    /// Construct a new [`Header`] with custom schema. If you wish to construct a default
70    /// header with `String` type you can use [`Header::default`] function.
71    ///
72    /// # Examples
73    ///
74    /// Creates a new [`Header`] with an integer type.
75    /// ```
76    /// # use salvo_oapi::{Header, Object, BasicType};
77    /// let header = Header::new(Object::with_type(BasicType::Integer));
78    /// ```
79    ///
80    /// Create a new [`Header`] with default type `String`
81    /// ```
82    /// # use salvo_oapi::Header;
83    /// let header = Header::default();
84    /// ```
85    #[must_use]
86    pub fn new<C: Into<RefOr<Schema>>>(component: C) -> Self {
87        Self {
88            schema: Some(component.into()),
89            ..Default::default()
90        }
91    }
92
93    /// Construct a [`Header`] that describes its value with a media type instead of a schema.
94    ///
95    /// ```
96    /// # use salvo_oapi::{Content, Header, Object, BasicType};
97    /// let header = Header::with_content(
98    ///     "application/linkset",
99    ///     Content::new(Object::with_type(BasicType::String)),
100    /// );
101    /// ```
102    #[must_use]
103    pub fn with_content<S: Into<String>, C: Into<Content>>(media_type: S, content: C) -> Self {
104        let mut header = Self {
105            schema: None,
106            ..Default::default()
107        };
108        header.content.insert(media_type.into(), content.into());
109        header
110    }
111
112    /// Add schema of header.
113    #[must_use]
114    pub fn schema<I: Into<RefOr<Schema>>>(mut self, component: I) -> Self {
115        self.schema = Some(component.into());
116        self
117    }
118
119    /// Add additional description for header.
120    #[must_use]
121    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
122        self.description = Some(description.into());
123        self
124    }
125
126    /// Declare whether the header is mandatory.
127    #[must_use]
128    pub fn required(mut self, required: bool) -> Self {
129        self.required = Some(required);
130        self
131    }
132
133    /// Declare the header deprecated.
134    #[must_use]
135    pub fn deprecated<D: Into<Deprecated>>(mut self, deprecated: D) -> Self {
136        self.deprecated = Some(deprecated.into());
137        self
138    }
139
140    /// Set the serialization style of the header. Only [`ParameterStyle::Simple`] is legal.
141    #[must_use]
142    pub fn style(mut self, style: ParameterStyle) -> Self {
143        self.style = Some(style);
144        self
145    }
146
147    /// Define whether `array` or `object` header values are exploded.
148    #[must_use]
149    pub fn explode(mut self, explode: bool) -> Self {
150        self.explode = Some(explode);
151        self
152    }
153
154    /// Add an example of the header's potential value.
155    #[must_use]
156    pub fn example(mut self, example: Value) -> Self {
157        self.example = Some(example);
158        self
159    }
160
161    /// Insert a named [`Example`] (or a [`Ref`](crate::Ref) to one) into [`Header::examples`].
162    #[must_use]
163    pub fn add_example<N: Into<String>, E: Into<RefOr<Example>>>(
164        mut self,
165        name: N,
166        example: E,
167    ) -> Self {
168        self.examples.insert(name.into(), example.into());
169        self
170    }
171
172    /// Insert a single media-type entry into [`Header::content`].
173    ///
174    /// Per spec the `content` map must contain exactly one entry. Mutually exclusive with
175    /// [`Header::schema`].
176    #[must_use]
177    pub fn content<S: Into<String>, C: Into<Content>>(mut self, media_type: S, content: C) -> Self {
178        self.content.insert(media_type.into(), content.into());
179        self
180    }
181
182    /// Add openapi extension (`x-something`) for [`Header`].
183    #[must_use]
184    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
185        self.extensions.insert(key.into(), value);
186        self
187    }
188}
189
190impl Default for Header {
191    fn default() -> Self {
192        Self {
193            description: Default::default(),
194            required: Default::default(),
195            deprecated: Default::default(),
196            schema: Some(Object::with_type(BasicType::String).into()),
197            style: Default::default(),
198            explode: Default::default(),
199            example: Default::default(),
200            examples: Default::default(),
201            content: Default::default(),
202            extensions: Default::default(),
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use assert_json_diff::assert_json_eq;
210    use serde_json::json;
211
212    use super::*;
213
214    #[test]
215    fn test_build_header() {
216        let header = Header::new(Object::with_type(BasicType::String));
217        assert_json_eq!(
218            header,
219            json!({
220                "schema": {
221                    "type": "string"
222                }
223            })
224        );
225
226        let header = header
227            .description("test description")
228            .schema(Object::with_type(BasicType::Number));
229        assert_json_eq!(
230            header,
231            json!({
232                "description": "test description",
233                "schema": {
234                    "type": "number"
235                }
236            })
237        );
238    }
239
240    #[test]
241    fn header_full_surface_round_trips() {
242        let header = Header::new(Object::with_type(BasicType::String))
243            .description("rate limit")
244            .required(true)
245            .deprecated(crate::Deprecated::False)
246            .style(ParameterStyle::Simple)
247            .explode(false)
248            .example(json!("100"))
249            .add_extension("x-vendor", json!("acme"));
250
251        let value = serde_json::to_value(&header).expect("serialize");
252        assert_json_eq!(
253            &value,
254            json!({
255                "description": "rate limit",
256                "required": true,
257                "deprecated": false,
258                "schema": { "type": "string" },
259                "style": "simple",
260                "explode": false,
261                "example": "100",
262                "x-vendor": "acme"
263            })
264        );
265
266        let parsed: Header = serde_json::from_value(value).expect("deserialize");
267        assert_eq!(parsed, header);
268    }
269
270    #[test]
271    fn header_with_content_omits_schema() {
272        let header = Header::with_content(
273            "application/linkset",
274            Content::new(Object::with_type(BasicType::String)),
275        );
276
277        let value = serde_json::to_value(&header).expect("serialize");
278        assert_json_eq!(
279            &value,
280            json!({
281                "content": {
282                    "application/linkset": { "schema": { "type": "string" } }
283                }
284            })
285        );
286
287        let parsed: Header = serde_json::from_value(value).expect("deserialize");
288        assert_eq!(parsed, header);
289    }
290}