Skip to main content

kynos_openapi/model/
example.rs

1//! The Example Object.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7    Map,
8    model::{extensions::Extensions, reference::RefOr},
9};
10
11/// A worked example of a parameter, request body, response body or header.
12///
13/// # Choosing a value field
14///
15/// OpenAPI 3.1 offers `value` and `externalValue`, which are mutually
16/// exclusive.
17///
18/// 3.2 adds `dataValue` and `serializedValue`, and deprecates `value` for
19/// non-JSON serialization targets — for those, `value` has
20/// implementation-defined behaviour, which is exactly the kind of ambiguity
21/// Kynos avoids. Prefer [`data`](Example::data) (the example as data, before
22/// serialization) and [`serialized`](Example::serialized) (the example as it
23/// appears on the wire) whenever `openapi32` is available and the target is not
24/// JSON.
25///
26/// The exclusions between those four fields are not a plain one-of, which is
27/// why they live in [`ExampleValue`] rather than in four `Option`s.
28#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(try_from = "RawExample", into = "RawExample")]
30pub struct Example {
31    /// A short description of the example.
32    pub summary: Option<String>,
33
34    /// A long description of the example. [CommonMark] syntax may be used.
35    ///
36    /// [CommonMark]: https://spec.commonmark.org/
37    pub description: Option<String>,
38
39    value: Option<ExampleValue>,
40
41    /// Specification extensions.
42    pub extensions: Extensions,
43}
44
45/// The example itself, in whichever form carries it.
46///
47/// The specification's exclusions are asymmetric, so this is not a one-of over
48/// four fields. `value` excludes all three others; `serializedValue` and
49/// `externalValue` exclude each other; but `dataValue` pairs with *either* of
50/// them, which is how the specification's own worked examples are written. The
51/// variants below are exactly the combinations that leaves.
52/// `#[non_exhaustive]` because OpenAPI 3.2 adds to this and the addition is
53/// `#[cfg]`-gated. Cargo unifies features across a dependency graph, so any
54/// crate enabling `openapi32` enables it for every crate in the build -- and
55/// without this attribute that would turn a downstream exhaustive `match` into
56/// a compile error, which is not what "purely additive" is supposed to mean.
57#[non_exhaustive]
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub enum ExampleValue {
60    /// An embedded literal example, written to `value`.
61    ///
62    /// Exclusive with every other form. Deprecated by 3.2 for non-JSON
63    /// serialization targets; see the type-level documentation.
64    ///
65    /// Named for what it is rather than for its field: a variant called `Value`
66    /// would collide with [`serde_json::Value`] in rustc's shortest-path table
67    /// and lengthen that type's name in every diagnostic mentioning it, this
68    /// crate's or anyone else's.
69    Embedded(Value),
70
71    /// A URI identifying the serialized example, written to `externalValue`.
72    ///
73    /// For payloads that cannot be embedded in JSON or YAML.
74    ///
75    /// `#[non_exhaustive]` on the *variant*, because 3.2 adds `data` to it.
76    /// The attribute on the enum covers a variant being added and says nothing
77    /// about this one's field list; see the type's own documentation.
78    #[non_exhaustive]
79    External {
80        /// The URI identifying the example.
81        uri: String,
82
83        /// The data the URI serializes, written to `dataValue`.
84        #[cfg(feature = "openapi32")]
85        data: Option<Value>,
86    },
87
88    /// The example as data, written to `dataValue`.
89    ///
90    /// Introduced in OpenAPI 3.2.
91    #[cfg(feature = "openapi32")]
92    Data {
93        /// The example as a data structure, before serialization.
94        data: Value,
95
96        /// The same example on the wire, written to `serializedValue`.
97        serialized: Option<String>,
98    },
99
100    /// The example exactly as it appears on the wire, written to
101    /// `serializedValue`, with no data form given.
102    ///
103    /// Introduced in OpenAPI 3.2.
104    #[cfg(feature = "openapi32")]
105    Serialized(String),
106}
107
108impl Example {
109    /// Creates an example holding an embedded value.
110    pub fn new(value: impl Into<Value>) -> Self {
111        Self::carrying(ExampleValue::Embedded(value.into()))
112    }
113
114    /// Creates an example pointing at an external payload.
115    pub fn external(uri: impl Into<String>) -> Self {
116        Self::carrying(ExampleValue::External {
117            uri: uri.into(),
118            #[cfg(feature = "openapi32")]
119            data: None,
120        })
121    }
122
123    /// Creates an example given as data, before serialization.
124    ///
125    /// Preferred over [`new`](Example::new) whenever the serialization target
126    /// is not JSON.
127    #[cfg(feature = "openapi32")]
128    pub fn data(data: impl Into<Value>) -> Self {
129        Self::carrying(ExampleValue::Data {
130            data: data.into(),
131            serialized: None,
132        })
133    }
134
135    /// Creates an example given only in its serialized form.
136    #[cfg(feature = "openapi32")]
137    pub fn serialized(serialized: impl Into<String>) -> Self {
138        Self::carrying(ExampleValue::Serialized(serialized.into()))
139    }
140
141    /// Creates an example given as data together with its serialization.
142    ///
143    /// The pair a boolean query parameter needs: `true` is the data, `flag=true`
144    /// is what goes on the wire, and neither implies the other.
145    #[cfg(feature = "openapi32")]
146    pub fn data_serialized(data: impl Into<Value>, serialized: impl Into<String>) -> Self {
147        Self::carrying(ExampleValue::Data {
148            data: data.into(),
149            serialized: Some(serialized.into()),
150        })
151    }
152
153    /// Creates an example given as data, serialized into an external payload.
154    #[cfg(feature = "openapi32")]
155    pub fn data_external(data: impl Into<Value>, uri: impl Into<String>) -> Self {
156        Self::carrying(ExampleValue::External {
157            uri: uri.into(),
158            data: Some(data.into()),
159        })
160    }
161
162    /// The example this object carries, if it carries one.
163    ///
164    /// An Example Object with only a summary is valid, if not useful.
165    #[must_use]
166    pub fn value(&self) -> Option<&ExampleValue> {
167        self.value.as_ref()
168    }
169
170    fn carrying(value: ExampleValue) -> Self {
171        Self {
172            value: Some(value),
173            ..Self::default()
174        }
175    }
176
177    /// Sets the short summary.
178    #[must_use]
179    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
180        self.summary = Some(summary.into());
181        self
182    }
183
184    /// Sets the long description.
185    #[must_use]
186    pub fn with_description(mut self, description: impl Into<String>) -> Self {
187        self.description = Some(description.into());
188        self
189    }
190}
191
192/// The wire shape: the value fields flat, as the specification writes them.
193#[derive(Serialize, Deserialize)]
194struct RawExample {
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    summary: Option<String>,
197
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    description: Option<String>,
200
201    #[serde(
202        default,
203        deserialize_with = "crate::model::nullable::some",
204        skip_serializing_if = "Option::is_none"
205    )]
206    value: Option<Value>,
207
208    #[cfg(feature = "openapi32")]
209    #[serde(
210        rename = "dataValue",
211        default,
212        deserialize_with = "crate::model::nullable::some",
213        skip_serializing_if = "Option::is_none"
214    )]
215    data_value: Option<Value>,
216
217    #[cfg(feature = "openapi32")]
218    #[serde(
219        rename = "serializedValue",
220        default,
221        skip_serializing_if = "Option::is_none"
222    )]
223    serialized_value: Option<String>,
224
225    #[serde(
226        rename = "externalValue",
227        default,
228        skip_serializing_if = "Option::is_none"
229    )]
230    external_value: Option<String>,
231
232    #[serde(flatten)]
233    extensions: Extensions,
234}
235
236/// An Example Object whose value fields cannot hold together.
237#[derive(Debug)]
238enum ExampleConflict {
239    /// `value` was set beside one of the fields that excludes it.
240    ValueNotAlone,
241
242    /// `serializedValue` and `externalValue` were both set.
243    #[cfg(feature = "openapi32")]
244    TwoSerializations,
245}
246
247impl std::fmt::Display for ExampleConflict {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        match self {
250            Self::ValueNotAlone => f.write_str(
251                "`value` is mutually exclusive with `dataValue`, `serializedValue` and \
252                 `externalValue` on an Example Object",
253            ),
254            #[cfg(feature = "openapi32")]
255            Self::TwoSerializations => f.write_str(
256                "`serializedValue` and `externalValue` are mutually exclusive on an Example \
257                 Object",
258            ),
259        }
260    }
261}
262
263impl TryFrom<RawExample> for Example {
264    type Error = ExampleConflict;
265
266    fn try_from(raw: RawExample) -> Result<Self, Self::Error> {
267        // One total match rather than guards and a fallthrough, so that the
268        // compiler is the thing checking these combinations are exhaustive.
269        #[cfg(feature = "openapi32")]
270        let value = match (
271            raw.value,
272            raw.data_value,
273            raw.serialized_value,
274            raw.external_value,
275        ) {
276            (Some(_), Some(_), _, _) | (Some(_), _, Some(_), _) | (Some(_), _, _, Some(_)) => {
277                return Err(ExampleConflict::ValueNotAlone);
278            }
279            (_, _, Some(_), Some(_)) => return Err(ExampleConflict::TwoSerializations),
280
281            (Some(value), None, None, None) => Some(ExampleValue::Embedded(value)),
282            (None, data, None, Some(uri)) => Some(ExampleValue::External { uri, data }),
283            (None, Some(data), serialized, None) => Some(ExampleValue::Data { data, serialized }),
284            (None, None, Some(serialized), None) => Some(ExampleValue::Serialized(serialized)),
285            (None, None, None, None) => None,
286        };
287
288        #[cfg(not(feature = "openapi32"))]
289        let value = match (raw.value, raw.external_value) {
290            (Some(_), Some(_)) => return Err(ExampleConflict::ValueNotAlone),
291            (Some(value), None) => Some(ExampleValue::Embedded(value)),
292            (None, Some(uri)) => Some(ExampleValue::External { uri }),
293            (None, None) => None,
294        };
295
296        Ok(Self {
297            summary: raw.summary,
298            description: raw.description,
299            value,
300            extensions: raw.extensions,
301        })
302    }
303}
304
305impl From<Example> for RawExample {
306    fn from(example: Example) -> Self {
307        #[cfg(feature = "openapi32")]
308        let (value, data_value, serialized_value, external_value) = match example.value {
309            Some(ExampleValue::Embedded(value)) => (Some(value), None, None, None),
310            Some(ExampleValue::External { uri, data }) => (None, data, None, Some(uri)),
311            Some(ExampleValue::Data { data, serialized }) => (None, Some(data), serialized, None),
312            Some(ExampleValue::Serialized(serialized)) => (None, None, Some(serialized), None),
313            None => (None, None, None, None),
314        };
315
316        #[cfg(not(feature = "openapi32"))]
317        let (value, external_value) = match example.value {
318            Some(ExampleValue::Embedded(value)) => (Some(value), None),
319            Some(ExampleValue::External { uri }) => (None, Some(uri)),
320            None => (None, None),
321        };
322
323        Self {
324            summary: example.summary,
325            description: example.description,
326            value,
327            #[cfg(feature = "openapi32")]
328            data_value,
329            #[cfg(feature = "openapi32")]
330            serialized_value,
331            external_value,
332            extensions: example.extensions,
333        }
334    }
335}
336
337/// The examples an object shows its value with, in whichever form it uses.
338///
339/// A Parameter, Header or Media Type Object may carry one inline `example` or a
340/// map of named `examples`, and the specification makes the two mutually
341/// exclusive. An enum rather than two `Option` fields, for the reason
342/// [`SecurityScheme`](crate::model::security::SecurityScheme) is one: an
343/// unusable combination that cannot be spelled needs no rule to reject it.
344///
345/// Note that the singular form is not an [`Example`]: `example` is the value
346/// itself, written inline, while `examples` maps names to Example Objects that
347/// can also carry a summary, a description or an external payload.
348#[derive(Clone, Debug, PartialEq)]
349pub enum Examples {
350    /// One example of the value, written to `example`.
351    Inline(Value),
352
353    /// Named examples, written to `examples`.
354    ///
355    /// Each is an [`Example`] or a reference to one in
356    /// [`Components::examples`](crate::Components::examples).
357    Named(Map<RefOr<Example>>),
358}
359
360/// An object setting both of the mutually exclusive example fields.
361#[derive(Debug)]
362pub(crate) struct ExamplesConflict;
363
364impl std::fmt::Display for ExamplesConflict {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        f.write_str("`example` and `examples` are mutually exclusive")
367    }
368}
369
370/// Folds the wire fields into one form, or says they will not go.
371pub(crate) fn examples_from(
372    example: Option<Value>,
373    examples: Option<Map<RefOr<Example>>>,
374) -> Result<Option<Examples>, ExamplesConflict> {
375    match (example, examples) {
376        (Some(_), Some(_)) => Err(ExamplesConflict),
377        (Some(value), None) => Ok(Some(Examples::Inline(value))),
378        (None, Some(named)) => Ok(Some(Examples::Named(named))),
379        (None, None) => Ok(None),
380    }
381}
382
383/// Splits one form back into the wire fields.
384pub(crate) fn examples_into(
385    examples: Option<Examples>,
386) -> (Option<Value>, Option<Map<RefOr<Example>>>) {
387    match examples {
388        Some(Examples::Inline(value)) => (Some(value), None),
389        Some(Examples::Named(named)) => (None, Some(named)),
390        None => (None, None),
391    }
392}
393
394/// Adds a named example to whatever an object carries already.
395///
396/// An inline example is dropped rather than kept beside the named one: the two
397/// forms exclude each other, so there is no state that holds both.
398pub(crate) fn examples_with_named(
399    examples: Option<Examples>,
400    name: String,
401    example: RefOr<Example>,
402) -> Examples {
403    let mut named = match examples {
404        Some(Examples::Named(named)) => named,
405        Some(Examples::Inline(_)) | None => Map::new(),
406    };
407    named.insert(name, example);
408    Examples::Named(named)
409}
410
411#[cfg(test)]
412mod tests;