1use salvo_core::cfg_feature;
4
5mod callback;
6mod components;
7mod content;
8mod encoding;
9mod example;
10mod external_docs;
11mod header;
12pub mod info;
13mod link;
14pub mod operation;
15pub mod parameter;
16pub mod path;
17pub mod request_body;
18pub mod response;
19pub mod schema;
20pub mod security;
21pub mod server;
22mod tag;
23mod xml;
24
25use std::collections::BTreeSet;
26use std::fmt::{self, Debug, Formatter};
27use std::sync::LazyLock;
28
29use regex::Regex;
30use salvo_core::{Depot, FlowCtrl, Handler, Router, async_trait, writing};
31use serde::de::{Error, Expected, Visitor};
32use serde::{Deserialize, Deserializer, Serialize, Serializer};
33
34pub use self::callback::Callback;
35pub use self::components::Components;
36pub use self::content::Content;
37pub use self::encoding::Encoding;
38pub use self::example::Example;
39pub use self::external_docs::ExternalDocs;
40pub use self::header::Header;
41pub use self::info::{Contact, Info, License};
42pub use self::link::Link;
43pub use self::operation::{Operation, Operations};
44pub use self::parameter::{Parameter, ParameterIn, ParameterStyle, Parameters};
45pub use self::path::{PathItem, PathItemType, Paths};
46pub use self::request_body::RequestBody;
47pub use self::response::{Response, Responses};
48pub use self::schema::{
49 Array, ArrayItems, BasicType, Discriminator, KnownFormat, Number, Object, Ref, Schema,
50 SchemaFormat, SchemaType, Schemas,
51};
52pub use self::security::{SecurityRequirement, SecurityScheme};
53pub use self::server::{Server, ServerVariable, ServerVariables, Servers};
54pub use self::tag::Tag;
55pub use self::xml::Xml;
56use crate::Endpoint;
57use crate::routing::NormNode;
58
59static PATH_PARAMETER_NAME_REGEX: LazyLock<Regex> =
60 LazyLock::new(|| Regex::new(r"\{([^}:]+)").expect("invalid regex"));
61
62#[cfg(not(feature = "preserve-path-order"))]
64pub type PathMap<K, V> = std::collections::BTreeMap<K, V>;
65#[cfg(feature = "preserve-path-order")]
67pub type PathMap<K, V> = indexmap::IndexMap<K, V>;
68
69#[cfg(not(feature = "preserve-prop-order"))]
71pub type PropMap<K, V> = std::collections::BTreeMap<K, V>;
72#[cfg(feature = "preserve-prop-order")]
74pub type PropMap<K, V> = indexmap::IndexMap<K, V>;
75
76#[non_exhaustive]
85#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
86#[serde(rename_all = "camelCase")]
87pub struct OpenApi {
88 pub openapi: OpenApiVersion,
90
91 #[serde(rename = "$self", default, skip_serializing_if = "String::is_empty")]
98 pub self_uri: String,
99
100 pub info: Info,
104
105 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
111 pub servers: BTreeSet<Server>,
112
113 pub paths: Paths,
117
118 #[serde(skip_serializing_if = "PathMap::is_empty", default)]
125 pub webhooks: PathMap<String, RefOr<PathItem>>,
126
127 #[serde(skip_serializing_if = "Components::is_empty")]
133 pub components: Components,
134
135 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
141 pub security: BTreeSet<SecurityRequirement>,
142
143 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
147 pub tags: BTreeSet<Tag>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
153 pub external_docs: Option<ExternalDocs>,
154
155 #[serde(default, skip_serializing_if = "String::is_empty")]
161 pub json_schema_dialect: String,
162
163 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
165 pub extensions: PropMap<String, serde_json::Value>,
166}
167
168impl OpenApi {
169 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 #[must_use]
196 pub fn with_info(info: Info) -> Self {
197 Self {
198 info,
199 ..Default::default()
200 }
201 }
202
203 #[must_use]
215 pub fn openapi_version(mut self, version: OpenApiVersion) -> Self {
216 self.openapi = version;
217 self
218 }
219
220 #[must_use]
235 pub fn self_uri(mut self, self_uri: impl Into<String>) -> Self {
236 self.self_uri = self_uri.into();
237 self
238 }
239
240 pub fn to_json(&self) -> Result<String, serde_json::Error> {
243 serde_json::to_string(self)
244 }
245
246 pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
249 serde_json::to_string_pretty(self)
250 }
251
252 cfg_feature! {
253 #![feature ="yaml"]
254 pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
256 serde_norway::to_string(self)
257 }
258 }
259
260 #[must_use]
276 pub fn merge(mut self, mut other: Self) -> Self {
277 self.servers.append(&mut other.servers);
278 self.paths.append(&mut other.paths);
279 for (name, item) in std::mem::take(&mut other.webhooks) {
280 self.webhooks.insert(name, item);
281 }
282 self.components.append(&mut other.components);
283 self.security.append(&mut other.security);
284 self.tags.append(&mut other.tags);
285 self
286 }
287
288 #[must_use]
302 pub fn nest<P: Into<String>>(self, path: P, other: Self) -> Self {
303 self.nest_with_path_composer(path, other, |base, item_path| {
304 format!(
305 "{}/{}",
306 base.trim_end_matches('/'),
307 item_path.trim_start_matches('/')
308 )
309 })
310 }
311
312 #[must_use]
320 pub fn nest_with_path_composer<P: Into<String>, F: Fn(&str, &str) -> String>(
321 mut self,
322 path: P,
323 mut other: Self,
324 composer: F,
325 ) -> Self {
326 let path: String = path.into();
327
328 let other_paths = std::mem::take(&mut other.paths);
330 for (item_path, item) in other_paths.iter() {
331 let composed = composer(&path, item_path);
332 self.paths.insert(composed, item.clone());
333 }
334
335 self.merge(other)
338 }
339
340 #[must_use]
342 pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
343 self.info = info.into();
344 self
345 }
346
347 #[must_use]
349 pub fn servers<S: IntoIterator<Item = Server>>(mut self, servers: S) -> Self {
350 self.servers = servers.into_iter().collect();
351 self
352 }
353 #[must_use]
355 pub fn add_server<S>(mut self, server: S) -> Self
356 where
357 S: Into<Server>,
358 {
359 self.servers.insert(server.into());
360 self
361 }
362
363 #[must_use]
365 pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
366 self.paths = paths.into();
367 self
368 }
369 #[must_use]
371 pub fn add_path<P, I>(mut self, path: P, item: I) -> Self
372 where
373 P: Into<String>,
374 I: Into<PathItem>,
375 {
376 self.paths.insert(path.into(), item.into());
377 self
378 }
379
380 #[must_use]
384 pub fn webhooks<I, K, V>(mut self, webhooks: I) -> Self
385 where
386 I: IntoIterator<Item = (K, V)>,
387 K: Into<String>,
388 V: Into<RefOr<PathItem>>,
389 {
390 self.webhooks = webhooks
391 .into_iter()
392 .map(|(name, item)| (name.into(), item.into()))
393 .collect();
394 self
395 }
396
397 #[must_use]
401 pub fn add_webhook<K: Into<String>, V: Into<RefOr<PathItem>>>(
402 mut self,
403 name: K,
404 webhook: V,
405 ) -> Self {
406 self.webhooks.insert(name.into(), webhook.into());
407 self
408 }
409
410 #[must_use]
412 pub fn components(mut self, components: impl Into<Components>) -> Self {
413 self.components = components.into();
414 self
415 }
416
417 #[must_use]
419 pub fn security<S: IntoIterator<Item = SecurityRequirement>>(mut self, security: S) -> Self {
420 self.security = security.into_iter().collect();
421 self
422 }
423
424 #[must_use]
432 pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
433 mut self,
434 name: N,
435 security_scheme: S,
436 ) -> Self {
437 self.components
438 .security_schemes
439 .insert(name.into(), security_scheme.into());
440
441 self
442 }
443
444 #[must_use]
452 pub fn extend_security_schemes<
453 I: IntoIterator<Item = (N, S)>,
454 N: Into<String>,
455 S: Into<SecurityScheme>,
456 >(
457 mut self,
458 schemas: I,
459 ) -> Self {
460 self.components.security_schemes.extend(
461 schemas
462 .into_iter()
463 .map(|(name, item)| (name.into(), item.into())),
464 );
465 self
466 }
467
468 #[must_use]
472 pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
473 mut self,
474 name: S,
475 schema: I,
476 ) -> Self {
477 self.components.schemas.insert(name, schema);
478 self
479 }
480
481 #[must_use]
496 pub fn extend_schemas<I, C, S>(mut self, schemas: I) -> Self
497 where
498 I: IntoIterator<Item = (S, C)>,
499 C: Into<RefOr<Schema>>,
500 S: Into<String>,
501 {
502 self.components.schemas.extend(
503 schemas
504 .into_iter()
505 .map(|(name, schema)| (name.into(), schema.into())),
506 );
507 self
508 }
509
510 #[must_use]
512 pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
513 mut self,
514 name: S,
515 response: R,
516 ) -> Self {
517 self.components
518 .responses
519 .insert(name.into(), response.into());
520 self
521 }
522
523 #[must_use]
525 pub fn extend_responses<
526 I: IntoIterator<Item = (S, R)>,
527 S: Into<String>,
528 R: Into<RefOr<Response>>,
529 >(
530 mut self,
531 responses: I,
532 ) -> Self {
533 self.components.responses.extend(
534 responses
535 .into_iter()
536 .map(|(name, response)| (name.into(), response.into())),
537 );
538 self
539 }
540
541 #[must_use]
543 pub fn tags<I, T>(mut self, tags: I) -> Self
544 where
545 I: IntoIterator<Item = T>,
546 T: Into<Tag>,
547 {
548 self.tags = tags.into_iter().map(Into::into).collect();
549 self
550 }
551
552 #[must_use]
554 pub fn external_docs(mut self, external_docs: ExternalDocs) -> Self {
555 self.external_docs = Some(external_docs);
556 self
557 }
558
559 #[must_use]
575 pub fn json_schema_dialect<S: Into<String>>(mut self, dialect: S) -> Self {
576 self.json_schema_dialect = dialect.into();
577 self
578 }
579
580 #[must_use]
582 pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
583 self.extensions.insert(key.into(), value);
584 self
585 }
586
587 pub fn into_router(self, path: impl Into<String>) -> Router {
589 Router::with_path(path.into()).goal(self)
590 }
591
592 #[must_use]
594 pub fn merge_router(self, router: &Router) -> Self {
595 self.merge_router_with_base(router, "/")
596 }
597
598 #[must_use]
600 pub fn merge_router_with_base(mut self, router: &Router, base: impl AsRef<str>) -> Self {
601 let mut node = NormNode::new(router, Default::default());
602 self.merge_norm_node(&mut node, base.as_ref());
603 self
604 }
605
606 fn merge_norm_node(&mut self, node: &mut NormNode, base_path: &str) {
607 fn join_path(a: &str, b: &str) -> String {
608 if a.is_empty() {
609 b.to_owned()
610 } else if b.is_empty() {
611 a.to_owned()
612 } else {
613 format!("{}/{}", a.trim_end_matches('/'), b.trim_start_matches('/'))
614 }
615 }
616
617 let path = join_path(base_path, node.path.as_deref().unwrap_or_default());
618 let path_parameter_names = PATH_PARAMETER_NAME_REGEX
619 .captures_iter(&path)
620 .filter_map(|captures| {
621 captures
622 .iter()
623 .skip(1)
624 .map(|capture| {
625 capture
626 .expect("regex captures should not be None")
627 .as_str()
628 .to_owned()
629 })
630 .next()
631 })
632 .collect::<Vec<_>>();
633
634 if let Some(handler_type_id) = &node.handler_type_id
635 && let Some(creator) = crate::EndpointRegistry::find(handler_type_id)
636 {
637 if let Some(method) = node.method {
638 let Endpoint {
639 mut operation,
640 mut components,
641 } = creator();
642 operation.tags.extend(node.metadata.tags.iter().cloned());
643 operation
644 .securities
645 .extend(node.metadata.securities.iter().cloned());
646 let not_exist_parameters = operation
647 .parameters
648 .0
649 .iter()
650 .filter(|p| {
651 p.parameter_in == ParameterIn::Path
652 && !path_parameter_names.contains(&p.name)
653 })
654 .map(|p| &p.name)
655 .collect::<Vec<_>>();
656 if !not_exist_parameters.is_empty() {
657 tracing::warn!(parameters = ?not_exist_parameters, path, handler_name = node.handler_type_name, "information for not exist parameters");
658 }
659 #[cfg(debug_assertions)]
660 {
661 let meta_not_exist_parameters = path_parameter_names
662 .iter()
663 .filter(|name| {
664 !name.starts_with('*')
665 && !operation.parameters.0.iter().any(|parameter| {
666 parameter.name == **name
667 && parameter.parameter_in == ParameterIn::Path
668 })
669 })
670 .collect::<Vec<_>>();
671
672 if !meta_not_exist_parameters.is_empty() {
673 tracing::warn!(parameters = ?meta_not_exist_parameters, path, handler_name = node.handler_type_name, "parameters information not provided");
674 }
675 }
676 let path_item = self.paths.entry(path.clone()).or_default();
677 if path_item.operations.contains_key(&method) {
678 tracing::warn!(
679 "path `{}` already contains operation for method `{:?}`",
680 path,
681 method
682 );
683 } else {
684 path_item.operations.insert(method, operation);
685 }
686 self.components.append(&mut components);
687 } else {
688 tracing::warn!(
693 path,
694 handler_name = node.handler_type_name,
695 "endpoint has no HTTP method filter; skipping in OpenAPI document. \
696 Add `.get()`, `.post()`, etc. to the router to include it"
697 );
698 }
699 }
700
701 for child in &mut node.children {
702 self.merge_norm_node(child, &path);
703 }
704 }
705}
706
707#[async_trait]
708impl Handler for OpenApi {
709 async fn handle(
710 &self,
711 req: &mut salvo_core::Request,
712 _depot: &mut Depot,
713 res: &mut salvo_core::Response,
714 _ctrl: &mut FlowCtrl,
715 ) {
716 let pretty = req
717 .queries()
718 .get("pretty")
719 .map(|v| &**v != "false")
720 .unwrap_or(false);
721 let content = if pretty {
722 self.to_pretty_json().unwrap_or_default()
723 } else {
724 self.to_json().unwrap_or_default()
725 };
726 res.render(writing::Text::Json(&content));
727 }
728}
729#[derive(Serialize, Clone, PartialEq, Eq, Default, Debug)]
733pub enum OpenApiVersion {
734 #[serde(rename = "3.1.0")]
736 #[default]
737 Version3_1,
738 #[serde(rename = "3.2.0")]
740 Version3_2,
741}
742
743impl<'de> Deserialize<'de> for OpenApiVersion {
744 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
745 where
746 D: Deserializer<'de>,
747 {
748 struct VersionVisitor;
749
750 impl Visitor<'_> for VersionVisitor {
751 type Value = OpenApiVersion;
752
753 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
754 formatter.write_str("a version string in 3.1.x or 3.2.x format")
755 }
756
757 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
758 where
759 E: Error,
760 {
761 self.visit_string(v.to_owned())
762 }
763
764 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
765 where
766 E: Error,
767 {
768 let mut digits = v.split('.').map(|digit| digit.parse::<u32>());
769 let version = match (digits.next(), digits.next(), digits.next(), digits.next()) {
770 (Some(Ok(3)), Some(Ok(minor)), Some(Ok(_)), None) => minor,
771 _ => {
772 let expected: &dyn Expected = &"3.1.x or 3.2.x";
773 return Err(Error::invalid_value(
774 serde::de::Unexpected::Str(&v),
775 expected,
776 ));
777 }
778 };
779
780 match version {
781 1 => Ok(OpenApiVersion::Version3_1),
782 2 => Ok(OpenApiVersion::Version3_2),
783 _ => {
784 let expected: &dyn Expected = &"3.1.x or 3.2.x";
785 Err(Error::invalid_value(
786 serde::de::Unexpected::Str(&v),
787 expected,
788 ))
789 }
790 }
791 }
792 }
793
794 deserializer.deserialize_string(VersionVisitor)
795 }
796}
797
798#[derive(PartialEq, Eq, Clone, Debug)]
802pub enum Deprecated {
803 True,
805 False,
807}
808impl From<bool> for Deprecated {
809 fn from(b: bool) -> Self {
810 if b { Self::True } else { Self::False }
811 }
812}
813
814impl Serialize for Deprecated {
815 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
816 where
817 S: Serializer,
818 {
819 serializer.serialize_bool(matches!(self, Self::True))
820 }
821}
822
823impl<'de> Deserialize<'de> for Deprecated {
824 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
825 where
826 D: serde::Deserializer<'de>,
827 {
828 struct BoolVisitor;
829 impl Visitor<'_> for BoolVisitor {
830 type Value = Deprecated;
831
832 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
833 formatter.write_str("a bool true or false")
834 }
835
836 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
837 where
838 E: serde::de::Error,
839 {
840 match v {
841 true => Ok(Deprecated::True),
842 false => Ok(Deprecated::False),
843 }
844 }
845 }
846 deserializer.deserialize_bool(BoolVisitor)
847 }
848}
849
850#[derive(PartialEq, Eq, Default, Clone, Debug)]
854pub enum Required {
855 True,
857 False,
859 #[default]
861 Unset,
862}
863
864impl From<bool> for Required {
865 fn from(value: bool) -> Self {
866 if value { Self::True } else { Self::False }
867 }
868}
869
870impl Serialize for Required {
871 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
872 where
873 S: Serializer,
874 {
875 serializer.serialize_bool(matches!(self, Self::True))
876 }
877}
878
879impl<'de> Deserialize<'de> for Required {
880 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
881 where
882 D: serde::Deserializer<'de>,
883 {
884 struct BoolVisitor;
885 impl Visitor<'_> for BoolVisitor {
886 type Value = Required;
887
888 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
889 formatter.write_str("a bool true or false")
890 }
891
892 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
893 where
894 E: serde::de::Error,
895 {
896 match v {
897 true => Ok(Required::True),
898 false => Ok(Required::False),
899 }
900 }
901 }
902 deserializer.deserialize_bool(BoolVisitor)
903 }
904}
905
906#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
911#[serde(untagged)]
912pub enum RefOr<T> {
913 Ref(schema::Ref),
915 Type(T),
917}
918
919#[cfg(test)]
920mod tests {
921 use std::fmt::Debug;
922 use std::str::FromStr;
923
924 use bytes::Bytes;
925 use salvo_core::http::ResBody;
926 use salvo_core::prelude::*;
927 use serde_json::{Value, json};
928
929 use super::response::Response;
930 use super::*;
931 use crate::ToSchema;
932 use crate::extract::*;
933 use crate::security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme};
934 use crate::server::Server;
935
936 #[test]
937 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
938 assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
939 assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_2)?, "3.2.0");
940 assert_eq!(
941 serde_json::from_str::<OpenApiVersion>(r#""3.1.9""#)?,
942 OpenApiVersion::Version3_1
943 );
944 assert_eq!(
945 serde_json::from_str::<OpenApiVersion>(r#""3.2.7""#)?,
946 OpenApiVersion::Version3_2
947 );
948 Ok(())
949 }
950
951 #[test]
952 fn deserialize_openapi_version_rejects_unsupported_or_malformed_versions() {
953 for version in ["3.0.4", "3.3.0", "3.2", "3.2.x", "4.0.0"] {
954 assert!(
955 serde_json::from_value::<OpenApiVersion>(version.into()).is_err(),
956 "expected {version} to be rejected"
957 );
958 }
959 }
960
961 #[test]
962 fn openapi_3_2_baseline_fields_serialize_and_deserialize() -> Result<(), serde_json::Error> {
963 let doc = OpenApi::new("pet api", "0.1.0")
964 .openapi_version(OpenApiVersion::Version3_2)
965 .self_uri("https://example.com/openapi.json");
966
967 let value = serde_json::to_value(&doc)?;
968 assert_eq!(value["openapi"], "3.2.0");
969 assert_eq!(value["$self"], "https://example.com/openapi.json");
970
971 let deserialized: OpenApi = serde_json::from_value(json!({
972 "openapi": "3.2.4",
973 "$self": "https://example.com/openapi.json",
974 "info": {
975 "title": "pet api",
976 "version": "0.1.0"
977 },
978 "servers": [],
979 "paths": {},
980 "components": {},
981 "security": [],
982 "tags": []
983 }))?;
984 assert_eq!(deserialized.openapi, OpenApiVersion::Version3_2);
985 assert_eq!(deserialized.self_uri, "https://example.com/openapi.json");
986 Ok(())
987 }
988
989 #[test]
990 fn openapi_defaults_to_3_1_and_omits_empty_self_uri() -> Result<(), serde_json::Error> {
991 let value = serde_json::to_value(OpenApi::new("pet api", "0.1.0"))?;
992
993 assert_eq!(value["openapi"], "3.1.0");
994 assert!(value.get("$self").is_none());
995 Ok(())
996 }
997
998 #[test]
999 fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
1000 let raw_json = r#"{
1001 "openapi": "3.1.0",
1002 "info": {
1003 "title": "My api",
1004 "description": "My api description",
1005 "license": {
1006 "name": "MIT",
1007 "url": "http://mit.licence"
1008 },
1009 "version": "1.0.0",
1010 "contact": {},
1011 "termsOfService": "terms of service"
1012 },
1013 "paths": {}
1014 }"#;
1015 let doc: OpenApi = OpenApi::with_info(
1016 Info::default()
1017 .description("My api description")
1018 .license(License::new("MIT").url("http://mit.licence"))
1019 .title("My api")
1020 .version("1.0.0")
1021 .terms_of_service("terms of service")
1022 .contact(Contact::default()),
1023 );
1024 let serialized = doc.to_json()?;
1025
1026 assert_eq!(
1027 Value::from_str(&serialized)?,
1028 Value::from_str(raw_json)?,
1029 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1030 );
1031 Ok(())
1032 }
1033
1034 #[test]
1035 fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
1036 let doc = OpenApi::new("My big api", "1.1.0").paths(
1037 Paths::new()
1038 .path(
1039 "/api/v1/users",
1040 PathItem::new(
1041 PathItemType::Get,
1042 Operation::new().add_response("200", Response::new("Get users list")),
1043 ),
1044 )
1045 .path(
1046 "/api/v1/users",
1047 PathItem::new(
1048 PathItemType::Post,
1049 Operation::new().add_response("200", Response::new("Post new user")),
1050 ),
1051 )
1052 .path(
1053 "/api/v1/users/{id}",
1054 PathItem::new(
1055 PathItemType::Get,
1056 Operation::new().add_response("200", Response::new("Get user by id")),
1057 ),
1058 ),
1059 );
1060
1061 let serialized = doc.to_json()?;
1062 let expected = r#"
1063 {
1064 "openapi": "3.1.0",
1065 "info": {
1066 "title": "My big api",
1067 "version": "1.1.0"
1068 },
1069 "paths": {
1070 "/api/v1/users": {
1071 "get": {
1072 "responses": {
1073 "200": {
1074 "description": "Get users list"
1075 }
1076 }
1077 },
1078 "post": {
1079 "responses": {
1080 "200": {
1081 "description": "Post new user"
1082 }
1083 }
1084 }
1085 },
1086 "/api/v1/users/{id}": {
1087 "get": {
1088 "responses": {
1089 "200": {
1090 "description": "Get user by id"
1091 }
1092 }
1093 }
1094 }
1095 }
1096 }
1097 "#
1098 .replace("\r\n", "\n");
1099
1100 assert_eq!(
1101 Value::from_str(&serialized)?,
1102 Value::from_str(&expected)?,
1103 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
1104 );
1105 Ok(())
1106 }
1107
1108 #[test]
1109 fn merge_2_openapi_documents() {
1110 let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
1111 "/api/v1/user",
1112 PathItem::new(
1113 PathItemType::Get,
1114 Operation::new().add_response("200", Response::new("This will not get added")),
1115 ),
1116 ));
1117
1118 let api_2 = OpenApi::new("Api", "v2")
1119 .paths(
1120 Paths::new()
1121 .path(
1122 "/api/v1/user",
1123 PathItem::new(
1124 PathItemType::Get,
1125 Operation::new().add_response("200", Response::new("Get user success")),
1126 ),
1127 )
1128 .path(
1129 "/ap/v2/user",
1130 PathItem::new(
1131 PathItemType::Get,
1132 Operation::new()
1133 .add_response("200", Response::new("Get user success 2")),
1134 ),
1135 )
1136 .path(
1137 "/api/v2/user",
1138 PathItem::new(
1139 PathItemType::Post,
1140 Operation::new().add_response("200", Response::new("Get user success")),
1141 ),
1142 ),
1143 )
1144 .components(
1145 Components::new().add_schema(
1146 "User2",
1147 Object::new()
1148 .schema_type(BasicType::Object)
1149 .property("name", Object::new().schema_type(BasicType::String)),
1150 ),
1151 );
1152
1153 api_1 = api_1.merge(api_2);
1154 let value = serde_json::to_value(&api_1).unwrap();
1155
1156 assert_eq!(
1157 value,
1158 json!(
1159 {
1160 "openapi": "3.1.0",
1161 "info": {
1162 "title": "Api",
1163 "version": "v1"
1164 },
1165 "paths": {
1166 "/ap/v2/user": {
1167 "get": {
1168 "responses": {
1169 "200": {
1170 "description": "Get user success 2"
1171 }
1172 }
1173 }
1174 },
1175 "/api/v1/user": {
1176 "get": {
1177 "responses": {
1178 "200": {
1179 "description": "Get user success"
1180 }
1181 }
1182 }
1183 },
1184 "/api/v2/user": {
1185 "post": {
1186 "responses": {
1187 "200": {
1188 "description": "Get user success"
1189 }
1190 }
1191 }
1192 }
1193 },
1194 "components": {
1195 "schemas": {
1196 "User2": {
1197 "type": "object",
1198 "properties": {
1199 "name": {
1200 "type": "string"
1201 }
1202 }
1203 }
1204 }
1205 }
1206 }
1207 )
1208 )
1209 }
1210
1211 #[test]
1212 fn test_simple_document_with_security() {
1213 #[derive(Deserialize, Serialize, ToSchema)]
1214 #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
1215 struct Pet {
1216 id: u64,
1217 name: String,
1218 age: Option<i32>,
1219 }
1220
1221 #[salvo_oapi::endpoint(
1225 responses(
1226 (status_code = 200, description = "Pet found successfully"),
1227 (status_code = 404, description = "Pet was not found")
1228 ),
1229 parameters(
1230 ("id", description = "Pet database id to get Pet for"),
1231 ),
1232 security(
1233 (),
1234 ("my_auth" = ["read:items", "edit:items"]),
1235 ("token_jwt" = []),
1236 ("api_key1" = [], "api_key2" = []),
1237 )
1238 )]
1239 pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1240 let pet = Pet {
1241 id: pet_id.into_inner(),
1242 age: None,
1243 name: "lightning".to_owned(),
1244 };
1245 Json(pet)
1246 }
1247
1248 let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1249 Server::new("/api/bar/")
1250 .description("this is description of the server")
1251 .add_variable(
1252 "username",
1253 ServerVariable::new()
1254 .default_value("the_user")
1255 .description("this is user"),
1256 ),
1257 );
1258 doc.components.security_schemes.insert(
1259 "token_jwt".into(),
1260 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1261 );
1262
1263 let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1264 let doc = doc.merge_router(&router);
1265
1266 assert_eq!(
1267 Value::from_str(
1268 r#"{
1269 "openapi": "3.1.0",
1270 "info": {
1271 "title": "my application",
1272 "version": "0.1.0"
1273 },
1274 "servers": [
1275 {
1276 "url": "/api/bar/",
1277 "description": "this is description of the server",
1278 "variables": {
1279 "username": {
1280 "default": "the_user",
1281 "description": "this is user"
1282 }
1283 }
1284 }
1285 ],
1286 "paths": {
1287 "/pets/{id}": {
1288 "get": {
1289 "summary": "Get pet by id",
1290 "description": "Get pet from database by pet database id",
1291 "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1292 "parameters": [
1293 {
1294 "name": "pet_id",
1295 "in": "path",
1296 "description": "Get parameter `pet_id` from request url path.",
1297 "required": true,
1298 "schema": {
1299 "type": "integer",
1300 "format": "uint64",
1301 "minimum": 0
1302 }
1303 },
1304 {
1305 "name": "id",
1306 "in": "path",
1307 "description": "Pet database id to get Pet for",
1308 "required": true
1309 }
1310 ],
1311 "responses": {
1312 "200": {
1313 "description": "Pet found successfully"
1314 },
1315 "404": {
1316 "description": "Pet was not found"
1317 }
1318 },
1319 "security": [
1320 {},
1321 {
1322 "my_auth": [
1323 "read:items",
1324 "edit:items"
1325 ]
1326 },
1327 {
1328 "token_jwt": []
1329 },
1330 {
1331 "api_key1": [],
1332 "api_key2": []
1333 }
1334 ]
1335 }
1336 }
1337 },
1338 "components": {
1339 "schemas": {
1340 "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1341 "type": "object",
1342 "required": [
1343 "id",
1344 "name"
1345 ],
1346 "properties": {
1347 "age": {
1348 "type": ["integer", "null"],
1349 "format": "int32"
1350 },
1351 "id": {
1352 "type": "integer",
1353 "format": "uint64",
1354 "minimum": 0
1355 },
1356 "name": {
1357 "type": "string"
1358 }
1359 },
1360 "examples": [{
1361 "id": 1,
1362 "name": "bob the cat"
1363 }]
1364 }
1365 },
1366 "securitySchemes": {
1367 "token_jwt": {
1368 "type": "http",
1369 "scheme": "bearer",
1370 "bearerFormat": "JWT"
1371 }
1372 }
1373 }
1374 }"#
1375 )
1376 .unwrap(),
1377 Value::from_str(&doc.to_json().unwrap()).unwrap()
1378 );
1379 }
1380
1381 #[test]
1382 fn merge_router_normalizes_constrained_path_params() {
1383 #[salvo_oapi::endpoint]
1384 async fn get_post(id: PathParam<i32>) -> &'static str {
1385 let _ = id;
1386 "ok"
1387 }
1388
1389 let router = Router::with_path("/posts/{id:num}").get(get_post);
1390 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1391
1392 assert!(doc.paths.contains_key("/posts/{id}"));
1393 assert!(!doc.paths.contains_key("/posts/{id:num}"));
1394 }
1395
1396 #[test]
1397 fn to_parameters_struct_defaults_to_query_with_required() {
1398 #[derive(Deserialize, crate::ToParameters)]
1405 #[allow(dead_code)]
1406 struct ListQuery {
1407 page: i32,
1408 #[serde(rename = "pageSize")]
1409 page_size: i32,
1410 name: String,
1411 keyword: Option<String>,
1412 }
1413
1414 #[salvo_oapi::endpoint]
1415 async fn list(query: ListQuery) -> &'static str {
1416 let _ = query;
1417 "ok"
1418 }
1419
1420 let router = Router::with_path("/list").get(list);
1421 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1422
1423 let path_item = doc.paths.get("/list").expect("/list entry should exist");
1424 let operation = path_item
1425 .operations
1426 .get(&PathItemType::Get)
1427 .expect("get operation should exist");
1428
1429 let by_name = |name: &str| {
1430 operation
1431 .parameters
1432 .0
1433 .iter()
1434 .find(|p| p.name == name)
1435 .unwrap_or_else(|| panic!("parameter `{name}` should exist"))
1436 };
1437
1438 for name in ["page", "pageSize", "name", "keyword"] {
1439 assert_eq!(
1440 by_name(name).parameter_in,
1441 ParameterIn::Query,
1442 "parameter `{name}` should be located in query"
1443 );
1444 }
1445 assert_eq!(by_name("page").required, Required::True);
1446 assert_eq!(by_name("pageSize").required, Required::True);
1447 assert_eq!(by_name("name").required, Required::True);
1448 assert_eq!(by_name("keyword").required, Required::False);
1449 }
1450
1451 #[test]
1452 fn to_parameters_accepts_singular_and_plural_keys() {
1453 #[derive(Deserialize, crate::ToParameters)]
1458 #[salvo(parameter(default_parameter_in = Header))]
1460 #[allow(dead_code)]
1461 struct AliasQuery {
1462 page: i32,
1463 #[salvo(parameters(rename = "renamed"))]
1465 raw: String,
1466 }
1467
1468 #[salvo_oapi::endpoint]
1469 async fn list(query: AliasQuery) -> &'static str {
1470 let _ = query;
1471 "ok"
1472 }
1473
1474 let router = Router::with_path("/alias").get(list);
1475 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1476 let operation = doc
1477 .paths
1478 .get("/alias")
1479 .and_then(|item| item.operations.get(&PathItemType::Get))
1480 .expect("get operation should exist");
1481 let names: Vec<&str> = operation
1482 .parameters
1483 .0
1484 .iter()
1485 .map(|p| p.name.as_str())
1486 .collect();
1487
1488 for param in &operation.parameters.0 {
1490 assert_eq!(
1491 param.parameter_in,
1492 ParameterIn::Header,
1493 "parameter `{}` should inherit the container `default_parameter_in`",
1494 param.name
1495 );
1496 }
1497 assert!(
1499 names.contains(&"renamed"),
1500 "field rename alias not applied: {names:?}"
1501 );
1502 assert!(!names.contains(&"raw"));
1503 }
1504
1505 #[test]
1506 fn merge_router_skips_route_without_method_filter() {
1507 #[salvo_oapi::endpoint]
1508 async fn any_handler() -> &'static str {
1509 "ok"
1510 }
1511
1512 let router = Router::with_path("/no-method").goal(any_handler);
1516 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1517
1518 assert!(
1519 !doc.paths.contains_key("/no-method"),
1520 "expected no path entry when the route lacks a method filter; \
1521 got: {:?}",
1522 doc.paths.keys().collect::<Vec<_>>()
1523 );
1524 }
1525
1526 #[test]
1527 fn merge_router_attaches_only_to_explicit_method() {
1528 #[salvo_oapi::endpoint]
1529 async fn delete_thing() -> &'static str {
1530 "ok"
1531 }
1532
1533 let router = Router::with_path("/thing").delete(delete_thing);
1536 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1537
1538 let path_item = doc.paths.get("/thing").expect("/thing entry should exist");
1539 assert!(path_item.operations.contains_key(&PathItemType::Delete));
1540 assert!(!path_item.operations.contains_key(&PathItemType::Get));
1541 assert!(!path_item.operations.contains_key(&PathItemType::Post));
1542 assert!(!path_item.operations.contains_key(&PathItemType::Put));
1543 assert!(!path_item.operations.contains_key(&PathItemType::Patch));
1544 }
1545
1546 #[test]
1547 fn test_build_openapi() {
1548 let _doc = OpenApi::new("pet api", "0.1.0")
1549 .info(Info::new("my pet api", "0.2.0"))
1550 .servers(Servers::new())
1551 .add_path(
1552 "/api/v1",
1553 PathItem::new(PathItemType::Get, Operation::new()),
1554 )
1555 .security([SecurityRequirement::default()])
1556 .add_security_scheme(
1557 "api_key",
1558 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1559 )
1560 .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1561 .add_schema("example", Schema::object(Object::new()))
1562 .extend_schemas([("", Schema::from(Object::new()))])
1563 .response("200", Response::new("OK"))
1564 .extend_responses([("404", Response::new("Not Found"))])
1565 .tags(["tag1", "tag2"])
1566 .external_docs(ExternalDocs::default())
1567 .into_router("/openapi/doc");
1568 }
1569
1570 #[test]
1571 fn json_schema_dialect_serializes_under_spec_field_name() -> Result<(), serde_json::Error> {
1572 let doc = OpenApi::new("api", "0.1.0")
1573 .json_schema_dialect("https://json-schema.org/draft/2020-12/schema");
1574 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1575
1576 assert_eq!(
1577 value["jsonSchemaDialect"],
1578 Value::String("https://json-schema.org/draft/2020-12/schema".to_owned()),
1579 "expected top-level `jsonSchemaDialect` field per OpenAPI 3.1.0"
1580 );
1581 assert!(
1582 value.get("$schema").is_none(),
1583 "`$schema` is the JSON Schema keyword inside Schema Objects, not the OpenAPI \
1584 document-level field"
1585 );
1586 Ok(())
1587 }
1588
1589 #[test]
1590 fn json_schema_dialect_omits_field_when_empty() -> Result<(), serde_json::Error> {
1591 let doc = OpenApi::new("api", "0.1.0");
1592 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1593
1594 assert!(value.get("jsonSchemaDialect").is_none());
1595 assert!(value.get("$schema").is_none());
1596 Ok(())
1597 }
1598
1599 #[test]
1600 fn webhooks_omits_field_when_empty() -> Result<(), serde_json::Error> {
1601 let doc = OpenApi::new("api", "0.1.0");
1602 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1603
1604 assert!(value.get("webhooks").is_none());
1605 Ok(())
1606 }
1607
1608 #[test]
1609 fn webhooks_serializes_inline_path_item() -> Result<(), serde_json::Error> {
1610 let doc = OpenApi::new("api", "0.1.0").add_webhook(
1611 "newPet",
1612 PathItem::new(
1613 PathItemType::Post,
1614 Operation::new().add_response("200", Response::new("acknowledged")),
1615 ),
1616 );
1617 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1618
1619 assert_eq!(
1620 value["webhooks"],
1621 json!({
1622 "newPet": {
1623 "post": {
1624 "responses": {
1625 "200": { "description": "acknowledged" }
1626 }
1627 }
1628 }
1629 })
1630 );
1631 Ok(())
1632 }
1633
1634 #[test]
1635 fn webhooks_serializes_reference_object() -> Result<(), serde_json::Error> {
1636 let doc = OpenApi::new("api", "0.1.0").add_webhook(
1637 "newPet",
1638 RefOr::Ref(Ref::new("#/components/pathItems/NewPetWebhook")),
1639 );
1640 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1641
1642 assert_eq!(
1643 value["webhooks"]["newPet"],
1644 json!({ "$ref": "#/components/pathItems/NewPetWebhook" })
1645 );
1646 Ok(())
1647 }
1648
1649 #[test]
1650 fn webhooks_merge_combines_entries() {
1651 let api_a = OpenApi::new("a", "1.0").add_webhook(
1652 "newPet",
1653 PathItem::new(PathItemType::Post, Operation::new()),
1654 );
1655 let api_b = OpenApi::new("b", "1.0").add_webhook(
1656 "deletedPet",
1657 PathItem::new(PathItemType::Post, Operation::new()),
1658 );
1659
1660 let merged = api_a.merge(api_b);
1661
1662 assert!(merged.webhooks.contains_key("newPet"));
1663 assert!(merged.webhooks.contains_key("deletedPet"));
1664 }
1665
1666 #[test]
1667 fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1668 let raw_json = r#"{
1669 "openapi": "3.1.0",
1670 "info": {
1671 "title": "My api",
1672 "description": "My api description",
1673 "license": {
1674 "name": "MIT",
1675 "url": "http://mit.licence"
1676 },
1677 "version": "1.0.0",
1678 "contact": {},
1679 "termsOfService": "terms of service"
1680 },
1681 "paths": {}
1682 }"#;
1683 let doc: OpenApi = OpenApi::with_info(
1684 Info::default()
1685 .description("My api description")
1686 .license(License::new("MIT").url("http://mit.licence"))
1687 .title("My api")
1688 .version("1.0.0")
1689 .terms_of_service("terms of service")
1690 .contact(Contact::default()),
1691 );
1692 let serialized = doc.to_pretty_json()?;
1693
1694 assert_eq!(
1695 Value::from_str(&serialized)?,
1696 Value::from_str(raw_json)?,
1697 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1698 );
1699 Ok(())
1700 }
1701
1702 #[test]
1703 fn test_deprecated_from_bool() {
1704 assert_eq!(Deprecated::True, Deprecated::from(true));
1705 assert_eq!(Deprecated::False, Deprecated::from(false));
1706 }
1707
1708 #[test]
1709 fn test_deprecated_deserialize() {
1710 let deserialize_result = serde_json::from_str::<Deprecated>("true");
1711 assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1712 let deserialize_result = serde_json::from_str::<Deprecated>("false");
1713 assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1714 }
1715
1716 #[test]
1717 fn test_required_from_bool() {
1718 assert_eq!(Required::True, Required::from(true));
1719 assert_eq!(Required::False, Required::from(false));
1720 }
1721
1722 #[test]
1723 fn test_required_deserialize() {
1724 let deserialize_result = serde_json::from_str::<Required>("true");
1725 assert_eq!(deserialize_result.unwrap(), Required::True);
1726 let deserialize_result = serde_json::from_str::<Required>("false");
1727 assert_eq!(deserialize_result.unwrap(), Required::False);
1728 }
1729
1730 #[tokio::test]
1731 async fn test_openapi_handle() {
1732 let doc = OpenApi::new("pet api", "0.1.0");
1733 let mut req = Request::new();
1734 let mut depot = Depot::new();
1735 let mut res = salvo_core::Response::new();
1736 let mut ctrl = FlowCtrl::default();
1737 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1738
1739 let bytes = match res.body.take() {
1740 ResBody::Once(bytes) => bytes,
1741 _ => Bytes::new(),
1742 };
1743
1744 assert_eq!(
1745 res.content_type()
1746 .expect("content type should exist")
1747 .to_string(),
1748 "application/json; charset=utf-8".to_owned()
1749 );
1750 assert_eq!(
1751 bytes,
1752 Bytes::from_static(
1753 b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1754 )
1755 );
1756 }
1757
1758 #[tokio::test]
1759 async fn test_openapi_handle_pretty() {
1760 let doc = OpenApi::new("pet api", "0.1.0");
1761
1762 let mut req = Request::new();
1763 req.queries_mut()
1764 .insert("pretty".to_owned(), "true".to_owned());
1765
1766 let mut depot = Depot::new();
1767 let mut res = salvo_core::Response::new();
1768 let mut ctrl = FlowCtrl::default();
1769 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1770
1771 let bytes = match res.body.take() {
1772 ResBody::Once(bytes) => bytes,
1773 _ => Bytes::new(),
1774 };
1775
1776 assert_eq!(
1777 res.content_type()
1778 .expect("content type should exist")
1779 .to_string(),
1780 "application/json; charset=utf-8".to_owned()
1781 );
1782 assert_eq!(
1783 bytes,
1784 Bytes::from_static(b"{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"pet api\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {}\n}")
1785 );
1786 }
1787
1788 #[test]
1789 fn test_openapi_schema_work_with_generics() {
1790 crate::naming::set_namer(crate::naming::FlexNamer::new());
1792
1793 #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1794 #[salvo(schema(name = City))]
1795 pub(crate) struct CityDTO {
1796 #[salvo(schema(rename = "id"))]
1797 pub(crate) id: String,
1798 #[salvo(schema(rename = "name"))]
1799 pub(crate) name: String,
1800 }
1801
1802 #[derive(Serialize, Deserialize, Debug, ToSchema)]
1803 #[salvo(schema(name = Response))]
1804 pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1805 #[salvo(schema(rename = "status"))]
1806 pub(crate) status: String,
1808 #[salvo(schema(rename = "msg"))]
1809 pub(crate) message: String,
1811 #[salvo(schema(rename = "data"))]
1812 pub(crate) data: T,
1814 }
1815
1816 #[salvo_oapi::endpoint(
1817 operation_id = "get_all_cities",
1818 tags("city"),
1819 status_codes(200, 400, 401, 403, 500)
1820 )]
1821 pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1822 Ok(Json(ApiResponse {
1823 status: "200".to_owned(),
1824 message: "OK".to_owned(),
1825 data: vec![CityDTO {
1826 id: "1".to_owned(),
1827 name: "Beijing".to_owned(),
1828 }],
1829 }))
1830 }
1831
1832 let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1833 .add_server(Server::new("/api/bar/").description("this is description of the server"));
1834
1835 let router = Router::with_path("/cities").get(get_all_cities);
1836 let doc = doc.merge_router(&router);
1837
1838 assert_eq!(
1839 json! {{
1840 "openapi": "3.1.0",
1841 "info": {
1842 "title": "my application",
1843 "version": "0.1.0"
1844 },
1845 "servers": [
1846 {
1847 "url": "/api/bar/",
1848 "description": "this is description of the server"
1849 }
1850 ],
1851 "paths": {
1852 "/cities": {
1853 "get": {
1854 "tags": [
1855 "city"
1856 ],
1857 "operationId": "get_all_cities",
1858 "responses": {
1859 "200": {
1860 "description": "Response with json format data",
1861 "content": {
1862 "application/json": {
1863 "schema": {
1864 "$ref": "#/components/schemas/Response<alloc.vec.Vec<City>>"
1865 }
1866 }
1867 }
1868 },
1869 "400": {
1870 "description": "The request could not be understood by the server due to malformed syntax.",
1871 "content": {
1872 "application/json": {
1873 "schema": {
1874 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1875 }
1876 }
1877 }
1878 },
1879 "401": {
1880 "description": "The request requires user authentication.",
1881 "content": {
1882 "application/json": {
1883 "schema": {
1884 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1885 }
1886 }
1887 }
1888 },
1889 "403": {
1890 "description": "The server refused to authorize the request.",
1891 "content": {
1892 "application/json": {
1893 "schema": {
1894 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1895 }
1896 }
1897 }
1898 },
1899 "500": {
1900 "description": "The server encountered an internal error while processing this request.",
1901 "content": {
1902 "application/json": {
1903 "schema": {
1904 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1905 }
1906 }
1907 }
1908 }
1909 }
1910 }
1911 }
1912 },
1913 "components": {
1914 "schemas": {
1915 "City": {
1916 "type": "object",
1917 "required": [
1918 "id",
1919 "name"
1920 ],
1921 "properties": {
1922 "id": {
1923 "type": "string"
1924 },
1925 "name": {
1926 "type": "string"
1927 }
1928 }
1929 },
1930 "Response<alloc.vec.Vec<City>>": {
1931 "type": "object",
1932 "required": [
1933 "status",
1934 "msg",
1935 "data"
1936 ],
1937 "properties": {
1938 "data": {
1939 "allOf": [
1940 {
1941 "type": "array",
1942 "items": {
1943 "$ref": "#/components/schemas/City"
1944 }
1945 },
1946 {
1947 "description": "The data returned"
1948 }
1949 ]
1950 },
1951 "msg": {
1952 "type": "string",
1953 "description": "Status msg"
1954 },
1955 "status": {
1956 "type": "string",
1957 "description": "status code"
1958 }
1959 }
1960 },
1961 "salvo_core.http.errors.status_error.StatusError": {
1962 "type": "object",
1963 "required": [
1964 "code",
1965 "name",
1966 "brief",
1967 "detail"
1968 ],
1969 "properties": {
1970 "brief": {
1971 "type": "string"
1972 },
1973 "cause": {
1974 "type": "string"
1975 },
1976 "code": {
1977 "type": "integer",
1978 "format": "uint16",
1979 "minimum": 0
1980 },
1981 "detail": {
1982 "type": "string"
1983 },
1984 "name": {
1985 "type": "string"
1986 }
1987 }
1988 }
1989 }
1990 }
1991 }},
1992 Value::from_str(&doc.to_json().unwrap()).unwrap()
1993 );
1994 }
1995}