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