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::{
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
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 serde_json::{Value, json};
757
758    use super::{response::Response, *};
759    use crate::{
760        ToSchema,
761        extract::*,
762        security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme},
763        server::Server,
764    };
765
766    use salvo_core::{http::ResBody, prelude::*};
767
768    #[test]
769    fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
770        assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
771        Ok(())
772    }
773
774    #[test]
775    fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
776        let raw_json = r#"{
777            "openapi": "3.1.0",
778            "info": {
779              "title": "My api",
780              "description": "My api description",
781              "license": {
782                "name": "MIT",
783                "url": "http://mit.licence"
784              },
785              "version": "1.0.0",
786              "contact": {},
787              "termsOfService": "terms of service"
788            },
789            "paths": {}
790          }"#;
791        let doc: OpenApi = OpenApi::with_info(
792            Info::default()
793                .description("My api description")
794                .license(License::new("MIT").url("http://mit.licence"))
795                .title("My api")
796                .version("1.0.0")
797                .terms_of_service("terms of service")
798                .contact(Contact::default()),
799        );
800        let serialized = doc.to_json()?;
801
802        assert_eq!(
803            Value::from_str(&serialized)?,
804            Value::from_str(raw_json)?,
805            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
806        );
807        Ok(())
808    }
809
810    #[test]
811    fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
812        let doc = OpenApi::new("My big api", "1.1.0").paths(
813            Paths::new()
814                .path(
815                    "/api/v1/users",
816                    PathItem::new(
817                        PathItemType::Get,
818                        Operation::new().add_response("200", Response::new("Get users list")),
819                    ),
820                )
821                .path(
822                    "/api/v1/users",
823                    PathItem::new(
824                        PathItemType::Post,
825                        Operation::new().add_response("200", Response::new("Post new user")),
826                    ),
827                )
828                .path(
829                    "/api/v1/users/{id}",
830                    PathItem::new(
831                        PathItemType::Get,
832                        Operation::new().add_response("200", Response::new("Get user by id")),
833                    ),
834                ),
835        );
836
837        let serialized = doc.to_json()?;
838        let expected = r#"
839        {
840            "openapi": "3.1.0",
841            "info": {
842              "title": "My big api",
843              "version": "1.1.0"
844            },
845            "paths": {
846              "/api/v1/users": {
847                "get": {
848                  "responses": {
849                    "200": {
850                      "description": "Get users list"
851                    }
852                  }
853                },
854                "post": {
855                  "responses": {
856                    "200": {
857                      "description": "Post new user"
858                    }
859                  }
860                }
861              },
862              "/api/v1/users/{id}": {
863                "get": {
864                  "responses": {
865                    "200": {
866                      "description": "Get user by id"
867                    }
868                  }
869                }
870              }
871            }
872          }
873        "#
874        .replace("\r\n", "\n");
875
876        assert_eq!(
877            Value::from_str(&serialized)?,
878            Value::from_str(&expected)?,
879            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
880        );
881        Ok(())
882    }
883
884    #[test]
885    fn merge_2_openapi_documents() {
886        let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
887            "/api/v1/user",
888            PathItem::new(
889                PathItemType::Get,
890                Operation::new().add_response("200", Response::new("This will not get added")),
891            ),
892        ));
893
894        let api_2 = OpenApi::new("Api", "v2")
895            .paths(
896                Paths::new()
897                    .path(
898                        "/api/v1/user",
899                        PathItem::new(
900                            PathItemType::Get,
901                            Operation::new().add_response("200", Response::new("Get user success")),
902                        ),
903                    )
904                    .path(
905                        "/ap/v2/user",
906                        PathItem::new(
907                            PathItemType::Get,
908                            Operation::new()
909                                .add_response("200", Response::new("Get user success 2")),
910                        ),
911                    )
912                    .path(
913                        "/api/v2/user",
914                        PathItem::new(
915                            PathItemType::Post,
916                            Operation::new().add_response("200", Response::new("Get user success")),
917                        ),
918                    ),
919            )
920            .components(
921                Components::new().add_schema(
922                    "User2",
923                    Object::new()
924                        .schema_type(BasicType::Object)
925                        .property("name", Object::new().schema_type(BasicType::String)),
926                ),
927            );
928
929        api_1 = api_1.merge(api_2);
930        let value = serde_json::to_value(&api_1).unwrap();
931
932        assert_eq!(
933            value,
934            json!(
935                {
936                  "openapi": "3.1.0",
937                  "info": {
938                    "title": "Api",
939                    "version": "v1"
940                  },
941                  "paths": {
942                    "/ap/v2/user": {
943                      "get": {
944                        "responses": {
945                          "200": {
946                            "description": "Get user success 2"
947                          }
948                        }
949                      }
950                    },
951                    "/api/v1/user": {
952                      "get": {
953                        "responses": {
954                          "200": {
955                            "description": "Get user success"
956                          }
957                        }
958                      }
959                    },
960                    "/api/v2/user": {
961                      "post": {
962                        "responses": {
963                          "200": {
964                            "description": "Get user success"
965                          }
966                        }
967                      }
968                    }
969                  },
970                  "components": {
971                    "schemas": {
972                      "User2": {
973                        "type": "object",
974                        "properties": {
975                          "name": {
976                            "type": "string"
977                          }
978                        }
979                      }
980                    }
981                  }
982                }
983            )
984        )
985    }
986
987    #[test]
988    fn test_simple_document_with_security() {
989        #[derive(Deserialize, Serialize, ToSchema)]
990        #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
991        struct Pet {
992            id: u64,
993            name: String,
994            age: Option<i32>,
995        }
996
997        /// Get pet by id
998        ///
999        /// Get pet from database by pet database id
1000        #[salvo_oapi::endpoint(
1001            responses(
1002                (status_code = 200, description = "Pet found successfully"),
1003                (status_code = 404, description = "Pet was not found")
1004            ),
1005            parameters(
1006                ("id", description = "Pet database id to get Pet for"),
1007            ),
1008            security(
1009                (),
1010                ("my_auth" = ["read:items", "edit:items"]),
1011                ("token_jwt" = []),
1012                ("api_key1" = [], "api_key2" = []),
1013            )
1014        )]
1015        pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1016            let pet = Pet {
1017                id: pet_id.into_inner(),
1018                age: None,
1019                name: "lightning".to_owned(),
1020            };
1021            Json(pet)
1022        }
1023
1024        let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1025            Server::new("/api/bar/")
1026                .description("this is description of the server")
1027                .add_variable(
1028                    "username",
1029                    ServerVariable::new()
1030                        .default_value("the_user")
1031                        .description("this is user"),
1032                ),
1033        );
1034        doc.components.security_schemes.insert(
1035            "token_jwt".into(),
1036            SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1037        );
1038
1039        let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1040        let doc = doc.merge_router(&router);
1041
1042        assert_eq!(
1043            Value::from_str(
1044                r#"{
1045                    "openapi": "3.1.0",
1046                    "info": {
1047                       "title": "my application",
1048                       "version": "0.1.0"
1049                    },
1050                    "servers": [
1051                       {
1052                          "url": "/api/bar/",
1053                          "description": "this is description of the server",
1054                          "variables": {
1055                             "username": {
1056                                "default": "the_user",
1057                                "description": "this is user"
1058                             }
1059                          }
1060                       }
1061                    ],
1062                    "paths": {
1063                       "/pets/{id}": {
1064                          "get": {
1065                             "summary": "Get pet by id",
1066                             "description": "Get pet from database by pet database id",
1067                             "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1068                             "parameters": [
1069                                {
1070                                   "name": "pet_id",
1071                                   "in": "path",
1072                                   "description": "Get parameter `pet_id` from request url path.",
1073                                   "required": true,
1074                                   "schema": {
1075                                      "type": "integer",
1076                                      "format": "uint64",
1077                                      "minimum": 0.0
1078                                   }
1079                                },
1080                                {
1081                                   "name": "id",
1082                                   "in": "path",
1083                                   "description": "Pet database id to get Pet for",
1084                                   "required": false
1085                                }
1086                             ],
1087                             "responses": {
1088                                "200": {
1089                                   "description": "Pet found successfully"
1090                                },
1091                                "404": {
1092                                   "description": "Pet was not found"
1093                                }
1094                             },
1095                             "security": [
1096                                {},
1097                                {
1098                                   "my_auth": [
1099                                      "read:items",
1100                                      "edit:items"
1101                                   ]
1102                                },
1103                                {
1104                                   "token_jwt": []
1105                                },
1106                                {
1107                                    "api_key1": [],
1108                                    "api_key2": []
1109                                }
1110                             ]
1111                          }
1112                       }
1113                    },
1114                    "components": {
1115                       "schemas": {
1116                          "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1117                             "type": "object",
1118                             "required": [
1119                                "id",
1120                                "name"
1121                             ],
1122                             "properties": {
1123                                "age": {
1124                                   "type": ["integer", "null"],
1125                                   "format": "int32"
1126                                },
1127                                "id": {
1128                                   "type": "integer",
1129                                   "format": "uint64",
1130                                   "minimum": 0.0
1131                                },
1132                                "name": {
1133                                   "type": "string"
1134                                }
1135                             },
1136                             "examples": [{
1137                                "id": 1,
1138                                "name": "bob the cat"
1139                             }]
1140                          }
1141                       },
1142                       "securitySchemes": {
1143                          "token_jwt": {
1144                             "type": "http",
1145                             "scheme": "bearer",
1146                             "bearerFormat": "JWT"
1147                          }
1148                       }
1149                    }
1150                 }"#
1151            )
1152            .unwrap(),
1153            Value::from_str(&doc.to_json().unwrap()).unwrap()
1154        );
1155    }
1156
1157    #[test]
1158    fn test_build_openapi() {
1159        let _doc = OpenApi::new("pet api", "0.1.0")
1160            .info(Info::new("my pet api", "0.2.0"))
1161            .servers(Servers::new())
1162            .add_path(
1163                "/api/v1",
1164                PathItem::new(PathItemType::Get, Operation::new()),
1165            )
1166            .security([SecurityRequirement::default()])
1167            .add_security_scheme(
1168                "api_key",
1169                SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1170            )
1171            .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1172            .add_schema("example", Schema::object(Object::new()))
1173            .extend_schemas([("", Schema::from(Object::new()))])
1174            .response("200", Response::new("OK"))
1175            .extend_responses([("404", Response::new("Not Found"))])
1176            .tags(["tag1", "tag2"])
1177            .external_docs(ExternalDocs::default())
1178            .into_router("/openapi/doc");
1179    }
1180
1181    #[test]
1182    fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1183        let raw_json = r#"{
1184            "openapi": "3.1.0",
1185            "info": {
1186                "title": "My api",
1187                "description": "My api description",
1188                "license": {
1189                "name": "MIT",
1190                "url": "http://mit.licence"
1191                },
1192                "version": "1.0.0",
1193                "contact": {},
1194                "termsOfService": "terms of service"
1195            },
1196            "paths": {}
1197        }"#;
1198        let doc: OpenApi = OpenApi::with_info(
1199            Info::default()
1200                .description("My api description")
1201                .license(License::new("MIT").url("http://mit.licence"))
1202                .title("My api")
1203                .version("1.0.0")
1204                .terms_of_service("terms of service")
1205                .contact(Contact::default()),
1206        );
1207        let serialized = doc.to_pretty_json()?;
1208
1209        assert_eq!(
1210            Value::from_str(&serialized)?,
1211            Value::from_str(raw_json)?,
1212            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1213        );
1214        Ok(())
1215    }
1216
1217    #[test]
1218    fn test_deprecated_from_bool() {
1219        assert_eq!(Deprecated::True, Deprecated::from(true));
1220        assert_eq!(Deprecated::False, Deprecated::from(false));
1221    }
1222
1223    #[test]
1224    fn test_deprecated_deserialize() {
1225        let deserialize_result = serde_json::from_str::<Deprecated>("true");
1226        assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1227        let deserialize_result = serde_json::from_str::<Deprecated>("false");
1228        assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1229    }
1230
1231    #[test]
1232    fn test_required_from_bool() {
1233        assert_eq!(Required::True, Required::from(true));
1234        assert_eq!(Required::False, Required::from(false));
1235    }
1236
1237    #[test]
1238    fn test_required_deserialize() {
1239        let deserialize_result = serde_json::from_str::<Required>("true");
1240        assert_eq!(deserialize_result.unwrap(), Required::True);
1241        let deserialize_result = serde_json::from_str::<Required>("false");
1242        assert_eq!(deserialize_result.unwrap(), Required::False);
1243    }
1244
1245    #[tokio::test]
1246    async fn test_openapi_handle() {
1247        let doc = OpenApi::new("pet api", "0.1.0");
1248        let mut req = Request::new();
1249        let mut depot = Depot::new();
1250        let mut res = salvo_core::Response::new();
1251        let mut ctrl = FlowCtrl::default();
1252        doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1253
1254        let bytes = match res.body.take() {
1255            ResBody::Once(bytes) => bytes,
1256            _ => Bytes::new(),
1257        };
1258
1259        assert_eq!(
1260            res.content_type()
1261                .expect("content type should exists")
1262                .to_string(),
1263            "application/json; charset=utf-8".to_owned()
1264        );
1265        assert_eq!(
1266            bytes,
1267            Bytes::from_static(
1268                b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1269            )
1270        );
1271    }
1272
1273    #[tokio::test]
1274    async fn test_openapi_handle_pretty() {
1275        let doc = OpenApi::new("pet api", "0.1.0");
1276
1277        let mut req = Request::new();
1278        req.queries_mut()
1279            .insert("pretty".to_owned(), "true".to_owned());
1280
1281        let mut depot = Depot::new();
1282        let mut res = salvo_core::Response::new();
1283        let mut ctrl = FlowCtrl::default();
1284        doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1285
1286        let bytes = match res.body.take() {
1287            ResBody::Once(bytes) => bytes,
1288            _ => Bytes::new(),
1289        };
1290
1291        assert_eq!(
1292            res.content_type()
1293                .expect("content type should exists")
1294                .to_string(),
1295            "application/json; charset=utf-8".to_owned()
1296        );
1297        assert_eq!(
1298            bytes,
1299            Bytes::from_static(b"{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"pet api\",\n    \"version\": \"0.1.0\"\n  },\n  \"paths\": {}\n}")
1300        );
1301    }
1302
1303    #[test]
1304    fn test_openapi_schema_work_with_generics() {
1305        #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1306        #[salvo(schema(name = City))]
1307        pub(crate) struct CityDTO {
1308            #[salvo(schema(rename = "id"))]
1309            pub(crate) id: String,
1310            #[salvo(schema(rename = "name"))]
1311            pub(crate) name: String,
1312        }
1313
1314        #[derive(Serialize, Deserialize, Debug, ToSchema)]
1315        #[salvo(schema(name = Response))]
1316        pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1317            #[salvo(schema(rename = "status"))]
1318            /// status code
1319            pub(crate) status: String,
1320            #[salvo(schema(rename = "msg"))]
1321            /// Status msg
1322            pub(crate) message: String,
1323            #[salvo(schema(rename = "data"))]
1324            /// The data returned
1325            pub(crate) data: T,
1326        }
1327
1328        #[salvo_oapi::endpoint(
1329            operation_id = "get_all_cities",
1330            tags("city"),
1331            status_codes(200, 400, 401, 403, 500)
1332        )]
1333        pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1334            Ok(Json(ApiResponse {
1335                status: "200".to_owned(),
1336                message: "OK".to_owned(),
1337                data: vec![CityDTO {
1338                    id: "1".to_owned(),
1339                    name: "Beijing".to_owned(),
1340                }],
1341            }))
1342        }
1343
1344        let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1345            .add_server(Server::new("/api/bar/").description("this is description of the server"));
1346
1347        let router = Router::with_path("/cities").get(get_all_cities);
1348        let doc = doc.merge_router(&router);
1349
1350        assert_eq!(
1351            json! {{
1352                "openapi": "3.1.0",
1353                "info": {
1354                    "title": "my application",
1355                    "version": "0.1.0"
1356                },
1357                "servers": [
1358                    {
1359                        "url": "/api/bar/",
1360                        "description": "this is description of the server"
1361                    }
1362                ],
1363                "paths": {
1364                    "/cities": {
1365                        "get": {
1366                            "tags": [
1367                                "city"
1368                            ],
1369                            "operationId": "get_all_cities",
1370                            "responses": {
1371                                "200": {
1372                                    "description": "Response with json format data",
1373                                    "content": {
1374                                        "application/json": {
1375                                            "schema": {
1376                                                "$ref": "#/components/schemas/Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>"
1377                                            }
1378                                        }
1379                                    }
1380                                },
1381                                "400": {
1382                                    "description": "The request could not be understood by the server due to malformed syntax.",
1383                                    "content": {
1384                                        "application/json": {
1385                                            "schema": {
1386                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1387                                            }
1388                                        }
1389                                    }
1390                                },
1391                                "401": {
1392                                    "description": "The request requires user authentication.",
1393                                    "content": {
1394                                        "application/json": {
1395                                            "schema": {
1396                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1397                                            }
1398                                        }
1399                                    }
1400                                },
1401                                "403": {
1402                                    "description": "The server refused to authorize the request.",
1403                                    "content": {
1404                                        "application/json": {
1405                                            "schema": {
1406                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1407                                            }
1408                                        }
1409                                    }
1410                                },
1411                                "500": {
1412                                    "description": "The server encountered an internal error while processing this request.",
1413                                    "content": {
1414                                        "application/json": {
1415                                            "schema": {
1416                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1417                                            }
1418                                        }
1419                                    }
1420                                }
1421                            }
1422                        }
1423                    }
1424                },
1425                "components": {
1426                    "schemas": {
1427                        "City": {
1428                            "type": "object",
1429                            "required": [
1430                                "id",
1431                                "name"
1432                            ],
1433                            "properties": {
1434                                "id": {
1435                                    "type": "string"
1436                                },
1437                                "name": {
1438                                    "type": "string"
1439                                }
1440                            }
1441                        },
1442                        "Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>": {
1443                            "type": "object",
1444                            "required": [
1445                                "status",
1446                                "msg",
1447                                "data"
1448                            ],
1449                            "properties": {
1450                                "data": {
1451                                    "type": "array",
1452                                    "items": {
1453                                        "$ref": "#/components/schemas/City"
1454                                    }
1455                                },
1456                                "msg": {
1457                                    "type": "string",
1458                                    "description": "Status msg"
1459                                },
1460                                "status": {
1461                                    "type": "string",
1462                                    "description": "status code"
1463                                }
1464                            }
1465                        },
1466                        "salvo_core.http.errors.status_error.StatusError": {
1467                            "type": "object",
1468                            "required": [
1469                                "code",
1470                                "name",
1471                                "brief",
1472                                "detail"
1473                            ],
1474                            "properties": {
1475                                "brief": {
1476                                    "type": "string"
1477                                },
1478                                "cause": {
1479                                    "type": "string"
1480                                },
1481                                "code": {
1482                                    "type": "integer",
1483                                    "format": "uint16",
1484                                    "minimum": 0.0
1485                                },
1486                                "detail": {
1487                                    "type": "string"
1488                                },
1489                                "name": {
1490                                    "type": "string"
1491                                }
1492                            }
1493                        }
1494                    }
1495                }
1496            }},
1497            Value::from_str(&doc.to_json().unwrap()).unwrap()
1498        );
1499    }
1500}