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;