Skip to main content

kynos_openapi/model/response/
mod.rs

1//! The Responses and Response Objects.
2
3pub mod status;
4
5use std::fmt;
6
7use serde::{
8    Deserialize, Deserializer, Serialize, Serializer,
9    de::{Error as DeError, MapAccess, Visitor},
10    ser::SerializeMap,
11};
12use serde_json::Value;
13
14use crate::{
15    Map,
16    model::{
17        body::media_type::MediaType, extensions::Extensions, link::Link, parameter::header::Header,
18        reference::RefOr, response::status::StatusPattern,
19    },
20};
21
22/// The responses an operation may return.
23///
24/// Serializes with [`default_response`](Responses::default_response) under the
25/// `default` key, each entry of [`responses`](Responses::responses) under its
26/// status pattern, and extensions alongside them.
27#[derive(Clone, Debug, Default, PartialEq)]
28pub struct Responses {
29    /// The response used for status codes not otherwise covered.
30    pub default_response: Option<RefOr<Response>>,
31
32    /// Responses keyed by status code or wildcard.
33    pub responses: Map<RefOr<Response>>,
34
35    /// Specification extensions.
36    pub extensions: Extensions,
37}
38
39impl Responses {
40    /// Creates an empty set of responses.
41    ///
42    /// A description must not keep it empty: the specification requires at
43    /// least one response, and [`crate::validate`] reports the omission.
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Declares a response for an exact status code.
50    #[must_use]
51    pub fn with(mut self, status: u16, response: Response) -> Self {
52        self.responses.insert(
53            StatusPattern::Code(status).to_string(),
54            RefOr::Item(response),
55        );
56        self
57    }
58
59    /// Declares a response for a status pattern.
60    #[must_use]
61    pub fn with_pattern(mut self, pattern: StatusPattern, response: RefOr<Response>) -> Self {
62        self.responses.insert(pattern.to_string(), response);
63        self
64    }
65
66    /// Sets the fallback response.
67    #[must_use]
68    pub fn with_default(mut self, response: Response) -> Self {
69        self.default_response = Some(RefOr::Item(response));
70        self
71    }
72
73    /// Returns `true` when nothing at all is declared.
74    ///
75    /// Extensions count. `Operation.responses` is skipped when this is true,
76    /// so ignoring them would silently drop a `Responses` that carries only
77    /// `x-` fields — which is exactly the drop a round trip must not make.
78    ///
79    /// This is therefore *not* the question the specification's "MUST contain
80    /// at least one response code" asks. [`declares_a_response`] is.
81    ///
82    /// [`declares_a_response`]: Responses::declares_a_response
83    #[must_use]
84    pub fn is_empty(&self) -> bool {
85        self.default_response.is_none() && self.responses.is_empty() && self.extensions.is_empty()
86    }
87
88    /// Returns `true` when a status code or `default` is declared.
89    ///
90    /// The distinction from [`is_empty`](Responses::is_empty) is the whole
91    /// point: an extension is not a response, so a Responses Object carrying
92    /// only `x-` fields is *not* empty and still declares nothing.
93    #[must_use]
94    pub fn declares_a_response(&self) -> bool {
95        self.default_response.is_some() || !self.responses.is_empty()
96    }
97
98    /// Looks up the response declared for an exact status code.
99    ///
100    /// Only exact keys are considered; wildcard resolution is a consumer
101    /// concern and depends on precedence rules this method does not apply.
102    #[must_use]
103    pub fn get(&self, status: u16) -> Option<&RefOr<Response>> {
104        self.responses.get(&StatusPattern::Code(status).to_string())
105    }
106
107    /// Merges another set into this one, keeping existing entries on conflict.
108    ///
109    /// This is how an interceptor's declared responses join an operation's own.
110    pub fn merge_from(&mut self, other: &Self) {
111        if self.default_response.is_none() {
112            self.default_response.clone_from(&other.default_response);
113        }
114        for (key, response) in &other.responses {
115            if !self.responses.contains_key(key) {
116                self.responses.insert(key.clone(), response.clone());
117            }
118        }
119    }
120}
121
122impl Serialize for Responses {
123    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
124        let len = usize::from(self.default_response.is_some())
125            + self.responses.len()
126            + self.extensions.0.len();
127        let mut map = serializer.serialize_map(Some(len))?;
128        if let Some(default) = &self.default_response {
129            map.serialize_entry("default", default)?;
130        }
131        for (key, response) in &self.responses {
132            map.serialize_entry(key, response)?;
133        }
134        for (key, value) in &self.extensions.0 {
135            map.serialize_entry(key, value)?;
136        }
137        map.end()
138    }
139}
140
141impl<'de> Deserialize<'de> for Responses {
142    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
143        struct ResponsesVisitor;
144
145        impl<'de> Visitor<'de> for ResponsesVisitor {
146            type Value = Responses;
147
148            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149                f.write_str("a map of status patterns to responses")
150            }
151
152            fn visit_map<A: MapAccess<'de>>(self, mut access: A) -> Result<Responses, A::Error> {
153                let mut responses = Responses::new();
154                while let Some(key) = access.next_key::<String>()? {
155                    if key == "default" {
156                        responses.default_response = Some(access.next_value()?);
157                    } else if key.starts_with(crate::model::extensions::EXTENSION_PREFIX) {
158                        responses.extensions.0.insert(key, access.next_value()?);
159                    } else {
160                        // Reject a malformed key here rather than carrying it
161                        // forward: an unparseable status is never meaningful.
162                        key.parse::<StatusPattern>().map_err(A::Error::custom)?;
163                        responses.responses.insert(key, access.next_value()?);
164                    }
165                }
166                Ok(responses)
167            }
168        }
169
170        deserializer.deserialize_map(ResponsesVisitor)
171    }
172}
173
174/// A single response.
175#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
176pub struct Response {
177    /// A short summary of the response.
178    ///
179    /// Introduced in OpenAPI 3.2. Under 3.1 the first line of the description
180    /// serves this purpose.
181    #[cfg(feature = "openapi32")]
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub summary: Option<String>,
184
185    /// A description of the response. [CommonMark] syntax may be used.
186    ///
187    /// **Required by 3.1, optional in 3.2.** 3.1 marks it `REQUIRED`; 3.2
188    /// drops the marker, so a response stating only a
189    /// [`summary`](Response::summary) is a legal 3.2 document. Modelling it as
190    /// a `String` enforced 3.1's rule on both versions and made such a
191    /// document unparseable, so the requirement lives in
192    /// [`validate`](crate::validate) instead, where it is checked against the
193    /// version the document claims.
194    ///
195    /// [`new`](Response::new) sets it, which is the common case and the only
196    /// one 3.1 admits.
197    ///
198    /// [CommonMark]: https://spec.commonmark.org/
199    #[serde(default, skip_serializing_if = "Option::is_none")]
200    pub description: Option<String>,
201
202    /// Headers sent with the response.
203    ///
204    /// A `Content-Type` entry is ignored, since [`content`](Response::content)
205    /// states it.
206    #[serde(default, skip_serializing_if = "Map::is_empty")]
207    pub headers: Map<RefOr<Header>>,
208
209    /// The response body's representations, keyed by media type.
210    #[serde(default, skip_serializing_if = "Map::is_empty")]
211    pub content: Map<MediaType>,
212
213    /// Design-time links to other operations.
214    #[serde(default, skip_serializing_if = "Map::is_empty")]
215    pub links: Map<RefOr<Link>>,
216
217    /// Specification extensions.
218    #[serde(flatten)]
219    pub extensions: Extensions,
220}
221
222impl Response {
223    /// Creates a response with no body.
224    pub fn new(description: impl Into<String>) -> Self {
225        Self {
226            description: Some(description.into()),
227            ..Self::default()
228        }
229    }
230
231    /// Creates a response with one body representation.
232    pub fn with_content(
233        description: impl Into<String>,
234        media_type: impl Into<String>,
235        content: MediaType,
236    ) -> Self {
237        let mut response = Self::new(description);
238        response.content.insert(media_type.into(), content);
239        response
240    }
241
242    /// Declares a response header.
243    #[must_use]
244    pub fn with_header(mut self, name: impl Into<String>, header: Header) -> Self {
245        self.headers.insert(name.into(), RefOr::Item(header));
246        self
247    }
248
249    /// Declares a link to another operation.
250    #[must_use]
251    pub fn with_link(mut self, name: impl Into<String>, link: Link) -> Self {
252        self.links.insert(name.into(), RefOr::Item(link));
253        self
254    }
255
256    /// Attaches an extension field.
257    #[must_use]
258    pub fn with_extension(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
259        self.extensions.insert(key, value);
260        self
261    }
262}
263
264#[cfg(test)]
265mod tests;