Skip to main content

kynos_openapi/model/parameter/
header.rs

1//! The Header Object, and the headers the specification refuses to describe.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7    Map,
8    model::{
9        body::media_type::MediaType,
10        example::{Example, Examples, examples_from, examples_into, examples_with_named},
11        extensions::Extensions,
12        parameter::{ParameterConflict, shape_from, style::HeaderStyle},
13        reference::{Ref, RefOr},
14        schema::Schema,
15    },
16};
17
18/// Header names that must not be declared as
19/// [`ParameterIn::Header`](crate::model::parameter::ParameterIn::Header)
20/// parameters.
21///
22/// The specification states that a parameter definition for any of these shall
23/// be ignored, which makes declaring one a silent lie in the description.
24pub const IGNORED_HEADER_PARAMETERS: &[&str] = &["Accept", "Content-Type", "Authorization"];
25
26/// Whether `name` is a header that must not be declared as a parameter.
27///
28/// Comparison is ASCII case-insensitive, matching HTTP header semantics.
29#[must_use]
30pub fn is_ignored_header_parameter(name: &str) -> bool {
31    IGNORED_HEADER_PARAMETERS
32        .iter()
33        .any(|ignored| ignored.eq_ignore_ascii_case(name))
34}
35
36/// Header names that must not be declared in a `headers` map.
37///
38/// A response states its media type in `content` and an encoded part states
39/// its own in `contentType`, so the specification says a `Content-Type` entry
40/// in either map shall be ignored. The list is shorter than
41/// [`IGNORED_HEADER_PARAMETERS`] because `Accept` and `Authorization` are
42/// request headers, which neither map describes.
43pub const IGNORED_HEADERS: &[&str] = &["Content-Type"];
44
45/// Whether `name` is a header that must not be declared in a `headers` map.
46///
47/// Comparison is ASCII case-insensitive, as it is for a parameter.
48#[must_use]
49pub fn is_ignored_header(name: &str) -> bool {
50    IGNORED_HEADERS
51        .iter()
52        .any(|ignored| ignored.eq_ignore_ascii_case(name))
53}
54
55/// A response header, or a header used by an [`Encoding`](crate::Encoding).
56///
57/// This is a Parameter Object without `name` and `in`, and without
58/// `allowEmptyValue`, which the specification refuses a header. Like a
59/// parameter it holds one [`HeaderShape`] and one [`Examples`].
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
61#[serde(try_from = "RawHeader", into = "RawHeader")]
62pub struct Header {
63    /// A description of the header. [CommonMark] syntax may be used.
64    ///
65    /// [CommonMark]: https://spec.commonmark.org/
66    pub description: Option<String>,
67
68    /// Whether the header is mandatory.
69    pub required: Option<bool>,
70
71    /// Whether the header is deprecated.
72    pub deprecated: Option<bool>,
73
74    shape: HeaderShape,
75
76    examples: Option<Examples>,
77
78    /// Specification extensions.
79    pub extensions: Extensions,
80}
81
82/// How a header's value is described.
83///
84/// [`ParameterShape`](crate::model::parameter::ParameterShape) without
85/// `allowReserved`: header values are not URI-encoded, so there is no reserved
86/// set to allow through, and a field for it would be a question with no answer.
87///
88/// That is 3.1's reasoning, and 3.1 agrees — it forbids `allowReserved` on a
89/// Header Object outright. **3.2 does not.** It drops the field from that
90/// prohibition, so a 3.2 Header Object may carry one and this type cannot hold
91/// it: such a header loses the field on a round trip. That is a missing 3.2
92/// feature rather than a wrong answer to 3.1's question, and it is deliberately
93/// not fixed here — adding it widens the model rather than correcting it.
94#[derive(Clone, Debug, PartialEq)]
95pub enum HeaderShape {
96    /// A schema, plus how its value is serialized.
97    Schema {
98        /// The schema defining the header's type.
99        schema: Schema,
100
101        /// How the value is serialized.
102        style: Option<HeaderStyle>,
103
104        /// Whether an array or object generates one value per member.
105        explode: Option<bool>,
106    },
107
108    /// One media type describing the value.
109    ///
110    /// Boxed for the reason
111    /// [`ParameterShape::Content`](crate::model::parameter::ParameterShape::Content)
112    /// is: a `MediaType` dwarfs the schema-side fields beside it.
113    Content {
114        /// The media type the value is carried as.
115        media_type: String,
116
117        /// What that media type describes.
118        value: Box<MediaType>,
119    },
120}
121
122impl Header {
123    /// Creates a header described by a schema.
124    #[must_use]
125    pub fn new(schema: Schema) -> Self {
126        Self::shaped(HeaderShape::Schema {
127            schema,
128            style: None,
129            explode: None,
130        })
131    }
132
133    /// Creates a header described by one media type.
134    pub fn with_content(media_type: impl Into<String>, value: MediaType) -> Self {
135        Self::shaped(HeaderShape::Content {
136            media_type: media_type.into(),
137            value: Box::new(value),
138        })
139    }
140
141    fn shaped(shape: HeaderShape) -> Self {
142        Self {
143            description: None,
144            required: None,
145            deprecated: None,
146            shape,
147            examples: None,
148            extensions: Extensions::default(),
149        }
150    }
151
152    /// How this header's value is described.
153    #[must_use]
154    pub fn shape(&self) -> &HeaderShape {
155        &self.shape
156    }
157
158    /// The same, mutably.
159    ///
160    /// Handing out `&mut` costs nothing here: every [`HeaderShape`] is a valid
161    /// description, so there is no combination a caller could reach by editing
162    /// one that it could not reach by building one.
163    pub fn shape_mut(&mut self) -> &mut HeaderShape {
164        &mut self.shape
165    }
166
167    /// The schema, when this header is described by one.
168    #[must_use]
169    pub fn schema(&self) -> Option<&Schema> {
170        match &self.shape {
171            HeaderShape::Schema { schema, .. } => Some(schema),
172            HeaderShape::Content { .. } => None,
173        }
174    }
175
176    /// The media type and its description, when this header uses `content`.
177    #[must_use]
178    pub fn content(&self) -> Option<(&str, &MediaType)> {
179        match &self.shape {
180            HeaderShape::Content { media_type, value } => Some((media_type, &**value)),
181            HeaderShape::Schema { .. } => None,
182        }
183    }
184
185    /// The declared style, if any.
186    #[must_use]
187    pub fn style(&self) -> Option<HeaderStyle> {
188        match self.shape {
189            HeaderShape::Schema { style, .. } => style,
190            HeaderShape::Content { .. } => None,
191        }
192    }
193
194    /// Sets the serialization style and explode flag.
195    ///
196    /// A no-op on a content-described header, which has no style to set.
197    ///
198    /// Naming [`HeaderStyle::Simple`] is not redundant even though it is the
199    /// only style a header may take: the specification distinguishes a header
200    /// that states it from one that leaves it out, and a description that
201    /// stated it is emitted back the way it arrived.
202    #[must_use]
203    pub fn with_style(mut self, style: HeaderStyle, explode: bool) -> Self {
204        if let HeaderShape::Schema {
205            style: slot,
206            explode: exploded,
207            ..
208        } = &mut self.shape
209        {
210            *slot = Some(style);
211            *exploded = Some(explode);
212        }
213        self
214    }
215
216    /// The examples this header carries, if it carries any.
217    #[must_use]
218    pub fn examples(&self) -> Option<&Examples> {
219        self.examples.as_ref()
220    }
221
222    /// The inline example, when the value is shown with one.
223    #[must_use]
224    pub fn example(&self) -> Option<&Value> {
225        match &self.examples {
226            Some(Examples::Inline(value)) => Some(value),
227            Some(Examples::Named(_)) | None => None,
228        }
229    }
230
231    /// The named examples, when the value is shown with those.
232    #[must_use]
233    pub fn named_examples(&self) -> Option<&Map<RefOr<Example>>> {
234        match &self.examples {
235            Some(Examples::Named(named)) => Some(named),
236            Some(Examples::Inline(_)) | None => None,
237        }
238    }
239
240    /// Shows the value with one inline example.
241    ///
242    /// Replaces any named examples: the two forms exclude each other, so there
243    /// is no state that holds both.
244    #[must_use]
245    pub fn with_example(mut self, value: impl Into<Value>) -> Self {
246        self.examples = Some(Examples::Inline(value.into()));
247        self
248    }
249
250    /// Adds a named example, replacing any inline one.
251    #[must_use]
252    pub fn with_named_example(mut self, name: impl Into<String>, example: Example) -> Self {
253        self.examples = Some(examples_with_named(
254            self.examples,
255            name.into(),
256            RefOr::Item(example),
257        ));
258        self
259    }
260
261    /// Adds a named example held in
262    /// [`Components::examples`](crate::Components::examples).
263    #[must_use]
264    pub fn with_named_example_ref(mut self, name: impl Into<String>, example: Ref) -> Self {
265        self.examples = Some(examples_with_named(
266            self.examples,
267            name.into(),
268            RefOr::Ref(example),
269        ));
270        self
271    }
272
273    /// Sets the description.
274    #[must_use]
275    pub fn with_description(mut self, description: impl Into<String>) -> Self {
276        self.description = Some(description.into());
277        self
278    }
279
280    /// Marks the header mandatory.
281    #[must_use]
282    pub fn required(mut self, required: bool) -> Self {
283        self.required = Some(required);
284        self
285    }
286}
287
288/// The wire shape: the value fields flat, as the specification writes them.
289#[derive(Serialize, Deserialize)]
290struct RawHeader {
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    description: Option<String>,
293
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    required: Option<bool>,
296
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    deprecated: Option<bool>,
299
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    style: Option<HeaderStyle>,
302
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    explode: Option<bool>,
305
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    schema: Option<Schema>,
308
309    #[serde(
310        default,
311        deserialize_with = "crate::model::nullable::some",
312        skip_serializing_if = "Option::is_none"
313    )]
314    example: Option<Value>,
315
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    examples: Option<Map<RefOr<Example>>>,
318
319    #[serde(default, skip_serializing_if = "Map::is_empty")]
320    content: Map<MediaType>,
321
322    #[serde(flatten)]
323    extensions: Extensions,
324}
325
326impl TryFrom<RawHeader> for Header {
327    type Error = ParameterConflict;
328
329    fn try_from(raw: RawHeader) -> Result<Self, Self::Error> {
330        let shape = match shape_from(raw.schema, raw.content)? {
331            (_, Some((media_type, value))) => HeaderShape::Content {
332                media_type,
333                value: Box::new(value),
334            },
335            (schema, None) => HeaderShape::Schema {
336                schema,
337                style: raw.style,
338                explode: raw.explode,
339            },
340        };
341
342        Ok(Self {
343            description: raw.description,
344            required: raw.required,
345            deprecated: raw.deprecated,
346            shape,
347            examples: examples_from(raw.example, raw.examples)?,
348            extensions: raw.extensions,
349        })
350    }
351}
352
353impl From<Header> for RawHeader {
354    fn from(header: Header) -> Self {
355        let (schema, style, explode, content) = match header.shape {
356            HeaderShape::Schema {
357                schema,
358                style,
359                explode,
360            } => (Some(schema), style, explode, Map::new()),
361            HeaderShape::Content { media_type, value } => {
362                (None, None, None, Map::from_iter([(media_type, *value)]))
363            }
364        };
365
366        let (example, examples) = examples_into(header.examples);
367
368        Self {
369            description: header.description,
370            required: header.required,
371            deprecated: header.deprecated,
372            style,
373            explode,
374            schema,
375            example,
376            examples,
377            content,
378            extensions: header.extensions,
379        }
380    }
381}