1mod 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#[cfg(not(feature = "preserve-path-order"))]
59pub type PathMap<K, V> = std::collections::BTreeMap<K, V>;
60#[cfg(feature = "preserve-path-order")]
62pub type PathMap<K, V> = indexmap::IndexMap<K, V>;
63
64#[cfg(not(feature = "preserve-prop-order"))]
66pub type PropMap<K, V> = std::collections::BTreeMap<K, V>;
67#[cfg(feature = "preserve-prop-order")]
69pub type PropMap<K, V> = indexmap::IndexMap<K, V>;
70
71#[non_exhaustive]
80#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
81#[serde(rename_all = "camelCase")]
82pub struct OpenApi {
83 pub openapi: OpenApiVersion,
85
86 pub info: Info,
90
91 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
97 pub servers: BTreeSet<Server>,
98
99 pub paths: Paths,
103
104 #[serde(skip_serializing_if = "Components::is_empty")]
110 pub components: Components,
111
112 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
118 pub security: BTreeSet<SecurityRequirement>,
119
120 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
124 pub tags: BTreeSet<Tag>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
130 pub external_docs: Option<ExternalDocs>,
131
132 #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
137 pub schema: String,
138
139 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
141 pub extensions: PropMap<String, serde_json::Value>,
142}
143
144impl OpenApi {
145 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 #[must_use]
172 pub fn with_info(info: Info) -> Self {
173 Self {
174 info,
175 ..Default::default()
176 }
177 }
178
179 pub fn to_json(&self) -> Result<String, serde_json::Error> {
182 serde_json::to_string(self)
183 }
184
185 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 pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
195 serde_norway::to_string(self)
196 }
197 }
198
199 #[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 #[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 #[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 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 self.merge(other)
272 }
273
274 #[must_use]
276 pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
277 self.info = info.into();
278 self
279 }
280
281 #[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 #[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 #[must_use]
299 pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
300 self.paths = paths.into();
301 self
302 }
303 #[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 #[must_use]
316 pub fn components(mut self, components: impl Into<Components>) -> Self {
317 self.components = components.into();
318 self
319 }
320
321 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
473 pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
474 self.schema = schema.into();
475 self
476 }
477
478 #[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 pub fn into_router(self, path: impl Into<String>) -> Router {
487 Router::with_path(path.into()).goal(self)
488 }
489
490 #[must_use]
492 pub fn merge_router(self, router: &Router) -> Self {
493 self.merge_router_with_base(router, "/")
494 }
495
496 #[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#[derive(Serialize, Clone, PartialEq, Eq, Default, Debug)]
629pub enum OpenApiVersion {
630 #[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#[derive(PartialEq, Eq, Clone, Debug)]
686pub enum Deprecated {
687 True,
689 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#[derive(PartialEq, Eq, Default, Clone, Debug)]
738pub enum Required {
739 True,
741 False,
743 #[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#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
795#[serde(untagged)]
796pub enum RefOr<T> {
797 Ref(schema::Ref),
799 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 #[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 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 pub(crate) status: String,
1390 #[salvo(schema(rename = "msg"))]
1391 pub(crate) message: String,
1393 #[salvo(schema(rename = "data"))]
1394 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}