Skip to main content

salvo_oapi/
openapi.rs

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