Skip to main content

kynos_openapi/model/parameter/
mod.rs

1//! The Parameter, Header and Style Objects.
2
3pub mod header;
4pub mod style;
5
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::{
12    Map,
13    model::{
14        body::media_type::MediaType,
15        example::{
16            Example, Examples, ExamplesConflict, examples_from, examples_into, examples_with_named,
17        },
18        extensions::Extensions,
19        parameter::style::Style,
20        reference::{Ref, RefOr},
21        schema::Schema,
22    },
23};
24
25/// Where a parameter is carried.
26/// `#[non_exhaustive]` because OpenAPI 3.2 adds to this and the addition is
27/// `#[cfg]`-gated. Cargo unifies features across a dependency graph, so any
28/// crate enabling `openapi32` enables it for every crate in the build -- and
29/// without this attribute that would turn a downstream exhaustive `match` into
30/// a compile error, which is not what "purely additive" is supposed to mean.
31#[non_exhaustive]
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub enum ParameterIn {
35    /// A named query string parameter.
36    ///
37    /// The default only because a location has to be one of these; a parameter
38    /// built through the constructors always has its location set explicitly.
39    #[default]
40    Query,
41    /// A request header.
42    ///
43    /// Note that `Accept`, `Content-Type` and `Authorization` must **not** be
44    /// declared this way: the specification says such a definition shall be
45    /// ignored. Content negotiation belongs in the `content` map, and
46    /// credentials belong in a [`SecurityScheme`](crate::SecurityScheme).
47    Header,
48    /// A variable in the path template. Always required.
49    Path,
50    /// A cookie.
51    Cookie,
52    /// The entire query string, described by media type.
53    ///
54    /// Introduced in OpenAPI 3.2. This is the sanctioned way to describe query
55    /// strings that a sequence of named parameters cannot express — nested
56    /// filters, JSON in the query, RFC 9535 JSONPath. It must be the only
57    /// query-related parameter on its operation.
58    #[cfg(feature = "openapi32")]
59    Querystring,
60}
61
62impl ParameterIn {
63    /// The location as it is spelled in a description.
64    #[must_use]
65    pub fn as_str(self) -> &'static str {
66        match self {
67            Self::Query => "query",
68            Self::Header => "header",
69            Self::Path => "path",
70            Self::Cookie => "cookie",
71            #[cfg(feature = "openapi32")]
72            Self::Querystring => "querystring",
73        }
74    }
75}
76
77impl fmt::Display for ParameterIn {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.write_str(self.as_str())
80    }
81}
82
83/// A single operation parameter.
84///
85/// The value is described by a [`ParameterShape`], which is one of `schema` or
86/// `content` and never both or neither, and shown by an [`Examples`], which is
87/// the inline `example` or the named `examples` and never both.
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89#[serde(try_from = "RawParameter", into = "RawParameter")]
90pub struct Parameter {
91    /// The name of the parameter, case-sensitive.
92    ///
93    /// For [`ParameterIn::Path`] this must correspond to exactly one template
94    /// expression in the path.
95    pub name: String,
96
97    /// Where the parameter is carried.
98    #[serde(rename = "in")]
99    pub location: ParameterIn,
100
101    /// A description of the parameter. [CommonMark] syntax may be used.
102    ///
103    /// [CommonMark]: https://spec.commonmark.org/
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub description: Option<String>,
106
107    /// Whether the parameter is mandatory.
108    ///
109    /// Must be `true` when [`location`](Parameter::location) is
110    /// [`ParameterIn::Path`].
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub required: Option<bool>,
113
114    /// Whether the parameter is deprecated.
115    pub deprecated: Option<bool>,
116
117    /// Whether an empty value is permitted. Query parameters only.
118    ///
119    /// Not recommended in 3.1, and formally deprecated in 3.2.
120    pub allow_empty_value: Option<bool>,
121
122    shape: ParameterShape,
123
124    examples: Option<Examples>,
125
126    /// Specification extensions.
127    pub extensions: Extensions,
128}
129
130/// How a parameter's value is described.
131///
132/// A parameter carries `schema` or `content`, never both and never neither.
133/// `style`, `explode` and `allowReserved` only mean anything alongside a
134/// schema, so they live in that variant rather than beside it — setting a style
135/// on a content-described parameter is not a mistake to report, it is a
136/// sentence with nowhere to be written.
137#[derive(Clone, Debug, PartialEq)]
138pub enum ParameterShape {
139    /// The simple case: a schema, plus how its value is serialized.
140    Schema {
141        /// The schema defining the parameter's type.
142        schema: Schema,
143
144        /// How the value is serialized.
145        style: Option<Style>,
146
147        /// Whether an array or object generates one parameter per member.
148        explode: Option<bool>,
149
150        /// Whether reserved URI characters may appear unencoded.
151        allow_reserved: Option<bool>,
152    },
153
154    /// The complex case: one media type describing the value.
155    ///
156    /// The specification allows exactly one entry, so this holds one pair
157    /// rather than a map that has to be counted. Boxed because a `MediaType`
158    /// dwarfs the schema-side fields, and every parameter would otherwise pay
159    /// for the larger of the two.
160    Content {
161        /// The media type the value is carried as.
162        media_type: String,
163
164        /// What that media type describes.
165        value: Box<MediaType>,
166    },
167}
168
169impl Parameter {
170    /// Creates a schema-described parameter.
171    pub fn new(name: impl Into<String>, location: ParameterIn, schema: Schema) -> Self {
172        Self::shaped(
173            name,
174            location,
175            ParameterShape::Schema {
176                schema,
177                style: None,
178                explode: None,
179                allow_reserved: None,
180            },
181        )
182    }
183
184    /// Creates a parameter described by one media type.
185    ///
186    /// For the values a sequence of `style` rules cannot express — JSON in a
187    /// query string, a nested filter.
188    pub fn with_content(
189        name: impl Into<String>,
190        location: ParameterIn,
191        media_type: impl Into<String>,
192        value: MediaType,
193    ) -> Self {
194        Self::shaped(
195            name,
196            location,
197            ParameterShape::Content {
198                media_type: media_type.into(),
199                value: Box::new(value),
200            },
201        )
202    }
203
204    fn shaped(name: impl Into<String>, location: ParameterIn, shape: ParameterShape) -> Self {
205        Self {
206            name: name.into(),
207            location,
208            description: None,
209            // A path parameter is required by definition, so filling this in is
210            // a correctness measure rather than a convenience.
211            required: (location == ParameterIn::Path).then_some(true),
212            deprecated: None,
213            allow_empty_value: None,
214            shape,
215            examples: None,
216            extensions: Extensions::default(),
217        }
218    }
219
220    /// Creates a required path parameter.
221    pub fn path(name: impl Into<String>, schema: Schema) -> Self {
222        Self::new(name, ParameterIn::Path, schema)
223    }
224
225    /// Creates a query parameter.
226    pub fn query(name: impl Into<String>, schema: Schema) -> Self {
227        Self::new(name, ParameterIn::Query, schema)
228    }
229
230    /// Creates a header parameter.
231    pub fn header(name: impl Into<String>, schema: Schema) -> Self {
232        Self::new(name, ParameterIn::Header, schema)
233    }
234
235    /// Creates a cookie parameter.
236    pub fn cookie(name: impl Into<String>, schema: Schema) -> Self {
237        Self::new(name, ParameterIn::Cookie, schema)
238    }
239
240    /// Marks the parameter mandatory.
241    #[must_use]
242    pub fn required(mut self, required: bool) -> Self {
243        self.required = Some(required);
244        self
245    }
246
247    /// Sets the description.
248    #[must_use]
249    pub fn with_description(mut self, description: impl Into<String>) -> Self {
250        self.description = Some(description.into());
251        self
252    }
253
254    /// Sets the serialization style and explode flag.
255    ///
256    /// A no-op on a content-described parameter, which has no style to set.
257    #[must_use]
258    pub fn with_style(mut self, style: Style, explode: bool) -> Self {
259        if let ParameterShape::Schema {
260            style: slot,
261            explode: exploded,
262            ..
263        } = &mut self.shape
264        {
265            *slot = Some(style);
266            *exploded = Some(explode);
267        }
268        self
269    }
270
271    /// Shows the value with one inline example.
272    ///
273    /// Replaces any named examples: the two forms exclude each other, so there
274    /// is no state that holds both.
275    #[must_use]
276    pub fn with_example(mut self, value: impl Into<Value>) -> Self {
277        self.examples = Some(Examples::Inline(value.into()));
278        self
279    }
280
281    /// Adds a named example, replacing any inline one.
282    #[must_use]
283    pub fn with_named_example(mut self, name: impl Into<String>, example: Example) -> Self {
284        self.examples = Some(examples_with_named(
285            self.examples,
286            name.into(),
287            RefOr::Item(example),
288        ));
289        self
290    }
291
292    /// Adds a named example held in
293    /// [`Components::examples`](crate::Components::examples).
294    #[must_use]
295    pub fn with_named_example_ref(mut self, name: impl Into<String>, example: Ref) -> Self {
296        self.examples = Some(examples_with_named(
297            self.examples,
298            name.into(),
299            RefOr::Ref(example),
300        ));
301        self
302    }
303
304    /// How this parameter's value is described.
305    #[must_use]
306    pub fn shape(&self) -> &ParameterShape {
307        &self.shape
308    }
309
310    /// The same, mutably.
311    ///
312    /// Handing out `&mut` costs nothing here: every [`ParameterShape`] is a
313    /// valid description, so there is no combination a caller could reach by
314    /// editing one that it could not reach by building one.
315    pub fn shape_mut(&mut self) -> &mut ParameterShape {
316        &mut self.shape
317    }
318
319    /// The schema, when this parameter is described by one.
320    #[must_use]
321    pub fn schema(&self) -> Option<&Schema> {
322        match &self.shape {
323            ParameterShape::Schema { schema, .. } => Some(schema),
324            ParameterShape::Content { .. } => None,
325        }
326    }
327
328    /// The media type and its description, when this parameter uses `content`.
329    #[must_use]
330    pub fn content(&self) -> Option<(&str, &MediaType)> {
331        match &self.shape {
332            ParameterShape::Content { media_type, value } => Some((media_type, &**value)),
333            ParameterShape::Schema { .. } => None,
334        }
335    }
336
337    /// The declared style, if any. Always `None` for a content-described
338    /// parameter.
339    #[must_use]
340    pub fn style(&self) -> Option<Style> {
341        match self.shape {
342            ParameterShape::Schema { style, .. } => style,
343            ParameterShape::Content { .. } => None,
344        }
345    }
346
347    /// Whether reserved URI characters may appear unencoded.
348    #[must_use]
349    pub fn allow_reserved(&self) -> Option<bool> {
350        match self.shape {
351            ParameterShape::Schema { allow_reserved, .. } => allow_reserved,
352            ParameterShape::Content { .. } => None,
353        }
354    }
355
356    /// The examples this parameter carries, if it carries any.
357    #[must_use]
358    pub fn examples(&self) -> Option<&Examples> {
359        self.examples.as_ref()
360    }
361
362    /// The inline example, when the value is shown with one.
363    #[must_use]
364    pub fn example(&self) -> Option<&Value> {
365        match &self.examples {
366            Some(Examples::Inline(value)) => Some(value),
367            Some(Examples::Named(_)) | None => None,
368        }
369    }
370
371    /// The named examples, when the value is shown with those.
372    #[must_use]
373    pub fn named_examples(&self) -> Option<&Map<RefOr<Example>>> {
374        match &self.examples {
375            Some(Examples::Named(named)) => Some(named),
376            Some(Examples::Inline(_)) | None => None,
377        }
378    }
379
380    /// Returns the effective style, falling back to the location's default.
381    ///
382    /// `None` for a content-described parameter: `style` does not apply to one,
383    /// so there is no default to fall back to either.
384    #[must_use]
385    pub fn effective_style(&self) -> Option<Style> {
386        match self.shape {
387            ParameterShape::Schema { style, .. } => {
388                Some(style.unwrap_or_else(|| Style::default_for(self.location)))
389            }
390            ParameterShape::Content { .. } => None,
391        }
392    }
393
394    /// Returns the effective explode flag, falling back to the style's default.
395    #[must_use]
396    pub fn effective_explode(&self) -> Option<bool> {
397        match self.shape {
398            ParameterShape::Schema { explode, .. } => Some(
399                explode
400                    .unwrap_or_else(|| self.effective_style().is_some_and(Style::default_explode)),
401            ),
402            ParameterShape::Content { .. } => None,
403        }
404    }
405}
406
407/// The wire shape: the value fields flat, as the specification writes them.
408#[derive(Serialize, Deserialize)]
409struct RawParameter {
410    name: String,
411
412    #[serde(rename = "in")]
413    location: ParameterIn,
414
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    description: Option<String>,
417
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    required: Option<bool>,
420
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    deprecated: Option<bool>,
423
424    #[serde(
425        rename = "allowEmptyValue",
426        default,
427        skip_serializing_if = "Option::is_none"
428    )]
429    allow_empty_value: Option<bool>,
430
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    style: Option<Style>,
433
434    #[serde(default, skip_serializing_if = "Option::is_none")]
435    explode: Option<bool>,
436
437    #[serde(
438        rename = "allowReserved",
439        default,
440        skip_serializing_if = "Option::is_none"
441    )]
442    allow_reserved: Option<bool>,
443
444    #[serde(default, skip_serializing_if = "Option::is_none")]
445    schema: Option<Schema>,
446
447    #[serde(
448        default,
449        deserialize_with = "crate::model::nullable::some",
450        skip_serializing_if = "Option::is_none"
451    )]
452    example: Option<Value>,
453
454    #[serde(default, skip_serializing_if = "Option::is_none")]
455    examples: Option<Map<RefOr<Example>>>,
456
457    #[serde(default, skip_serializing_if = "Map::is_empty")]
458    content: Map<MediaType>,
459
460    #[serde(flatten)]
461    extensions: Extensions,
462}
463
464/// A Parameter or Header Object whose fields do not hold together.
465///
466/// Two independent ways to be ill-formed, kept apart so that each reads as the
467/// sentence the specification writes.
468#[derive(Debug)]
469pub(crate) enum ParameterConflict {
470    /// The value is described by neither `schema` nor `content`, or by both.
471    Shape(ShapeConflict),
472
473    /// The value is shown by both `example` and `examples`.
474    Examples(ExamplesConflict),
475}
476
477impl fmt::Display for ParameterConflict {
478    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479        match self {
480            Self::Shape(conflict) => conflict.fmt(f),
481            Self::Examples(conflict) => conflict.fmt(f),
482        }
483    }
484}
485
486impl From<ShapeConflict> for ParameterConflict {
487    fn from(conflict: ShapeConflict) -> Self {
488        Self::Shape(conflict)
489    }
490}
491
492impl From<ExamplesConflict> for ParameterConflict {
493    fn from(conflict: ExamplesConflict) -> Self {
494        Self::Examples(conflict)
495    }
496}
497
498/// A Parameter or Header Object whose value description does not hold together.
499#[derive(Debug)]
500pub(crate) enum ShapeConflict {
501    /// Neither `schema` nor `content` was given.
502    Neither,
503
504    /// Both `schema` and `content` were given.
505    Both,
506
507    /// `content` held a number of entries other than one.
508    ContentNotSingular(usize),
509}
510
511impl fmt::Display for ShapeConflict {
512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513        match self {
514            Self::Neither => f.write_str("one of `schema` and `content` is required"),
515            Self::Both => f.write_str("`schema` and `content` are mutually exclusive"),
516            Self::ContentNotSingular(found) => {
517                write!(f, "`content` must hold exactly one entry, found {found}")
518            }
519        }
520    }
521}
522
523/// Splits the wire fields into a shape, or says why they will not go.
524pub(crate) fn shape_from(
525    schema: Option<Schema>,
526    content: Map<MediaType>,
527) -> Result<(Schema, Option<(String, MediaType)>), ShapeConflict> {
528    match (schema, content.len()) {
529        (Some(_), 1..) => Err(ShapeConflict::Both),
530        (None, 0) => Err(ShapeConflict::Neither),
531        (None, 1) => {
532            let (media_type, value) = content
533                .into_iter()
534                .next()
535                .expect("a map of length one has an entry");
536            Ok((Schema::Bool(true), Some((media_type, value))))
537        }
538        (None, found) => Err(ShapeConflict::ContentNotSingular(found)),
539        (Some(schema), 0) => Ok((schema, None)),
540    }
541}
542
543impl TryFrom<RawParameter> for Parameter {
544    type Error = ParameterConflict;
545
546    fn try_from(raw: RawParameter) -> Result<Self, Self::Error> {
547        let shape = match shape_from(raw.schema, raw.content)? {
548            (_, Some((media_type, value))) => ParameterShape::Content {
549                media_type,
550                value: Box::new(value),
551            },
552            (schema, None) => ParameterShape::Schema {
553                schema,
554                style: raw.style,
555                explode: raw.explode,
556                allow_reserved: raw.allow_reserved,
557            },
558        };
559
560        Ok(Self {
561            name: raw.name,
562            location: raw.location,
563            description: raw.description,
564            required: raw.required,
565            deprecated: raw.deprecated,
566            allow_empty_value: raw.allow_empty_value,
567            shape,
568            examples: examples_from(raw.example, raw.examples)?,
569            extensions: raw.extensions,
570        })
571    }
572}
573
574impl From<Parameter> for RawParameter {
575    fn from(parameter: Parameter) -> Self {
576        let (schema, style, explode, allow_reserved, content) = match parameter.shape {
577            ParameterShape::Schema {
578                schema,
579                style,
580                explode,
581                allow_reserved,
582            } => (Some(schema), style, explode, allow_reserved, Map::new()),
583            ParameterShape::Content { media_type, value } => (
584                None,
585                None,
586                None,
587                None,
588                Map::from_iter([(media_type, *value)]),
589            ),
590        };
591
592        let (example, examples) = examples_into(parameter.examples);
593
594        Self {
595            name: parameter.name,
596            location: parameter.location,
597            description: parameter.description,
598            required: parameter.required,
599            deprecated: parameter.deprecated,
600            allow_empty_value: parameter.allow_empty_value,
601            style,
602            explode,
603            allow_reserved,
604            schema,
605            example,
606            examples,
607            content,
608            extensions: parameter.extensions,
609        }
610    }
611}
612
613#[cfg(test)]
614mod tests;