Skip to main content

kynos_openapi/model/response/
mod.rs

1//! The Responses and Response Objects.
2
3pub mod status;
4
5// Private, because it declares no item of its own that a path could point at:
6// [`Responses::union_from`]'s rule is what it holds, and that rule is reachable
7// only through the method stating it.
8mod union;
9
10use std::fmt;
11
12use serde::{
13    Deserialize, Deserializer, Serialize, Serializer,
14    de::{Error as DeError, MapAccess, Visitor},
15    ser::SerializeMap,
16};
17use serde_json::Value;
18
19use crate::{
20    Map,
21    model::{
22        body::media_type::MediaType, extensions::Extensions, link::Link, parameter::header::Header,
23        reference::RefOr, response::status::StatusPattern, response::union::unioned,
24    },
25};
26
27/// The responses an operation may return.
28///
29/// Serializes with [`default_response`](Responses::default_response) under the
30/// `default` key, each entry of [`responses`](Responses::responses) under its
31/// status pattern, and extensions alongside them.
32#[derive(Clone, Debug, Default, PartialEq)]
33pub struct Responses {
34    /// The response used for status codes not otherwise covered.
35    pub default_response: Option<RefOr<Response>>,
36
37    /// Responses keyed by status code or wildcard.
38    pub responses: Map<RefOr<Response>>,
39
40    /// Specification extensions.
41    pub extensions: Extensions,
42}
43
44impl Responses {
45    /// Creates an empty set of responses.
46    ///
47    /// A description must not keep it empty: the specification requires at
48    /// least one response, and [`crate::validate`] reports the omission.
49    #[must_use]
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Declares a response for an exact status code.
55    #[must_use]
56    pub fn with(mut self, status: u16, response: Response) -> Self {
57        self.responses.insert(
58            StatusPattern::Code(status).to_string(),
59            RefOr::Item(response),
60        );
61        self
62    }
63
64    /// Declares a response for a status pattern.
65    #[must_use]
66    pub fn with_pattern(mut self, pattern: StatusPattern, response: RefOr<Response>) -> Self {
67        self.responses.insert(pattern.to_string(), response);
68        self
69    }
70
71    /// Sets the fallback response.
72    #[must_use]
73    pub fn with_default(mut self, response: Response) -> Self {
74        self.default_response = Some(RefOr::Item(response));
75        self
76    }
77
78    /// Returns `true` when nothing at all is declared.
79    ///
80    /// Extensions count. `Operation.responses` is skipped when this is true,
81    /// so ignoring them would silently drop a `Responses` that carries only
82    /// `x-` fields — which is exactly the drop a round trip must not make.
83    ///
84    /// This is therefore *not* the question the specification's "MUST contain
85    /// at least one response code" asks. [`declares_a_response`] is.
86    ///
87    /// [`declares_a_response`]: Responses::declares_a_response
88    #[must_use]
89    pub fn is_empty(&self) -> bool {
90        self.default_response.is_none() && self.responses.is_empty() && self.extensions.is_empty()
91    }
92
93    /// Returns `true` when a status code or `default` is declared.
94    ///
95    /// The distinction from [`is_empty`](Responses::is_empty) is the whole
96    /// point: an extension is not a response, so a Responses Object carrying
97    /// only `x-` fields is *not* empty and still declares nothing.
98    #[must_use]
99    pub fn declares_a_response(&self) -> bool {
100        self.default_response.is_some() || !self.responses.is_empty()
101    }
102
103    /// Looks up the response declared for an exact status code.
104    ///
105    /// Only exact keys are considered; wildcard resolution is a consumer
106    /// concern and depends on precedence rules this method does not apply.
107    #[must_use]
108    pub fn get(&self, status: u16) -> Option<&RefOr<Response>> {
109        self.responses.get(&StatusPattern::Code(status).to_string())
110    }
111
112    /// Merges another set into this one, keeping existing entries on conflict.
113    ///
114    /// This is how an interceptor's declared responses join an operation's own.
115    pub fn merge_from(&mut self, other: &Self) {
116        if self.default_response.is_none() {
117            self.default_response.clone_from(&other.default_response);
118        }
119        for (key, response) in &other.responses {
120            if !self.responses.contains_key(key) {
121                self.responses.insert(key.clone(), response.clone());
122            }
123        }
124    }
125
126    /// Merges another set into this one, unioning two problem responses that
127    /// meet on one status.
128    ///
129    /// [`merge_from`](Responses::merge_from) with one exception, and the
130    /// exception is the only reason this exists. A status is one key and a
131    /// response is what a client is told about it, so where two contributors
132    /// both name a status — an extractor's rejection and the handler's error
133    /// type is the case that motivates this — keeping whichever arrived first
134    /// publishes half of what the operation can send.
135    ///
136    /// The exception is deliberately narrow. It applies where both entries
137    /// declare an `application/problem+json` schema, because two problem
138    /// documents under one status are two branches of a choice over the same
139    /// component — which merging two arbitrary responses is not. Anything else
140    /// keeps the entry already declared, exactly as `merge_from` would.
141    ///
142    /// # What the union is
143    ///
144    /// The entry already declared, with two of its fields replaced, so
145    /// everything else it carries — a `WWW-Authenticate` header, a link, an
146    /// extension — survives a contributor arriving after it.
147    ///
148    /// * **The schema.** A *narrowed* problem schema constrains `type` to a
149    ///   `const` on every branch, which is the shape `#[derive(ApiError)]`
150    ///   emits: one `allOf` for a single type, a `oneOf` of them for several.
151    ///   Where both sides are narrowed the branches are flattened, deduplicated
152    ///   by the URI they publish and rebuilt — a single `allOf` where one
153    ///   survives, a `oneOf` where several do. The dedup is what keeps `oneOf`
154    ///   sound: two branches repeating a `const` are satisfied at once, which
155    ///   is exactly what the keyword forbids.
156    ///
157    ///   Where one side instead admits *everything* — `true`, or a bare `$ref`
158    ///   to the shared component, which every problem document satisfies — that
159    ///   side is the union: it already admits every document the other
160    ///   describes, and narrowing to the other would declare less than the
161    ///   operation sends.
162    ///
163    ///   A side that is neither is not read as either. A schema satisfied by
164    ///   nothing, an empty `oneOf`, and a `oneOf` whose branches overlap all
165    ///   fail the narrowing read while admitting strictly *less* than a
166    ///   narrowed side, so adopting one would declare a schema the operation's
167    ///   own bodies fail. Those keep the entry already declared, as
168    ///   `merge_from` would.
169    ///
170    /// * **The description.** Both, joined with `"; "`, dropping a sentence
171    ///   already written word for word. Prose is under no exactly-one rule, so
172    ///   a status two contributors reach says what each of them means.
173    pub fn union_from(&mut self, other: &Self) {
174        if self.default_response.is_none() {
175            self.default_response.clone_from(&other.default_response);
176        }
177        for (key, incoming) in &other.responses {
178            // Resolved to an owned entry before anything is inserted, so the
179            // read of the declared response ends where the write begins.
180            let replacement = match (self.responses.get(key), incoming) {
181                (None, incoming) => Some(incoming.clone()),
182                (Some(RefOr::Item(declared)), RefOr::Item(incoming)) => {
183                    unioned(declared, incoming).map(RefOr::Item)
184                }
185                // A response held as a `$ref` is not reached into, on either
186                // side: what it refers to is not this document's to read.
187                (Some(_), _) => None,
188            };
189
190            if let Some(response) = replacement {
191                self.responses.insert(key.clone(), response);
192            }
193        }
194    }
195}
196
197impl Serialize for Responses {
198    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
199        let len = usize::from(self.default_response.is_some())
200            + self.responses.len()
201            + self.extensions.0.len();
202        let mut map = serializer.serialize_map(Some(len))?;
203        if let Some(default) = &self.default_response {
204            map.serialize_entry("default", default)?;
205        }
206        for (key, response) in &self.responses {
207            map.serialize_entry(key, response)?;
208        }
209        for (key, value) in &self.extensions.0 {
210            map.serialize_entry(key, value)?;
211        }
212        map.end()
213    }
214}
215
216impl<'de> Deserialize<'de> for Responses {
217    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
218        struct ResponsesVisitor;
219
220        impl<'de> Visitor<'de> for ResponsesVisitor {
221            type Value = Responses;
222
223            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224                f.write_str("a map of status patterns to responses")
225            }
226
227            fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Responses, A::Error> {
228                let mut responses = Responses::new();
229                while let Some(key) = access.next_key::<String>()? {
230                    if key == "default" {
231                        responses.default_response = Some(access.next_value()?);
232                    } else if key.starts_with(crate::model::extensions::EXTENSION_PREFIX) {
233                        responses.extensions.0.insert(key, access.next_value()?);
234                    } else {
235                        // Reject a malformed key here rather than carrying it
236                        // forward: an unparseable status is never meaningful.
237                        key.parse::<StatusPattern>().map_err(A::Error::custom)?;
238                        responses.responses.insert(key, access.next_value()?);
239                    }
240                }
241                Ok(responses)
242            }
243        }
244
245        deserializer.deserialize_map(ResponsesVisitor)
246    }
247}
248
249/// A single response.
250#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
251pub struct Response {
252    /// A short summary of the response.
253    ///
254    /// Introduced in OpenAPI 3.2. Under 3.1 the first line of the description
255    /// serves this purpose.
256    #[cfg(feature = "openapi32")]
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub summary: Option<String>,
259
260    /// A description of the response. [CommonMark] syntax may be used.
261    ///
262    /// **Required by 3.1, optional in 3.2.** 3.1 marks it `REQUIRED`; 3.2
263    /// drops the marker, so a response stating only a
264    /// [`summary`](Response::summary) is a legal 3.2 document. Modelling it as
265    /// a `String` enforced 3.1's rule on both versions and made such a
266    /// document unparseable, so the requirement lives in
267    /// [`validate`](crate::validate) instead, where it is checked against the
268    /// version the document claims.
269    ///
270    /// [`new`](Response::new) sets it, which is the common case and the only
271    /// one 3.1 admits.
272    ///
273    /// [CommonMark]: https://spec.commonmark.org/
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub description: Option<String>,
276
277    /// Headers sent with the response.
278    ///
279    /// A `Content-Type` entry is ignored, since [`content`](Response::content)
280    /// states it.
281    #[serde(default, skip_serializing_if = "Map::is_empty")]
282    pub headers: Map<RefOr<Header>>,
283
284    /// The response body's representations, keyed by media type.
285    #[serde(default, skip_serializing_if = "Map::is_empty")]
286    pub content: Map<MediaType>,
287
288    /// Design-time links to other operations.
289    #[serde(default, skip_serializing_if = "Map::is_empty")]
290    pub links: Map<RefOr<Link>>,
291
292    /// Specification extensions.
293    #[serde(flatten)]
294    pub extensions: Extensions,
295}
296
297impl Response {
298    /// Creates a response with no body.
299    pub fn new(description: impl Into<String>) -> Self {
300        Self {
301            description: Some(description.into()),
302            ..Self::default()
303        }
304    }
305
306    /// Creates a response with one body representation.
307    pub fn with_content(
308        description: impl Into<String>,
309        media_type: impl Into<String>,
310        content: MediaType,
311    ) -> Self {
312        let mut response = Self::new(description);
313        response.content.insert(media_type.into(), content);
314        response
315    }
316
317    /// Declares a response header.
318    #[must_use]
319    pub fn with_header(mut self, name: impl Into<String>, header: Header) -> Self {
320        self.headers.insert(name.into(), RefOr::Item(header));
321        self
322    }
323
324    /// Declares a link to another operation.
325    #[must_use]
326    pub fn with_link(mut self, name: impl Into<String>, link: Link) -> Self {
327        self.links.insert(name.into(), RefOr::Item(link));
328        self
329    }
330
331    /// Attaches an extension field.
332    #[must_use]
333    pub fn with_extension(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
334        self.extensions.insert(key, value);
335        self
336    }
337}
338
339#[cfg(test)]
340mod tests;