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::example::Example;
34pub use self::external_docs::ExternalDocs;
35pub use self::header::Header;
36pub use self::info::{Contact, Info, License};
37pub use self::operation::{Operation, Operations};
38pub use self::parameter::{Parameter, ParameterIn, ParameterStyle, Parameters};
39pub use self::path::{PathItem, PathItemType, Paths};
40pub use self::request_body::RequestBody;
41pub use self::response::{Response, Responses};
42pub use self::schema::{
43 Array, BasicType, Discriminator, KnownFormat, Object, Ref, Schema, SchemaFormat, SchemaType,
44 Schemas,
45};
46pub use self::security::{SecurityRequirement, SecurityScheme};
47pub use self::server::{Server, ServerVariable, ServerVariables, Servers};
48pub use self::tag::Tag;
49pub use self::xml::Xml;
50use crate::Endpoint;
51use crate::routing::NormNode;
52
53static PATH_PARAMETER_NAME_REGEX: LazyLock<Regex> =
54 LazyLock::new(|| Regex::new(r"\{([^}:]+)").expect("invalid regex"));
55
56#[cfg(not(feature = "preserve-path-order"))]
58pub type PathMap<K, V> = std::collections::BTreeMap<K, V>;
59#[cfg(feature = "preserve-path-order")]
61pub type PathMap<K, V> = indexmap::IndexMap<K, V>;
62
63#[cfg(not(feature = "preserve-prop-order"))]
65pub type PropMap<K, V> = std::collections::BTreeMap<K, V>;
66#[cfg(feature = "preserve-prop-order")]
68pub type PropMap<K, V> = indexmap::IndexMap<K, V>;
69
70#[non_exhaustive]
79#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
80#[serde(rename_all = "camelCase")]
81pub struct OpenApi {
82 pub openapi: OpenApiVersion,
84
85 pub info: Info,
89
90 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
96 pub servers: BTreeSet<Server>,
97
98 pub paths: Paths,
102
103 #[serde(skip_serializing_if = "Components::is_empty")]
109 pub components: Components,
110
111 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
117 pub security: BTreeSet<SecurityRequirement>,
118
119 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
123 pub tags: BTreeSet<Tag>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
129 pub external_docs: Option<ExternalDocs>,
130
131 #[serde(rename = "$schema", default, skip_serializing_if = "String::is_empty")]
136 pub schema: String,
137
138 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
140 pub extensions: PropMap<String, serde_json::Value>,
141}
142
143impl OpenApi {
144 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
154 Self {
155 info: Info::new(title, version),
156 ..Default::default()
157 }
158 }
159 #[must_use]
171 pub fn with_info(info: Info) -> Self {
172 Self {
173 info,
174 ..Default::default()
175 }
176 }
177
178 pub fn to_json(&self) -> Result<String, serde_json::Error> {
181 serde_json::to_string(self)
182 }
183
184 pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
187 serde_json::to_string_pretty(self)
188 }
189
190 cfg_feature! {
191 #![feature ="yaml"]
192 pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
194 serde_norway::to_string(self)
195 }
196 }
197
198 #[must_use]
212 pub fn merge(mut self, mut other: Self) -> Self {
213 self.servers.append(&mut other.servers);
214 self.paths.append(&mut other.paths);
215 self.components.append(&mut other.components);
216 self.security.append(&mut other.security);
217 self.tags.append(&mut other.tags);
218 self
219 }
220
221 #[must_use]
223 pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
224 self.info = info.into();
225 self
226 }
227
228 #[must_use]
230 pub fn servers<S: IntoIterator<Item = Server>>(mut self, servers: S) -> Self {
231 self.servers = servers.into_iter().collect();
232 self
233 }
234 #[must_use]
236 pub fn add_server<S>(mut self, server: S) -> Self
237 where
238 S: Into<Server>,
239 {
240 self.servers.insert(server.into());
241 self
242 }
243
244 #[must_use]
246 pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
247 self.paths = paths.into();
248 self
249 }
250 #[must_use]
252 pub fn add_path<P, I>(mut self, path: P, item: I) -> Self
253 where
254 P: Into<String>,
255 I: Into<PathItem>,
256 {
257 self.paths.insert(path.into(), item.into());
258 self
259 }
260
261 #[must_use]
263 pub fn components(mut self, components: impl Into<Components>) -> Self {
264 self.components = components.into();
265 self
266 }
267
268 #[must_use]
270 pub fn security<S: IntoIterator<Item = SecurityRequirement>>(mut self, security: S) -> Self {
271 self.security = security.into_iter().collect();
272 self
273 }
274
275 #[must_use]
283 pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
284 mut self,
285 name: N,
286 security_scheme: S,
287 ) -> Self {
288 self.components
289 .security_schemes
290 .insert(name.into(), security_scheme.into());
291
292 self
293 }
294
295 #[must_use]
303 pub fn extend_security_schemes<
304 I: IntoIterator<Item = (N, S)>,
305 N: Into<String>,
306 S: Into<SecurityScheme>,
307 >(
308 mut self,
309 schemas: I,
310 ) -> Self {
311 self.components.security_schemes.extend(
312 schemas
313 .into_iter()
314 .map(|(name, item)| (name.into(), item.into())),
315 );
316 self
317 }
318
319 #[must_use]
323 pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
324 mut self,
325 name: S,
326 schema: I,
327 ) -> Self {
328 self.components.schemas.insert(name, schema);
329 self
330 }
331
332 #[must_use]
347 pub fn extend_schemas<I, C, S>(mut self, schemas: I) -> Self
348 where
349 I: IntoIterator<Item = (S, C)>,
350 C: Into<RefOr<Schema>>,
351 S: Into<String>,
352 {
353 self.components.schemas.extend(
354 schemas
355 .into_iter()
356 .map(|(name, schema)| (name.into(), schema.into())),
357 );
358 self
359 }
360
361 #[must_use]
363 pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
364 mut self,
365 name: S,
366 response: R,
367 ) -> Self {
368 self.components
369 .responses
370 .insert(name.into(), response.into());
371 self
372 }
373
374 #[must_use]
376 pub fn extend_responses<
377 I: IntoIterator<Item = (S, R)>,
378 S: Into<String>,
379 R: Into<RefOr<Response>>,
380 >(
381 mut self,
382 responses: I,
383 ) -> Self {
384 self.components.responses.extend(
385 responses
386 .into_iter()
387 .map(|(name, response)| (name.into(), response.into())),
388 );
389 self
390 }
391
392 #[must_use]
394 pub fn tags<I, T>(mut self, tags: I) -> Self
395 where
396 I: IntoIterator<Item = T>,
397 T: Into<Tag>,
398 {
399 self.tags = tags.into_iter().map(Into::into).collect();
400 self
401 }
402
403 #[must_use]
405 pub fn external_docs(mut self, external_docs: ExternalDocs) -> Self {
406 self.external_docs = Some(external_docs);
407 self
408 }
409
410 #[must_use]
420 pub fn schema<S: Into<String>>(mut self, schema: S) -> Self {
421 self.schema = schema.into();
422 self
423 }
424
425 #[must_use]
427 pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
428 self.extensions.insert(key.into(), value);
429 self
430 }
431
432 pub fn into_router(self, path: impl Into<String>) -> Router {
434 Router::with_path(path.into()).goal(self)
435 }
436
437 #[must_use]
439 pub fn merge_router(self, router: &Router) -> Self {
440 self.merge_router_with_base(router, "/")
441 }
442
443 #[must_use]
445 pub fn merge_router_with_base(mut self, router: &Router, base: impl AsRef<str>) -> Self {
446 let mut node = NormNode::new(router, Default::default());
447 self.merge_norm_node(&mut node, base.as_ref());
448 self
449 }
450
451 fn merge_norm_node(&mut self, node: &mut NormNode, base_path: &str) {
452 fn join_path(a: &str, b: &str) -> String {
453 if a.is_empty() {
454 b.to_owned()
455 } else if b.is_empty() {
456 a.to_owned()
457 } else {
458 format!("{}/{}", a.trim_end_matches('/'), b.trim_start_matches('/'))
459 }
460 }
461
462 let path = join_path(base_path, node.path.as_deref().unwrap_or_default());
463 let path_parameter_names = PATH_PARAMETER_NAME_REGEX
464 .captures_iter(&path)
465 .filter_map(|captures| {
466 captures
467 .iter()
468 .skip(1)
469 .map(|capture| {
470 capture
471 .expect("Regex captures should not be None.")
472 .as_str()
473 .to_owned()
474 })
475 .next()
476 })
477 .collect::<Vec<_>>();
478
479 if let Some(handler_type_id) = &node.handler_type_id
480 && let Some(creator) = crate::EndpointRegistry::find(handler_type_id)
481 {
482 let Endpoint {
483 mut operation,
484 mut components,
485 } = (creator)();
486 operation.tags.extend(node.metadata.tags.iter().cloned());
487 operation
488 .securities
489 .extend(node.metadata.securities.iter().cloned());
490 let methods = if let Some(method) = &node.method {
491 vec![*method]
492 } else {
493 vec![
494 PathItemType::Get,
495 PathItemType::Post,
496 PathItemType::Put,
497 PathItemType::Patch,
498 ]
499 };
500 let not_exist_parameters = operation
501 .parameters
502 .0
503 .iter()
504 .filter(|p| {
505 p.parameter_in == ParameterIn::Path && !path_parameter_names.contains(&p.name)
506 })
507 .map(|p| &p.name)
508 .collect::<Vec<_>>();
509 if !not_exist_parameters.is_empty() {
510 tracing::warn!(parameters = ?not_exist_parameters, path, handler_name = node.handler_type_name, "information for not exist parameters");
511 }
512 #[cfg(debug_assertions)]
513 {
514 let meta_not_exist_parameters = path_parameter_names
515 .iter()
516 .filter(|name| {
517 !name.starts_with('*')
518 && !operation.parameters.0.iter().any(|parameter| {
519 parameter.name == **name
520 && parameter.parameter_in == ParameterIn::Path
521 })
522 })
523 .collect::<Vec<_>>();
524
525 if !meta_not_exist_parameters.is_empty() {
526 tracing::warn!(parameters = ?meta_not_exist_parameters, path, handler_name = node.handler_type_name, "parameters information not provided");
527 }
528 }
529 let path_item = self.paths.entry(path.clone()).or_default();
530 for method in methods {
531 if path_item.operations.contains_key(&method) {
532 tracing::warn!(
533 "path `{}` already contains operation for method `{:?}`",
534 path,
535 method
536 );
537 } else {
538 path_item.operations.insert(method, operation.clone());
539 }
540 }
541 self.components.append(&mut components);
542 }
543
544 for child in &mut node.children {
545 self.merge_norm_node(child, &path);
546 }
547 }
548}
549
550#[async_trait]
551impl Handler for OpenApi {
552 async fn handle(
553 &self,
554 req: &mut salvo_core::Request,
555 _depot: &mut Depot,
556 res: &mut salvo_core::Response,
557 _ctrl: &mut FlowCtrl,
558 ) {
559 let pretty = req
560 .queries()
561 .get("pretty")
562 .map(|v| &**v != "false")
563 .unwrap_or(false);
564 let content = if pretty {
565 self.to_pretty_json().unwrap_or_default()
566 } else {
567 self.to_json().unwrap_or_default()
568 };
569 res.render(writing::Text::Json(&content));
570 }
571}
572#[derive(Serialize, Clone, PartialEq, Eq, Default, Debug)]
576pub enum OpenApiVersion {
577 #[serde(rename = "3.1.0")]
579 #[default]
580 Version3_1,
581}
582
583impl<'de> Deserialize<'de> for OpenApiVersion {
584 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
585 where
586 D: Deserializer<'de>,
587 {
588 struct VersionVisitor;
589
590 impl Visitor<'_> for VersionVisitor {
591 type Value = OpenApiVersion;
592
593 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
594 formatter.write_str("a version string in 3.1.x format")
595 }
596
597 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
598 where
599 E: Error,
600 {
601 self.visit_string(v.to_owned())
602 }
603
604 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
605 where
606 E: Error,
607 {
608 let version = v
609 .split('.')
610 .flat_map(|digit| digit.parse::<i8>())
611 .collect::<Vec<_>>();
612
613 if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
614 Ok(OpenApiVersion::Version3_1)
615 } else {
616 let expected: &dyn Expected = &"3.1.0";
617 Err(Error::invalid_value(
618 serde::de::Unexpected::Str(&v),
619 expected,
620 ))
621 }
622 }
623 }
624
625 deserializer.deserialize_string(VersionVisitor)
626 }
627}
628
629#[derive(PartialEq, Eq, Clone, Debug)]
633pub enum Deprecated {
634 True,
636 False,
638}
639impl From<bool> for Deprecated {
640 fn from(b: bool) -> Self {
641 if b { Self::True } else { Self::False }
642 }
643}
644
645impl Serialize for Deprecated {
646 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
647 where
648 S: Serializer,
649 {
650 serializer.serialize_bool(matches!(self, Self::True))
651 }
652}
653
654impl<'de> Deserialize<'de> for Deprecated {
655 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
656 where
657 D: serde::Deserializer<'de>,
658 {
659 struct BoolVisitor;
660 impl Visitor<'_> for BoolVisitor {
661 type Value = Deprecated;
662
663 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
664 formatter.write_str("a bool true or false")
665 }
666
667 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
668 where
669 E: serde::de::Error,
670 {
671 match v {
672 true => Ok(Deprecated::True),
673 false => Ok(Deprecated::False),
674 }
675 }
676 }
677 deserializer.deserialize_bool(BoolVisitor)
678 }
679}
680
681#[derive(PartialEq, Eq, Default, Clone, Debug)]
685pub enum Required {
686 True,
688 False,
690 #[default]
692 Unset,
693}
694
695impl From<bool> for Required {
696 fn from(value: bool) -> Self {
697 if value { Self::True } else { Self::False }
698 }
699}
700
701impl Serialize for Required {
702 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
703 where
704 S: Serializer,
705 {
706 serializer.serialize_bool(matches!(self, Self::True))
707 }
708}
709
710impl<'de> Deserialize<'de> for Required {
711 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
712 where
713 D: serde::Deserializer<'de>,
714 {
715 struct BoolVisitor;
716 impl Visitor<'_> for BoolVisitor {
717 type Value = Required;
718
719 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
720 formatter.write_str("a bool true or false")
721 }
722
723 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
724 where
725 E: serde::de::Error,
726 {
727 match v {
728 true => Ok(Required::True),
729 false => Ok(Required::False),
730 }
731 }
732 }
733 deserializer.deserialize_bool(BoolVisitor)
734 }
735}
736
737#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
742#[serde(untagged)]
743pub enum RefOr<T> {
744 Ref(schema::Ref),
746 Type(T),
748}
749
750#[cfg(test)]
751mod tests {
752 use std::fmt::Debug;
753 use std::str::FromStr;
754
755 use bytes::Bytes;
756 use salvo_core::http::ResBody;
757 use salvo_core::prelude::*;
758 use serde_json::{Value, json};
759
760 use super::response::Response;
761 use super::*;
762 use crate::ToSchema;
763 use crate::extract::*;
764 use crate::security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme};
765 use crate::server::Server;
766
767 #[test]
768 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
769 assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
770 Ok(())
771 }
772
773 #[test]
774 fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
775 let raw_json = r#"{
776 "openapi": "3.1.0",
777 "info": {
778 "title": "My api",
779 "description": "My api description",
780 "license": {
781 "name": "MIT",
782 "url": "http://mit.licence"
783 },
784 "version": "1.0.0",
785 "contact": {},
786 "termsOfService": "terms of service"
787 },
788 "paths": {}
789 }"#;
790 let doc: OpenApi = OpenApi::with_info(
791 Info::default()
792 .description("My api description")
793 .license(License::new("MIT").url("http://mit.licence"))
794 .title("My api")
795 .version("1.0.0")
796 .terms_of_service("terms of service")
797 .contact(Contact::default()),
798 );
799 let serialized = doc.to_json()?;
800
801 assert_eq!(
802 Value::from_str(&serialized)?,
803 Value::from_str(raw_json)?,
804 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
805 );
806 Ok(())
807 }
808
809 #[test]
810 fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
811 let doc = OpenApi::new("My big api", "1.1.0").paths(
812 Paths::new()
813 .path(
814 "/api/v1/users",
815 PathItem::new(
816 PathItemType::Get,
817 Operation::new().add_response("200", Response::new("Get users list")),
818 ),
819 )
820 .path(
821 "/api/v1/users",
822 PathItem::new(
823 PathItemType::Post,
824 Operation::new().add_response("200", Response::new("Post new user")),
825 ),
826 )
827 .path(
828 "/api/v1/users/{id}",
829 PathItem::new(
830 PathItemType::Get,
831 Operation::new().add_response("200", Response::new("Get user by id")),
832 ),
833 ),
834 );
835
836 let serialized = doc.to_json()?;
837 let expected = r#"
838 {
839 "openapi": "3.1.0",
840 "info": {
841 "title": "My big api",
842 "version": "1.1.0"
843 },
844 "paths": {
845 "/api/v1/users": {
846 "get": {
847 "responses": {
848 "200": {
849 "description": "Get users list"
850 }
851 }
852 },
853 "post": {
854 "responses": {
855 "200": {
856 "description": "Post new user"
857 }
858 }
859 }
860 },
861 "/api/v1/users/{id}": {
862 "get": {
863 "responses": {
864 "200": {
865 "description": "Get user by id"
866 }
867 }
868 }
869 }
870 }
871 }
872 "#
873 .replace("\r\n", "\n");
874
875 assert_eq!(
876 Value::from_str(&serialized)?,
877 Value::from_str(&expected)?,
878 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
879 );
880 Ok(())
881 }
882
883 #[test]
884 fn merge_2_openapi_documents() {
885 let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
886 "/api/v1/user",
887 PathItem::new(
888 PathItemType::Get,
889 Operation::new().add_response("200", Response::new("This will not get added")),
890 ),
891 ));
892
893 let api_2 = OpenApi::new("Api", "v2")
894 .paths(
895 Paths::new()
896 .path(
897 "/api/v1/user",
898 PathItem::new(
899 PathItemType::Get,
900 Operation::new().add_response("200", Response::new("Get user success")),
901 ),
902 )
903 .path(
904 "/ap/v2/user",
905 PathItem::new(
906 PathItemType::Get,
907 Operation::new()
908 .add_response("200", Response::new("Get user success 2")),
909 ),
910 )
911 .path(
912 "/api/v2/user",
913 PathItem::new(
914 PathItemType::Post,
915 Operation::new().add_response("200", Response::new("Get user success")),
916 ),
917 ),
918 )
919 .components(
920 Components::new().add_schema(
921 "User2",
922 Object::new()
923 .schema_type(BasicType::Object)
924 .property("name", Object::new().schema_type(BasicType::String)),
925 ),
926 );
927
928 api_1 = api_1.merge(api_2);
929 let value = serde_json::to_value(&api_1).unwrap();
930
931 assert_eq!(
932 value,
933 json!(
934 {
935 "openapi": "3.1.0",
936 "info": {
937 "title": "Api",
938 "version": "v1"
939 },
940 "paths": {
941 "/ap/v2/user": {
942 "get": {
943 "responses": {
944 "200": {
945 "description": "Get user success 2"
946 }
947 }
948 }
949 },
950 "/api/v1/user": {
951 "get": {
952 "responses": {
953 "200": {
954 "description": "Get user success"
955 }
956 }
957 }
958 },
959 "/api/v2/user": {
960 "post": {
961 "responses": {
962 "200": {
963 "description": "Get user success"
964 }
965 }
966 }
967 }
968 },
969 "components": {
970 "schemas": {
971 "User2": {
972 "type": "object",
973 "properties": {
974 "name": {
975 "type": "string"
976 }
977 }
978 }
979 }
980 }
981 }
982 )
983 )
984 }
985
986 #[test]
987 fn test_simple_document_with_security() {
988 #[derive(Deserialize, Serialize, ToSchema)]
989 #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
990 struct Pet {
991 id: u64,
992 name: String,
993 age: Option<i32>,
994 }
995
996 #[salvo_oapi::endpoint(
1000 responses(
1001 (status_code = 200, description = "Pet found successfully"),
1002 (status_code = 404, description = "Pet was not found")
1003 ),
1004 parameters(
1005 ("id", description = "Pet database id to get Pet for"),
1006 ),
1007 security(
1008 (),
1009 ("my_auth" = ["read:items", "edit:items"]),
1010 ("token_jwt" = []),
1011 ("api_key1" = [], "api_key2" = []),
1012 )
1013 )]
1014 pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1015 let pet = Pet {
1016 id: pet_id.into_inner(),
1017 age: None,
1018 name: "lightning".to_owned(),
1019 };
1020 Json(pet)
1021 }
1022
1023 let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1024 Server::new("/api/bar/")
1025 .description("this is description of the server")
1026 .add_variable(
1027 "username",
1028 ServerVariable::new()
1029 .default_value("the_user")
1030 .description("this is user"),
1031 ),
1032 );
1033 doc.components.security_schemes.insert(
1034 "token_jwt".into(),
1035 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1036 );
1037
1038 let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1039 let doc = doc.merge_router(&router);
1040
1041 assert_eq!(
1042 Value::from_str(
1043 r#"{
1044 "openapi": "3.1.0",
1045 "info": {
1046 "title": "my application",
1047 "version": "0.1.0"
1048 },
1049 "servers": [
1050 {
1051 "url": "/api/bar/",
1052 "description": "this is description of the server",
1053 "variables": {
1054 "username": {
1055 "default": "the_user",
1056 "description": "this is user"
1057 }
1058 }
1059 }
1060 ],
1061 "paths": {
1062 "/pets/{id}": {
1063 "get": {
1064 "summary": "Get pet by id",
1065 "description": "Get pet from database by pet database id",
1066 "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1067 "parameters": [
1068 {
1069 "name": "pet_id",
1070 "in": "path",
1071 "description": "Get parameter `pet_id` from request url path.",
1072 "required": true,
1073 "schema": {
1074 "type": "integer",
1075 "format": "uint64",
1076 "minimum": 0.0
1077 }
1078 },
1079 {
1080 "name": "id",
1081 "in": "path",
1082 "description": "Pet database id to get Pet for",
1083 "required": false
1084 }
1085 ],
1086 "responses": {
1087 "200": {
1088 "description": "Pet found successfully"
1089 },
1090 "404": {
1091 "description": "Pet was not found"
1092 }
1093 },
1094 "security": [
1095 {},
1096 {
1097 "my_auth": [
1098 "read:items",
1099 "edit:items"
1100 ]
1101 },
1102 {
1103 "token_jwt": []
1104 },
1105 {
1106 "api_key1": [],
1107 "api_key2": []
1108 }
1109 ]
1110 }
1111 }
1112 },
1113 "components": {
1114 "schemas": {
1115 "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1116 "type": "object",
1117 "required": [
1118 "id",
1119 "name"
1120 ],
1121 "properties": {
1122 "age": {
1123 "type": ["integer", "null"],
1124 "format": "int32"
1125 },
1126 "id": {
1127 "type": "integer",
1128 "format": "uint64",
1129 "minimum": 0.0
1130 },
1131 "name": {
1132 "type": "string"
1133 }
1134 },
1135 "examples": [{
1136 "id": 1,
1137 "name": "bob the cat"
1138 }]
1139 }
1140 },
1141 "securitySchemes": {
1142 "token_jwt": {
1143 "type": "http",
1144 "scheme": "bearer",
1145 "bearerFormat": "JWT"
1146 }
1147 }
1148 }
1149 }"#
1150 )
1151 .unwrap(),
1152 Value::from_str(&doc.to_json().unwrap()).unwrap()
1153 );
1154 }
1155
1156 #[test]
1157 fn test_build_openapi() {
1158 let _doc = OpenApi::new("pet api", "0.1.0")
1159 .info(Info::new("my pet api", "0.2.0"))
1160 .servers(Servers::new())
1161 .add_path(
1162 "/api/v1",
1163 PathItem::new(PathItemType::Get, Operation::new()),
1164 )
1165 .security([SecurityRequirement::default()])
1166 .add_security_scheme(
1167 "api_key",
1168 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1169 )
1170 .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1171 .add_schema("example", Schema::object(Object::new()))
1172 .extend_schemas([("", Schema::from(Object::new()))])
1173 .response("200", Response::new("OK"))
1174 .extend_responses([("404", Response::new("Not Found"))])
1175 .tags(["tag1", "tag2"])
1176 .external_docs(ExternalDocs::default())
1177 .into_router("/openapi/doc");
1178 }
1179
1180 #[test]
1181 fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1182 let raw_json = r#"{
1183 "openapi": "3.1.0",
1184 "info": {
1185 "title": "My api",
1186 "description": "My api description",
1187 "license": {
1188 "name": "MIT",
1189 "url": "http://mit.licence"
1190 },
1191 "version": "1.0.0",
1192 "contact": {},
1193 "termsOfService": "terms of service"
1194 },
1195 "paths": {}
1196 }"#;
1197 let doc: OpenApi = OpenApi::with_info(
1198 Info::default()
1199 .description("My api description")
1200 .license(License::new("MIT").url("http://mit.licence"))
1201 .title("My api")
1202 .version("1.0.0")
1203 .terms_of_service("terms of service")
1204 .contact(Contact::default()),
1205 );
1206 let serialized = doc.to_pretty_json()?;
1207
1208 assert_eq!(
1209 Value::from_str(&serialized)?,
1210 Value::from_str(raw_json)?,
1211 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1212 );
1213 Ok(())
1214 }
1215
1216 #[test]
1217 fn test_deprecated_from_bool() {
1218 assert_eq!(Deprecated::True, Deprecated::from(true));
1219 assert_eq!(Deprecated::False, Deprecated::from(false));
1220 }
1221
1222 #[test]
1223 fn test_deprecated_deserialize() {
1224 let deserialize_result = serde_json::from_str::<Deprecated>("true");
1225 assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1226 let deserialize_result = serde_json::from_str::<Deprecated>("false");
1227 assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1228 }
1229
1230 #[test]
1231 fn test_required_from_bool() {
1232 assert_eq!(Required::True, Required::from(true));
1233 assert_eq!(Required::False, Required::from(false));
1234 }
1235
1236 #[test]
1237 fn test_required_deserialize() {
1238 let deserialize_result = serde_json::from_str::<Required>("true");
1239 assert_eq!(deserialize_result.unwrap(), Required::True);
1240 let deserialize_result = serde_json::from_str::<Required>("false");
1241 assert_eq!(deserialize_result.unwrap(), Required::False);
1242 }
1243
1244 #[tokio::test]
1245 async fn test_openapi_handle() {
1246 let doc = OpenApi::new("pet api", "0.1.0");
1247 let mut req = Request::new();
1248 let mut depot = Depot::new();
1249 let mut res = salvo_core::Response::new();
1250 let mut ctrl = FlowCtrl::default();
1251 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1252
1253 let bytes = match res.body.take() {
1254 ResBody::Once(bytes) => bytes,
1255 _ => Bytes::new(),
1256 };
1257
1258 assert_eq!(
1259 res.content_type()
1260 .expect("content type should exists")
1261 .to_string(),
1262 "application/json; charset=utf-8".to_owned()
1263 );
1264 assert_eq!(
1265 bytes,
1266 Bytes::from_static(
1267 b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1268 )
1269 );
1270 }
1271
1272 #[tokio::test]
1273 async fn test_openapi_handle_pretty() {
1274 let doc = OpenApi::new("pet api", "0.1.0");
1275
1276 let mut req = Request::new();
1277 req.queries_mut()
1278 .insert("pretty".to_owned(), "true".to_owned());
1279
1280 let mut depot = Depot::new();
1281 let mut res = salvo_core::Response::new();
1282 let mut ctrl = FlowCtrl::default();
1283 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1284
1285 let bytes = match res.body.take() {
1286 ResBody::Once(bytes) => bytes,
1287 _ => Bytes::new(),
1288 };
1289
1290 assert_eq!(
1291 res.content_type()
1292 .expect("content type should exists")
1293 .to_string(),
1294 "application/json; charset=utf-8".to_owned()
1295 );
1296 assert_eq!(
1297 bytes,
1298 Bytes::from_static(b"{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"pet api\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {}\n}")
1299 );
1300 }
1301
1302 #[test]
1303 fn test_openapi_schema_work_with_generics() {
1304 #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1305 #[salvo(schema(name = City))]
1306 pub(crate) struct CityDTO {
1307 #[salvo(schema(rename = "id"))]
1308 pub(crate) id: String,
1309 #[salvo(schema(rename = "name"))]
1310 pub(crate) name: String,
1311 }
1312
1313 #[derive(Serialize, Deserialize, Debug, ToSchema)]
1314 #[salvo(schema(name = Response))]
1315 pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1316 #[salvo(schema(rename = "status"))]
1317 pub(crate) status: String,
1319 #[salvo(schema(rename = "msg"))]
1320 pub(crate) message: String,
1322 #[salvo(schema(rename = "data"))]
1323 pub(crate) data: T,
1325 }
1326
1327 #[salvo_oapi::endpoint(
1328 operation_id = "get_all_cities",
1329 tags("city"),
1330 status_codes(200, 400, 401, 403, 500)
1331 )]
1332 pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1333 Ok(Json(ApiResponse {
1334 status: "200".to_owned(),
1335 message: "OK".to_owned(),
1336 data: vec![CityDTO {
1337 id: "1".to_owned(),
1338 name: "Beijing".to_owned(),
1339 }],
1340 }))
1341 }
1342
1343 let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1344 .add_server(Server::new("/api/bar/").description("this is description of the server"));
1345
1346 let router = Router::with_path("/cities").get(get_all_cities);
1347 let doc = doc.merge_router(&router);
1348
1349 assert_eq!(
1350 json! {{
1351 "openapi": "3.1.0",
1352 "info": {
1353 "title": "my application",
1354 "version": "0.1.0"
1355 },
1356 "servers": [
1357 {
1358 "url": "/api/bar/",
1359 "description": "this is description of the server"
1360 }
1361 ],
1362 "paths": {
1363 "/cities": {
1364 "get": {
1365 "tags": [
1366 "city"
1367 ],
1368 "operationId": "get_all_cities",
1369 "responses": {
1370 "200": {
1371 "description": "Response with json format data",
1372 "content": {
1373 "application/json": {
1374 "schema": {
1375 "$ref": "#/components/schemas/Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>"
1376 }
1377 }
1378 }
1379 },
1380 "400": {
1381 "description": "The request could not be understood by the server due to malformed syntax.",
1382 "content": {
1383 "application/json": {
1384 "schema": {
1385 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1386 }
1387 }
1388 }
1389 },
1390 "401": {
1391 "description": "The request requires user authentication.",
1392 "content": {
1393 "application/json": {
1394 "schema": {
1395 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1396 }
1397 }
1398 }
1399 },
1400 "403": {
1401 "description": "The server refused to authorize the request.",
1402 "content": {
1403 "application/json": {
1404 "schema": {
1405 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1406 }
1407 }
1408 }
1409 },
1410 "500": {
1411 "description": "The server encountered an internal error while processing this request.",
1412 "content": {
1413 "application/json": {
1414 "schema": {
1415 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1416 }
1417 }
1418 }
1419 }
1420 }
1421 }
1422 }
1423 },
1424 "components": {
1425 "schemas": {
1426 "City": {
1427 "type": "object",
1428 "required": [
1429 "id",
1430 "name"
1431 ],
1432 "properties": {
1433 "id": {
1434 "type": "string"
1435 },
1436 "name": {
1437 "type": "string"
1438 }
1439 }
1440 },
1441 "Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>": {
1442 "type": "object",
1443 "required": [
1444 "status",
1445 "msg",
1446 "data"
1447 ],
1448 "properties": {
1449 "data": {
1450 "type": "array",
1451 "items": {
1452 "$ref": "#/components/schemas/City"
1453 }
1454 },
1455 "msg": {
1456 "type": "string",
1457 "description": "Status msg"
1458 },
1459 "status": {
1460 "type": "string",
1461 "description": "status code"
1462 }
1463 }
1464 },
1465 "salvo_core.http.errors.status_error.StatusError": {
1466 "type": "object",
1467 "required": [
1468 "code",
1469 "name",
1470 "brief",
1471 "detail"
1472 ],
1473 "properties": {
1474 "brief": {
1475 "type": "string"
1476 },
1477 "cause": {
1478 "type": "string"
1479 },
1480 "code": {
1481 "type": "integer",
1482 "format": "uint16",
1483 "minimum": 0.0
1484 },
1485 "detail": {
1486 "type": "string"
1487 },
1488 "name": {
1489 "type": "string"
1490 }
1491 }
1492 }
1493 }
1494 }
1495 }},
1496 Value::from_str(&doc.to_json().unwrap()).unwrap()
1497 );
1498 }
1499}