Skip to main content

salvo_oapi/openapi/
components.rs

1//! Implements [OpenAPI Components Object][components] holding reusable parts of an OpenAPI
2//! document.
3//!
4//! [components]: https://spec.openapis.org/oas/latest.html#components-object
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    Callback, Content, Example, Header, Link, Parameter, PathItem, PropMap, RefOr, RequestBody,
9    Response, Responses, Schema, Schemas, SecurityScheme,
10};
11
12/// Implements [OpenAPI Components Object][components] which holds supported
13/// reusable objects.
14///
15/// Components can hold either reusable types themselves or references to other reusable
16/// types.
17///
18/// [components]: https://spec.openapis.org/oas/latest.html#components-object
19#[non_exhaustive]
20#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
21#[serde(rename_all = "camelCase")]
22pub struct Components {
23    /// Map of reusable [OpenAPI Schema Object][schema]s.
24    ///
25    /// [schema]: https://spec.openapis.org/oas/latest.html#schema-object
26    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
27    pub schemas: Schemas,
28
29    /// Map of reusable response name, to [OpenAPI Response Object][response]s or [OpenAPI
30    /// Reference][reference]s to [OpenAPI Response Object][response]s.
31    ///
32    /// [response]: https://spec.openapis.org/oas/latest.html#response-object
33    /// [reference]: https://spec.openapis.org/oas/latest.html#reference-object
34    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
35    pub responses: Responses,
36
37    /// Map of reusable [OpenAPI Parameter Object][parameter]s, indexed by name.
38    ///
39    /// [parameter]: https://spec.openapis.org/oas/latest.html#parameter-object
40    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
41    pub parameters: PropMap<String, RefOr<Parameter>>,
42
43    /// Map of reusable [OpenAPI Example Object][example]s, indexed by name.
44    ///
45    /// [example]: https://spec.openapis.org/oas/latest.html#example-object
46    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
47    pub examples: PropMap<String, RefOr<Example>>,
48
49    /// Map of reusable [OpenAPI Request Body Object][request_body]s, indexed by name.
50    ///
51    /// [request_body]: https://spec.openapis.org/oas/latest.html#request-body-object
52    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
53    pub request_bodies: PropMap<String, RefOr<RequestBody>>,
54
55    /// Map of reusable [OpenAPI Header Object][header]s, indexed by header name.
56    ///
57    /// [header]: https://spec.openapis.org/oas/latest.html#header-object
58    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
59    pub headers: PropMap<String, RefOr<Header>>,
60
61    /// Map of reusable [OpenAPI Security Scheme Object][security_scheme]s.
62    ///
63    /// [security_scheme]: https://spec.openapis.org/oas/latest.html#security-scheme-object
64    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
65    pub security_schemes: PropMap<String, SecurityScheme>,
66
67    /// Map of reusable [OpenAPI Link Object][link]s, indexed by name.
68    ///
69    /// [link]: https://spec.openapis.org/oas/latest.html#link-object
70    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
71    pub links: PropMap<String, RefOr<Link>>,
72
73    /// Map of reusable [OpenAPI Callback Object][callback]s, indexed by name.
74    ///
75    /// [callback]: https://spec.openapis.org/oas/latest.html#callback-object
76    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
77    pub callbacks: PropMap<String, RefOr<Callback>>,
78
79    /// Map of reusable [OpenAPI Path Item Object][path_item]s. Added in OpenAPI 3.1; entries
80    /// here can be referenced from `paths` or `webhooks` via [`RefOr::Ref`].
81    ///
82    /// [path_item]: https://spec.openapis.org/oas/v3.1.0#path-item-object
83    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
84    pub path_items: PropMap<String, RefOr<PathItem>>,
85
86    /// Map of reusable [OpenAPI Media Type Object][media_type]s, indexed by name. Added in
87    /// OpenAPI 3.2; entries here can be referenced from any `content` map via [`RefOr::Ref`].
88    ///
89    /// Note that the key is a component name (matching `^[a-zA-Z0-9\.\-_]+$`), not a media type.
90    ///
91    /// [media_type]: https://spec.openapis.org/oas/v3.2.0.html#media-type-object
92    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
93    pub media_types: PropMap<String, RefOr<Content>>,
94
95    /// Optional extensions "x-something"
96    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
97    pub extensions: PropMap<String, serde_json::Value>,
98}
99
100impl Components {
101    /// Construct a new empty [`Components`]. This is effectively same as calling
102    /// [`Components::default`].
103    #[must_use]
104    pub fn new() -> Self {
105        Default::default()
106    }
107
108    /// Add [`SecurityScheme`] to [`Components`] and returns `Self`.
109    ///
110    /// Accepts two arguments: the name of the [`SecurityScheme`] (used later when referenced
111    /// by [`SecurityRequirement`][requirement]s) and the [`SecurityScheme`] itself.
112    ///
113    /// [requirement]: crate::SecurityRequirement
114    #[must_use]
115    pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
116        mut self,
117        name: N,
118        security_scheme: S,
119    ) -> Self {
120        self.security_schemes
121            .insert(name.into(), security_scheme.into());
122
123        self
124    }
125
126    /// Add iterator of [`SecurityScheme`]s to [`Components`].
127    ///
128    /// Accepts two arguments: the name of the [`SecurityScheme`] (used later when referenced
129    /// by [`SecurityRequirement`][requirement]s) and the [`SecurityScheme`] itself.
130    ///
131    /// [requirement]: crate::SecurityRequirement
132    #[must_use]
133    pub fn extend_security_schemes<
134        I: IntoIterator<Item = (N, S)>,
135        N: Into<String>,
136        S: Into<SecurityScheme>,
137    >(
138        mut self,
139        schemas: I,
140    ) -> Self {
141        self.security_schemes.extend(
142            schemas
143                .into_iter()
144                .map(|(name, item)| (name.into(), item.into())),
145        );
146        self
147    }
148
149    /// Add [`Schema`] to [`Components`] and returns `Self`.
150    ///
151    /// Accepts two arguments where first is name of the schema and second is the schema itself.
152    #[must_use]
153    pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
154        mut self,
155        name: S,
156        schema: I,
157    ) -> Self {
158        self.schemas.insert(name, schema);
159        self
160    }
161
162    /// Add [`Schema`]s from iterator.
163    ///
164    /// # Examples
165    /// ```
166    /// # use salvo_oapi::{Components, Object, BasicType, Schema};
167    /// Components::new().extend_schemas([(
168    ///     "Pet",
169    ///     Schema::from(
170    ///         Object::new()
171    ///             .property("name", Object::new().schema_type(BasicType::String))
172    ///             .required("name"),
173    ///     ),
174    /// )]);
175    /// ```
176    #[must_use]
177    pub fn extend_schemas<I, C, S>(mut self, schemas: I) -> Self
178    where
179        I: IntoIterator<Item = (S, C)>,
180        C: Into<RefOr<Schema>>,
181        S: Into<String>,
182    {
183        self.schemas.extend(
184            schemas
185                .into_iter()
186                .map(|(name, schema)| (name.into(), schema.into())),
187        );
188        self
189    }
190
191    /// Add a new response and returns `self`.
192    #[must_use]
193    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
194        mut self,
195        name: S,
196        response: R,
197    ) -> Self {
198        self.responses.insert(name.into(), response.into());
199        self
200    }
201
202    /// Extends responses with the contents of an iterator.
203    #[must_use]
204    pub fn extend_responses<
205        I: IntoIterator<Item = (S, R)>,
206        S: Into<String>,
207        R: Into<RefOr<Response>>,
208    >(
209        mut self,
210        responses: I,
211    ) -> Self {
212        self.responses.extend(
213            responses
214                .into_iter()
215                .map(|(name, response)| (name.into(), response.into())),
216        );
217        self
218    }
219
220    /// Insert a reusable [`Parameter`] (or a [`Ref`](crate::Ref) to one) and return `self`.
221    #[must_use]
222    pub fn add_parameter<N: Into<String>, P: Into<RefOr<Parameter>>>(
223        mut self,
224        name: N,
225        parameter: P,
226    ) -> Self {
227        self.parameters.insert(name.into(), parameter.into());
228        self
229    }
230
231    /// Insert a reusable [`Example`] (or a [`Ref`](crate::Ref) to one) and return `self`.
232    #[must_use]
233    pub fn add_example<N: Into<String>, E: Into<RefOr<Example>>>(
234        mut self,
235        name: N,
236        example: E,
237    ) -> Self {
238        self.examples.insert(name.into(), example.into());
239        self
240    }
241
242    /// Insert a reusable [`RequestBody`] (or a [`Ref`](crate::Ref) to one) and return `self`.
243    #[must_use]
244    pub fn add_request_body<N: Into<String>, B: Into<RefOr<RequestBody>>>(
245        mut self,
246        name: N,
247        request_body: B,
248    ) -> Self {
249        self.request_bodies.insert(name.into(), request_body.into());
250        self
251    }
252
253    /// Insert a reusable [`Header`] (or a [`Ref`](crate::Ref) to one) and return `self`.
254    #[must_use]
255    pub fn add_header<N: Into<String>, H: Into<RefOr<Header>>>(
256        mut self,
257        name: N,
258        header: H,
259    ) -> Self {
260        self.headers.insert(name.into(), header.into());
261        self
262    }
263
264    /// Insert a reusable [`Link`] (or a [`Ref`](crate::Ref) to one) and return `self`.
265    #[must_use]
266    pub fn add_link<N: Into<String>, L: Into<RefOr<Link>>>(mut self, name: N, link: L) -> Self {
267        self.links.insert(name.into(), link.into());
268        self
269    }
270
271    /// Insert a reusable [`Callback`] (or a [`Ref`](crate::Ref) to one) and return `self`.
272    #[must_use]
273    pub fn add_callback<N: Into<String>, C: Into<RefOr<Callback>>>(
274        mut self,
275        name: N,
276        callback: C,
277    ) -> Self {
278        self.callbacks.insert(name.into(), callback.into());
279        self
280    }
281
282    /// Insert a reusable [`PathItem`] (or a [`Ref`](crate::Ref) to one) and return `self`.
283    ///
284    /// Path Item entries in `components.pathItems` were introduced in OpenAPI 3.1 to support
285    /// reusable webhooks and shared path operations.
286    #[must_use]
287    pub fn add_path_item<N: Into<String>, P: Into<RefOr<PathItem>>>(
288        mut self,
289        name: N,
290        path_item: P,
291    ) -> Self {
292        self.path_items.insert(name.into(), path_item.into());
293        self
294    }
295
296    /// Insert a reusable media type (or a [`Ref`](crate::Ref) to one) and return `self`.
297    ///
298    /// Reusable Media Type Objects were introduced in OpenAPI 3.2. `name` is a component name,
299    /// not a media type; entries are referenced as
300    /// `#/components/mediaTypes/{name}` from a `content` map.
301    #[must_use]
302    pub fn add_media_type<N: Into<String>, C: Into<RefOr<Content>>>(
303        mut self,
304        name: N,
305        media_type: C,
306    ) -> Self {
307        self.media_types.insert(name.into(), media_type.into());
308        self
309    }
310
311    /// Moves all elements from `other` into `self`, leaving `other` empty.
312    ///
313    /// If a key from `other` is already present in `self`, the existing value is kept and
314    /// the duplicate from `other` is dropped.
315    pub fn append(&mut self, other: &mut Self) {
316        other
317            .schemas
318            .retain(|name, _| !self.schemas.contains_key(name));
319        self.schemas.append(&mut other.schemas);
320
321        other
322            .responses
323            .retain(|name, _| !self.responses.contains_key(name));
324        self.responses.append(&mut other.responses);
325
326        other
327            .parameters
328            .retain(|name, _| !self.parameters.contains_key(name));
329        self.parameters.append(&mut other.parameters);
330
331        other
332            .examples
333            .retain(|name, _| !self.examples.contains_key(name));
334        self.examples.append(&mut other.examples);
335
336        other
337            .request_bodies
338            .retain(|name, _| !self.request_bodies.contains_key(name));
339        self.request_bodies.append(&mut other.request_bodies);
340
341        other
342            .headers
343            .retain(|name, _| !self.headers.contains_key(name));
344        self.headers.append(&mut other.headers);
345
346        other
347            .security_schemes
348            .retain(|name, _| !self.security_schemes.contains_key(name));
349        self.security_schemes.append(&mut other.security_schemes);
350
351        other.links.retain(|name, _| !self.links.contains_key(name));
352        self.links.append(&mut other.links);
353
354        other
355            .callbacks
356            .retain(|name, _| !self.callbacks.contains_key(name));
357        self.callbacks.append(&mut other.callbacks);
358
359        other
360            .path_items
361            .retain(|name, _| !self.path_items.contains_key(name));
362        self.path_items.append(&mut other.path_items);
363
364        other
365            .media_types
366            .retain(|name, _| !self.media_types.contains_key(name));
367        self.media_types.append(&mut other.media_types);
368
369        other
370            .extensions
371            .retain(|name, _| !self.extensions.contains_key(name));
372        self.extensions.append(&mut other.extensions);
373    }
374
375    /// Add openapi extensions (`x-something`) for [`Components`].
376    #[must_use]
377    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
378        self.extensions = extensions;
379        self
380    }
381
382    /// Returns `true` if instance contains no elements.
383    #[must_use]
384    pub fn is_empty(&self) -> bool {
385        self.schemas.is_empty()
386            && self.responses.is_empty()
387            && self.parameters.is_empty()
388            && self.examples.is_empty()
389            && self.request_bodies.is_empty()
390            && self.headers.is_empty()
391            && self.security_schemes.is_empty()
392            && self.links.is_empty()
393            && self.callbacks.is_empty()
394            && self.path_items.is_empty()
395            && self.media_types.is_empty()
396            && self.extensions.is_empty()
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use assert_json_diff::assert_json_eq;
403    use serde_json::json;
404
405    use super::*;
406    use crate::{Operation, ParameterIn, PathItemType, Ref};
407
408    #[test]
409    fn empty_components_serializes_with_no_fields() {
410        assert_json_eq!(Components::new(), json!({}));
411        assert!(Components::new().is_empty());
412    }
413
414    #[test]
415    fn each_new_field_serializes_under_spec_name() {
416        let components = Components::new()
417            .add_parameter(
418                "PageParam",
419                Parameter::new("page").location(ParameterIn::Query),
420            )
421            .add_example(
422                "PetExample",
423                RefOr::Ref(Ref::new("#/components/examples/UpstreamPet")),
424            )
425            .add_request_body("PetBody", RequestBody::new())
426            .add_header("X-Rate-Limit", Header::default())
427            .add_link("GetPetLink", Link::default())
428            .add_callback(
429                "OrderShipped",
430                Callback::new().path(
431                    "{$request.body#/callbackUrl}",
432                    PathItem::new(PathItemType::Post, Operation::new()),
433                ),
434            )
435            .add_path_item(
436                "PingWebhook",
437                PathItem::new(PathItemType::Post, Operation::new()),
438            );
439
440        let value = serde_json::to_value(&components).expect("serialize");
441
442        assert!(value.get("parameters").is_some(), "expected parameters");
443        assert!(value.get("examples").is_some(), "expected examples");
444        assert!(
445            value.get("requestBodies").is_some(),
446            "expected requestBodies (camelCase)"
447        );
448        assert!(value.get("headers").is_some(), "expected headers");
449        assert!(value.get("links").is_some(), "expected links");
450        assert!(value.get("callbacks").is_some(), "expected callbacks");
451        assert!(
452            value.get("pathItems").is_some(),
453            "expected pathItems (camelCase, OAS 3.1)"
454        );
455    }
456
457    #[test]
458    fn is_empty_recognizes_each_new_field() {
459        // Adding any one of the new component maps should flip is_empty to false.
460        assert!(
461            !Components::new()
462                .add_parameter("p", Parameter::new("q"))
463                .is_empty()
464        );
465        assert!(
466            !Components::new()
467                .add_example("e", crate::Example::default())
468                .is_empty()
469        );
470        assert!(
471            !Components::new()
472                .add_request_body("rb", RequestBody::new())
473                .is_empty()
474        );
475        assert!(
476            !Components::new()
477                .add_header("h", Header::default())
478                .is_empty()
479        );
480        assert!(!Components::new().add_link("l", Link::default()).is_empty());
481        assert!(
482            !Components::new()
483                .add_callback("cb", Callback::new())
484                .is_empty()
485        );
486        assert!(
487            !Components::new()
488                .add_path_item("pi", PathItem::new(PathItemType::Get, Operation::new()))
489                .is_empty()
490        );
491    }
492
493    #[test]
494    fn append_preserves_self_on_key_collision() {
495        let mut a = Components::new().add_parameter("dup", Parameter::new("a_param"));
496        let mut b = Components::new()
497            .add_parameter("dup", Parameter::new("b_param"))
498            .add_parameter("only_b", Parameter::new("b_only_param"));
499
500        a.append(&mut b);
501
502        let dup = a.parameters.get("dup").expect("dup retained");
503        match dup {
504            RefOr::Type(p) => assert_eq!(p.name, "a_param", "self's value should win on collision"),
505            RefOr::Ref(_) => panic!("unexpected ref"),
506        }
507        assert!(a.parameters.contains_key("only_b"));
508    }
509}