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