Skip to main content

salvo_oapi/
openapi.rs

1//! Rust implementation of Openapi Spec V3.1.
2
3use salvo_core::cfg_feature;
4
5mod callback;
6mod components;
7mod content;
8mod encoding;
9mod example;
10mod external_docs;
11mod header;
12pub mod info;
13mod link;
14pub mod operation;
15pub mod parameter;
16pub mod path;
17pub mod request_body;
18pub mod response;
19pub mod schema;
20pub mod security;
21pub mod server;
22mod tag;
23mod xml;
24
25use std::collections::BTreeSet;
26use std::fmt::{self, Debug, Formatter};
27use std::sync::LazyLock;
28
29use regex::Regex;
30use salvo_core::{Depot, FlowCtrl, Handler, Router, async_trait, writing};
31use serde::de::{Error, Expected, Visitor};
32use serde::{Deserialize, Deserializer, Serialize, Serializer};
33
34pub use self::callback::Callback;
35pub use self::components::Components;
36pub use self::content::Content;
37pub use self::encoding::Encoding;
38pub use self::example::Example;
39pub use self::external_docs::ExternalDocs;
40pub use self::header::Header;
41pub use self::info::{Contact, Info, License};
42pub use self::link::Link;
43pub use self::operation::{Operation, Operations};
44pub use self::parameter::{Parameter, ParameterIn, ParameterStyle, Parameters};
45pub use self::path::{PathItem, PathItemType, Paths};
46pub use self::request_body::RequestBody;
47pub use self::response::{Response, Responses};
48pub use self::schema::{
49    Array, ArrayItems, BasicType, Discriminator, KnownFormat, Number, Object, Ref, Schema,
50    SchemaFormat, SchemaType, Schemas,
51};
52pub use self::security::{SecurityRequirement, SecurityScheme};
53pub use self::server::{Server, ServerVariable, ServerVariables, Servers};
54pub use self::tag::Tag;
55pub use self::xml::Xml;
56use crate::Endpoint;
57use crate::routing::NormNode;
58
59static PATH_PARAMETER_NAME_REGEX: LazyLock<Regex> =
60    LazyLock::new(|| Regex::new(r"\{([^}:]+)").expect("invalid regex"));
61
62/// The structure of the internal storage object paths.
63#[cfg(not(feature = "preserve-path-order"))]
64pub type PathMap<K, V> = std::collections::BTreeMap<K, V>;
65/// The structure of the internal storage object paths.
66#[cfg(feature = "preserve-path-order")]
67pub type PathMap<K, V> = indexmap::IndexMap<K, V>;
68
69/// The structure of the internal storage object properties.
70#[cfg(not(feature = "preserve-prop-order"))]
71pub type PropMap<K, V> = std::collections::BTreeMap<K, V>;
72/// The structure of the internal storage object properties.
73#[cfg(feature = "preserve-prop-order")]
74pub type PropMap<K, V> = indexmap::IndexMap<K, V>;
75
76/// Root object of the OpenAPI document.
77///
78/// You can use [`OpenApi::new`] function to construct a new [`OpenApi`] instance and then
79/// use the fields with mutable access to modify them. This is quite tedious if you are not simply
80/// just changing one thing thus you can also use the [`OpenApi::new`] to use builder to
81/// construct a new [`OpenApi`] object.
82///
83/// See more details at <https://spec.openapis.org/oas/latest.html#openapi-object>.
84#[non_exhaustive]
85#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
86#[serde(rename_all = "camelCase")]
87pub struct OpenApi {
88    /// OpenAPI document version.
89    pub openapi: OpenApiVersion,
90
91    /// Provides metadata about the API.
92    ///
93    /// See more details at <https://spec.openapis.org/oas/latest.html#info-object>.
94    pub info: Info,
95
96    /// List of servers that provides the connectivity information to target servers.
97    ///
98    /// This is implicitly one server with `url` set to `/`.
99    ///
100    /// See more details at <https://spec.openapis.org/oas/latest.html#server-object>.
101    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
102    pub servers: BTreeSet<Server>,
103
104    /// Available paths and operations for the API.
105    ///
106    /// See more details at <https://spec.openapis.org/oas/latest.html#paths-object>.
107    pub paths: Paths,
108
109    /// Incoming webhooks that may be received as part of this API.
110    ///
111    /// Each value is a [`PathItem`] (or a [`Ref`] to one) keyed by a unique name. Added in
112    /// OpenAPI 3.1.
113    ///
114    /// See more details at <https://spec.openapis.org/oas/v3.1.0#openapi-object>.
115    #[serde(skip_serializing_if = "PathMap::is_empty", default)]
116    pub webhooks: PathMap<String, RefOr<PathItem>>,
117
118    /// Holds various reusable schemas for the OpenAPI document.
119    ///
120    /// Few of these elements are security schemas and object schemas.
121    ///
122    /// See more details at <https://spec.openapis.org/oas/latest.html#components-object>.
123    #[serde(skip_serializing_if = "Components::is_empty")]
124    pub components: Components,
125
126    /// Declaration of global security mechanisms that can be used across the API. The individual
127    /// operations can override the declarations. You can use `SecurityRequirement::default()`
128    /// if you wish to make security optional by adding it to the list of securities.
129    ///
130    /// See more details at <https://spec.openapis.org/oas/latest.html#security-requirement-object>.
131    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
132    pub security: BTreeSet<SecurityRequirement>,
133
134    /// List of tags can be used to add additional documentation to matching tags of operations.
135    ///
136    /// See more details at <https://spec.openapis.org/oas/latest.html#tag-object>.
137    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
138    pub tags: BTreeSet<Tag>,
139
140    /// Global additional documentation reference.
141    ///
142    /// See more details at <https://spec.openapis.org/oas/latest.html#external-documentation-object>.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub external_docs: Option<ExternalDocs>,
145
146    /// The default value for the `$schema` keyword within Schema Objects contained in this
147    /// document. It defaults to `<https://spec.openapis.org/oas/3.1/dialect/base>` and, when
148    /// set, must be a URI.
149    ///
150    /// See <https://spec.openapis.org/oas/v3.1.0#openapi-object> for details.
151    #[serde(default, skip_serializing_if = "String::is_empty")]
152    pub json_schema_dialect: String,
153
154    /// Optional extensions "x-something".
155    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
156    pub extensions: PropMap<String, serde_json::Value>,
157}
158
159impl OpenApi {
160    /// Construct a new [`OpenApi`] object.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// # use salvo_oapi::{Info, Paths, OpenApi};
166    /// #
167    /// let openapi = OpenApi::new("pet api", "0.1.0");
168    /// ```
169    pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
170        Self {
171            info: Info::new(title, version),
172            ..Default::default()
173        }
174    }
175    /// Construct a new [`OpenApi`] object.
176    ///
177    /// Function accepts [`Info`] metadata of the API;
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// # use salvo_oapi::{Info, Paths, OpenApi};
183    /// #
184    /// let openapi = OpenApi::new("pet api", "0.1.0");
185    /// ```
186    #[must_use]
187    pub fn with_info(info: Info) -> Self {
188        Self {
189            info,
190            ..Default::default()
191        }
192    }
193
194    /// Converts this [`OpenApi`] to JSON String. This method essentially calls
195    /// [`serde_json::to_string`] method.
196    pub fn to_json(&self) -> Result<String, serde_json::Error> {
197        serde_json::to_string(self)
198    }
199
200    /// Converts this [`OpenApi`] to pretty JSON String. This method essentially calls
201    /// [`serde_json::to_string_pretty`] method.
202    pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
203        serde_json::to_string_pretty(self)
204    }
205
206    cfg_feature! {
207        #![feature ="yaml"]
208        /// Converts this [`OpenApi`] to YAML String. This method essentially calls [`serde_norway::to_string`] method.
209        pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
210            serde_norway::to_string(self)
211        }
212    }
213
214    /// Merge `other` [`OpenApi`] consuming it and resuming its content.
215    ///
216    /// Merge function will take all `self` nonexistent _`servers`, `paths`, `webhooks`,
217    /// `schemas`, `responses`, `security_schemes`, `security_requirements` and `tags`_ from
218    /// _`other`_ [`OpenApi`].
219    ///
220    /// This function performs a shallow comparison for `paths`, `webhooks`, `schemas`,
221    /// `responses` and `security schemes` which means that only _`name`_ and _`path`_ is used
222    /// for comparison. When a match occurs the existing item will be overwritten.
223    ///
224    /// For _`servers`_, _`tags`_ and _`security_requirements`_ the whole item will be used for
225    /// comparison.
226    ///
227    /// **Note!** `info`, `openapi`, `external_docs` and `json_schema_dialect` will not be merged.
228    #[must_use]
229    pub fn merge(mut self, mut other: Self) -> Self {
230        self.servers.append(&mut other.servers);
231        self.paths.append(&mut other.paths);
232        for (name, item) in std::mem::take(&mut other.webhooks) {
233            self.webhooks.insert(name, item);
234        }
235        self.components.append(&mut other.components);
236        self.security.append(&mut other.security);
237        self.tags.append(&mut other.tags);
238        self
239    }
240
241    /// Nest another [`OpenApi`] document under the given path prefix.
242    ///
243    /// All paths from `other` will be prefixed with `path` and then merged into `self`.
244    /// Components, security, tags, and servers are merged as in [`OpenApi::merge`].
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// # use salvo_oapi::OpenApi;
250    /// let api = OpenApi::new("My Api", "1.0.0");
251    /// let user_api = OpenApi::new("User Api", "1.0.0");
252    /// let nested = api.nest("/api/v1", user_api);
253    /// ```
254    #[must_use]
255    pub fn nest<P: Into<String>>(self, path: P, other: Self) -> Self {
256        self.nest_with_path_composer(path, other, |base, item_path| {
257            format!(
258                "{}/{}",
259                base.trim_end_matches('/'),
260                item_path.trim_start_matches('/')
261            )
262        })
263    }
264
265    /// Nest another [`OpenApi`] document with a custom path composer.
266    ///
267    /// In most cases you should use [`OpenApi::nest`] instead.
268    /// Only use this method if you need custom path composition for a specific use case.
269    ///
270    /// `composer` is a function that takes two strings, the base path and the path to nest,
271    /// and returns the composed path for the API Specification.
272    #[must_use]
273    pub fn nest_with_path_composer<P: Into<String>, F: Fn(&str, &str) -> String>(
274        mut self,
275        path: P,
276        mut other: Self,
277        composer: F,
278    ) -> Self {
279        let path: String = path.into();
280
281        // Take paths out of other, prefix them, and insert into self
282        let other_paths = std::mem::take(&mut other.paths);
283        for (item_path, item) in other_paths.iter() {
284            let composed = composer(&path, item_path);
285            self.paths.insert(composed, item.clone());
286        }
287
288        // Merge the remaining parts (servers, components, security, tags)
289        // Paths in other are already empty so merge won't duplicate them
290        self.merge(other)
291    }
292
293    /// Add [`Info`] metadata of the API.
294    #[must_use]
295    pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
296        self.info = info.into();
297        self
298    }
299
300    /// Add iterator of [`Server`]s to configure target servers.
301    #[must_use]
302    pub fn servers<S: IntoIterator<Item = Server>>(mut self, servers: S) -> Self {
303        self.servers = servers.into_iter().collect();
304        self
305    }
306    /// Add [`Server`] to configure operations and endpoints of the API and returns `Self`.
307    #[must_use]
308    pub fn add_server<S>(mut self, server: S) -> Self
309    where
310        S: Into<Server>,
311    {
312        self.servers.insert(server.into());
313        self
314    }
315
316    /// Set paths to configure operations and endpoints of the API.
317    #[must_use]
318    pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
319        self.paths = paths.into();
320        self
321    }
322    /// Add [`PathItem`] to configure operations and endpoints of the API and returns `Self`.
323    #[must_use]
324    pub fn add_path<P, I>(mut self, path: P, item: I) -> Self
325    where
326        P: Into<String>,
327        I: Into<PathItem>,
328    {
329        self.paths.insert(path.into(), item.into());
330        self
331    }
332
333    /// Replace the incoming webhooks map and return `Self`.
334    ///
335    /// Webhooks were added in OpenAPI 3.1. See [`OpenApi::webhooks`].
336    #[must_use]
337    pub fn webhooks<I, K, V>(mut self, webhooks: I) -> Self
338    where
339        I: IntoIterator<Item = (K, V)>,
340        K: Into<String>,
341        V: Into<RefOr<PathItem>>,
342    {
343        self.webhooks = webhooks
344            .into_iter()
345            .map(|(name, item)| (name.into(), item.into()))
346            .collect();
347        self
348    }
349
350    /// Insert a single named webhook and return `Self`.
351    ///
352    /// The value may be an inline [`PathItem`] or a [`Ref`] to one stored elsewhere.
353    #[must_use]
354    pub fn add_webhook<K: Into<String>, V: Into<RefOr<PathItem>>>(
355        mut self,
356        name: K,
357        webhook: V,
358    ) -> Self {
359        self.webhooks.insert(name.into(), webhook.into());
360        self
361    }
362
363    /// Add [`Components`] to configure reusable schemas.
364    #[must_use]
365    pub fn components(mut self, components: impl Into<Components>) -> Self {
366        self.components = components.into();
367        self
368    }
369
370    /// Add iterator of [`SecurityRequirement`]s that are globally available for all operations.
371    #[must_use]
372    pub fn security<S: IntoIterator<Item = SecurityRequirement>>(mut self, security: S) -> Self {
373        self.security = security.into_iter().collect();
374        self
375    }
376
377    /// Add [`SecurityScheme`] to [`Components`] and returns `Self`.
378    ///
379    /// Accepts two arguments where first is the name of the [`SecurityScheme`]. This is later when
380    /// referenced by [`SecurityRequirement`][requirement]s. Second parameter is the
381    /// [`SecurityScheme`].
382    ///
383    /// [requirement]: crate::SecurityRequirement
384    #[must_use]
385    pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
386        mut self,
387        name: N,
388        security_scheme: S,
389    ) -> Self {
390        self.components
391            .security_schemes
392            .insert(name.into(), security_scheme.into());
393
394        self
395    }
396
397    /// Add iterator of [`SecurityScheme`]s to [`Components`].
398    ///
399    /// Accepts two arguments where first is the name of the [`SecurityScheme`]. This is later when
400    /// referenced by [`SecurityRequirement`][requirement]s. Second parameter is the
401    /// [`SecurityScheme`].
402    ///
403    /// [requirement]: crate::SecurityRequirement
404    #[must_use]
405    pub fn extend_security_schemes<
406        I: IntoIterator<Item = (N, S)>,
407        N: Into<String>,
408        S: Into<SecurityScheme>,
409    >(
410        mut self,
411        schemas: I,
412    ) -> Self {
413        self.components.security_schemes.extend(
414            schemas
415                .into_iter()
416                .map(|(name, item)| (name.into(), item.into())),
417        );
418        self
419    }
420
421    /// Add [`Schema`] to [`Components`] and returns `Self`.
422    ///
423    /// Accepts two arguments where first is name of the schema and second is the schema itself.
424    #[must_use]
425    pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
426        mut self,
427        name: S,
428        schema: I,
429    ) -> Self {
430        self.components.schemas.insert(name, schema);
431        self
432    }
433
434    /// Add [`Schema`]s from iterator.
435    ///
436    /// # Examples
437    /// ```
438    /// # use salvo_oapi::{OpenApi, Object, BasicType, Schema};
439    /// OpenApi::new("api", "0.0.1").extend_schemas([(
440    ///     "Pet",
441    ///     Schema::from(
442    ///         Object::new()
443    ///             .property("name", Object::new().schema_type(BasicType::String))
444    ///             .required("name"),
445    ///     ),
446    /// )]);
447    /// ```
448    #[must_use]
449    pub fn extend_schemas<I, C, S>(mut self, schemas: I) -> Self
450    where
451        I: IntoIterator<Item = (S, C)>,
452        C: Into<RefOr<Schema>>,
453        S: Into<String>,
454    {
455        self.components.schemas.extend(
456            schemas
457                .into_iter()
458                .map(|(name, schema)| (name.into(), schema.into())),
459        );
460        self
461    }
462
463    /// Add a new response and returns `self`.
464    #[must_use]
465    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
466        mut self,
467        name: S,
468        response: R,
469    ) -> Self {
470        self.components
471            .responses
472            .insert(name.into(), response.into());
473        self
474    }
475
476    /// Extends responses with the contents of an iterator.
477    #[must_use]
478    pub fn extend_responses<
479        I: IntoIterator<Item = (S, R)>,
480        S: Into<String>,
481        R: Into<RefOr<Response>>,
482    >(
483        mut self,
484        responses: I,
485    ) -> Self {
486        self.components.responses.extend(
487            responses
488                .into_iter()
489                .map(|(name, response)| (name.into(), response.into())),
490        );
491        self
492    }
493
494    /// Add iterator of [`Tag`]s to add additional documentation for **operations** tags.
495    #[must_use]
496    pub fn tags<I, T>(mut self, tags: I) -> Self
497    where
498        I: IntoIterator<Item = T>,
499        T: Into<Tag>,
500    {
501        self.tags = tags.into_iter().map(Into::into).collect();
502        self
503    }
504
505    /// Add [`ExternalDocs`] for referring additional documentation.
506    #[must_use]
507    pub fn external_docs(mut self, external_docs: ExternalDocs) -> Self {
508        self.external_docs = Some(external_docs);
509        self
510    }
511
512    /// Override the default JSON Schema dialect for this OpenAPI document.
513    ///
514    /// Sets the [`jsonSchemaDialect`][spec] top-level field, which provides the default
515    /// `$schema` value used by Schema Objects contained in this document.
516    ///
517    /// [spec]: https://spec.openapis.org/oas/v3.1.0#openapi-object
518    ///
519    /// # Examples
520    ///
521    /// _**Override default schema dialect.**_
522    /// ```rust
523    /// # use salvo_oapi::OpenApi;
524    /// let _ = OpenApi::new("openapi", "0.1.0")
525    ///     .json_schema_dialect("http://json-schema.org/draft-07/schema#");
526    /// ```
527    #[must_use]
528    pub fn json_schema_dialect<S: Into<String>>(mut self, dialect: S) -> Self {
529        self.json_schema_dialect = dialect.into();
530        self
531    }
532
533    /// Add openapi extension (`x-something`) for [`OpenApi`].
534    #[must_use]
535    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
536        self.extensions.insert(key.into(), value);
537        self
538    }
539
540    /// Consumes the [`OpenApi`] and returns [`Router`] with the [`OpenApi`] as handler.
541    pub fn into_router(self, path: impl Into<String>) -> Router {
542        Router::with_path(path.into()).goal(self)
543    }
544
545    /// Consumes the [`OpenApi`] and information from a [`Router`].
546    #[must_use]
547    pub fn merge_router(self, router: &Router) -> Self {
548        self.merge_router_with_base(router, "/")
549    }
550
551    /// Consumes the [`OpenApi`] and information from a [`Router`] with base path.
552    #[must_use]
553    pub fn merge_router_with_base(mut self, router: &Router, base: impl AsRef<str>) -> Self {
554        let mut node = NormNode::new(router, Default::default());
555        self.merge_norm_node(&mut node, base.as_ref());
556        self
557    }
558
559    fn merge_norm_node(&mut self, node: &mut NormNode, base_path: &str) {
560        fn join_path(a: &str, b: &str) -> String {
561            if a.is_empty() {
562                b.to_owned()
563            } else if b.is_empty() {
564                a.to_owned()
565            } else {
566                format!("{}/{}", a.trim_end_matches('/'), b.trim_start_matches('/'))
567            }
568        }
569
570        let path = join_path(base_path, node.path.as_deref().unwrap_or_default());
571        let path_parameter_names = PATH_PARAMETER_NAME_REGEX
572            .captures_iter(&path)
573            .filter_map(|captures| {
574                captures
575                    .iter()
576                    .skip(1)
577                    .map(|capture| {
578                        capture
579                            .expect("regex captures should not be None")
580                            .as_str()
581                            .to_owned()
582                    })
583                    .next()
584            })
585            .collect::<Vec<_>>();
586
587        if let Some(handler_type_id) = &node.handler_type_id
588            && let Some(creator) = crate::EndpointRegistry::find(handler_type_id)
589        {
590            if let Some(method) = node.method {
591                let Endpoint {
592                    mut operation,
593                    mut components,
594                } = creator();
595                operation.tags.extend(node.metadata.tags.iter().cloned());
596                operation
597                    .securities
598                    .extend(node.metadata.securities.iter().cloned());
599                let not_exist_parameters = operation
600                    .parameters
601                    .0
602                    .iter()
603                    .filter(|p| {
604                        p.parameter_in == ParameterIn::Path
605                            && !path_parameter_names.contains(&p.name)
606                    })
607                    .map(|p| &p.name)
608                    .collect::<Vec<_>>();
609                if !not_exist_parameters.is_empty() {
610                    tracing::warn!(parameters = ?not_exist_parameters, path, handler_name = node.handler_type_name, "information for not exist parameters");
611                }
612                #[cfg(debug_assertions)]
613                {
614                    let meta_not_exist_parameters = path_parameter_names
615                        .iter()
616                        .filter(|name| {
617                            !name.starts_with('*')
618                                && !operation.parameters.0.iter().any(|parameter| {
619                                    parameter.name == **name
620                                        && parameter.parameter_in == ParameterIn::Path
621                                })
622                        })
623                        .collect::<Vec<_>>();
624
625                    if !meta_not_exist_parameters.is_empty() {
626                        tracing::warn!(parameters = ?meta_not_exist_parameters, path, handler_name = node.handler_type_name, "parameters information not provided");
627                    }
628                }
629                let path_item = self.paths.entry(path.clone()).or_default();
630                if path_item.operations.contains_key(&method) {
631                    tracing::warn!(
632                        "path `{}` already contains operation for method `{:?}`",
633                        path,
634                        method
635                    );
636                } else {
637                    path_item.operations.insert(method, operation);
638                }
639                self.components.append(&mut components);
640            } else {
641                // No method filter on this route: OpenAPI 3.1 has no "any-method"
642                // operation slot, so attaching the same handler to GET/POST/PUT/PATCH
643                // would lie about the API. Skip and warn so the user can attach a
644                // method filter (`.get(handler)`, `.post(handler)`, ...) explicitly.
645                tracing::warn!(
646                    path,
647                    handler_name = node.handler_type_name,
648                    "endpoint has no HTTP method filter; skipping in OpenAPI document. \
649                     Add `.get()`, `.post()`, etc. to the router to include it"
650                );
651            }
652        }
653
654        for child in &mut node.children {
655            self.merge_norm_node(child, &path);
656        }
657    }
658}
659
660#[async_trait]
661impl Handler for OpenApi {
662    async fn handle(
663        &self,
664        req: &mut salvo_core::Request,
665        _depot: &mut Depot,
666        res: &mut salvo_core::Response,
667        _ctrl: &mut FlowCtrl,
668    ) {
669        let pretty = req
670            .queries()
671            .get("pretty")
672            .map(|v| &**v != "false")
673            .unwrap_or(false);
674        let content = if pretty {
675            self.to_pretty_json().unwrap_or_default()
676        } else {
677            self.to_json().unwrap_or_default()
678        };
679        res.render(writing::Text::Json(&content));
680    }
681}
682/// Represents available [OpenAPI versions][version].
683///
684/// [version]: <https://spec.openapis.org/oas/latest.html#versions>
685#[derive(Serialize, Clone, PartialEq, Eq, Default, Debug)]
686pub enum OpenApiVersion {
687    /// Will serialize to `3.1.0` the latest released OpenAPI version.
688    #[serde(rename = "3.1.0")]
689    #[default]
690    Version3_1,
691}
692
693impl<'de> Deserialize<'de> for OpenApiVersion {
694    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
695    where
696        D: Deserializer<'de>,
697    {
698        struct VersionVisitor;
699
700        impl Visitor<'_> for VersionVisitor {
701            type Value = OpenApiVersion;
702
703            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
704                formatter.write_str("a version string in 3.1.x format")
705            }
706
707            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
708            where
709                E: Error,
710            {
711                self.visit_string(v.to_owned())
712            }
713
714            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
715            where
716                E: Error,
717            {
718                let version = v
719                    .split('.')
720                    .flat_map(|digit| digit.parse::<i8>())
721                    .collect::<Vec<_>>();
722
723                if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
724                    Ok(OpenApiVersion::Version3_1)
725                } else {
726                    let expected: &dyn Expected = &"3.1.0";
727                    Err(Error::invalid_value(
728                        serde::de::Unexpected::Str(&v),
729                        expected,
730                    ))
731                }
732            }
733        }
734
735        deserializer.deserialize_string(VersionVisitor)
736    }
737}
738
739/// Value used to indicate whether reusable schema, parameter or operation is deprecated.
740///
741/// The value will serialize to boolean.
742#[derive(PartialEq, Eq, Clone, Debug)]
743pub enum Deprecated {
744    /// Is deprecated.
745    True,
746    /// Is not deprecated.
747    False,
748}
749impl From<bool> for Deprecated {
750    fn from(b: bool) -> Self {
751        if b { Self::True } else { Self::False }
752    }
753}
754
755impl Serialize for Deprecated {
756    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
757    where
758        S: Serializer,
759    {
760        serializer.serialize_bool(matches!(self, Self::True))
761    }
762}
763
764impl<'de> Deserialize<'de> for Deprecated {
765    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
766    where
767        D: serde::Deserializer<'de>,
768    {
769        struct BoolVisitor;
770        impl Visitor<'_> for BoolVisitor {
771            type Value = Deprecated;
772
773            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
774                formatter.write_str("a bool true or false")
775            }
776
777            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
778            where
779                E: serde::de::Error,
780            {
781                match v {
782                    true => Ok(Deprecated::True),
783                    false => Ok(Deprecated::False),
784                }
785            }
786        }
787        deserializer.deserialize_bool(BoolVisitor)
788    }
789}
790
791/// Value used to indicate whether parameter or property is required.
792///
793/// The value will serialize to boolean.
794#[derive(PartialEq, Eq, Default, Clone, Debug)]
795pub enum Required {
796    /// Is required.
797    True,
798    /// Is not required.
799    False,
800    /// This value is not set, it will treat as `False` when serialize to boolean.
801    #[default]
802    Unset,
803}
804
805impl From<bool> for Required {
806    fn from(value: bool) -> Self {
807        if value { Self::True } else { Self::False }
808    }
809}
810
811impl Serialize for Required {
812    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
813    where
814        S: Serializer,
815    {
816        serializer.serialize_bool(matches!(self, Self::True))
817    }
818}
819
820impl<'de> Deserialize<'de> for Required {
821    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
822    where
823        D: serde::Deserializer<'de>,
824    {
825        struct BoolVisitor;
826        impl Visitor<'_> for BoolVisitor {
827            type Value = Required;
828
829            fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
830                formatter.write_str("a bool true or false")
831            }
832
833            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
834            where
835                E: serde::de::Error,
836            {
837                match v {
838                    true => Ok(Required::True),
839                    false => Ok(Required::False),
840                }
841            }
842        }
843        deserializer.deserialize_bool(BoolVisitor)
844    }
845}
846
847/// A [`Ref`] or some other type `T`.
848///
849/// Typically used in combination with [`Components`] and is an union type between [`Ref`] and any
850/// other given type such as [`Schema`] or [`Response`].
851#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
852#[serde(untagged)]
853pub enum RefOr<T> {
854    /// A [`Ref`] to a reusable component.
855    Ref(schema::Ref),
856    /// Some other type `T`.
857    Type(T),
858}
859
860#[cfg(test)]
861mod tests {
862    use std::fmt::Debug;
863    use std::str::FromStr;
864
865    use bytes::Bytes;
866    use salvo_core::http::ResBody;
867    use salvo_core::prelude::*;
868    use serde_json::{Value, json};
869
870    use super::response::Response;
871    use super::*;
872    use crate::ToSchema;
873    use crate::extract::*;
874    use crate::security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme};
875    use crate::server::Server;
876
877    #[test]
878    fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
879        assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
880        Ok(())
881    }
882
883    #[test]
884    fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
885        let raw_json = r#"{
886            "openapi": "3.1.0",
887            "info": {
888              "title": "My api",
889              "description": "My api description",
890              "license": {
891                "name": "MIT",
892                "url": "http://mit.licence"
893              },
894              "version": "1.0.0",
895              "contact": {},
896              "termsOfService": "terms of service"
897            },
898            "paths": {}
899          }"#;
900        let doc: OpenApi = OpenApi::with_info(
901            Info::default()
902                .description("My api description")
903                .license(License::new("MIT").url("http://mit.licence"))
904                .title("My api")
905                .version("1.0.0")
906                .terms_of_service("terms of service")
907                .contact(Contact::default()),
908        );
909        let serialized = doc.to_json()?;
910
911        assert_eq!(
912            Value::from_str(&serialized)?,
913            Value::from_str(raw_json)?,
914            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
915        );
916        Ok(())
917    }
918
919    #[test]
920    fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
921        let doc = OpenApi::new("My big api", "1.1.0").paths(
922            Paths::new()
923                .path(
924                    "/api/v1/users",
925                    PathItem::new(
926                        PathItemType::Get,
927                        Operation::new().add_response("200", Response::new("Get users list")),
928                    ),
929                )
930                .path(
931                    "/api/v1/users",
932                    PathItem::new(
933                        PathItemType::Post,
934                        Operation::new().add_response("200", Response::new("Post new user")),
935                    ),
936                )
937                .path(
938                    "/api/v1/users/{id}",
939                    PathItem::new(
940                        PathItemType::Get,
941                        Operation::new().add_response("200", Response::new("Get user by id")),
942                    ),
943                ),
944        );
945
946        let serialized = doc.to_json()?;
947        let expected = r#"
948        {
949            "openapi": "3.1.0",
950            "info": {
951              "title": "My big api",
952              "version": "1.1.0"
953            },
954            "paths": {
955              "/api/v1/users": {
956                "get": {
957                  "responses": {
958                    "200": {
959                      "description": "Get users list"
960                    }
961                  }
962                },
963                "post": {
964                  "responses": {
965                    "200": {
966                      "description": "Post new user"
967                    }
968                  }
969                }
970              },
971              "/api/v1/users/{id}": {
972                "get": {
973                  "responses": {
974                    "200": {
975                      "description": "Get user by id"
976                    }
977                  }
978                }
979              }
980            }
981          }
982        "#
983        .replace("\r\n", "\n");
984
985        assert_eq!(
986            Value::from_str(&serialized)?,
987            Value::from_str(&expected)?,
988            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
989        );
990        Ok(())
991    }
992
993    #[test]
994    fn merge_2_openapi_documents() {
995        let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
996            "/api/v1/user",
997            PathItem::new(
998                PathItemType::Get,
999                Operation::new().add_response("200", Response::new("This will not get added")),
1000            ),
1001        ));
1002
1003        let api_2 = OpenApi::new("Api", "v2")
1004            .paths(
1005                Paths::new()
1006                    .path(
1007                        "/api/v1/user",
1008                        PathItem::new(
1009                            PathItemType::Get,
1010                            Operation::new().add_response("200", Response::new("Get user success")),
1011                        ),
1012                    )
1013                    .path(
1014                        "/ap/v2/user",
1015                        PathItem::new(
1016                            PathItemType::Get,
1017                            Operation::new()
1018                                .add_response("200", Response::new("Get user success 2")),
1019                        ),
1020                    )
1021                    .path(
1022                        "/api/v2/user",
1023                        PathItem::new(
1024                            PathItemType::Post,
1025                            Operation::new().add_response("200", Response::new("Get user success")),
1026                        ),
1027                    ),
1028            )
1029            .components(
1030                Components::new().add_schema(
1031                    "User2",
1032                    Object::new()
1033                        .schema_type(BasicType::Object)
1034                        .property("name", Object::new().schema_type(BasicType::String)),
1035                ),
1036            );
1037
1038        api_1 = api_1.merge(api_2);
1039        let value = serde_json::to_value(&api_1).unwrap();
1040
1041        assert_eq!(
1042            value,
1043            json!(
1044                {
1045                  "openapi": "3.1.0",
1046                  "info": {
1047                    "title": "Api",
1048                    "version": "v1"
1049                  },
1050                  "paths": {
1051                    "/ap/v2/user": {
1052                      "get": {
1053                        "responses": {
1054                          "200": {
1055                            "description": "Get user success 2"
1056                          }
1057                        }
1058                      }
1059                    },
1060                    "/api/v1/user": {
1061                      "get": {
1062                        "responses": {
1063                          "200": {
1064                            "description": "Get user success"
1065                          }
1066                        }
1067                      }
1068                    },
1069                    "/api/v2/user": {
1070                      "post": {
1071                        "responses": {
1072                          "200": {
1073                            "description": "Get user success"
1074                          }
1075                        }
1076                      }
1077                    }
1078                  },
1079                  "components": {
1080                    "schemas": {
1081                      "User2": {
1082                        "type": "object",
1083                        "properties": {
1084                          "name": {
1085                            "type": "string"
1086                          }
1087                        }
1088                      }
1089                    }
1090                  }
1091                }
1092            )
1093        )
1094    }
1095
1096    #[test]
1097    fn test_simple_document_with_security() {
1098        #[derive(Deserialize, Serialize, ToSchema)]
1099        #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
1100        struct Pet {
1101            id: u64,
1102            name: String,
1103            age: Option<i32>,
1104        }
1105
1106        /// Get pet by id
1107        ///
1108        /// Get pet from database by pet database id
1109        #[salvo_oapi::endpoint(
1110            responses(
1111                (status_code = 200, description = "Pet found successfully"),
1112                (status_code = 404, description = "Pet was not found")
1113            ),
1114            parameters(
1115                ("id", description = "Pet database id to get Pet for"),
1116            ),
1117            security(
1118                (),
1119                ("my_auth" = ["read:items", "edit:items"]),
1120                ("token_jwt" = []),
1121                ("api_key1" = [], "api_key2" = []),
1122            )
1123        )]
1124        pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1125            let pet = Pet {
1126                id: pet_id.into_inner(),
1127                age: None,
1128                name: "lightning".to_owned(),
1129            };
1130            Json(pet)
1131        }
1132
1133        let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1134            Server::new("/api/bar/")
1135                .description("this is description of the server")
1136                .add_variable(
1137                    "username",
1138                    ServerVariable::new()
1139                        .default_value("the_user")
1140                        .description("this is user"),
1141                ),
1142        );
1143        doc.components.security_schemes.insert(
1144            "token_jwt".into(),
1145            SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1146        );
1147
1148        let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1149        let doc = doc.merge_router(&router);
1150
1151        assert_eq!(
1152            Value::from_str(
1153                r#"{
1154                    "openapi": "3.1.0",
1155                    "info": {
1156                       "title": "my application",
1157                       "version": "0.1.0"
1158                    },
1159                    "servers": [
1160                       {
1161                          "url": "/api/bar/",
1162                          "description": "this is description of the server",
1163                          "variables": {
1164                             "username": {
1165                                "default": "the_user",
1166                                "description": "this is user"
1167                             }
1168                          }
1169                       }
1170                    ],
1171                    "paths": {
1172                       "/pets/{id}": {
1173                          "get": {
1174                             "summary": "Get pet by id",
1175                             "description": "Get pet from database by pet database id",
1176                             "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1177                             "parameters": [
1178                                {
1179                                   "name": "pet_id",
1180                                   "in": "path",
1181                                   "description": "Get parameter `pet_id` from request url path.",
1182                                   "required": true,
1183                                   "schema": {
1184                                      "type": "integer",
1185                                      "format": "uint64",
1186                                      "minimum": 0
1187                                   }
1188                                },
1189                                {
1190                                   "name": "id",
1191                                   "in": "path",
1192                                   "description": "Pet database id to get Pet for",
1193                                   "required": true
1194                                }
1195                             ],
1196                             "responses": {
1197                                "200": {
1198                                   "description": "Pet found successfully"
1199                                },
1200                                "404": {
1201                                   "description": "Pet was not found"
1202                                }
1203                             },
1204                             "security": [
1205                                {},
1206                                {
1207                                   "my_auth": [
1208                                      "read:items",
1209                                      "edit:items"
1210                                   ]
1211                                },
1212                                {
1213                                   "token_jwt": []
1214                                },
1215                                {
1216                                    "api_key1": [],
1217                                    "api_key2": []
1218                                }
1219                             ]
1220                          }
1221                       }
1222                    },
1223                    "components": {
1224                       "schemas": {
1225                          "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1226                             "type": "object",
1227                             "required": [
1228                                "id",
1229                                "name"
1230                             ],
1231                             "properties": {
1232                                "age": {
1233                                   "type": ["integer", "null"],
1234                                   "format": "int32"
1235                                },
1236                                "id": {
1237                                   "type": "integer",
1238                                   "format": "uint64",
1239                                   "minimum": 0
1240                                },
1241                                "name": {
1242                                   "type": "string"
1243                                }
1244                             },
1245                             "examples": [{
1246                                "id": 1,
1247                                "name": "bob the cat"
1248                             }]
1249                          }
1250                       },
1251                       "securitySchemes": {
1252                          "token_jwt": {
1253                             "type": "http",
1254                             "scheme": "bearer",
1255                             "bearerFormat": "JWT"
1256                          }
1257                       }
1258                    }
1259                 }"#
1260            )
1261            .unwrap(),
1262            Value::from_str(&doc.to_json().unwrap()).unwrap()
1263        );
1264    }
1265
1266    #[test]
1267    fn merge_router_normalizes_constrained_path_params() {
1268        #[salvo_oapi::endpoint]
1269        async fn get_post(id: PathParam<i32>) -> &'static str {
1270            let _ = id;
1271            "ok"
1272        }
1273
1274        let router = Router::with_path("/posts/{id:num}").get(get_post);
1275        let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1276
1277        assert!(doc.paths.contains_key("/posts/{id}"));
1278        assert!(!doc.paths.contains_key("/posts/{id:num}"));
1279    }
1280
1281    #[test]
1282    fn to_parameters_struct_defaults_to_query_with_required() {
1283        // Regression test for https://github.com/salvo-rs/salvo/issues/1609
1284        // A struct used directly as a handler argument extracts from the query string by
1285        // default, so its parameters must be reported with `in: query`. Non-`Option` fields
1286        // must be `required: true`, and `Option` fields `required: false`. Previously the
1287        // parameter location fell back to `ParameterIn::Path`, which mislabeled the location
1288        // and forced every field to `required: true`.
1289        #[derive(Deserialize, crate::ToParameters)]
1290        #[allow(dead_code)]
1291        struct ListQuery {
1292            page: i32,
1293            #[serde(rename = "pageSize")]
1294            page_size: i32,
1295            name: String,
1296            keyword: Option<String>,
1297        }
1298
1299        #[salvo_oapi::endpoint]
1300        async fn list(query: ListQuery) -> &'static str {
1301            let _ = query;
1302            "ok"
1303        }
1304
1305        let router = Router::with_path("/list").get(list);
1306        let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1307
1308        let path_item = doc.paths.get("/list").expect("/list entry should exist");
1309        let operation = path_item
1310            .operations
1311            .get(&PathItemType::Get)
1312            .expect("get operation should exist");
1313
1314        let by_name = |name: &str| {
1315            operation
1316                .parameters
1317                .0
1318                .iter()
1319                .find(|p| p.name == name)
1320                .unwrap_or_else(|| panic!("parameter `{name}` should exist"))
1321        };
1322
1323        for name in ["page", "pageSize", "name", "keyword"] {
1324            assert_eq!(
1325                by_name(name).parameter_in,
1326                ParameterIn::Query,
1327                "parameter `{name}` should be located in query"
1328            );
1329        }
1330        assert_eq!(by_name("page").required, Required::True);
1331        assert_eq!(by_name("pageSize").required, Required::True);
1332        assert_eq!(by_name("name").required, Required::True);
1333        assert_eq!(by_name("keyword").required, Required::False);
1334    }
1335
1336    #[test]
1337    fn to_parameters_accepts_singular_and_plural_keys() {
1338        // The container key was `parameters(...)` and the field key `parameter(...)`;
1339        // the opposite spelling used to be a hard error / silently ignored. Both
1340        // spellings are now accepted at both levels, so the drift is invisible to
1341        // users. This test drives each level with its *alias* spelling.
1342        #[derive(Deserialize, crate::ToParameters)]
1343        // singular `parameter(...)` on the container (previously an error):
1344        #[salvo(parameter(default_parameter_in = Header))]
1345        #[allow(dead_code)]
1346        struct AliasQuery {
1347            page: i32,
1348            // plural `parameters(...)` on a field (previously ignored):
1349            #[salvo(parameters(rename = "renamed"))]
1350            raw: String,
1351        }
1352
1353        #[salvo_oapi::endpoint]
1354        async fn list(query: AliasQuery) -> &'static str {
1355            let _ = query;
1356            "ok"
1357        }
1358
1359        let router = Router::with_path("/alias").get(list);
1360        let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1361        let operation = doc
1362            .paths
1363            .get("/alias")
1364            .and_then(|item| item.operations.get(&PathItemType::Get))
1365            .expect("get operation should exist");
1366        let names: Vec<&str> = operation
1367            .parameters
1368            .0
1369            .iter()
1370            .map(|p| p.name.as_str())
1371            .collect();
1372
1373        // container singular alias honored: every parameter is in `header`.
1374        for param in &operation.parameters.0 {
1375            assert_eq!(
1376                param.parameter_in,
1377                ParameterIn::Header,
1378                "parameter `{}` should inherit the container `default_parameter_in`",
1379                param.name
1380            );
1381        }
1382        // field plural alias honored: `raw` was renamed to `renamed`.
1383        assert!(
1384            names.contains(&"renamed"),
1385            "field rename alias not applied: {names:?}"
1386        );
1387        assert!(!names.contains(&"raw"));
1388    }
1389
1390    #[test]
1391    fn merge_router_skips_route_without_method_filter() {
1392        #[salvo_oapi::endpoint]
1393        async fn any_handler() -> &'static str {
1394            "ok"
1395        }
1396
1397        // `goal()` does not attach a method filter; the route matches every HTTP method.
1398        // OpenAPI 3.1 has no equivalent "any" operation, so the route should be skipped
1399        // rather than fabricating four operations under it.
1400        let router = Router::with_path("/no-method").goal(any_handler);
1401        let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1402
1403        assert!(
1404            !doc.paths.contains_key("/no-method"),
1405            "expected no path entry when the route lacks a method filter; \
1406             got: {:?}",
1407            doc.paths.keys().collect::<Vec<_>>()
1408        );
1409    }
1410
1411    #[test]
1412    fn merge_router_attaches_only_to_explicit_method() {
1413        #[salvo_oapi::endpoint]
1414        async fn delete_thing() -> &'static str {
1415            "ok"
1416        }
1417
1418        // Sanity check that explicit method filters still work and do not pull in
1419        // sibling methods (a regression guard against the prior 4-method fan-out).
1420        let router = Router::with_path("/thing").delete(delete_thing);
1421        let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1422
1423        let path_item = doc.paths.get("/thing").expect("/thing entry should exist");
1424        assert!(path_item.operations.contains_key(&PathItemType::Delete));
1425        assert!(!path_item.operations.contains_key(&PathItemType::Get));
1426        assert!(!path_item.operations.contains_key(&PathItemType::Post));
1427        assert!(!path_item.operations.contains_key(&PathItemType::Put));
1428        assert!(!path_item.operations.contains_key(&PathItemType::Patch));
1429    }
1430
1431    #[test]
1432    fn test_build_openapi() {
1433        let _doc = OpenApi::new("pet api", "0.1.0")
1434            .info(Info::new("my pet api", "0.2.0"))
1435            .servers(Servers::new())
1436            .add_path(
1437                "/api/v1",
1438                PathItem::new(PathItemType::Get, Operation::new()),
1439            )
1440            .security([SecurityRequirement::default()])
1441            .add_security_scheme(
1442                "api_key",
1443                SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1444            )
1445            .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1446            .add_schema("example", Schema::object(Object::new()))
1447            .extend_schemas([("", Schema::from(Object::new()))])
1448            .response("200", Response::new("OK"))
1449            .extend_responses([("404", Response::new("Not Found"))])
1450            .tags(["tag1", "tag2"])
1451            .external_docs(ExternalDocs::default())
1452            .into_router("/openapi/doc");
1453    }
1454
1455    #[test]
1456    fn json_schema_dialect_serializes_under_spec_field_name() -> Result<(), serde_json::Error> {
1457        let doc = OpenApi::new("api", "0.1.0")
1458            .json_schema_dialect("https://json-schema.org/draft/2020-12/schema");
1459        let value: Value = serde_json::from_str(&doc.to_json()?)?;
1460
1461        assert_eq!(
1462            value["jsonSchemaDialect"],
1463            Value::String("https://json-schema.org/draft/2020-12/schema".to_owned()),
1464            "expected top-level `jsonSchemaDialect` field per OpenAPI 3.1.0"
1465        );
1466        assert!(
1467            value.get("$schema").is_none(),
1468            "`$schema` is the JSON Schema keyword inside Schema Objects, not the OpenAPI \
1469             document-level field"
1470        );
1471        Ok(())
1472    }
1473
1474    #[test]
1475    fn json_schema_dialect_omits_field_when_empty() -> Result<(), serde_json::Error> {
1476        let doc = OpenApi::new("api", "0.1.0");
1477        let value: Value = serde_json::from_str(&doc.to_json()?)?;
1478
1479        assert!(value.get("jsonSchemaDialect").is_none());
1480        assert!(value.get("$schema").is_none());
1481        Ok(())
1482    }
1483
1484    #[test]
1485    fn webhooks_omits_field_when_empty() -> Result<(), serde_json::Error> {
1486        let doc = OpenApi::new("api", "0.1.0");
1487        let value: Value = serde_json::from_str(&doc.to_json()?)?;
1488
1489        assert!(value.get("webhooks").is_none());
1490        Ok(())
1491    }
1492
1493    #[test]
1494    fn webhooks_serializes_inline_path_item() -> Result<(), serde_json::Error> {
1495        let doc = OpenApi::new("api", "0.1.0").add_webhook(
1496            "newPet",
1497            PathItem::new(
1498                PathItemType::Post,
1499                Operation::new().add_response("200", Response::new("acknowledged")),
1500            ),
1501        );
1502        let value: Value = serde_json::from_str(&doc.to_json()?)?;
1503
1504        assert_eq!(
1505            value["webhooks"],
1506            json!({
1507                "newPet": {
1508                    "post": {
1509                        "responses": {
1510                            "200": { "description": "acknowledged" }
1511                        }
1512                    }
1513                }
1514            })
1515        );
1516        Ok(())
1517    }
1518
1519    #[test]
1520    fn webhooks_serializes_reference_object() -> Result<(), serde_json::Error> {
1521        let doc = OpenApi::new("api", "0.1.0").add_webhook(
1522            "newPet",
1523            RefOr::Ref(Ref::new("#/components/pathItems/NewPetWebhook")),
1524        );
1525        let value: Value = serde_json::from_str(&doc.to_json()?)?;
1526
1527        assert_eq!(
1528            value["webhooks"]["newPet"],
1529            json!({ "$ref": "#/components/pathItems/NewPetWebhook" })
1530        );
1531        Ok(())
1532    }
1533
1534    #[test]
1535    fn webhooks_merge_combines_entries() {
1536        let api_a = OpenApi::new("a", "1.0").add_webhook(
1537            "newPet",
1538            PathItem::new(PathItemType::Post, Operation::new()),
1539        );
1540        let api_b = OpenApi::new("b", "1.0").add_webhook(
1541            "deletedPet",
1542            PathItem::new(PathItemType::Post, Operation::new()),
1543        );
1544
1545        let merged = api_a.merge(api_b);
1546
1547        assert!(merged.webhooks.contains_key("newPet"));
1548        assert!(merged.webhooks.contains_key("deletedPet"));
1549    }
1550
1551    #[test]
1552    fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1553        let raw_json = r#"{
1554            "openapi": "3.1.0",
1555            "info": {
1556                "title": "My api",
1557                "description": "My api description",
1558                "license": {
1559                "name": "MIT",
1560                "url": "http://mit.licence"
1561                },
1562                "version": "1.0.0",
1563                "contact": {},
1564                "termsOfService": "terms of service"
1565            },
1566            "paths": {}
1567        }"#;
1568        let doc: OpenApi = OpenApi::with_info(
1569            Info::default()
1570                .description("My api description")
1571                .license(License::new("MIT").url("http://mit.licence"))
1572                .title("My api")
1573                .version("1.0.0")
1574                .terms_of_service("terms of service")
1575                .contact(Contact::default()),
1576        );
1577        let serialized = doc.to_pretty_json()?;
1578
1579        assert_eq!(
1580            Value::from_str(&serialized)?,
1581            Value::from_str(raw_json)?,
1582            "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1583        );
1584        Ok(())
1585    }
1586
1587    #[test]
1588    fn test_deprecated_from_bool() {
1589        assert_eq!(Deprecated::True, Deprecated::from(true));
1590        assert_eq!(Deprecated::False, Deprecated::from(false));
1591    }
1592
1593    #[test]
1594    fn test_deprecated_deserialize() {
1595        let deserialize_result = serde_json::from_str::<Deprecated>("true");
1596        assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1597        let deserialize_result = serde_json::from_str::<Deprecated>("false");
1598        assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1599    }
1600
1601    #[test]
1602    fn test_required_from_bool() {
1603        assert_eq!(Required::True, Required::from(true));
1604        assert_eq!(Required::False, Required::from(false));
1605    }
1606
1607    #[test]
1608    fn test_required_deserialize() {
1609        let deserialize_result = serde_json::from_str::<Required>("true");
1610        assert_eq!(deserialize_result.unwrap(), Required::True);
1611        let deserialize_result = serde_json::from_str::<Required>("false");
1612        assert_eq!(deserialize_result.unwrap(), Required::False);
1613    }
1614
1615    #[tokio::test]
1616    async fn test_openapi_handle() {
1617        let doc = OpenApi::new("pet api", "0.1.0");
1618        let mut req = Request::new();
1619        let mut depot = Depot::new();
1620        let mut res = salvo_core::Response::new();
1621        let mut ctrl = FlowCtrl::default();
1622        doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1623
1624        let bytes = match res.body.take() {
1625            ResBody::Once(bytes) => bytes,
1626            _ => Bytes::new(),
1627        };
1628
1629        assert_eq!(
1630            res.content_type()
1631                .expect("content type should exist")
1632                .to_string(),
1633            "application/json; charset=utf-8".to_owned()
1634        );
1635        assert_eq!(
1636            bytes,
1637            Bytes::from_static(
1638                b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1639            )
1640        );
1641    }
1642
1643    #[tokio::test]
1644    async fn test_openapi_handle_pretty() {
1645        let doc = OpenApi::new("pet api", "0.1.0");
1646
1647        let mut req = Request::new();
1648        req.queries_mut()
1649            .insert("pretty".to_owned(), "true".to_owned());
1650
1651        let mut depot = Depot::new();
1652        let mut res = salvo_core::Response::new();
1653        let mut ctrl = FlowCtrl::default();
1654        doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1655
1656        let bytes = match res.body.take() {
1657            ResBody::Once(bytes) => bytes,
1658            _ => Bytes::new(),
1659        };
1660
1661        assert_eq!(
1662            res.content_type()
1663                .expect("content type should exist")
1664                .to_string(),
1665            "application/json; charset=utf-8".to_owned()
1666        );
1667        assert_eq!(
1668            bytes,
1669            Bytes::from_static(b"{\n  \"openapi\": \"3.1.0\",\n  \"info\": {\n    \"title\": \"pet api\",\n    \"version\": \"0.1.0\"\n  },\n  \"paths\": {}\n}")
1670        );
1671    }
1672
1673    #[test]
1674    fn test_openapi_schema_work_with_generics() {
1675        // Reset global namer state to ensure deterministic test results
1676        crate::naming::set_namer(crate::naming::FlexNamer::new());
1677
1678        #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1679        #[salvo(schema(name = City))]
1680        pub(crate) struct CityDTO {
1681            #[salvo(schema(rename = "id"))]
1682            pub(crate) id: String,
1683            #[salvo(schema(rename = "name"))]
1684            pub(crate) name: String,
1685        }
1686
1687        #[derive(Serialize, Deserialize, Debug, ToSchema)]
1688        #[salvo(schema(name = Response))]
1689        pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1690            #[salvo(schema(rename = "status"))]
1691            /// status code
1692            pub(crate) status: String,
1693            #[salvo(schema(rename = "msg"))]
1694            /// Status msg
1695            pub(crate) message: String,
1696            #[salvo(schema(rename = "data"))]
1697            /// The data returned
1698            pub(crate) data: T,
1699        }
1700
1701        #[salvo_oapi::endpoint(
1702            operation_id = "get_all_cities",
1703            tags("city"),
1704            status_codes(200, 400, 401, 403, 500)
1705        )]
1706        pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1707            Ok(Json(ApiResponse {
1708                status: "200".to_owned(),
1709                message: "OK".to_owned(),
1710                data: vec![CityDTO {
1711                    id: "1".to_owned(),
1712                    name: "Beijing".to_owned(),
1713                }],
1714            }))
1715        }
1716
1717        let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1718            .add_server(Server::new("/api/bar/").description("this is description of the server"));
1719
1720        let router = Router::with_path("/cities").get(get_all_cities);
1721        let doc = doc.merge_router(&router);
1722
1723        assert_eq!(
1724            json! {{
1725                "openapi": "3.1.0",
1726                "info": {
1727                    "title": "my application",
1728                    "version": "0.1.0"
1729                },
1730                "servers": [
1731                    {
1732                        "url": "/api/bar/",
1733                        "description": "this is description of the server"
1734                    }
1735                ],
1736                "paths": {
1737                    "/cities": {
1738                        "get": {
1739                            "tags": [
1740                                "city"
1741                            ],
1742                            "operationId": "get_all_cities",
1743                            "responses": {
1744                                "200": {
1745                                    "description": "Response with json format data",
1746                                    "content": {
1747                                        "application/json": {
1748                                            "schema": {
1749                                                "$ref": "#/components/schemas/Response<alloc.vec.Vec<City>>"
1750                                            }
1751                                        }
1752                                    }
1753                                },
1754                                "400": {
1755                                    "description": "The request could not be understood by the server due to malformed syntax.",
1756                                    "content": {
1757                                        "application/json": {
1758                                            "schema": {
1759                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1760                                            }
1761                                        }
1762                                    }
1763                                },
1764                                "401": {
1765                                    "description": "The request requires user authentication.",
1766                                    "content": {
1767                                        "application/json": {
1768                                            "schema": {
1769                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1770                                            }
1771                                        }
1772                                    }
1773                                },
1774                                "403": {
1775                                    "description": "The server refused to authorize the request.",
1776                                    "content": {
1777                                        "application/json": {
1778                                            "schema": {
1779                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1780                                            }
1781                                        }
1782                                    }
1783                                },
1784                                "500": {
1785                                    "description": "The server encountered an internal error while processing this request.",
1786                                    "content": {
1787                                        "application/json": {
1788                                            "schema": {
1789                                                "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1790                                            }
1791                                        }
1792                                    }
1793                                }
1794                            }
1795                        }
1796                    }
1797                },
1798                "components": {
1799                    "schemas": {
1800                        "City": {
1801                            "type": "object",
1802                            "required": [
1803                                "id",
1804                                "name"
1805                            ],
1806                            "properties": {
1807                                "id": {
1808                                    "type": "string"
1809                                },
1810                                "name": {
1811                                    "type": "string"
1812                                }
1813                            }
1814                        },
1815                        "Response<alloc.vec.Vec<City>>": {
1816                            "type": "object",
1817                            "required": [
1818                                "status",
1819                                "msg",
1820                                "data"
1821                            ],
1822                            "properties": {
1823                                "data": {
1824                                    "allOf": [
1825                                        {
1826                                            "type": "array",
1827                                            "items": {
1828                                                "$ref": "#/components/schemas/City"
1829                                            }
1830                                        },
1831                                        {
1832                                            "description": "The data returned"
1833                                        }
1834                                    ]
1835                                },
1836                                "msg": {
1837                                    "type": "string",
1838                                    "description": "Status msg"
1839                                },
1840                                "status": {
1841                                    "type": "string",
1842                                    "description": "status code"
1843                                }
1844                            }
1845                        },
1846                        "salvo_core.http.errors.status_error.StatusError": {
1847                            "type": "object",
1848                            "required": [
1849                                "code",
1850                                "name",
1851                                "brief",
1852                                "detail"
1853                            ],
1854                            "properties": {
1855                                "brief": {
1856                                    "type": "string"
1857                                },
1858                                "cause": {
1859                                    "type": "string"
1860                                },
1861                                "code": {
1862                                    "type": "integer",
1863                                    "format": "uint16",
1864                                    "minimum": 0
1865                                },
1866                                "detail": {
1867                                    "type": "string"
1868                                },
1869                                "name": {
1870                                    "type": "string"
1871                                }
1872                            }
1873                        }
1874                    }
1875                }
1876            }},
1877            Value::from_str(&doc.to_json().unwrap()).unwrap()
1878        );
1879    }
1880}