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