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, Example, Header, Link, Parameter, PathItem, PropMap, RefOr, RequestBody, Response,
9    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    /// Optional extensions "x-something"
87    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
88    pub extensions: PropMap<String, serde_json::Value>,
89}
90
91impl Components {
92    /// Construct a new empty [`Components`]. This is effectively same as calling
93    /// [`Components::default`].
94    #[must_use]
95    pub fn new() -> Self {
96        Default::default()
97    }
98
99    /// Add [`SecurityScheme`] to [`Components`] and returns `Self`.
100    ///
101    /// Accepts two arguments: the name of the [`SecurityScheme`] (used later when referenced
102    /// by [`SecurityRequirement`][requirement]s) and the [`SecurityScheme`] itself.
103    ///
104    /// [requirement]: crate::SecurityRequirement
105    #[must_use]
106    pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
107        mut self,
108        name: N,
109        security_scheme: S,
110    ) -> Self {
111        self.security_schemes
112            .insert(name.into(), security_scheme.into());
113
114        self
115    }
116
117    /// Add iterator of [`SecurityScheme`]s to [`Components`].
118    ///
119    /// Accepts two arguments: the name of the [`SecurityScheme`] (used later when referenced
120    /// by [`SecurityRequirement`][requirement]s) and the [`SecurityScheme`] itself.
121    ///
122    /// [requirement]: crate::SecurityRequirement
123    #[must_use]
124    pub fn extend_security_schemes<
125        I: IntoIterator<Item = (N, S)>,
126        N: Into<String>,
127        S: Into<SecurityScheme>,
128    >(
129        mut self,
130        schemas: I,
131    ) -> Self {
132        self.security_schemes.extend(
133            schemas
134                .into_iter()
135                .map(|(name, item)| (name.into(), item.into())),
136        );
137        self
138    }
139
140    /// Add [`Schema`] to [`Components`] and returns `Self`.
141    ///
142    /// Accepts two arguments where first is name of the schema and second is the schema itself.
143    #[must_use]
144    pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
145        mut self,
146        name: S,
147        schema: I,
148    ) -> Self {
149        self.schemas.insert(name, schema);
150        self
151    }
152
153    /// Add [`Schema`]s from iterator.
154    ///
155    /// # Examples
156    /// ```
157    /// # use salvo_oapi::{Components, Object, BasicType, Schema};
158    /// Components::new().extend_schemas([(
159    ///     "Pet",
160    ///     Schema::from(
161    ///         Object::new()
162    ///             .property("name", Object::new().schema_type(BasicType::String))
163    ///             .required("name"),
164    ///     ),
165    /// )]);
166    /// ```
167    #[must_use]
168    pub fn extend_schemas<I, C, S>(mut self, schemas: I) -> Self
169    where
170        I: IntoIterator<Item = (S, C)>,
171        C: Into<RefOr<Schema>>,
172        S: Into<String>,
173    {
174        self.schemas.extend(
175            schemas
176                .into_iter()
177                .map(|(name, schema)| (name.into(), schema.into())),
178        );
179        self
180    }
181
182    /// Add a new response and returns `self`.
183    #[must_use]
184    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
185        mut self,
186        name: S,
187        response: R,
188    ) -> Self {
189        self.responses.insert(name.into(), response.into());
190        self
191    }
192
193    /// Extends responses with the contents of an iterator.
194    #[must_use]
195    pub fn extend_responses<
196        I: IntoIterator<Item = (S, R)>,
197        S: Into<String>,
198        R: Into<RefOr<Response>>,
199    >(
200        mut self,
201        responses: I,
202    ) -> Self {
203        self.responses.extend(
204            responses
205                .into_iter()
206                .map(|(name, response)| (name.into(), response.into())),
207        );
208        self
209    }
210
211    /// Insert a reusable [`Parameter`] (or a [`Ref`](crate::Ref) to one) and return `self`.
212    #[must_use]
213    pub fn add_parameter<N: Into<String>, P: Into<RefOr<Parameter>>>(
214        mut self,
215        name: N,
216        parameter: P,
217    ) -> Self {
218        self.parameters.insert(name.into(), parameter.into());
219        self
220    }
221
222    /// Insert a reusable [`Example`] (or a [`Ref`](crate::Ref) to one) and return `self`.
223    #[must_use]
224    pub fn add_example<N: Into<String>, E: Into<RefOr<Example>>>(
225        mut self,
226        name: N,
227        example: E,
228    ) -> Self {
229        self.examples.insert(name.into(), example.into());
230        self
231    }
232
233    /// Insert a reusable [`RequestBody`] (or a [`Ref`](crate::Ref) to one) and return `self`.
234    #[must_use]
235    pub fn add_request_body<N: Into<String>, B: Into<RefOr<RequestBody>>>(
236        mut self,
237        name: N,
238        request_body: B,
239    ) -> Self {
240        self.request_bodies.insert(name.into(), request_body.into());
241        self
242    }
243
244    /// Insert a reusable [`Header`] (or a [`Ref`](crate::Ref) to one) and return `self`.
245    #[must_use]
246    pub fn add_header<N: Into<String>, H: Into<RefOr<Header>>>(
247        mut self,
248        name: N,
249        header: H,
250    ) -> Self {
251        self.headers.insert(name.into(), header.into());
252        self
253    }
254
255    /// Insert a reusable [`Link`] (or a [`Ref`](crate::Ref) to one) and return `self`.
256    #[must_use]
257    pub fn add_link<N: Into<String>, L: Into<RefOr<Link>>>(mut self, name: N, link: L) -> Self {
258        self.links.insert(name.into(), link.into());
259        self
260    }
261
262    /// Insert a reusable [`Callback`] (or a [`Ref`](crate::Ref) to one) and return `self`.
263    #[must_use]
264    pub fn add_callback<N: Into<String>, C: Into<RefOr<Callback>>>(
265        mut self,
266        name: N,
267        callback: C,
268    ) -> Self {
269        self.callbacks.insert(name.into(), callback.into());
270        self
271    }
272
273    /// Insert a reusable [`PathItem`] (or a [`Ref`](crate::Ref) to one) and return `self`.
274    ///
275    /// Path Item entries in `components.pathItems` were introduced in OpenAPI 3.1 to support
276    /// reusable webhooks and shared path operations.
277    #[must_use]
278    pub fn add_path_item<N: Into<String>, P: Into<RefOr<PathItem>>>(
279        mut self,
280        name: N,
281        path_item: P,
282    ) -> Self {
283        self.path_items.insert(name.into(), path_item.into());
284        self
285    }
286
287    /// Moves all elements from `other` into `self`, leaving `other` empty.
288    ///
289    /// If a key from `other` is already present in `self`, the existing value is kept and
290    /// the duplicate from `other` is dropped.
291    pub fn append(&mut self, other: &mut Self) {
292        other
293            .schemas
294            .retain(|name, _| !self.schemas.contains_key(name));
295        self.schemas.append(&mut other.schemas);
296
297        other
298            .responses
299            .retain(|name, _| !self.responses.contains_key(name));
300        self.responses.append(&mut other.responses);
301
302        other
303            .parameters
304            .retain(|name, _| !self.parameters.contains_key(name));
305        self.parameters.append(&mut other.parameters);
306
307        other
308            .examples
309            .retain(|name, _| !self.examples.contains_key(name));
310        self.examples.append(&mut other.examples);
311
312        other
313            .request_bodies
314            .retain(|name, _| !self.request_bodies.contains_key(name));
315        self.request_bodies.append(&mut other.request_bodies);
316
317        other
318            .headers
319            .retain(|name, _| !self.headers.contains_key(name));
320        self.headers.append(&mut other.headers);
321
322        other
323            .security_schemes
324            .retain(|name, _| !self.security_schemes.contains_key(name));
325        self.security_schemes.append(&mut other.security_schemes);
326
327        other.links.retain(|name, _| !self.links.contains_key(name));
328        self.links.append(&mut other.links);
329
330        other
331            .callbacks
332            .retain(|name, _| !self.callbacks.contains_key(name));
333        self.callbacks.append(&mut other.callbacks);
334
335        other
336            .path_items
337            .retain(|name, _| !self.path_items.contains_key(name));
338        self.path_items.append(&mut other.path_items);
339
340        other
341            .extensions
342            .retain(|name, _| !self.extensions.contains_key(name));
343        self.extensions.append(&mut other.extensions);
344    }
345
346    /// Add openapi extensions (`x-something`) for [`Components`].
347    #[must_use]
348    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
349        self.extensions = extensions;
350        self
351    }
352
353    /// Returns `true` if instance contains no elements.
354    #[must_use]
355    pub fn is_empty(&self) -> bool {
356        self.schemas.is_empty()
357            && self.responses.is_empty()
358            && self.parameters.is_empty()
359            && self.examples.is_empty()
360            && self.request_bodies.is_empty()
361            && self.headers.is_empty()
362            && self.security_schemes.is_empty()
363            && self.links.is_empty()
364            && self.callbacks.is_empty()
365            && self.path_items.is_empty()
366            && self.extensions.is_empty()
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use assert_json_diff::assert_json_eq;
373    use serde_json::json;
374
375    use super::*;
376    use crate::{Operation, ParameterIn, PathItemType, Ref};
377
378    #[test]
379    fn empty_components_serializes_with_no_fields() {
380        assert_json_eq!(Components::new(), json!({}));
381        assert!(Components::new().is_empty());
382    }
383
384    #[test]
385    fn each_new_field_serializes_under_spec_name() {
386        let components = Components::new()
387            .add_parameter(
388                "PageParam",
389                Parameter::new("page").location(ParameterIn::Query),
390            )
391            .add_example(
392                "PetExample",
393                RefOr::Ref(Ref::new("#/components/examples/UpstreamPet")),
394            )
395            .add_request_body("PetBody", RequestBody::new())
396            .add_header("X-Rate-Limit", Header::default())
397            .add_link("GetPetLink", Link::default())
398            .add_callback(
399                "OrderShipped",
400                Callback::new().path(
401                    "{$request.body#/callbackUrl}",
402                    PathItem::new(PathItemType::Post, Operation::new()),
403                ),
404            )
405            .add_path_item(
406                "PingWebhook",
407                PathItem::new(PathItemType::Post, Operation::new()),
408            );
409
410        let value = serde_json::to_value(&components).expect("serialize");
411
412        assert!(value.get("parameters").is_some(), "expected parameters");
413        assert!(value.get("examples").is_some(), "expected examples");
414        assert!(
415            value.get("requestBodies").is_some(),
416            "expected requestBodies (camelCase)"
417        );
418        assert!(value.get("headers").is_some(), "expected headers");
419        assert!(value.get("links").is_some(), "expected links");
420        assert!(value.get("callbacks").is_some(), "expected callbacks");
421        assert!(
422            value.get("pathItems").is_some(),
423            "expected pathItems (camelCase, OAS 3.1)"
424        );
425    }
426
427    #[test]
428    fn is_empty_recognizes_each_new_field() {
429        // Adding any one of the new component maps should flip is_empty to false.
430        assert!(
431            !Components::new()
432                .add_parameter("p", Parameter::new("q"))
433                .is_empty()
434        );
435        assert!(
436            !Components::new()
437                .add_example("e", crate::Example::default())
438                .is_empty()
439        );
440        assert!(
441            !Components::new()
442                .add_request_body("rb", RequestBody::new())
443                .is_empty()
444        );
445        assert!(
446            !Components::new()
447                .add_header("h", Header::default())
448                .is_empty()
449        );
450        assert!(!Components::new().add_link("l", Link::default()).is_empty());
451        assert!(
452            !Components::new()
453                .add_callback("cb", Callback::new())
454                .is_empty()
455        );
456        assert!(
457            !Components::new()
458                .add_path_item("pi", PathItem::new(PathItemType::Get, Operation::new()))
459                .is_empty()
460        );
461    }
462
463    #[test]
464    fn append_preserves_self_on_key_collision() {
465        let mut a = Components::new().add_parameter("dup", Parameter::new("a_param"));
466        let mut b = Components::new()
467            .add_parameter("dup", Parameter::new("b_param"))
468            .add_parameter("only_b", Parameter::new("b_only_param"));
469
470        a.append(&mut b);
471
472        let dup = a.parameters.get("dup").expect("dup retained");
473        match dup {
474            RefOr::Type(p) => assert_eq!(p.name, "a_param", "self's value should win on collision"),
475            RefOr::Ref(_) => panic!("unexpected ref"),
476        }
477        assert!(a.parameters.contains_key("only_b"));
478    }
479}