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::{
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,
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 #[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> {
181 serde_json::to_string(self)
182 }
183
184 pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
186 serde_json::to_string_pretty(self)
187 }
188
189 cfg_feature! {
190 #![feature ="yaml"]
191 pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
193 serde_norway::to_string(self)
194 }
195 }
196
197 #[must_use]
211 pub fn merge(mut self, mut other: Self) -> Self {
212 self.servers.append(&mut other.servers);
213 self.paths.append(&mut other.paths);
214 self.components.append(&mut other.components);
215 self.security.append(&mut other.security);
216 self.tags.append(&mut other.tags);
217 self
218 }
219
220 #[must_use]
222 pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
223 self.info = info.into();
224 self
225 }
226
227 #[must_use]
229 pub fn servers<S: IntoIterator<Item = Server>>(mut self, servers: S) -> Self {
230 self.servers = servers.into_iter().collect();
231 self
232 }
233 #[must_use]
235 pub fn add_server<S>(mut self, server: S) -> Self
236 where
237 S: Into<Server>,
238 {
239 self.servers.insert(server.into());
240 self
241 }
242
243 #[must_use]
245 pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
246 self.paths = paths.into();
247 self
248 }
249 #[must_use]
251 pub fn add_path<P, I>(mut self, path: P, item: I) -> Self
252 where
253 P: Into<String>,
254 I: Into<PathItem>,
255 {
256 self.paths.insert(path.into(), item.into());
257 self
258 }
259
260 #[must_use]
262 pub fn components(mut self, components: impl Into<Components>) -> Self {
263 self.components = components.into();
264 self
265 }
266
267 #[must_use]
269 pub fn security<S: IntoIterator<Item = SecurityRequirement>>(mut self, security: S) -> Self {
270 self.security = security.into_iter().collect();
271 self
272 }
273
274 #[must_use]
281 pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
282 mut self,
283 name: N,
284 security_scheme: S,
285 ) -> Self {
286 self.components
287 .security_schemes
288 .insert(name.into(), security_scheme.into());
289
290 self
291 }
292
293 #[must_use]
300 pub fn extend_security_schemes<
301 I: IntoIterator<Item = (N, S)>,
302 N: Into<String>,
303 S: Into<SecurityScheme>,
304 >(
305 mut self,
306 schemas: I,
307 ) -> Self {
308 self.components.security_schemes.extend(
309 schemas
310 .into_iter()
311 .map(|(name, item)| (name.into(), item.into())),
312 );
313 self
314 }
315
316 #[must_use]
320 pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
321 mut self,
322 name: S,
323 schema: I,
324 ) -> Self {
325 self.components.schemas.insert(name, schema);
326 self
327 }
328
329 #[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 serde_json::{Value, json};
757
758 use super::{response::Response, *};
759 use crate::{
760 ToSchema,
761 extract::*,
762 security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme},
763 server::Server,
764 };
765
766 use salvo_core::{http::ResBody, prelude::*};
767
768 #[test]
769 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
770 assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
771 Ok(())
772 }
773
774 #[test]
775 fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
776 let raw_json = r#"{
777 "openapi": "3.1.0",
778 "info": {
779 "title": "My api",
780 "description": "My api description",
781 "license": {
782 "name": "MIT",
783 "url": "http://mit.licence"
784 },
785 "version": "1.0.0",
786 "contact": {},
787 "termsOfService": "terms of service"
788 },
789 "paths": {}
790 }"#;
791 let doc: OpenApi = OpenApi::with_info(
792 Info::default()
793 .description("My api description")
794 .license(License::new("MIT").url("http://mit.licence"))
795 .title("My api")
796 .version("1.0.0")
797 .terms_of_service("terms of service")
798 .contact(Contact::default()),
799 );
800 let serialized = doc.to_json()?;
801
802 assert_eq!(
803 Value::from_str(&serialized)?,
804 Value::from_str(raw_json)?,
805 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
806 );
807 Ok(())
808 }
809
810 #[test]
811 fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
812 let doc = OpenApi::new("My big api", "1.1.0").paths(
813 Paths::new()
814 .path(
815 "/api/v1/users",
816 PathItem::new(
817 PathItemType::Get,
818 Operation::new().add_response("200", Response::new("Get users list")),
819 ),
820 )
821 .path(
822 "/api/v1/users",
823 PathItem::new(
824 PathItemType::Post,
825 Operation::new().add_response("200", Response::new("Post new user")),
826 ),
827 )
828 .path(
829 "/api/v1/users/{id}",
830 PathItem::new(
831 PathItemType::Get,
832 Operation::new().add_response("200", Response::new("Get user by id")),
833 ),
834 ),
835 );
836
837 let serialized = doc.to_json()?;
838 let expected = r#"
839 {
840 "openapi": "3.1.0",
841 "info": {
842 "title": "My big api",
843 "version": "1.1.0"
844 },
845 "paths": {
846 "/api/v1/users": {
847 "get": {
848 "responses": {
849 "200": {
850 "description": "Get users list"
851 }
852 }
853 },
854 "post": {
855 "responses": {
856 "200": {
857 "description": "Post new user"
858 }
859 }
860 }
861 },
862 "/api/v1/users/{id}": {
863 "get": {
864 "responses": {
865 "200": {
866 "description": "Get user by id"
867 }
868 }
869 }
870 }
871 }
872 }
873 "#
874 .replace("\r\n", "\n");
875
876 assert_eq!(
877 Value::from_str(&serialized)?,
878 Value::from_str(&expected)?,
879 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
880 );
881 Ok(())
882 }
883
884 #[test]
885 fn merge_2_openapi_documents() {
886 let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
887 "/api/v1/user",
888 PathItem::new(
889 PathItemType::Get,
890 Operation::new().add_response("200", Response::new("This will not get added")),
891 ),
892 ));
893
894 let api_2 = OpenApi::new("Api", "v2")
895 .paths(
896 Paths::new()
897 .path(
898 "/api/v1/user",
899 PathItem::new(
900 PathItemType::Get,
901 Operation::new().add_response("200", Response::new("Get user success")),
902 ),
903 )
904 .path(
905 "/ap/v2/user",
906 PathItem::new(
907 PathItemType::Get,
908 Operation::new()
909 .add_response("200", Response::new("Get user success 2")),
910 ),
911 )
912 .path(
913 "/api/v2/user",
914 PathItem::new(
915 PathItemType::Post,
916 Operation::new().add_response("200", Response::new("Get user success")),
917 ),
918 ),
919 )
920 .components(
921 Components::new().add_schema(
922 "User2",
923 Object::new()
924 .schema_type(BasicType::Object)
925 .property("name", Object::new().schema_type(BasicType::String)),
926 ),
927 );
928
929 api_1 = api_1.merge(api_2);
930 let value = serde_json::to_value(&api_1).unwrap();
931
932 assert_eq!(
933 value,
934 json!(
935 {
936 "openapi": "3.1.0",
937 "info": {
938 "title": "Api",
939 "version": "v1"
940 },
941 "paths": {
942 "/ap/v2/user": {
943 "get": {
944 "responses": {
945 "200": {
946 "description": "Get user success 2"
947 }
948 }
949 }
950 },
951 "/api/v1/user": {
952 "get": {
953 "responses": {
954 "200": {
955 "description": "Get user success"
956 }
957 }
958 }
959 },
960 "/api/v2/user": {
961 "post": {
962 "responses": {
963 "200": {
964 "description": "Get user success"
965 }
966 }
967 }
968 }
969 },
970 "components": {
971 "schemas": {
972 "User2": {
973 "type": "object",
974 "properties": {
975 "name": {
976 "type": "string"
977 }
978 }
979 }
980 }
981 }
982 }
983 )
984 )
985 }
986
987 #[test]
988 fn test_simple_document_with_security() {
989 #[derive(Deserialize, Serialize, ToSchema)]
990 #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
991 struct Pet {
992 id: u64,
993 name: String,
994 age: Option<i32>,
995 }
996
997 #[salvo_oapi::endpoint(
1001 responses(
1002 (status_code = 200, description = "Pet found successfully"),
1003 (status_code = 404, description = "Pet was not found")
1004 ),
1005 parameters(
1006 ("id", description = "Pet database id to get Pet for"),
1007 ),
1008 security(
1009 (),
1010 ("my_auth" = ["read:items", "edit:items"]),
1011 ("token_jwt" = []),
1012 ("api_key1" = [], "api_key2" = []),
1013 )
1014 )]
1015 pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1016 let pet = Pet {
1017 id: pet_id.into_inner(),
1018 age: None,
1019 name: "lightning".to_owned(),
1020 };
1021 Json(pet)
1022 }
1023
1024 let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1025 Server::new("/api/bar/")
1026 .description("this is description of the server")
1027 .add_variable(
1028 "username",
1029 ServerVariable::new()
1030 .default_value("the_user")
1031 .description("this is user"),
1032 ),
1033 );
1034 doc.components.security_schemes.insert(
1035 "token_jwt".into(),
1036 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1037 );
1038
1039 let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1040 let doc = doc.merge_router(&router);
1041
1042 assert_eq!(
1043 Value::from_str(
1044 r#"{
1045 "openapi": "3.1.0",
1046 "info": {
1047 "title": "my application",
1048 "version": "0.1.0"
1049 },
1050 "servers": [
1051 {
1052 "url": "/api/bar/",
1053 "description": "this is description of the server",
1054 "variables": {
1055 "username": {
1056 "default": "the_user",
1057 "description": "this is user"
1058 }
1059 }
1060 }
1061 ],
1062 "paths": {
1063 "/pets/{id}": {
1064 "get": {
1065 "summary": "Get pet by id",
1066 "description": "Get pet from database by pet database id",
1067 "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1068 "parameters": [
1069 {
1070 "name": "pet_id",
1071 "in": "path",
1072 "description": "Get parameter `pet_id` from request url path.",
1073 "required": true,
1074 "schema": {
1075 "type": "integer",
1076 "format": "uint64",
1077 "minimum": 0.0
1078 }
1079 },
1080 {
1081 "name": "id",
1082 "in": "path",
1083 "description": "Pet database id to get Pet for",
1084 "required": false
1085 }
1086 ],
1087 "responses": {
1088 "200": {
1089 "description": "Pet found successfully"
1090 },
1091 "404": {
1092 "description": "Pet was not found"
1093 }
1094 },
1095 "security": [
1096 {},
1097 {
1098 "my_auth": [
1099 "read:items",
1100 "edit:items"
1101 ]
1102 },
1103 {
1104 "token_jwt": []
1105 },
1106 {
1107 "api_key1": [],
1108 "api_key2": []
1109 }
1110 ]
1111 }
1112 }
1113 },
1114 "components": {
1115 "schemas": {
1116 "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1117 "type": "object",
1118 "required": [
1119 "id",
1120 "name"
1121 ],
1122 "properties": {
1123 "age": {
1124 "type": ["integer", "null"],
1125 "format": "int32"
1126 },
1127 "id": {
1128 "type": "integer",
1129 "format": "uint64",
1130 "minimum": 0.0
1131 },
1132 "name": {
1133 "type": "string"
1134 }
1135 },
1136 "examples": [{
1137 "id": 1,
1138 "name": "bob the cat"
1139 }]
1140 }
1141 },
1142 "securitySchemes": {
1143 "token_jwt": {
1144 "type": "http",
1145 "scheme": "bearer",
1146 "bearerFormat": "JWT"
1147 }
1148 }
1149 }
1150 }"#
1151 )
1152 .unwrap(),
1153 Value::from_str(&doc.to_json().unwrap()).unwrap()
1154 );
1155 }
1156
1157 #[test]
1158 fn test_build_openapi() {
1159 let _doc = OpenApi::new("pet api", "0.1.0")
1160 .info(Info::new("my pet api", "0.2.0"))
1161 .servers(Servers::new())
1162 .add_path(
1163 "/api/v1",
1164 PathItem::new(PathItemType::Get, Operation::new()),
1165 )
1166 .security([SecurityRequirement::default()])
1167 .add_security_scheme(
1168 "api_key",
1169 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1170 )
1171 .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1172 .add_schema("example", Schema::object(Object::new()))
1173 .extend_schemas([("", Schema::from(Object::new()))])
1174 .response("200", Response::new("OK"))
1175 .extend_responses([("404", Response::new("Not Found"))])
1176 .tags(["tag1", "tag2"])
1177 .external_docs(ExternalDocs::default())
1178 .into_router("/openapi/doc");
1179 }
1180
1181 #[test]
1182 fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1183 let raw_json = r#"{
1184 "openapi": "3.1.0",
1185 "info": {
1186 "title": "My api",
1187 "description": "My api description",
1188 "license": {
1189 "name": "MIT",
1190 "url": "http://mit.licence"
1191 },
1192 "version": "1.0.0",
1193 "contact": {},
1194 "termsOfService": "terms of service"
1195 },
1196 "paths": {}
1197 }"#;
1198 let doc: OpenApi = OpenApi::with_info(
1199 Info::default()
1200 .description("My api description")
1201 .license(License::new("MIT").url("http://mit.licence"))
1202 .title("My api")
1203 .version("1.0.0")
1204 .terms_of_service("terms of service")
1205 .contact(Contact::default()),
1206 );
1207 let serialized = doc.to_pretty_json()?;
1208
1209 assert_eq!(
1210 Value::from_str(&serialized)?,
1211 Value::from_str(raw_json)?,
1212 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1213 );
1214 Ok(())
1215 }
1216
1217 #[test]
1218 fn test_deprecated_from_bool() {
1219 assert_eq!(Deprecated::True, Deprecated::from(true));
1220 assert_eq!(Deprecated::False, Deprecated::from(false));
1221 }
1222
1223 #[test]
1224 fn test_deprecated_deserialize() {
1225 let deserialize_result = serde_json::from_str::<Deprecated>("true");
1226 assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1227 let deserialize_result = serde_json::from_str::<Deprecated>("false");
1228 assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1229 }
1230
1231 #[test]
1232 fn test_required_from_bool() {
1233 assert_eq!(Required::True, Required::from(true));
1234 assert_eq!(Required::False, Required::from(false));
1235 }
1236
1237 #[test]
1238 fn test_required_deserialize() {
1239 let deserialize_result = serde_json::from_str::<Required>("true");
1240 assert_eq!(deserialize_result.unwrap(), Required::True);
1241 let deserialize_result = serde_json::from_str::<Required>("false");
1242 assert_eq!(deserialize_result.unwrap(), Required::False);
1243 }
1244
1245 #[tokio::test]
1246 async fn test_openapi_handle() {
1247 let doc = OpenApi::new("pet api", "0.1.0");
1248 let mut req = Request::new();
1249 let mut depot = Depot::new();
1250 let mut res = salvo_core::Response::new();
1251 let mut ctrl = FlowCtrl::default();
1252 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1253
1254 let bytes = match res.body.take() {
1255 ResBody::Once(bytes) => bytes,
1256 _ => Bytes::new(),
1257 };
1258
1259 assert_eq!(
1260 res.content_type()
1261 .expect("content type should exists")
1262 .to_string(),
1263 "application/json; charset=utf-8".to_owned()
1264 );
1265 assert_eq!(
1266 bytes,
1267 Bytes::from_static(
1268 b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1269 )
1270 );
1271 }
1272
1273 #[tokio::test]
1274 async fn test_openapi_handle_pretty() {
1275 let doc = OpenApi::new("pet api", "0.1.0");
1276
1277 let mut req = Request::new();
1278 req.queries_mut()
1279 .insert("pretty".to_owned(), "true".to_owned());
1280
1281 let mut depot = Depot::new();
1282 let mut res = salvo_core::Response::new();
1283 let mut ctrl = FlowCtrl::default();
1284 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1285
1286 let bytes = match res.body.take() {
1287 ResBody::Once(bytes) => bytes,
1288 _ => Bytes::new(),
1289 };
1290
1291 assert_eq!(
1292 res.content_type()
1293 .expect("content type should exists")
1294 .to_string(),
1295 "application/json; charset=utf-8".to_owned()
1296 );
1297 assert_eq!(
1298 bytes,
1299 Bytes::from_static(b"{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"pet api\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {}\n}")
1300 );
1301 }
1302
1303 #[test]
1304 fn test_openapi_schema_work_with_generics() {
1305 #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1306 #[salvo(schema(name = City))]
1307 pub(crate) struct CityDTO {
1308 #[salvo(schema(rename = "id"))]
1309 pub(crate) id: String,
1310 #[salvo(schema(rename = "name"))]
1311 pub(crate) name: String,
1312 }
1313
1314 #[derive(Serialize, Deserialize, Debug, ToSchema)]
1315 #[salvo(schema(name = Response))]
1316 pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1317 #[salvo(schema(rename = "status"))]
1318 pub(crate) status: String,
1320 #[salvo(schema(rename = "msg"))]
1321 pub(crate) message: String,
1323 #[salvo(schema(rename = "data"))]
1324 pub(crate) data: T,
1326 }
1327
1328 #[salvo_oapi::endpoint(
1329 operation_id = "get_all_cities",
1330 tags("city"),
1331 status_codes(200, 400, 401, 403, 500)
1332 )]
1333 pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1334 Ok(Json(ApiResponse {
1335 status: "200".to_owned(),
1336 message: "OK".to_owned(),
1337 data: vec![CityDTO {
1338 id: "1".to_owned(),
1339 name: "Beijing".to_owned(),
1340 }],
1341 }))
1342 }
1343
1344 let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1345 .add_server(Server::new("/api/bar/").description("this is description of the server"));
1346
1347 let router = Router::with_path("/cities").get(get_all_cities);
1348 let doc = doc.merge_router(&router);
1349
1350 assert_eq!(
1351 json! {{
1352 "openapi": "3.1.0",
1353 "info": {
1354 "title": "my application",
1355 "version": "0.1.0"
1356 },
1357 "servers": [
1358 {
1359 "url": "/api/bar/",
1360 "description": "this is description of the server"
1361 }
1362 ],
1363 "paths": {
1364 "/cities": {
1365 "get": {
1366 "tags": [
1367 "city"
1368 ],
1369 "operationId": "get_all_cities",
1370 "responses": {
1371 "200": {
1372 "description": "Response with json format data",
1373 "content": {
1374 "application/json": {
1375 "schema": {
1376 "$ref": "#/components/schemas/Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>"
1377 }
1378 }
1379 }
1380 },
1381 "400": {
1382 "description": "The request could not be understood by the server due to malformed syntax.",
1383 "content": {
1384 "application/json": {
1385 "schema": {
1386 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1387 }
1388 }
1389 }
1390 },
1391 "401": {
1392 "description": "The request requires user authentication.",
1393 "content": {
1394 "application/json": {
1395 "schema": {
1396 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1397 }
1398 }
1399 }
1400 },
1401 "403": {
1402 "description": "The server refused to authorize the request.",
1403 "content": {
1404 "application/json": {
1405 "schema": {
1406 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1407 }
1408 }
1409 }
1410 },
1411 "500": {
1412 "description": "The server encountered an internal error while processing this request.",
1413 "content": {
1414 "application/json": {
1415 "schema": {
1416 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1417 }
1418 }
1419 }
1420 }
1421 }
1422 }
1423 }
1424 },
1425 "components": {
1426 "schemas": {
1427 "City": {
1428 "type": "object",
1429 "required": [
1430 "id",
1431 "name"
1432 ],
1433 "properties": {
1434 "id": {
1435 "type": "string"
1436 },
1437 "name": {
1438 "type": "string"
1439 }
1440 }
1441 },
1442 "Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>": {
1443 "type": "object",
1444 "required": [
1445 "status",
1446 "msg",
1447 "data"
1448 ],
1449 "properties": {
1450 "data": {
1451 "type": "array",
1452 "items": {
1453 "$ref": "#/components/schemas/City"
1454 }
1455 },
1456 "msg": {
1457 "type": "string",
1458 "description": "Status msg"
1459 },
1460 "status": {
1461 "type": "string",
1462 "description": "status code"
1463 }
1464 }
1465 },
1466 "salvo_core.http.errors.status_error.StatusError": {
1467 "type": "object",
1468 "required": [
1469 "code",
1470 "name",
1471 "brief",
1472 "detail"
1473 ],
1474 "properties": {
1475 "brief": {
1476 "type": "string"
1477 },
1478 "cause": {
1479 "type": "string"
1480 },
1481 "code": {
1482 "type": "integer",
1483 "format": "uint16",
1484 "minimum": 0.0
1485 },
1486 "detail": {
1487 "type": "string"
1488 },
1489 "name": {
1490 "type": "string"
1491 }
1492 }
1493 }
1494 }
1495 }
1496 }},
1497 Value::from_str(&doc.to_json().unwrap()).unwrap()
1498 );
1499 }
1500}