Skip to main content

salvo_oapi/
openapi.rs

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