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    /// When _`true`_, values are serialized using reserved expansion, letting the reserved
159    /// characters defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-2.2)
160    /// _`:/?#[]@!$&'()*+,;=`_ and percent-encoded triples pass through unchanged.
161    /// Default value is _`false`_.
162    ///
163    /// OpenAPI 3.1 restricted this to [`ParameterIn::Query`]; OpenAPI 3.2 defines it in terms of
164    /// RFC6570 reserved expansion, so it also applies to [`ParameterIn::Path`] parameters using
165    /// an RFC6570-based style.
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub allow_reserved: Option<bool>,
168
169    /// Example of the [`Parameter`]'s potential value. This example will override any
170    /// example defined within [`Parameter::schema`].
171    #[serde(skip_serializing_if = "Option::is_none")]
172    example: Option<Value>,
173
174    /// Examples of the parameter's potential value, indexed by name. When both
175    /// [`Parameter::example`] and `examples` are present, `examples` takes precedence.
176    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
177    pub examples: PropMap<String, RefOr<Example>>,
178
179    /// A map containing the representations for the parameter, keyed by media type.
180    ///
181    /// Per the OpenAPI 3.1 spec the map must contain exactly one entry. Mutually exclusive
182    /// with [`Parameter::schema`].
183    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
184    pub content: PropMap<String, Content>,
185
186    /// Optional extensions "x-something"
187    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
188    pub extensions: PropMap<String, serde_json::Value>,
189}
190
191impl Parameter {
192    /// Constructs a new required [`Parameter`] with given name.
193    #[must_use]
194    pub fn new<S: Into<String>>(name: S) -> Self {
195        Self {
196            name: name.into(),
197            required: Required::Unset,
198            ..Default::default()
199        }
200    }
201    /// Add name of the [`Parameter`].
202    #[must_use]
203    pub fn name<I: Into<String>>(mut self, name: I) -> Self {
204        self.name = name.into();
205        self
206    }
207
208    /// Sets the location (`in`) of the [`Parameter`].
209    ///
210    /// If the location is [`ParameterIn::Path`], the parameter is also marked required.
211    #[must_use]
212    pub fn location(mut self, location: ParameterIn) -> Self {
213        self.parameter_in = location;
214        if self.parameter_in == ParameterIn::Path {
215            self.required = Required::True;
216        }
217        self
218    }
219
220    /// Sets the location (`in`) of the [`Parameter`].
221    #[deprecated(since = "0.94.0", note = "use `Parameter::location` instead")]
222    #[must_use]
223    pub fn parameter_in(self, parameter_in: ParameterIn) -> Self {
224        self.location(parameter_in)
225    }
226
227    /// Fill [`Parameter`] with values from another [`Parameter`]. Fields will replaced if it is not
228    /// set.
229    pub fn merge(&mut self, other: Self) -> bool {
230        let Self {
231            name,
232            parameter_in,
233            description,
234            required,
235            deprecated,
236            allow_empty_value,
237            schema,
238            style,
239            explode,
240            allow_reserved,
241            example,
242            examples,
243            content,
244            extensions,
245        } = other;
246        if name != self.name || parameter_in != self.parameter_in {
247            return false;
248        }
249        if let Some(description) = description {
250            self.description = Some(description);
251        }
252
253        if required != Required::Unset {
254            self.required = required;
255        }
256        // Per the OpenAPI 3.1 spec, path parameters MUST have `required: true`.
257        if self.parameter_in == ParameterIn::Path {
258            self.required = Required::True;
259        }
260
261        if let Some(deprecated) = deprecated {
262            self.deprecated = Some(deprecated);
263        }
264        if let Some(allow_empty_value) = allow_empty_value {
265            self.allow_empty_value = Some(allow_empty_value);
266        }
267        if let Some(schema) = schema {
268            self.schema = Some(schema);
269        }
270        if let Some(style) = style {
271            self.style = Some(style);
272        }
273        if let Some(explode) = explode {
274            self.explode = Some(explode);
275        }
276        if let Some(allow_reserved) = allow_reserved {
277            self.allow_reserved = Some(allow_reserved);
278        }
279        if let Some(example) = example {
280            self.example = Some(example);
281        }
282        for (k, v) in examples {
283            self.examples.insert(k, v);
284        }
285        for (k, v) in content {
286            self.content.insert(k, v);
287        }
288
289        self.extensions.extend(extensions);
290        true
291    }
292
293    /// Add required declaration of the [`Parameter`]. If [`ParameterIn::Path`] is
294    /// defined this is always [`Required::True`].
295    #[must_use]
296    pub fn required(mut self, required: impl Into<Required>) -> Self {
297        self.required = required.into();
298        // required must be true, if parameter_in is Path
299        if self.parameter_in == ParameterIn::Path {
300            self.required = Required::True;
301        }
302
303        self
304    }
305
306    /// Add or change description of the [`Parameter`].
307    #[must_use]
308    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
309        self.description = Some(description.into());
310        self
311    }
312
313    /// Add or change [`Parameter`] deprecated declaration.
314    #[must_use]
315    pub fn deprecated<D: Into<Deprecated>>(mut self, deprecated: D) -> Self {
316        self.deprecated = Some(deprecated.into());
317        self
318    }
319
320    /// Add or change [`Parameter`]s schema.
321    #[must_use]
322    pub fn schema<I: Into<RefOr<Schema>>>(mut self, component: I) -> Self {
323        self.schema = Some(component.into());
324        self
325    }
326
327    /// Add or change serialization style of [`Parameter`].
328    #[must_use]
329    pub fn style(mut self, style: ParameterStyle) -> Self {
330        self.style = Some(style);
331        self
332    }
333
334    /// Define whether [`Parameter`]s are exploded or not.
335    #[must_use]
336    pub fn explode(mut self, explode: bool) -> Self {
337        self.explode = Some(explode);
338        self
339    }
340
341    /// Add or change whether [`Parameter`] should allow reserved characters.
342    #[must_use]
343    pub fn allow_reserved(mut self, allow_reserved: bool) -> Self {
344        self.allow_reserved = Some(allow_reserved);
345        self
346    }
347
348    /// Add or change example of [`Parameter`]'s potential value.
349    #[must_use]
350    pub fn example(mut self, example: Value) -> Self {
351        self.example = Some(example);
352        self
353    }
354
355    /// Insert a named [`Example`] (or a [`Ref`](crate::Ref) to one) into [`Parameter::examples`].
356    ///
357    /// When set, `examples` takes precedence over [`Parameter::example`].
358    #[must_use]
359    pub fn add_example<N: Into<String>, E: Into<RefOr<Example>>>(
360        mut self,
361        name: N,
362        example: E,
363    ) -> Self {
364        self.examples.insert(name.into(), example.into());
365        self
366    }
367
368    /// Replace [`Parameter::examples`] with the contents of an iterator of named examples.
369    #[must_use]
370    pub fn examples<I, N, E>(mut self, examples: I) -> Self
371    where
372        I: IntoIterator<Item = (N, E)>,
373        N: Into<String>,
374        E: Into<RefOr<Example>>,
375    {
376        self.examples = examples
377            .into_iter()
378            .map(|(name, example)| (name.into(), example.into()))
379            .collect();
380        self
381    }
382
383    /// Insert a single media-type entry into [`Parameter::content`].
384    ///
385    /// Per spec the `content` map must contain exactly one entry. Mutually exclusive with
386    /// [`Parameter::schema`].
387    #[must_use]
388    pub fn content<S: Into<String>, C: Into<Content>>(mut self, media_type: S, content: C) -> Self {
389        self.content.insert(media_type.into(), content.into());
390        self
391    }
392
393    /// Allow or disallow empty-valued query parameters.
394    ///
395    /// Note: per the OpenAPI 3.1 spec the use of this property is discouraged.
396    #[must_use]
397    pub fn allow_empty_value(mut self, allow: bool) -> Self {
398        self.allow_empty_value = Some(allow);
399        self
400    }
401}
402
403/// Possible values for the OpenAPI parameter `in` field, indicating where the parameter
404/// is located in the request.
405#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Default, Copy, Debug)]
406#[serde(rename_all = "lowercase")]
407pub enum ParameterIn {
408    /// Declares that parameter is used as query parameter.
409    ///
410    /// Must not appear in the same operation as a [`ParameterIn::QueryString`] parameter.
411    Query,
412    /// Declares that the parameter is the entire URL query string, treated as a single value.
413    /// Added in OpenAPI 3.2.
414    ///
415    /// Such a parameter must describe its value with [`Parameter::content`] rather than
416    /// [`Parameter::schema`], must not appear more than once, and must not be combined with any
417    /// [`ParameterIn::Query`] parameter in the same operation.
418    ///
419    /// See <https://spec.openapis.org/oas/v3.2.0.html#parameter-locations>.
420    #[serde(rename = "querystring")]
421    QueryString,
422    /// Declares that parameter is used as path parameter.
423    #[default]
424    Path,
425    /// Declares that parameter is used as header value.
426    Header,
427    /// Declares that parameter is used as cookie value.
428    Cookie,
429}
430
431/// Defines how [`Parameter`] should be serialized.
432#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
433#[serde(rename_all = "camelCase")]
434pub enum ParameterStyle {
435    /// Path style parameters defined by [RFC6570](https://tools.ietf.org/html/rfc6570#section-3.2.7)
436    /// e.g _`;color=blue`_.
437    /// Allowed with [`ParameterIn::Path`].
438    Matrix,
439    /// Label style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.5)
440    /// e.g _`.color=blue`_.
441    /// Allowed with [`ParameterIn::Path`].
442    Label,
443    /// Form style parameters defined by [RFC6570](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8)
444    /// e.g. _`color=blue`_. Default value for [`ParameterIn::Query`] [`ParameterIn::Cookie`].
445    /// Allowed with [`ParameterIn::Query`] or [`ParameterIn::Cookie`].
446    Form,
447    /// Default value for [`ParameterIn::Path`] [`ParameterIn::Header`]. e.g. _`blue`_.
448    /// Allowed with [`ParameterIn::Path`] or [`ParameterIn::Header`].
449    Simple,
450    /// Space separated array values e.g. _`blue%20black%20brown`_.
451    /// Allowed with [`ParameterIn::Query`].
452    SpaceDelimited,
453    /// Pipe separated array values e.g. _`blue|black|brown`_.
454    /// Allowed with [`ParameterIn::Query`].
455    PipeDelimited,
456    /// Simple way of rendering nested objects using form parameters .e.g. _`color[B]=150`_.
457    /// Allowed with [`ParameterIn::Query`].
458    DeepObject,
459    /// Cookie style parameters as defined by [RFC6265](https://www.rfc-editor.org/rfc/rfc6265),
460    /// e.g. _`name=value; other=thing`_. Added in OpenAPI 3.2 and allowed only with
461    /// [`ParameterIn::Cookie`].
462    ///
463    /// See <https://spec.openapis.org/oas/v3.2.0.html#style-values>.
464    Cookie,
465}
466
467#[cfg(test)]
468mod tests {
469    use assert_json_diff::assert_json_eq;
470    use serde_json::json;
471
472    use super::*;
473    use crate::Object;
474
475    #[test]
476    fn test_build_parameter() {
477        let parameter = Parameter::new("name");
478        assert_eq!(parameter.name, "name");
479
480        let parameter = parameter
481            .name("new name")
482            .location(ParameterIn::Query)
483            .required(Required::True)
484            .description("description")
485            .deprecated(Deprecated::False)
486            .schema(Schema::object(Object::new()))
487            .style(ParameterStyle::Simple)
488            .explode(true)
489            .allow_reserved(true)
490            .example(Value::String("example".to_owned()));
491        assert_json_eq!(
492            parameter,
493            json!({
494                "name": "new name",
495                "in": "query",
496                "required": true,
497                "description": "description",
498                "deprecated": false,
499                "schema": {
500                    "type": "object"
501                },
502                "style": "simple",
503                "explode": true,
504                "allowReserved": true,
505                "example": "example"
506            })
507        );
508    }
509
510    #[test]
511    fn test_parameter_merge_fail() {
512        let mut parameter1 = Parameter::new("param1");
513        let parameter2 = Parameter::new("param2");
514
515        assert!(!parameter1.merge(parameter2));
516    }
517
518    #[test]
519    fn test_parameter_merge_success() {
520        let mut parameter1 = Parameter::new("param1");
521        let mut parameter2 = Parameter::new("param1")
522            .description("description")
523            .required(Required::True)
524            .deprecated(Deprecated::True)
525            .schema(Schema::object(Object::new()))
526            .style(ParameterStyle::Form)
527            .explode(true)
528            .allow_reserved(true)
529            .example(Value::String("example".to_owned()));
530
531        parameter1.extensions =
532            PropMap::from([("key1".to_owned(), Value::String("value1".to_owned()))]);
533        parameter2.extensions =
534            PropMap::from([("key2".to_owned(), Value::String("value2".to_owned()))]);
535
536        assert!(parameter1.merge(parameter2));
537        assert_json_eq!(
538            parameter1,
539            json!({
540                "name": "param1",
541                "in": "path",
542                "description": "description",
543                "required": true,
544                "deprecated": true,
545                "schema": {
546                    "type": "object"
547                },
548                "style": "form",
549                "explode": true,
550                "allowReserved": true,
551                "example": "example",
552                "key1": "value1",
553                "key2": "value2"
554            })
555        )
556    }
557
558    #[test]
559    fn test_parameter_merge_no_extensions() {
560        let mut parameter1 = Parameter::new("param1");
561        let mut parameter2 = Parameter::new("param1")
562            .description("description")
563            .required(Required::True)
564            .deprecated(Deprecated::True)
565            .schema(Schema::object(Object::new()))
566            .style(ParameterStyle::Form)
567            .explode(true)
568            .allow_reserved(true)
569            .example(Value::String("example".to_owned()));
570
571        parameter2.extensions =
572            PropMap::from([("key2".to_owned(), Value::String("value2".to_owned()))]);
573
574        assert!(parameter1.merge(parameter2));
575        assert_json_eq!(
576            parameter1,
577            json!({
578                "name": "param1",
579                "in": "path",
580                "description": "description",
581                "required": true,
582                "deprecated": true,
583                "schema": {
584                    "type": "object"
585                },
586                "style": "form",
587                "explode": true,
588                "allowReserved": true,
589                "example": "example",
590                "key2": "value2",
591            })
592        )
593    }
594
595    #[test]
596    fn test_build_parameters() {
597        let parameters = Parameters::new();
598        assert!(parameters.is_empty());
599    }
600
601    #[test]
602    fn parameter_in_querystring_round_trips() {
603        let parameter = Parameter::new("query")
604            .location(ParameterIn::QueryString)
605            .content(
606                "application/x-www-form-urlencoded",
607                crate::Content::new(Schema::object(Object::new())),
608            );
609
610        let value = serde_json::to_value(&parameter).expect("serialize");
611        assert_eq!(value["in"], json!("querystring"));
612
613        let parsed: Parameter = serde_json::from_value(value).expect("deserialize");
614        assert_eq!(parsed.parameter_in, ParameterIn::QueryString);
615    }
616
617    #[test]
618    fn parameter_style_cookie_round_trips() {
619        let parameter = Parameter::new("session")
620            .location(ParameterIn::Cookie)
621            .style(ParameterStyle::Cookie);
622
623        let value = serde_json::to_value(&parameter).expect("serialize");
624        assert_eq!(value["style"], json!("cookie"));
625
626        let parsed: Parameter = serde_json::from_value(value).expect("deserialize");
627        assert_eq!(parsed.style, Some(ParameterStyle::Cookie));
628    }
629
630    #[test]
631    fn querystring_and_query_parameters_are_distinct_entries() {
632        let mut parameters = Parameters::new();
633        parameters.insert(Parameter::new("q").location(ParameterIn::Query));
634        parameters.insert(Parameter::new("q").location(ParameterIn::QueryString));
635
636        assert!(parameters.contains("q", ParameterIn::Query));
637        assert!(parameters.contains("q", ParameterIn::QueryString));
638        assert_eq!(parameters.0.len(), 2);
639    }
640
641    #[test]
642    fn test_parameters_into_iter() {
643        let parameters = Parameters::new().parameter(Parameter::new("param"));
644        let mut iter = parameters.into_iter();
645        // `Parameters::insert` forces `required: True` for path parameters per spec, so the
646        // round-tripped item is not equal to a fresh `Parameter::new("param")` with
647        // `Required::Unset`.
648        assert_eq!(
649            iter.next(),
650            Some(Parameter::new("param").required(Required::True))
651        );
652        assert!(iter.next().is_none());
653    }
654
655    #[test]
656    fn test_parameters_contain() {
657        let parameters = Parameters::new().parameter(Parameter::new("param"));
658        assert!(parameters.contains("param", ParameterIn::Path));
659    }
660
661    #[test]
662    fn test_parameters_insert_existed_item() {
663        let mut parameters = Parameters::new();
664        parameters.insert(Parameter::new("param"));
665        assert!(parameters.contains("param", ParameterIn::Path));
666
667        parameters.insert(Parameter::new("param"));
668        assert_eq!(parameters.0.len(), 1);
669    }
670
671    #[test]
672    fn test_parameters_append() {
673        let mut parameters1 = Parameters::new().parameter(Parameter::new("param1"));
674        let mut parameters2 = Parameters::new().parameter(Parameter::new("param2"));
675
676        parameters1.append(&mut parameters2);
677        assert_json_eq!(
678            parameters1,
679            json!([
680                {
681                    "in": "path",
682                    "name": "param1",
683                    "required": true
684                },
685                {
686                    "in": "path",
687                    "name": "param2",
688                    "required": true
689                }
690            ])
691        );
692    }
693
694    #[test]
695    fn parameter_required_unset_is_omitted() {
696        let parameter = Parameter::new("filter").location(ParameterIn::Query);
697        let value = serde_json::to_value(&parameter).expect("serialize");
698        assert!(
699            value.get("required").is_none(),
700            "expected `required` to be omitted when Unset; got: {value}"
701        );
702    }
703
704    #[test]
705    fn parameter_required_true_is_emitted_for_path_in() {
706        let parameter = Parameter::new("id").location(ParameterIn::Path);
707        // parameter_in(Path) should force required=true (per spec) and serialize it.
708        let value = serde_json::to_value(&parameter).expect("serialize");
709        assert_eq!(value["required"], serde_json::Value::Bool(true));
710    }
711
712    #[test]
713    fn parameter_with_examples_serializes_under_camel_case_field() {
714        use crate::Example;
715
716        let parameter = Parameter::new("filter")
717            .location(ParameterIn::Query)
718            .add_example("first", Example::new().value(json!("foo")))
719            .add_example("second", Example::new().value(json!("bar")));
720
721        let value = serde_json::to_value(&parameter).expect("serialize");
722        assert_eq!(
723            value["examples"],
724            json!({
725                "first":  { "value": "foo" },
726                "second": { "value": "bar" }
727            })
728        );
729    }
730
731    #[test]
732    fn parameter_with_content_serializes_as_media_type_map() {
733        let parameter = Parameter::new("filter")
734            .location(ParameterIn::Query)
735            .content(
736                "application/json",
737                Content::new(RefOr::Ref(crate::Ref::from_schema_name("Filter"))),
738            );
739
740        let value = serde_json::to_value(&parameter).expect("serialize");
741        assert_eq!(
742            value["content"],
743            json!({
744                "application/json": {
745                    "schema": { "$ref": "#/components/schemas/Filter" }
746                }
747            })
748        );
749    }
750
751    #[test]
752    fn parameter_allow_empty_value_round_trips_under_camel_case() {
753        let parameter = Parameter::new("flag")
754            .location(ParameterIn::Query)
755            .allow_empty_value(true);
756
757        let value = serde_json::to_value(&parameter).expect("serialize");
758        assert_eq!(value["allowEmptyValue"], serde_json::Value::Bool(true));
759    }
760}