salvo_oapi/openapi/
parameter.rs

1//! Implements [OpenAPI Parameter Object][parameter] types.
2//!
3//! [parameter]: https://spec.openapis.org/oas/latest.html#parameter-object
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use super::{Deprecated, RefOr, Required, Schema};
8use crate::PropMap;
9
10/// Collection for OpenAPI Parameter Objects.
11#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)]
12pub struct Parameters(pub Vec<Parameter>);
13
14impl IntoIterator for Parameters {
15    type Item = Parameter;
16    type IntoIter = <Vec<Parameter> as IntoIterator>::IntoIter;
17
18    fn into_iter(self) -> Self::IntoIter {
19        self.0.into_iter()
20    }
21}
22
23impl Parameters {
24    /// Construct a new empty [`Parameters`]. This is effectively same as calling [`Parameters::default`].
25    #[must_use]
26    pub fn new() -> Self {
27        Default::default()
28    }
29    /// Returns `true` if instance contains no elements.
30    #[must_use]
31    pub fn is_empty(&self) -> bool {
32        self.0.is_empty()
33    }
34    /// Add a new parameter and returns `self`.
35    #[must_use]
36    pub fn parameter<P: Into<Parameter>>(mut self, parameter: P) -> Self {
37        self.insert(parameter);
38        self
39    }
40    /// Returns `true` if instance contains a parameter with the given name and location.
41    #[must_use]
42    pub fn contains(&self, name: &str, parameter_in: ParameterIn) -> bool {
43        self.0
44            .iter()
45            .any(|item| item.name == name && item.parameter_in == parameter_in)
46    }
47    /// Inserts a parameter into the instance.
48    pub fn insert<P: Into<Parameter>>(&mut self, parameter: P) {
49        let parameter = parameter.into();
50        let exist_item = self.0.iter_mut().find(|item| {
51            item.name == parameter.name && item.parameter_in == parameter.parameter_in
52        });
53
54        if let Some(exist_item) = exist_item {
55            exist_item.merge(parameter);
56        } else {
57            self.0.push(parameter);
58        }
59    }
60    /// Moves all elements from `other` into `self`, leaving `other` empty.
61    ///
62    /// If a key from `other` is already present in `self`, the respective
63    /// value from `self` will be overwritten with the respective value from `other`.
64    pub fn append(&mut self, other: &mut Self) {
65        for item in other.0.drain(..) {
66            self.insert(item);
67        }
68    }
69    /// Extends a collection with the contents of an iterator.
70    pub fn extend<I>(&mut self, iter: I)
71    where
72        I: IntoIterator<Item = Parameter>,
73    {
74        for item in iter {
75            self.insert(item);
76        }
77    }
78}
79
80/// Implements [OpenAPI Parameter Object][parameter] for [`Operation`](struct.Operation).
81///
82/// [parameter]: https://spec.openapis.org/oas/latest.html#parameter-object
83#[non_exhaustive]
84#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
85#[serde(rename_all = "camelCase")]
86pub struct Parameter {
87    /// Name of the parameter.
88    ///
89    /// * For [`ParameterIn::Path`] this must in accordance to path templating.
90    /// * For [`ParameterIn::Query`] `Content-Type` or `Authorization` value will be ignored.
91    pub name: String,
92
93    /// Parameter location.
94    #[serde(rename = "in")]
95    pub parameter_in: ParameterIn,
96
97    /// Markdown supported description of the parameter.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub description: Option<String>,
100
101    /// Declares whether the parameter is required or not for api.
102    ///
103    /// * For [`ParameterIn::Path`] this must and will be [`Required::True`].
104    pub required: Required,
105
106    /// Declares the parameter deprecated status.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub deprecated: Option<Deprecated>,
109    // pub allow_empty_value: bool, this is going to be removed from further open api spec releases
110    /// Schema of the parameter. Typically [`Schema::Object`] is used.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub schema: Option<RefOr<Schema>>,
113
114    /// Describes how [`Parameter`] is being serialized depending on [`Parameter::schema`] (type of a content).
115    /// Default value is based on [`ParameterIn`].
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub style: Option<ParameterStyle>,
118
119    /// When _`true`_ it will generate separate parameter value for each parameter with _`array`_ and _`object`_ type.
120    /// This is also _`true`_ by default for [`ParameterStyle::Form`].
121    ///
122    /// With explode _`false`_:
123    /// ```text
124    ///color=blue,black,brown
125    /// ```
126    ///
127    /// With explode _`true`_:
128    /// ```text
129    ///color=blue&color=black&color=brown
130    /// ```
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub explode: Option<bool>,
133
134    /// Defines whether parameter should allow reserved characters defined by
135    /// [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) _`:/?#[]@!$&'()*+,;=`_.
136    /// This is only applicable with [`ParameterIn::Query`]. Default value is _`false`_.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub allow_reserved: Option<bool>,
139
140    /// Example of [`Parameter`]'s potential value. This examples will override example
141    /// within [`Parameter::schema`] if defined.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    example: Option<Value>,
144
145    /// Optional extensions "x-something"
146    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
147    pub extensions: PropMap<String, serde_json::Value>,
148}
149
150impl Parameter {
151    /// Constructs a new required [`Parameter`] with given name.
152    #[must_use]
153    pub fn new<S: Into<String>>(name: S) -> Self {
154        Self {
155            name: name.into(),
156            required: Required::Unset,
157            ..Default::default()
158        }
159    }
160    /// Add name of the [`Parameter`].
161    #[must_use]
162    pub fn name<I: Into<String>>(mut self, name: I) -> Self {
163        self.name = name.into();
164        self
165    }
166
167    /// Add in of the [`Parameter`].
168    #[must_use]
169    pub fn parameter_in(mut self, parameter_in: ParameterIn) -> Self {
170        self.parameter_in = parameter_in;
171        if self.parameter_in == ParameterIn::Path {
172            self.required = Required::True;
173        }
174        self
175    }
176
177    /// Fill [`Parameter`] with values from another [`Parameter`]. Fields will replaced if it is not set.
178    pub fn merge(&mut self, other: Self) -> bool {
179        let Self {
180            name,
181            parameter_in,
182            description,
183            required,
184            deprecated,
185            schema,
186            style,
187            explode,
188            allow_reserved,
189            example,
190            extensions,
191        } = other;
192        if name != self.name || parameter_in != self.parameter_in {
193            return false;
194        }
195        if let Some(description) = description {
196            self.description = Some(description);
197        }
198
199        if required != Required::Unset {
200            self.required = required;
201        }
202
203        if let Some(deprecated) = deprecated {
204            self.deprecated = Some(deprecated);
205        }
206        if let Some(schema) = schema {
207            self.schema = Some(schema);
208        }
209        if let Some(style) = style {
210            self.style = Some(style);
211        }
212        if let Some(explode) = explode {
213            self.explode = Some(explode);
214        }
215        if let Some(allow_reserved) = allow_reserved {
216            self.allow_reserved = Some(allow_reserved);
217        }
218        if let Some(example) = example {
219            self.example = Some(example);
220        }
221
222        self.extensions.extend(extensions);
223        true
224    }
225
226    /// Add required declaration of the [`Parameter`]. If [`ParameterIn::Path`] is
227    /// defined this is always [`Required::True`].
228    #[must_use]
229    pub fn required(mut self, required: impl Into<Required>) -> Self {
230        self.required = required.into();
231        // required must be true, if parameter_in is Path
232        if self.parameter_in == ParameterIn::Path {
233            self.required = Required::True;
234        }
235
236        self
237    }
238
239    /// Add or change description of the [`Parameter`].
240    #[must_use]
241    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
242        self.description = Some(description.into());
243        self
244    }
245
246    /// Add or change [`Parameter`] deprecated declaration.
247    #[must_use]
248    pub fn deprecated<D: Into<Deprecated>>(mut self, deprecated: D) -> Self {
249        self.deprecated = Some(deprecated.into());
250        self
251    }
252
253    /// Add or change [`Parameter`]s schema.
254    #[must_use]
255    pub fn schema<I: Into<RefOr<Schema>>>(mut self, component: I) -> Self {
256        self.schema = Some(component.into());
257        self
258    }
259
260    /// Add or change serialization style of [`Parameter`].
261    #[must_use]
262    pub fn style(mut self, style: ParameterStyle) -> Self {
263        self.style = Some(style);
264        self
265    }
266
267    /// Define whether [`Parameter`]s are exploded or not.
268    #[must_use]
269    pub fn explode(mut self, explode: bool) -> Self {
270        self.explode = Some(explode);
271        self
272    }
273
274    /// Add or change whether [`Parameter`] should allow reserved characters.
275    #[must_use]
276    pub fn allow_reserved(mut self, allow_reserved: bool) -> Self {
277        self.allow_reserved = Some(allow_reserved);
278        self
279    }
280
281    /// Add or change example of [`Parameter`]'s potential value.
282    #[must_use]
283    pub fn example(mut self, example: Value) -> Self {
284        self.example = Some(example);
285        self
286    }
287}
288
289/// In definition of [`Parameter`].
290#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
291#[serde(rename_all = "lowercase")]
292pub enum ParameterIn {
293    /// Declares that parameter is used as query parameter.
294    Query,
295    /// Declares that parameter is used as path parameter.
296    Path,
297    /// Declares that parameter is used as header value.
298    Header,
299    /// Declares that parameter is used as cookie value.
300    Cookie,
301}
302
303impl Default for ParameterIn {
304    fn default() -> Self {
305        Self::Path
306    }
307}
308
309/// Defines how [`Parameter`] should be serialized.
310#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
311#[serde(rename_all = "camelCase")]
312pub enum ParameterStyle {
313    /// Path style parameters defined by [RFC6570](https://tools.ietf.org/html/rfc6570#section-3.2.7)
314    /// e.g _`;color=blue`_.
315    /// Allowed with [`ParameterIn::Path`].
316    Matrix,
317    /// Label style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.5)
318    /// e.g _`.color=blue`_.
319    /// Allowed with [`ParameterIn::Path`].
320    Label,
321    /// Form style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8)
322    /// e.g. _`color=blue`_. Default value for [`ParameterIn::Query`] [`ParameterIn::Cookie`].
323    /// Allowed with [`ParameterIn::Query`] or [`ParameterIn::Cookie`].
324    Form,
325    /// Default value for [`ParameterIn::Path`] [`ParameterIn::Header`]. e.g. _`blue`_.
326    /// Allowed with [`ParameterIn::Path`] or [`ParameterIn::Header`].
327    Simple,
328    /// Space separated array values e.g. _`blue%20black%20brown`_.
329    /// Allowed with [`ParameterIn::Query`].
330    SpaceDelimited,
331    /// Pipe separated array values e.g. _`blue|black|brown`_.
332    /// Allowed with [`ParameterIn::Query`].
333    PipeDelimited,
334    /// Simple way of rendering nested objects using form parameters .e.g. _`color[B]=150`_.
335    /// Allowed with [`ParameterIn::Query`].
336    DeepObject,
337}
338
339#[cfg(test)]
340mod tests {
341    use assert_json_diff::assert_json_eq;
342    use serde_json::json;
343
344    use crate::Object;
345
346    use super::*;
347
348    #[test]
349    fn test_build_parameter() {
350        let parameter = Parameter::new("name");
351        assert_eq!(parameter.name, "name");
352
353        let parameter = parameter
354            .name("new name")
355            .parameter_in(ParameterIn::Query)
356            .required(Required::True)
357            .description("description")
358            .deprecated(Deprecated::False)
359            .schema(Schema::object(Object::new()))
360            .style(ParameterStyle::Simple)
361            .explode(true)
362            .allow_reserved(true)
363            .example(Value::String("example".to_owned()));
364        assert_json_eq!(
365            parameter,
366            json!({
367                "name": "new name",
368                "in": "query",
369                "required": true,
370                "description": "description",
371                "deprecated": false,
372                "schema": {
373                    "type": "object"
374                },
375                "style": "simple",
376                "explode": true,
377                "allowReserved": true,
378                "example": "example"
379            })
380        );
381    }
382
383    #[test]
384    fn test_parameter_merge_fail() {
385        let mut parameter1 = Parameter::new("param1");
386        let parameter2 = Parameter::new("param2");
387
388        assert!(!parameter1.merge(parameter2));
389    }
390
391    #[test]
392    fn test_parameter_merge_success() {
393        let mut parameter1 = Parameter::new("param1");
394        let mut parameter2 = Parameter::new("param1")
395            .description("description")
396            .required(Required::True)
397            .deprecated(Deprecated::True)
398            .schema(Schema::object(Object::new()))
399            .style(ParameterStyle::Form)
400            .explode(true)
401            .allow_reserved(true)
402            .example(Value::String("example".to_owned()));
403
404        parameter1.extensions =
405            PropMap::from([("key1".to_owned(), Value::String("value1".to_owned()))]);
406        parameter2.extensions =
407            PropMap::from([("key2".to_owned(), Value::String("value2".to_owned()))]);
408
409        assert!(parameter1.merge(parameter2));
410        assert_json_eq!(
411            parameter1,
412            json!({
413                "name": "param1",
414                "in": "path",
415                "description": "description",
416                "required": true,
417                "deprecated": true,
418                "schema": {
419                    "type": "object"
420                },
421                "style": "form",
422                "explode": true,
423                "allowReserved": true,
424                "example": "example",
425                "key1": "value1",
426                "key2": "value2"
427            })
428        )
429    }
430
431    #[test]
432    fn test_parameter_merge_no_extensions() {
433        let mut parameter1 = Parameter::new("param1");
434        let mut parameter2 = Parameter::new("param1")
435            .description("description")
436            .required(Required::True)
437            .deprecated(Deprecated::True)
438            .schema(Schema::object(Object::new()))
439            .style(ParameterStyle::Form)
440            .explode(true)
441            .allow_reserved(true)
442            .example(Value::String("example".to_owned()));
443
444        parameter2.extensions =
445            PropMap::from([("key2".to_owned(), Value::String("value2".to_owned()))]);
446
447        assert!(parameter1.merge(parameter2));
448        assert_json_eq!(
449            parameter1,
450            json!({
451                "name": "param1",
452                "in": "path",
453                "description": "description",
454                "required": true,
455                "deprecated": true,
456                "schema": {
457                    "type": "object"
458                },
459                "style": "form",
460                "explode": true,
461                "allowReserved": true,
462                "example": "example",
463                "key2": "value2",
464            })
465        )
466    }
467
468    #[test]
469    fn test_build_parameters() {
470        let parameters = Parameters::new();
471        assert!(parameters.is_empty());
472    }
473
474    #[test]
475    fn test_parameters_into_iter() {
476        let parameters = Parameters::new().parameter(Parameter::new("param"));
477        let mut iter = parameters.into_iter();
478        assert_eq!(iter.next(), Some(Parameter::new("param")));
479        assert!(iter.next().is_none());
480    }
481
482    #[test]
483    fn test_parameters_contain() {
484        let parameters = Parameters::new().parameter(Parameter::new("param"));
485        assert!(parameters.contains("param", ParameterIn::Path));
486    }
487
488    #[test]
489    fn test_parameters_insert_existed_item() {
490        let mut parameters = Parameters::new();
491        parameters.insert(Parameter::new("param"));
492        assert!(parameters.contains("param", ParameterIn::Path));
493
494        parameters.insert(Parameter::new("param"));
495        assert_eq!(parameters.0.len(), 1);
496    }
497
498    #[test]
499    fn test_parameters_append() {
500        let mut parameters1 = Parameters::new().parameter(Parameter::new("param1"));
501        let mut parameters2 = Parameters::new().parameter(Parameter::new("param2"));
502
503        parameters1.append(&mut parameters2);
504        assert_json_eq!(
505            parameters1,
506            json!([
507                {
508                    "in": "path",
509                    "name": "param1",
510                    "required": false
511                },
512                {
513                    "in": "path",
514                    "name": "param2",
515                    "required": false
516                }
517            ])
518        );
519    }
520}