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