Skip to main content

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::content::Content;
8use super::example::Example;
9use super::{Deprecated, RefOr, Required, Schema};
10use crate::PropMap;
11
12fn is_required_unset(value: &Required) -> bool {
13    matches!(value, Required::Unset)
14}
15
16/// Collection for OpenAPI Parameter Objects.
17#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)]
18pub struct Parameters(pub Vec<Parameter>);
19
20impl IntoIterator for Parameters {
21    type Item = Parameter;
22    type IntoIter = <Vec<Parameter> as IntoIterator>::IntoIter;
23
24    fn into_iter(self) -> Self::IntoIter {
25        self.0.into_iter()
26    }
27}
28
29impl Parameters {
30    /// Construct a new empty [`Parameters`]. This is effectively same as calling
31    /// [`Parameters::default`].
32    #[must_use]
33    pub fn new() -> Self {
34        Default::default()
35    }
36    /// Returns `true` if instance contains no elements.
37    #[must_use]
38    pub fn is_empty(&self) -> bool {
39        self.0.is_empty()
40    }
41    /// Add a new parameter and returns `self`.
42    #[must_use]
43    pub fn parameter<P: Into<Parameter>>(mut self, parameter: P) -> Self {
44        self.insert(parameter);
45        self
46    }
47    /// Returns `true` if instance contains a parameter with the given name and location.
48    #[must_use]
49    pub fn contains(&self, name: &str, parameter_in: ParameterIn) -> bool {
50        self.0
51            .iter()
52            .any(|item| item.name == name && item.parameter_in == parameter_in)
53    }
54    /// Inserts a parameter into the instance.
55    pub fn insert<P: Into<Parameter>>(&mut self, parameter: P) {
56        let mut parameter = parameter.into();
57        // Per the OpenAPI 3.1 spec, path parameters MUST have `required: true`.
58        if parameter.parameter_in == ParameterIn::Path {
59            parameter.required = Required::True;
60        }
61        let exist_item = self.0.iter_mut().find(|item| {
62            item.name == parameter.name && item.parameter_in == parameter.parameter_in
63        });
64
65        if let Some(exist_item) = exist_item {
66            exist_item.merge(parameter);
67        } else {
68            self.0.push(parameter);
69        }
70    }
71    /// Moves all elements from `other` into `self`, leaving `other` empty.
72    ///
73    /// If a key from `other` is already present in `self`, the respective
74    /// value from `self` will be overwritten with the respective value from `other`.
75    pub fn append(&mut self, other: &mut Self) {
76        for item in other.0.drain(..) {
77            self.insert(item);
78        }
79    }
80    /// Extends a collection with the contents of an iterator.
81    pub fn extend<I>(&mut self, iter: I)
82    where
83        I: IntoIterator<Item = Parameter>,
84    {
85        for item in iter {
86            self.insert(item);
87        }
88    }
89}
90
91/// Implements [OpenAPI Parameter Object][parameter] for [`Operation`](struct.Operation).
92///
93/// [parameter]: https://spec.openapis.org/oas/latest.html#parameter-object
94#[non_exhaustive]
95#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
96#[serde(rename_all = "camelCase")]
97pub struct Parameter {
98    /// Name of the parameter.
99    ///
100    /// * For [`ParameterIn::Path`] this must in accordance to path templating.
101    /// * For [`ParameterIn::Query`] `Content-Type` or `Authorization` value will be ignored.
102    pub name: String,
103
104    /// Parameter location.
105    #[serde(rename = "in")]
106    pub parameter_in: ParameterIn,
107
108    /// Markdown supported description of the parameter.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub description: Option<String>,
111
112    /// Declares whether the parameter is required or not for api.
113    ///
114    /// * For [`ParameterIn::Path`] this must and will be [`Required::True`].
115    /// * Defaults to [`Required::Unset`], which is omitted from the serialized output.
116    #[serde(default, skip_serializing_if = "is_required_unset")]
117    pub required: Required,
118
119    /// Declares the parameter deprecated status.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub deprecated: Option<Deprecated>,
122
123    /// Sets the ability to pass empty-valued parameters. Only applicable to
124    /// [`ParameterIn::Query`] parameters. Defaults to `false`.
125    ///
126    /// Note: per the OpenAPI 3.1 spec the use of this property is discouraged and may be
127    /// removed in a future version.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub allow_empty_value: Option<bool>,
130
131    /// Schema of the parameter. Typically [`Schema::Object`] is used.
132    ///
133    /// Mutually exclusive with [`Parameter::content`]; per the spec a parameter must
134    /// describe its value with one or the other, not both.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub schema: Option<RefOr<Schema>>,
137
138    /// Describes how [`Parameter`] is being serialized depending on [`Parameter::schema`] (type of
139    /// a content). Default value is based on [`ParameterIn`].
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub style: Option<ParameterStyle>,
142
143    /// When _`true`_ it will generate separate parameter value for each parameter with _`array`_
144    /// and _`object`_ type. This is also _`true`_ by default for [`ParameterStyle::Form`].
145    ///
146    /// With explode _`false`_:
147    /// ```text
148    /// color=blue,black,brown
149    /// ```
150    ///
151    /// With explode _`true`_:
152    /// ```text
153    /// color=blue&color=black&color=brown
154    /// ```
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub explode: Option<bool>,
157
158    /// Defines whether parameter should allow reserved characters defined by
159    /// [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2) _`:/?#[]@!$&'()*+,;=`_.
160    /// This is only applicable with [`ParameterIn::Query`]. Default value is _`false`_.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub allow_reserved: Option<bool>,
163
164    /// Example of the [`Parameter`]'s potential value. This example will override any
165    /// example defined within [`Parameter::schema`].
166    #[serde(skip_serializing_if = "Option::is_none")]
167    example: Option<Value>,
168
169    /// Examples of the parameter's potential value, indexed by name. When both
170    /// [`Parameter::example`] and `examples` are present, `examples` takes precedence.
171    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
172    pub examples: PropMap<String, RefOr<Example>>,
173
174    /// A map containing the representations for the parameter, keyed by media type.
175    ///
176    /// Per the OpenAPI 3.1 spec the map must contain exactly one entry. Mutually exclusive
177    /// with [`Parameter::schema`].
178    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
179    pub content: PropMap<String, Content>,
180
181    /// Optional extensions "x-something"
182    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
183    pub extensions: PropMap<String, serde_json::Value>,
184}
185
186impl Parameter {
187    /// Constructs a new required [`Parameter`] with given name.
188    #[must_use]
189    pub fn new<S: Into<String>>(name: S) -> Self {
190        Self {
191            name: name.into(),
192            required: Required::Unset,
193            ..Default::default()
194        }
195    }
196    /// Add name of the [`Parameter`].
197    #[must_use]
198    pub fn name<I: Into<String>>(mut self, name: I) -> Self {
199        self.name = name.into();
200        self
201    }
202
203    /// Sets the location (`in`) of the [`Parameter`].
204    ///
205    /// If the location is [`ParameterIn::Path`], the parameter is also marked required.
206    #[must_use]
207    pub fn location(mut self, location: ParameterIn) -> Self {
208        self.parameter_in = location;
209        if self.parameter_in == ParameterIn::Path {
210            self.required = Required::True;
211        }
212        self
213    }
214
215    /// Sets the location (`in`) of the [`Parameter`].
216    #[deprecated(since = "0.94.0", note = "use `Parameter::location` instead")]
217    #[must_use]
218    pub fn parameter_in(self, parameter_in: ParameterIn) -> Self {
219        self.location(parameter_in)
220    }
221
222    /// Fill [`Parameter`] with values from another [`Parameter`]. Fields will replaced if it is not
223    /// set.
224    pub fn merge(&mut self, other: Self) -> bool {
225        let Self {
226            name,
227            parameter_in,
228            description,
229            required,
230            deprecated,
231            allow_empty_value,
232            schema,
233            style,
234            explode,
235            allow_reserved,
236            example,
237            examples,
238            content,
239            extensions,
240        } = other;
241        if name != self.name || parameter_in != self.parameter_in {
242            return false;
243        }
244        if let Some(description) = description {
245            self.description = Some(description);
246        }
247
248        if required != Required::Unset {
249            self.required = required;
250        }
251        // Per the OpenAPI 3.1 spec, path parameters MUST have `required: true`.
252        if self.parameter_in == ParameterIn::Path {
253            self.required = Required::True;
254        }
255
256        if let Some(deprecated) = deprecated {
257            self.deprecated = Some(deprecated);
258        }
259        if let Some(allow_empty_value) = allow_empty_value {
260            self.allow_empty_value = Some(allow_empty_value);
261        }
262        if let Some(schema) = schema {
263            self.schema = Some(schema);
264        }
265        if let Some(style) = style {
266            self.style = Some(style);
267        }
268        if let Some(explode) = explode {
269            self.explode = Some(explode);
270        }
271        if let Some(allow_reserved) = allow_reserved {
272            self.allow_reserved = Some(allow_reserved);
273        }
274        if let Some(example) = example {
275            self.example = Some(example);
276        }
277        for (k, v) in examples {
278            self.examples.insert(k, v);
279        }
280        for (k, v) in content {
281            self.content.insert(k, v);
282        }
283
284        self.extensions.extend(extensions);
285        true
286    }
287
288    /// Add required declaration of the [`Parameter`]. If [`ParameterIn::Path`] is
289    /// defined this is always [`Required::True`].
290    #[must_use]
291    pub fn required(mut self, required: impl Into<Required>) -> Self {
292        self.required = required.into();
293        // required must be true, if parameter_in is Path
294        if self.parameter_in == ParameterIn::Path {
295            self.required = Required::True;
296        }
297
298        self
299    }
300
301    /// Add or change description of the [`Parameter`].
302    #[must_use]
303    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
304        self.description = Some(description.into());
305        self
306    }
307
308    /// Add or change [`Parameter`] deprecated declaration.
309    #[must_use]
310    pub fn deprecated<D: Into<Deprecated>>(mut self, deprecated: D) -> Self {
311        self.deprecated = Some(deprecated.into());
312        self
313    }
314
315    /// Add or change [`Parameter`]s schema.
316    #[must_use]
317    pub fn schema<I: Into<RefOr<Schema>>>(mut self, component: I) -> Self {
318        self.schema = Some(component.into());
319        self
320    }
321
322    /// Add or change serialization style of [`Parameter`].
323    #[must_use]
324    pub fn style(mut self, style: ParameterStyle) -> Self {
325        self.style = Some(style);
326        self
327    }
328
329    /// Define whether [`Parameter`]s are exploded or not.
330    #[must_use]
331    pub fn explode(mut self, explode: bool) -> Self {
332        self.explode = Some(explode);
333        self
334    }
335
336    /// Add or change whether [`Parameter`] should allow reserved characters.
337    #[must_use]
338    pub fn allow_reserved(mut self, allow_reserved: bool) -> Self {
339        self.allow_reserved = Some(allow_reserved);
340        self
341    }
342
343    /// Add or change example of [`Parameter`]'s potential value.
344    #[must_use]
345    pub fn example(mut self, example: Value) -> Self {
346        self.example = Some(example);
347        self
348    }
349
350    /// Insert a named [`Example`] (or a [`Ref`](crate::Ref) to one) into [`Parameter::examples`].
351    ///
352    /// When set, `examples` takes precedence over [`Parameter::example`].
353    #[must_use]
354    pub fn add_example<N: Into<String>, E: Into<RefOr<Example>>>(
355        mut self,
356        name: N,
357        example: E,
358    ) -> Self {
359        self.examples.insert(name.into(), example.into());
360        self
361    }
362
363    /// Replace [`Parameter::examples`] with the contents of an iterator of named examples.
364    #[must_use]
365    pub fn examples<I, N, E>(mut self, examples: I) -> Self
366    where
367        I: IntoIterator<Item = (N, E)>,
368        N: Into<String>,
369        E: Into<RefOr<Example>>,
370    {
371        self.examples = examples
372            .into_iter()
373            .map(|(name, example)| (name.into(), example.into()))
374            .collect();
375        self
376    }
377
378    /// Insert a single media-type entry into [`Parameter::content`].
379    ///
380    /// Per spec the `content` map must contain exactly one entry. Mutually exclusive with
381    /// [`Parameter::schema`].
382    #[must_use]
383    pub fn content<S: Into<String>, C: Into<Content>>(mut self, media_type: S, content: C) -> Self {
384        self.content.insert(media_type.into(), content.into());
385        self
386    }
387
388    /// Allow or disallow empty-valued query parameters.
389    ///
390    /// Note: per the OpenAPI 3.1 spec the use of this property is discouraged.
391    #[must_use]
392    pub fn allow_empty_value(mut self, allow: bool) -> Self {
393        self.allow_empty_value = Some(allow);
394        self
395    }
396}
397
398/// Possible values for the OpenAPI parameter `in` field, indicating where the parameter
399/// is located in the request.
400#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Default, Copy, Debug)]
401#[serde(rename_all = "lowercase")]
402pub enum ParameterIn {
403    /// Declares that parameter is used as query parameter.
404    Query,
405    /// Declares that parameter is used as path parameter.
406    #[default]
407    Path,
408    /// Declares that parameter is used as header value.
409    Header,
410    /// Declares that parameter is used as cookie value.
411    Cookie,
412}
413
414/// Defines how [`Parameter`] should be serialized.
415#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
416#[serde(rename_all = "camelCase")]
417pub enum ParameterStyle {
418    /// Path style parameters defined by [RFC6570](https://tools.ietf.org/html/rfc6570#section-3.2.7)
419    /// e.g _`;color=blue`_.
420    /// Allowed with [`ParameterIn::Path`].
421    Matrix,
422    /// Label style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.5)
423    /// e.g _`.color=blue`_.
424    /// Allowed with [`ParameterIn::Path`].
425    Label,
426    /// Form style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8)
427    /// e.g. _`color=blue`_. Default value for [`ParameterIn::Query`] [`ParameterIn::Cookie`].
428    /// Allowed with [`ParameterIn::Query`] or [`ParameterIn::Cookie`].
429    Form,
430    /// Default value for [`ParameterIn::Path`] [`ParameterIn::Header`]. e.g. _`blue`_.
431    /// Allowed with [`ParameterIn::Path`] or [`ParameterIn::Header`].
432    Simple,
433    /// Space separated array values e.g. _`blue%20black%20brown`_.
434    /// Allowed with [`ParameterIn::Query`].
435    SpaceDelimited,
436    /// Pipe separated array values e.g. _`blue|black|brown`_.
437    /// Allowed with [`ParameterIn::Query`].
438    PipeDelimited,
439    /// Simple way of rendering nested objects using form parameters .e.g. _`color[B]=150`_.
440    /// Allowed with [`ParameterIn::Query`].
441    DeepObject,
442}
443
444#[cfg(test)]
445mod tests {
446    use assert_json_diff::assert_json_eq;
447    use serde_json::json;
448
449    use super::*;
450    use crate::Object;
451
452    #[test]
453    fn test_build_parameter() {
454        let parameter = Parameter::new("name");
455        assert_eq!(parameter.name, "name");
456
457        let parameter = parameter
458            .name("new name")
459            .location(ParameterIn::Query)
460            .required(Required::True)
461            .description("description")
462            .deprecated(Deprecated::False)
463            .schema(Schema::object(Object::new()))
464            .style(ParameterStyle::Simple)
465            .explode(true)
466            .allow_reserved(true)
467            .example(Value::String("example".to_owned()));
468        assert_json_eq!(
469            parameter,
470            json!({
471                "name": "new name",
472                "in": "query",
473                "required": true,
474                "description": "description",
475                "deprecated": false,
476                "schema": {
477                    "type": "object"
478                },
479                "style": "simple",
480                "explode": true,
481                "allowReserved": true,
482                "example": "example"
483            })
484        );
485    }
486
487    #[test]
488    fn test_parameter_merge_fail() {
489        let mut parameter1 = Parameter::new("param1");
490        let parameter2 = Parameter::new("param2");
491
492        assert!(!parameter1.merge(parameter2));
493    }
494
495    #[test]
496    fn test_parameter_merge_success() {
497        let mut parameter1 = Parameter::new("param1");
498        let mut parameter2 = Parameter::new("param1")
499            .description("description")
500            .required(Required::True)
501            .deprecated(Deprecated::True)
502            .schema(Schema::object(Object::new()))
503            .style(ParameterStyle::Form)
504            .explode(true)
505            .allow_reserved(true)
506            .example(Value::String("example".to_owned()));
507
508        parameter1.extensions =
509            PropMap::from([("key1".to_owned(), Value::String("value1".to_owned()))]);
510        parameter2.extensions =
511            PropMap::from([("key2".to_owned(), Value::String("value2".to_owned()))]);
512
513        assert!(parameter1.merge(parameter2));
514        assert_json_eq!(
515            parameter1,
516            json!({
517                "name": "param1",
518                "in": "path",
519                "description": "description",
520                "required": true,
521                "deprecated": true,
522                "schema": {
523                    "type": "object"
524                },
525                "style": "form",
526                "explode": true,
527                "allowReserved": true,
528                "example": "example",
529                "key1": "value1",
530                "key2": "value2"
531            })
532        )
533    }
534
535    #[test]
536    fn test_parameter_merge_no_extensions() {
537        let mut parameter1 = Parameter::new("param1");
538        let mut parameter2 = Parameter::new("param1")
539            .description("description")
540            .required(Required::True)
541            .deprecated(Deprecated::True)
542            .schema(Schema::object(Object::new()))
543            .style(ParameterStyle::Form)
544            .explode(true)
545            .allow_reserved(true)
546            .example(Value::String("example".to_owned()));
547
548        parameter2.extensions =
549            PropMap::from([("key2".to_owned(), Value::String("value2".to_owned()))]);
550
551        assert!(parameter1.merge(parameter2));
552        assert_json_eq!(
553            parameter1,
554            json!({
555                "name": "param1",
556                "in": "path",
557                "description": "description",
558                "required": true,
559                "deprecated": true,
560                "schema": {
561                    "type": "object"
562                },
563                "style": "form",
564                "explode": true,
565                "allowReserved": true,
566                "example": "example",
567                "key2": "value2",
568            })
569        )
570    }
571
572    #[test]
573    fn test_build_parameters() {
574        let parameters = Parameters::new();
575        assert!(parameters.is_empty());
576    }
577
578    #[test]
579    fn test_parameters_into_iter() {
580        let parameters = Parameters::new().parameter(Parameter::new("param"));
581        let mut iter = parameters.into_iter();
582        // `Parameters::insert` forces `required: True` for path parameters per spec, so the
583        // round-tripped item is not equal to a fresh `Parameter::new("param")` with
584        // `Required::Unset`.
585        assert_eq!(
586            iter.next(),
587            Some(Parameter::new("param").required(Required::True))
588        );
589        assert!(iter.next().is_none());
590    }
591
592    #[test]
593    fn test_parameters_contain() {
594        let parameters = Parameters::new().parameter(Parameter::new("param"));
595        assert!(parameters.contains("param", ParameterIn::Path));
596    }
597
598    #[test]
599    fn test_parameters_insert_existed_item() {
600        let mut parameters = Parameters::new();
601        parameters.insert(Parameter::new("param"));
602        assert!(parameters.contains("param", ParameterIn::Path));
603
604        parameters.insert(Parameter::new("param"));
605        assert_eq!(parameters.0.len(), 1);
606    }
607
608    #[test]
609    fn test_parameters_append() {
610        let mut parameters1 = Parameters::new().parameter(Parameter::new("param1"));
611        let mut parameters2 = Parameters::new().parameter(Parameter::new("param2"));
612
613        parameters1.append(&mut parameters2);
614        assert_json_eq!(
615            parameters1,
616            json!([
617                {
618                    "in": "path",
619                    "name": "param1",
620                    "required": true
621                },
622                {
623                    "in": "path",
624                    "name": "param2",
625                    "required": true
626                }
627            ])
628        );
629    }
630
631    #[test]
632    fn parameter_required_unset_is_omitted() {
633        let parameter = Parameter::new("filter").location(ParameterIn::Query);
634        let value = serde_json::to_value(&parameter).expect("serialize");
635        assert!(
636            value.get("required").is_none(),
637            "expected `required` to be omitted when Unset; got: {value}"
638        );
639    }
640
641    #[test]
642    fn parameter_required_true_is_emitted_for_path_in() {
643        let parameter = Parameter::new("id").location(ParameterIn::Path);
644        // parameter_in(Path) should force required=true (per spec) and serialize it.
645        let value = serde_json::to_value(&parameter).expect("serialize");
646        assert_eq!(value["required"], serde_json::Value::Bool(true));
647    }
648
649    #[test]
650    fn parameter_with_examples_serializes_under_camel_case_field() {
651        use crate::Example;
652
653        let parameter = Parameter::new("filter")
654            .location(ParameterIn::Query)
655            .add_example("first", Example::new().value(json!("foo")))
656            .add_example("second", Example::new().value(json!("bar")));
657
658        let value = serde_json::to_value(&parameter).expect("serialize");
659        assert_eq!(
660            value["examples"],
661            json!({
662                "first":  { "value": "foo" },
663                "second": { "value": "bar" }
664            })
665        );
666    }
667
668    #[test]
669    fn parameter_with_content_serializes_as_media_type_map() {
670        let parameter = Parameter::new("filter")
671            .location(ParameterIn::Query)
672            .content(
673                "application/json",
674                Content::new(RefOr::Ref(crate::Ref::from_schema_name("Filter"))),
675            );
676
677        let value = serde_json::to_value(&parameter).expect("serialize");
678        assert_eq!(
679            value["content"],
680            json!({
681                "application/json": {
682                    "schema": { "$ref": "#/components/schemas/Filter" }
683                }
684            })
685        );
686    }
687
688    #[test]
689    fn parameter_allow_empty_value_round_trips_under_camel_case() {
690        let parameter = Parameter::new("flag")
691            .location(ParameterIn::Query)
692            .allow_empty_value(true);
693
694        let value = serde_json::to_value(&parameter).expect("serialize");
695        assert_eq!(value["allowEmptyValue"], serde_json::Value::Bool(true));
696    }
697}