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