salvo_oapi/
openapi.rs

1//! Rust implementation of Openapi Spec V3.1.
2
3mod components;
4mod content;
5mod encoding;
6mod example;
7mod external_docs;
8mod header;
9pub mod info;
10mod link;
11pub mod operation;
12pub mod parameter;
13pub mod path;
14pub mod request_body;
15pub mod response;
16pub mod schema;
17pub mod security;
18pub mod server;
19mod tag;
20mod xml;
21
22use std::collections::BTreeSet;
23use std::fmt::{self, Debug, Formatter};
24use std::sync::LazyLock;
25
26use regex::Regex;
27use salvo_core::{Depot, FlowCtrl, Handler, Router, async_trait, writing};
28use serde::de::{Error, Expected, Visitor};
29use serde::{Deserialize, Deserializer, Serialize, Serializer};
30
31pub use self::components::Components;
32pub use self::content::Content;
33pub use self::example::Example;
34pub use self::external_docs::ExternalDocs;
35pub use self::header::Header;
36pub use self::info::{Contact, Info, License};
37pub use self::operation::{Operation, Operations};
38pub use self::parameter::{Parameter, ParameterIn, ParameterStyle, Parameters};
39pub use self::path::{PathItem, PathItemType, Paths};
40pub use self::request_body::RequestBody;
41pub use self::response::{Response, Responses};
42pub use self::schema::{
43    Array, BasicType, Discriminator, KnownFormat, Object, Ref, Schema, SchemaFormat, SchemaType,
44    Schemas,
45};
46pub use self::security::{SecurityRequirement, SecurityScheme};
47pub use self::server::{Server, ServerVariable, ServerVariables, Servers};
48pub use self::tag::Tag;
49pub use self::xml::Xml;
50use crate::Endpoint;
51use crate::routing::NormNode;
52
53static PATH_PARAMETER_NAME_REGEX: LazyLock<Regex> =
54    LazyLock::new(|| Regex::new(r"\{([^}:]+)").expect("invalid regex"));
55
56/// The structure of the internal storage object paths.
57#[cfg(not(feature = "preserve-path-order"))]
58pub type PathMap<K, V> = std::collections::BTreeMap<K, V>;
59/// The structure of the internal storage object paths.
60#[cfg(feature = "preserve-path-order")]
61pub type PathMap<K, V> = indexmap::IndexMap<K, V>;
62
63/// The structure of the internal storage object properties.
64#[cfg(not(feature = "preserve-prop-order"))]
65pub type PropMap<K, V> = std::collections::BTreeMap<K, V>;
66/// The structure of the internal storage object properties.
67#[cfg(feature = "preserve-prop-order")]
68pub type PropMap<K, V> = indexmap::IndexMap<K, V>;
69
70/// Root object of the OpenAPI document.
71///
72/// You can use [`OpenApi::new`] function to construct a new [`OpenApi`] instance and then
73/// use the fields with mutable access to modify them. This is quite tedious if you are not simply
74/// just changing one thing thus you can also use the [`OpenApi::new`] to use builder to
75/// construct a new [`OpenApi`] object.
76///
77/// See more details at <https://spec.openapis.org/oas/latest.html#openapi-object>.
78#[non_exhaustive]
79#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
80#[serde(rename_all = "camelCase")]
81pub struct OpenApi {
82    /// OpenAPI document version.
83    pub openapi: OpenApiVersion,
84
85    /// Provides metadata about the API.
86    ///
87    /// See more details at <https://spec.openapis.org/oas/latest.html#info-object>.
88    pub info: Info,
89
90    /// List of servers that provides the connectivity information to target servers.
91    ///
92    /// This is implicitly one server with `url` set to `/`.
93    ///
94    /// See more details at <https://spec.openapis.org/oas/latest.html#server-object>.
95    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
96    pub servers: BTreeSet<Server>,
97
98    /// Available paths and operations for the API.
99    ///
100    /// See more details at <https://spec.openapis.org/oas/latest.html#paths-object>.
101    pub paths: Paths,
102
103    /// Holds various reusable schemas for the OpenAPI document.
104    ///
105    /// Few of these elements are security schemas and object schemas.
106    ///
107    /// See more details at <https://spec.openapis.org/oas/latest.html#components-object>.
108    #[serde(skip_serializing_if = "Components::is_empty")]
109    pub components: Components,
110
111    /// Declaration of global security mechanisms that can be used across the API. The individual
112    /// operations can override the declarations. You can use `SecurityRequirement::default()`
113    /// if you wish to make security optional by adding it to the list of securities.
114    ///
115    /// See more details at <https://spec.openapis.org/oas/latest.html#security-requirement-object>.
116    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
117    pub security: BTreeSet<SecurityRequirement>,
118
119    /// List of tags can be used to add additional documentation to matching tags of operations.
120    ///
121    /// See more details at <https://spec.openapis.org/oas/latest.html#tag-object>.
122    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
123    pub tags: BTreeSet<Tag>,
124
125    /// Global additional documentation reference.
126    ///
127    /// See more details at <https://spec.openapis.org/oas/latest.html#external-documentation-object>.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub external_docs: Option<ExternalDocs>,
130
131    /// Schema keyword can be used to override default _`$schema`_ dialect which is by default
132    /// “<https://spec.openapis.org/oas/3.1/dialect/base>”.
133    ///
134    /// All the references and individual files could use their own schema dialect.
135    #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
136    pub schema: String,
137
138    /// Optional extensions "x-something".
139    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
140    pub extensions: PropMap<String, serde_json::Value>,
141}
142
143impl OpenApi {
144    /// Construct a new [`OpenApi`] object.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// # use salvo_oapi::{Info, Paths, OpenApi};
150    /// #
151    /// let openapi = OpenApi::new("pet api", "0.1.0");
152    /// ```
153    pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
154        Self {
155            info: Info::new(title, version),
156            ..Default::default()
157        }
158    }
159    /// Construct a new [`OpenApi`] object.
160    ///
161    /// Function accepts [`Info`] metadata of the API;
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// # use salvo_oapi::{Info, Paths, OpenApi};
167    /// #
168    /// let openapi = OpenApi::new("pet api", "0.1.0");
169    /// ```
170    #[must_use]
171    pub fn with_info(info: Info) -> Self {
172        Self {
173            info,
174            ..Default::default()
175        }
176    }
177
178    /// Converts this [`OpenApi`] to JSON String. This method essentially calls
179    /// [`serde_json::to_string`] method.
180    pub fn to_json(&self) -> Result<String, serde_json::Error> {
181        serde_json::to_string(self)
182    }
183
184    /// Converts this [`OpenApi`] to pretty JSON String. This method essentially calls
185    /// [`serde_json::to_string_pretty`] method.
186    pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
187        serde_json::to_string_pretty(self)
188    }
189
190    cfg_feature! {
191        #![feature ="yaml"]
192        /// Converts this [`OpenApi`] to YAML String. This method essentially calls [`serde_norway::to_string`] method.
193        pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
194            serde_norway::to_string(self)
195        }
196    }
197
198    /// Merge `other` [`OpenApi`] consuming it and resuming it's content.
199    ///
200    /// Merge function will take all `self` nonexistent _`servers`, `paths`, `schemas`, `responses`,
201    /// `security_schemes`, `security_requirements` and `tags`_ from _`other`_ [`OpenApi`].
202    ///
203    /// This function performs a shallow comparison for `paths`, `schemas`, `responses` and
204    /// `security schemes` which means that only _`name`_ and _`path`_ is used for comparison. When
205    /// match occurs the exists item will be overwrite.
206    ///
207    /// For _`servers`_, _`tags`_ and _`security_requirements`_ the whole item will be used for
208    /// comparison.
209    ///
210    /// **Note!** `info`, `openapi` and `external_docs` and `schema` will not be merged.
211    #[must_use]
212    pub fn merge(mut self, mut other: Self) -> Self {
213        self.servers.append(&mut other.servers);
214        self.paths.append(&mut other.paths);
215        self.components.append(&mut other.components);
216        self.security.append(&mut other.security);
217        self.tags.append(&mut other.tags);
218        self
219    }
220
221    /// Add [`Info`] metadata of the API.
222    #[must_use]
223    pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
224        self.info = info.into();
225        self
226    }
227
228    /// Add iterator of [`Server`]s to configure target servers.
229    #[must_use]
230    pub fn servers<S: IntoIterator<Item = Server>>(mut self, servers: S) -> Self {
231        self.servers = servers.into_iter().collect();
232        self
233    }
234    /// Add [`Server`] to configure operations and endpoints of the API and returns `Self`.
235    #[must_use]
236    pub fn add_server<S>(mut self, server: S) -> Self
237    where
238        S: Into<Server>,
239    {
240        self.servers.insert(server.into());
241        self
242    }
243
244    /// Set paths to configure operations and endpoints of the API.
245    #[must_use]
246    pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
247        self.paths = paths.into();
248        self
249    }
250    /// Add [`PathItem`] to configure operations and endpoints of the API and returns `Self`.
251    #[must_use]
252    pub fn add_path<P, I>(mut self, path: P, item: I) -> Self
253    where
254        P: Into<String>,
255        I: Into<PathItem>,
256    {
257        self.paths.insert(path.into(), item.into());
258        self
259    }
260
261    /// Add [`Components`] to configure reusable schemas.
262    #[must_use]
263    pub fn components(mut self, components: impl Into<Components>) -> Self {
264        self.components = components.into();
265        self
266    }
267
268    /// Add iterator of [`SecurityRequirement`]s that are globally available for all operations.
269    #[must_use]
270    pub fn security<S: IntoIterator<Item = SecurityRequirement>>(mut self, security: S) -> Self {
271        self.security = security.into_iter().collect();
272        self
273    }
274
275    /// Add [`SecurityScheme`] to [`Components`] and returns `Self`.
276    ///
277    /// Accepts two arguments where first is the name of the [`SecurityScheme`]. This is later when
278    /// referenced by [`SecurityRequirement`][requirement]s. Second parameter is the
279    /// [`SecurityScheme`].
280    ///
281    /// [requirement]: crate::SecurityRequirement
282    #[must_use]
283    pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
284        mut self,
285        name: N,
286        security_scheme: S,
287    ) -> Self {
288        self.components
289            .security_schemes
290            .insert(name.into(), security_scheme.into());
291
292        self
293    }
294
295    /// Add iterator of [`SecurityScheme`]s to [`Components`].
296    ///
297    /// Accepts two arguments where first is the name of the [`SecurityScheme`]. This is later when
298    /// referenced by [`SecurityRequirement`][requirement]s. Second parameter is the
299    /// [`SecurityScheme`].
300    ///
301    /// [requirement]: crate::SecurityRequirement
302    #[must_use]
303    pub fn extend_security_schemes<
304        I: IntoIterator<Item = (N, S)>,
305        N: Into<String>,
306        S: Into<SecurityScheme>,
307    >(
308        mut self,
309        schemas: I,
310    ) -> Self {
311        self.components.security_schemes.extend(
312            schemas
313                .into_iter()
314                .map(|(name, item)| (name.into(), item.into())),
315        );
316        self
317    }
318
319    /// Add [`Schema`] to [`Components`] and returns `Self`.
320    ///
321    /// Accepts two arguments where first is name of the schema and second is the schema itself.
322    #[must_use]
323    pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
324        mut self,
325        name: S,
326        schema: I,
327    ) -> Self {
328        self.components.schemas.insert(name, schema);
329        self
330    }
331
332    /// Add [`Schema`]s from iterator.
333    ///
334    /// # Examples
335    /// ```
336    /// # use salvo_oapi::{OpenApi, Object, BasicType, Schema};
337    /// OpenApi::new("api", "0.0.1").extend_schemas([(
338    ///     "Pet",
339    ///     Schema::from(
340    ///         Object::new()
341    ///             .property("name", Object::new().schema_type(BasicType::String))
342    ///             .required("name"),
343    ///     ),
344    /// )]);
345    /// ```
346    #[must_use]
347    pub fn extend_schemas<I, C, S>(mut self, schemas: I) -> Self
348    where
349        I: IntoIterator<Item = (S, C)>,
350        C: Into<RefOr<Schema>>,
351        S: Into<String>,
352    {
353        self.components.schemas.extend(
354            schemas
355                .into_iter()
356                .map(|(name, schema)| (name.into(), schema.into())),
357        );
358        self
359    }
360
361    /// Add a new response and returns `self`.
362    #[must_use]
363    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
364        mut self,
365        name: S,
366        response: R,
367    ) -> Self {
368        self.components
369            .responses
370            .insert(name.into(), response.into());
371        self
372    }
373
374    /// Extends responses with the contents of an iterator.
375    #[must_use]
376    pub fn extend_responses<
377        I: IntoIterator<Item = (S, R)>,
378        S: Into<String>,
379        R: Into<RefOr<Response>>,
380    >(
381        mut self,
382        responses: I,
383    ) -> Self {
384        self.components.responses.extend(
385            responses
386                .into_iter()
387                .map(|(name, response)| (name.into(), response.into())),
388        );
389        self
390    }
391
392    /// Add iterator of [`Tag`]s to add additional documentation for **operations** tags.
393    #[must_use]
394    pub fn tags<I, T>(mut self, tags: I) -> Self
395    where
396        I: IntoIterator<Item = T>,
397        T: Into<Tag>,
398    {
399        self.tags = tags.into_iter().map(Into::into).collect();
400        self
401    }
402
403    /// Add [`ExternalDocs`] for referring additional documentation.
404    #[must_use]
405    pub fn external_docs(mut self, external_docs: ExternalDocs) -> Self {
406        self.external_docs = Some(external_docs);
407        self
408    }
409
410    /// Override default `$schema` dialect for the Open API doc.
411    ///
412    /// # Examples
413    ///
414    /// _**Override default schema dialect.**_
415    /// ```rust
416    /// # use salvo_oapi::OpenApi;
417    /// let _ = OpenApi::new("openapi", "0.1.0").schema("http://json-schema.org/draft-07/schema#");
418    /// ```
419    #[must_use]
420    pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
421        self.schema = schema.into();
422        self
423    }
424
425    /// Add openapi extension (`x-something`) for [`OpenApi`].
426    #[must_use]
427    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
428        self.extensions.insert(key.into(), value);
429        self
430    }
431
432    /// Consusmes the [`OpenApi`] and returns [`Router`] with the [`OpenApi`] as handler.
433    pub fn into_router(self, path: impl Into<String>) -> Router {
434        Router::with_path(path.into()).goal(self)
435    }
436
437    /// Consusmes the [`OpenApi`] and information from a [`Router`].
438    #[must_use]
439    pub fn merge_router(self, router: &Router) -> Self {
440        self.merge_router_with_base(router, "/")
441    }
442
443    /// Consusmes the [`OpenApi`] and information from a [`Router`] with base path.
444    #[must_use]
445    pub fn merge_router_with_base(mut self, router: &Router, base: impl AsRef<str>) -> Self {
446        let mut node = NormNode::new(router, Default::default());
447        self.merge_norm_node(&mut node, base.as_ref());
448        self
449    }
450
451    fn merge_norm_node(&mut self, node: &mut NormNode, base_path: &str) {
452        fn join_path(a: &str, b: &str) -> String {
453            if a.is_empty() {
454                b.to_owned()
455            } else if b.is_empty() {
456                a.to_owned()
457            } else {
458                format!("{}/{}", a.trim_end_matches('/'), b.trim_start_matches('/'))
459            }
460        }
461
462        let path = join_path(base_path, node.path.as_deref().unwrap_or_default());
463        let path_parameter_names = PATH_PARAMETER_NAME_REGEX
464            .captures_iter(&path)
465            .filter_map(|captures| {
466                captures
467                    .iter()
468                    .skip(1)
469                    .map(|capture| {
470                        capture
471                            .expect("Regex captures should not be None.")
472                            .as_str()
473                            .to_owned()
474                    })
475                    .next()
476            })
477            .collect::<Vec<_>>();
478
479        if let Some(handler_type_id) = &node.handler_type_id
480            && let Some(creator) = crate::EndpointRegistry::find(handler_type_id)
481        {
482            let Endpoint {
483                mut operation,
484                mut components,
485            } = (creator)();
486            operation.tags.extend(node.metadata.tags.iter().cloned());
487            operation
488                .securities
489                .extend(node.metadata.securities.iter().cloned());
490            let methods = if let Some(method) = &node.method {
491                vec![*method]
492            } else {
493                vec![
494                    PathItemType::Get,
495                    PathItemType::Post,
496                    PathItemType::Put,
497                    PathItemType::Patch,
498                ]
499            };
500            let not_exist_parameters = operation
501                .parameters
502                .0
503                .iter()
504                .filter(|p| {
505                    p.parameter_in == ParameterIn::Path && !path_parameter_names.contains(&p.name)
506                })
507                .map(|p| &p.name)
508                .collect::<Vec<_>>();
509            if !not_exist_parameters.is_empty() {
510                tracing::warn!(parameters = ?not_exist_parameters, path, handler_name = node.handler_type_name, "information for not exist parameters");
511            }
512            #[cfg(debug_assertions)]
513            {
514                let meta_not_exist_parameters = path_parameter_names
515                    .iter()
516                    .filter(|name| {
517                        !name.starts_with('*')
518                            && !operation.parameters.0.iter().any(|parameter| {
519                                parameter.name == **name
520                                    && parameter.parameter_in == ParameterIn::Path
521                            })
522                    })
523                    .collect::<Vec<_>>();
524
525                if !meta_not_exist_parameters.is_empty() {
526                    tracing::warn!(parameters = ?meta_not_exist_parameters, path, handler_name = node.handler_type_name, "parameters information not provided");
527                }
528            }
529            let path_item = self.paths.entry(path.clone()).or_default();
530            for method in methods {
531                if path_item.operations.contains_key(&method) {
532                    tracing::warn!(
533                        "path `{}` already contains operation for method `{:?}`",
534                        path,
535                        method
536                    );
537                } else {
538                    path_item.operations.insert(method, operation.clone());
539                }
540            }
541            self.components.append(&mut components);
542        }
543
544        for child in &mut node.children {
545            self.merge_norm_node(child, &path);
546        }
547    }
548}
549
550#[async_trait]
551impl Handler for OpenApi {
552    async fn handle(
553        &self,
554        req: &mut salvo_core::Request,
555        _depot: &mut Depot,
556        res: &mut salvo_core::Response,
557        _ctrl: &mut FlowCtrl,
558    ) {
559        let pretty = req
560            .queries()
561            .get("pretty")
562            .map(|v| &**v != "false")
563            .unwrap_or(false);
564        let content = if pretty {
565            self.to_pretty_json().unwrap_or_default()
566        } else {
567            self.to_json().unwrap_or_default()
568        };
569        res.render(writing::Text::Json(&content));
570    }
571}
572/// Represents available [OpenAPI versions][version].
573///
574/// [version]: <https://spec.openapis.org/oas/latest.html#versions>
575#[derive(Serialize, Clone, PartialEq, Eq, Default, Debug)]
576pub enum OpenApiVersion {
577    /// Will serialize to `3.1.0` the latest released OpenAPI version.
578    #[serde(rename = "3.1.0")]
579    #[default]
580    Version3_1,
581}
582
583impl<'de> Deserialize<'de> for OpenApiVersion {
584    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
585    where
586        D: Deserializer<'de>,
587    {
588        struct VersionVisitor;
589
590        impl Visitor<'_> for VersionVisitor {
591            type Value = OpenApiVersion;
592
593            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
594                formatter.write_str("a version string in 3.1.x format")
595            }
596
597            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
598            where
599                E: Error,
600            {
601                self.visit_string(v.to_owned())
602            }
603
604            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
605            where
606                E: Error,
607            {
608                let version = v
609                    .split('.')
610                    .flat_map(|digit| digit.parse::<i8>())
611                    .collect::<Vec<_>>();
612
613                if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
614                    Ok(OpenApiVersion::Version3_1)
615                } else {
616                    let expected: &dyn Expected = &"3.1.0";
617                    Err(Error::invalid_value(
618                        serde::de::Unexpected::Str(&v),
619                        expected,
620                    ))
621                }
622            }
623        }
624
625        deserializer.deserialize_string(VersionVisitor)
626    }
627}
628
629/// Value used to indicate whether reusable schema, parameter or operation is deprecated.
630///
631/// The value will serialize to boolean.
632#[derive(PartialEq, Eq, Clone, Debug)]
633pub enum Deprecated {
634    /// Is deprecated.
635    True,
636    /// Is not deprecated.
637    False,
638}
639impl From<bool> for Deprecated {
640    fn from(b: bool) -> Self {
641        if b { Self::True } else { Self::False }
642    }
643}
644
645impl Serialize for Deprecated {
646    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
647    where
648        S: Serializer,
649    {
650        serializer.serialize_bool(matches!(self, Self::True))
651    }
652}
653
654impl<'de> Deserialize<'de> for Deprecated {
655    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
656    where
657        D: serde::Deserializer<'de>,
658    {
659        struct BoolVisitor;
660        impl Visitor<'_> for BoolVisitor {
661            type Value = Deprecated;
662
663            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
664                formatter.write_str("a bool true or false")
665            }
666
667            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
668            where
669                E: serde::de::Error,
670            {
671                match v {
672                    true => Ok(Deprecated::True),
673                    false => Ok(Deprecated::False),
674                }
675            }
676        }
677        deserializer.deserialize_bool(BoolVisitor)
678    }
679}
680
681/// Value used to indicate whether parameter or property is required.
682///
683/// The value will serialize to boolean.
684#[derive(PartialEq, Eq, Default, Clone, Debug)]
685pub enum Required {
686    /// Is required.
687    True,
688    /// Is not required.
689    False,
690    /// This value is not set, it will treat as `False` when serialize to boolean.
691    #[default]
692    Unset,
693}
694
695impl From<bool> for Required {
696    fn from(value: bool) -> Self {
697        if value { Self::True } else { Self::False }
698    }
699}
700
701impl Serialize for Required {
702    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
703    where
704        S: Serializer,
705    {
706        serializer.serialize_bool(matches!(self, Self::True))
707    }
708}
709
710impl<'de> Deserialize<'de> for Required {
711    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
712    where
713        D: serde::Deserializer<'de>,
714    {
715        struct BoolVisitor;
716        impl Visitor<'_> for BoolVisitor {
717            type Value = Required;
718
719            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
720                formatter.write_str("a bool true or false")
721            }
722
723            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
724            where
725                E: serde::de::Error,
726            {
727                match v {
728                    true => Ok(Required::True),
729                    false => Ok(Required::False),
730                }
731            }
732        }
733        deserializer.deserialize_bool(BoolVisitor)
734    }
735}
736
737/// A [`Ref`] or some other type `T`.
738///
739/// Typically used in combination with [`Components`] and is an union type between [`Ref`] and any
740/// other given type such as [`Schema`] or [`Response`].
741#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
742#[serde(untagged)]
743pub enum RefOr<T> {
744    /// A [`Ref`] to a reusable component.
745    Ref(schema::Ref),
746    /// Some other type `T`.
747    Type(T),
748}
749
750#[cfg(test)]
751mod tests {
752    use std::fmt::Debug;
753    use std::str::FromStr;
754
755    use bytes::Bytes;
756    use salvo_core::http::ResBody;
757    use salvo_core::prelude::*;
758    use serde_json::{Value, json};
759
760    use super::response::Response;
761    use super::*;
762    use crate::ToSchema;
763    use crate::extract::*;
764    use crate::security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme};
765    use crate::server::Server;
766
767    #[test]
768    fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
769        assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
770        Ok(())
771    }
772
773    #[test]
774    fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
775        let raw_json = r#"{
776            "openapi": "3.1.0",
777            "info": {
778              "title": "My api",
779              "description": "My api description",
780              "license": {
781                "name": "MIT",
782                "url": "http://mit.licence"
783              },
784              "version": "1.0.0",
785              "contact": {},
786              "termsOfService": "terms of service"
787            },
788            "paths": {}
789          }"#;
790        let doc: OpenApi = OpenApi::with_info(
791            Info::default()
792                .description("My api description")
793                .license(License::new("MIT").url("http://mit.licence"))
794                .title("My api")
795                .version("1.0.0")
796                .terms_of_service("terms of service")
797                .contact(Contact::default()),
798        );
799        let serialized = doc.to_json()?;
800
801        assert_eq!(
802            Value::from_str(&serialized)?,
803            Value::from_str(raw_json)?,
804            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
805        );
806        Ok(())
807    }
808
809    #[test]
810    fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
811        let doc = OpenApi::new("My big api", "1.1.0").paths(
812            Paths::new()
813                .path(
814                    "/api/v1/users",
815                    PathItem::new(
816                        PathItemType::Get,
817                        Operation::new().add_response("200", Response::new("Get users list")),
818                    ),
819                )
820                .path(
821                    "/api/v1/users",
822                    PathItem::new(
823                        PathItemType::Post,
824                        Operation::new().add_response("200", Response::new("Post new user")),
825                    ),
826                )
827                .path(
828                    "/api/v1/users/{id}",
829                    PathItem::new(
830                        PathItemType::Get,
831                        Operation::new().add_response("200", Response::new("Get user by id")),
832                    ),
833                ),
834        );
835
836        let serialized = doc.to_json()?;
837        let expected = r#"
838        {
839            "openapi": "3.1.0",
840            "info": {
841              "title": "My big api",
842              "version": "1.1.0"
843            },
844            "paths": {
845              "/api/v1/users": {
846                "get": {
847                  "responses": {
848                    "200": {
849                      "description": "Get users list"
850                    }
851                  }
852                },
853                "post": {
854                  "responses": {
855                    "200": {
856                      "description": "Post new user"
857                    }
858                  }
859                }
860              },
861              "/api/v1/users/{id}": {
862                "get": {
863                  "responses": {
864                    "200": {
865                      "description": "Get user by id"
866                    }
867                  }
868                }
869              }
870            }
871          }
872        "#
873        .replace("\r\n", "\n");
874
875        assert_eq!(
876            Value::from_str(&serialized)?,
877            Value::from_str(&expected)?,
878            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
879        );
880        Ok(())
881    }
882
883    #[test]
884    fn merge_2_openapi_documents() {
885        let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
886            "/api/v1/user",
887            PathItem::new(
888                PathItemType::Get,
889                Operation::new().add_response("200", Response::new("This will not get added")),
890            ),
891        ));
892
893        let api_2 = OpenApi::new("Api", "v2")
894            .paths(
895                Paths::new()
896                    .path(
897                        "/api/v1/user",
898                        PathItem::new(
899                            PathItemType::Get,
900                            Operation::new().add_response("200", Response::new("Get user success")),
901                        ),
902                    )
903                    .path(
904                        "/ap/v2/user",
905                        PathItem::new(
906                            PathItemType::Get,
907                            Operation::new()
908                                .add_response("200", Response::new("Get user success 2")),
909                        ),
910                    )
911                    .path(
912                        "/api/v2/user",
913                        PathItem::new(
914                            PathItemType::Post,
915                            Operation::new().add_response("200", Response::new("Get user success")),
916                        ),
917                    ),
918            )
919            .components(
920                Components::new().add_schema(
921                    "User2",
922                    Object::new()
923                        .schema_type(BasicType::Object)
924                        .property("name", Object::new().schema_type(BasicType::String)),
925                ),
926            );
927
928        api_1 = api_1.merge(api_2);
929        let value = serde_json::to_value(&api_1).unwrap();
930
931        assert_eq!(
932            value,
933            json!(
934                {
935                  "openapi": "3.1.0",
936                  "info": {
937                    "title": "Api",
938                    "version": "v1"
939                  },
940                  "paths": {
941                    "/ap/v2/user": {
942                      "get": {
943                        "responses": {
944                          "200": {
945                            "description": "Get user success 2"
946                          }
947                        }
948                      }
949                    },
950                    "/api/v1/user": {
951                      "get": {
952                        "responses": {
953                          "200": {
954                            "description": "Get user success"
955                          }
956                        }
957                      }
958                    },
959                    "/api/v2/user": {
960                      "post": {
961                        "responses": {
962                          "200": {
963                            "description": "Get user success"
964                          }
965                        }
966                      }
967                    }
968                  },
969                  "components": {
970                    "schemas": {
971                      "User2": {
972                        "type": "object",
973                        "properties": {
974                          "name": {
975                            "type": "string"
976                          }
977                        }
978                      }
979                    }
980                  }
981                }
982            )
983        )
984    }
985
986    #[test]
987    fn test_simple_document_with_security() {
988        #[derive(Deserialize, Serialize, ToSchema)]
989        #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
990        struct Pet {
991            id: u64,
992            name: String,
993            age: Option<i32>,
994        }
995
996        /// Get pet by id
997        ///
998        /// Get pet from database by pet database id
999        #[salvo_oapi::endpoint(
1000            responses(
1001                (status_code = 200, description = "Pet found successfully"),
1002                (status_code = 404, description = "Pet was not found")
1003            ),
1004            parameters(
1005                ("id", description = "Pet database id to get Pet for"),
1006            ),
1007            security(
1008                (),
1009                ("my_auth" = ["read:items", "edit:items"]),
1010                ("token_jwt" = []),
1011                ("api_key1" = [], "api_key2" = []),
1012            )
1013        )]
1014        pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1015            let pet = Pet {
1016                id: pet_id.into_inner(),
1017                age: None,
1018                name: "lightning".to_owned(),
1019            };
1020            Json(pet)
1021        }
1022
1023        let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1024            Server::new("/api/bar/")
1025                .description("this is description of the server")
1026                .add_variable(
1027                    "username",
1028                    ServerVariable::new()
1029                        .default_value("the_user")
1030                        .description("this is user"),
1031                ),
1032        );
1033        doc.components.security_schemes.insert(
1034            "token_jwt".into(),
1035            SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1036        );
1037
1038        let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1039        let doc = doc.merge_router(&router);
1040
1041        assert_eq!(
1042            Value::from_str(
1043                r#"{
1044                    "openapi": "3.1.0",
1045                    "info": {
1046                       "title": "my application",
1047                       "version": "0.1.0"
1048                    },
1049                    "servers": [
1050                       {
1051                          "url": "/api/bar/",
1052                          "description": "this is description of the server",
1053                          "variables": {
1054                             "username": {
1055                                "default": "the_user",
1056                                "description": "this is user"
1057                             }
1058                          }
1059                       }
1060                    ],
1061                    "paths": {
1062                       "/pets/{id}": {
1063                          "get": {
1064                             "summary": "Get pet by id",
1065                             "description": "Get pet from database by pet database id",
1066                             "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1067                             "parameters": [
1068                                {
1069                                   "name": "pet_id",
1070                                   "in": "path",
1071                                   "description": "Get parameter `pet_id` from request url path.",
1072                                   "required": true,
1073                                   "schema": {
1074                                      "type": "integer",
1075                                      "format": "uint64",
1076                                      "minimum": 0.0
1077                                   }
1078                                },
1079                                {
1080                                   "name": "id",
1081                                   "in": "path",
1082                                   "description": "Pet database id to get Pet for",
1083                                   "required": false
1084                                }
1085                             ],
1086                             "responses": {
1087                                "200": {
1088                                   "description": "Pet found successfully"
1089                                },
1090                                "404": {
1091                                   "description": "Pet was not found"
1092                                }
1093                             },
1094                             "security": [
1095                                {},
1096                                {
1097                                   "my_auth": [
1098                                      "read:items",
1099                                      "edit:items"
1100                                   ]
1101                                },
1102                                {
1103                                   "token_jwt": []
1104                                },
1105                                {
1106                                    "api_key1": [],
1107                                    "api_key2": []
1108                                }
1109                             ]
1110                          }
1111                       }
1112                    },
1113                    "components": {
1114                       "schemas": {
1115                          "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1116                             "type": "object",
1117                             "required": [
1118                                "id",
1119                                "name"
1120                             ],
1121                             "properties": {
1122                                "age": {
1123                                   "type": ["integer", "null"],
1124                                   "format": "int32"
1125                                },
1126                                "id": {
1127                                   "type": "integer",
1128                                   "format": "uint64",
1129                                   "minimum": 0.0
1130                                },
1131                                "name": {
1132                                   "type": "string"
1133                                }
1134                             },
1135                             "examples": [{
1136                                "id": 1,
1137                                "name": "bob the cat"
1138                             }]
1139                          }
1140                       },
1141                       "securitySchemes": {
1142                          "token_jwt": {
1143                             "type": "http",
1144                             "scheme": "bearer",
1145                             "bearerFormat": "JWT"
1146                          }
1147                       }
1148                    }
1149                 }"#
1150            )
1151            .unwrap(),
1152            Value::from_str(&doc.to_json().unwrap()).unwrap()
1153        );
1154    }
1155
1156    #[test]
1157    fn test_build_openapi() {
1158        let _doc = OpenApi::new("pet api", "0.1.0")
1159            .info(Info::new("my pet api", "0.2.0"))
1160            .servers(Servers::new())
1161            .add_path(
1162                "/api/v1",
1163                PathItem::new(PathItemType::Get, Operation::new()),
1164            )
1165            .security([SecurityRequirement::default()])
1166            .add_security_scheme(
1167                "api_key",
1168                SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1169            )
1170            .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1171            .add_schema("example", Schema::object(Object::new()))
1172            .extend_schemas([("", Schema::from(Object::new()))])
1173            .response("200", Response::new("OK"))
1174            .extend_responses([("404", Response::new("Not Found"))])
1175            .tags(["tag1", "tag2"])
1176            .external_docs(ExternalDocs::default())
1177            .into_router("/openapi/doc");
1178    }
1179
1180    #[test]
1181    fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1182        let raw_json = r#"{
1183            "openapi": "3.1.0",
1184            "info": {
1185                "title": "My api",
1186                "description": "My api description",
1187                "license": {
1188                "name": "MIT",
1189                "url": "http://mit.licence"
1190                },
1191                "version": "1.0.0",
1192                "contact": {},
1193                "termsOfService": "terms of service"
1194            },
1195            "paths": {}
1196        }"#;
1197        let doc: OpenApi = OpenApi::with_info(
1198            Info::default()
1199                .description("My api description")
1200                .license(License::new("MIT").url("http://mit.licence"))
1201                .title("My api")
1202                .version("1.0.0")
1203                .terms_of_service("terms of service")
1204                .contact(Contact::default()),
1205        );
1206        let serialized = doc.to_pretty_json()?;
1207
1208        assert_eq!(
1209            Value::from_str(&serialized)?,
1210            Value::from_str(raw_json)?,
1211            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1212        );
1213        Ok(())
1214    }
1215
1216    #[test]
1217    fn test_deprecated_from_bool() {
1218        assert_eq!(Deprecated::True, Deprecated::from(true));
1219        assert_eq!(Deprecated::False, Deprecated::from(false));
1220    }
1221
1222    #[test]
1223    fn test_deprecated_deserialize() {
1224        let deserialize_result = serde_json::from_str::<Deprecated>("true");
1225        assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1226        let deserialize_result = serde_json::from_str::<Deprecated>("false");
1227        assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1228    }
1229
1230    #[test]
1231    fn test_required_from_bool() {
1232        assert_eq!(Required::True, Required::from(true));
1233        assert_eq!(Required::False, Required::from(false));
1234    }
1235
1236    #[test]
1237    fn test_required_deserialize() {
1238        let deserialize_result = serde_json::from_str::<Required>("true");
1239        assert_eq!(deserialize_result.unwrap(), Required::True);
1240        let deserialize_result = serde_json::from_str::<Required>("false");
1241        assert_eq!(deserialize_result.unwrap(), Required::False);
1242    }
1243
1244    #[tokio::test]
1245    async fn test_openapi_handle() {
1246        let doc = OpenApi::new("pet api", "0.1.0");
1247        let mut req = Request::new();
1248        let mut depot = Depot::new();
1249        let mut res = salvo_core::Response::new();
1250        let mut ctrl = FlowCtrl::default();
1251        doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1252
1253        let bytes = match res.body.take() {
1254            ResBody::Once(bytes) => bytes,
1255            _ => Bytes::new(),
1256        };
1257
1258        assert_eq!(
1259            res.content_type()
1260                .expect("content type should exists")
1261                .to_string(),
1262            "application/json; charset=utf-8".to_owned()
1263        );
1264        assert_eq!(
1265            bytes,
1266            Bytes::from_static(
1267                b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1268            )
1269        );
1270    }
1271
1272    #[tokio::test]
1273    async fn test_openapi_handle_pretty() {
1274        let doc = OpenApi::new("pet api", "0.1.0");
1275
1276        let mut req = Request::new();
1277        req.queries_mut()
1278            .insert("pretty".to_owned(), "true".to_owned());
1279
1280        let mut depot = Depot::new();
1281        let mut res = salvo_core::Response::new();
1282        let mut ctrl = FlowCtrl::default();
1283        doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1284
1285        let bytes = match res.body.take() {
1286            ResBody::Once(bytes) => bytes,
1287            _ => Bytes::new(),
1288        };
1289
1290        assert_eq!(
1291            res.content_type()
1292                .expect("content type should exists")
1293                .to_string(),
1294            "application/json; charset=utf-8".to_owned()
1295        );
1296        assert_eq!(
1297            bytes,
1298            Bytes::from_static(b"{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"pet api\",\n    \"version\": \"0.1.0\"\n  },\n  \"paths\": {}\n}")
1299        );
1300    }
1301
1302    #[test]
1303    fn test_openapi_schema_work_with_generics() {
1304        #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1305        #[salvo(schema(name = City))]
1306        pub(crate) struct CityDTO {
1307            #[salvo(schema(rename = "id"))]
1308            pub(crate) id: String,
1309            #[salvo(schema(rename = "name"))]
1310            pub(crate) name: String,
1311        }
1312
1313        #[derive(Serialize, Deserialize, Debug, ToSchema)]
1314        #[salvo(schema(name = Response))]
1315        pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1316            #[salvo(schema(rename = "status"))]
1317            /// status code
1318            pub(crate) status: String,
1319            #[salvo(schema(rename = "msg"))]
1320            /// Status msg
1321            pub(crate) message: String,
1322            #[salvo(schema(rename = "data"))]
1323            /// The data returned
1324            pub(crate) data: T,
1325        }
1326
1327        #[salvo_oapi::endpoint(
1328            operation_id = "get_all_cities",
1329            tags("city"),
1330            status_codes(200, 400, 401, 403, 500)
1331        )]
1332        pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1333            Ok(Json(ApiResponse {
1334                status: "200".to_owned(),
1335                message: "OK".to_owned(),
1336                data: vec![CityDTO {
1337                    id: "1".to_owned(),
1338                    name: "Beijing".to_owned(),
1339                }],
1340            }))
1341        }
1342
1343        let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1344            .add_server(Server::new("/api/bar/").description("this is description of the server"));
1345
1346        let router = Router::with_path("/cities").get(get_all_cities);
1347        let doc = doc.merge_router(&router);
1348
1349        assert_eq!(
1350            json! {{
1351                "openapi": "3.1.0",
1352                "info": {
1353                    "title": "my application",
1354                    "version": "0.1.0"
1355                },
1356                "servers": [
1357                    {
1358                        "url": "/api/bar/",
1359                        "description": "this is description of the server"
1360                    }
1361                ],
1362                "paths": {
1363                    "/cities": {
1364                        "get": {
1365                            "tags": [
1366                                "city"
1367                            ],
1368                            "operationId": "get_all_cities",
1369                            "responses": {
1370                                "200": {
1371                                    "description": "Response with json format data",
1372                                    "content": {
1373                                        "application/json": {
1374                                            "schema": {
1375                                                "$ref": "#/components/schemas/Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>"
1376                                            }
1377                                        }
1378                                    }
1379                                },
1380                                "400": {
1381                                    "description": "The request could not be understood by the server due to malformed syntax.",
1382                                    "content": {
1383                                        "application/json": {
1384                                            "schema": {
1385                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1386                                            }
1387                                        }
1388                                    }
1389                                },
1390                                "401": {
1391                                    "description": "The request requires user authentication.",
1392                                    "content": {
1393                                        "application/json": {
1394                                            "schema": {
1395                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1396                                            }
1397                                        }
1398                                    }
1399                                },
1400                                "403": {
1401                                    "description": "The server refused to authorize the request.",
1402                                    "content": {
1403                                        "application/json": {
1404                                            "schema": {
1405                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1406                                            }
1407                                        }
1408                                    }
1409                                },
1410                                "500": {
1411                                    "description": "The server encountered an internal error while processing this request.",
1412                                    "content": {
1413                                        "application/json": {
1414                                            "schema": {
1415                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1416                                            }
1417                                        }
1418                                    }
1419                                }
1420                            }
1421                        }
1422                    }
1423                },
1424                "components": {
1425                    "schemas": {
1426                        "City": {
1427                            "type": "object",
1428                            "required": [
1429                                "id",
1430                                "name"
1431                            ],
1432                            "properties": {
1433                                "id": {
1434                                    "type": "string"
1435                                },
1436                                "name": {
1437                                    "type": "string"
1438                                }
1439                            }
1440                        },
1441                        "Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>": {
1442                            "type": "object",
1443                            "required": [
1444                                "status",
1445                                "msg",
1446                                "data"
1447                            ],
1448                            "properties": {
1449                                "data": {
1450                                    "type": "array",
1451                                    "items": {
1452                                        "$ref": "#/components/schemas/City"
1453                                    }
1454                                },
1455                                "msg": {
1456                                    "type": "string",
1457                                    "description": "Status msg"
1458                                },
1459                                "status": {
1460                                    "type": "string",
1461                                    "description": "status code"
1462                                }
1463                            }
1464                        },
1465                        "salvo_core.http.errors.status_error.StatusError": {
1466                            "type": "object",
1467                            "required": [
1468                                "code",
1469                                "name",
1470                                "brief",
1471                                "detail"
1472                            ],
1473                            "properties": {
1474                                "brief": {
1475                                    "type": "string"
1476                                },
1477                                "cause": {
1478                                    "type": "string"
1479                                },
1480                                "code": {
1481                                    "type": "integer",
1482                                    "format": "uint16",
1483                                    "minimum": 0.0
1484                                },
1485                                "detail": {
1486                                    "type": "string"
1487                                },
1488                                "name": {
1489                                    "type": "string"
1490                                }
1491                            }
1492                        }
1493                    }
1494                }
1495            }},
1496            Value::from_str(&doc.to_json().unwrap()).unwrap()
1497        );
1498    }
1499}