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