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 if let Some(handler_type_id) = &node.handler_type_id {
479 if let Some(creator) = crate::EndpointRegistry::find(handler_type_id) {
480 let Endpoint {
481 mut operation,
482 mut components,
483 } = (creator)();
484 operation.tags.extend(node.metadata.tags.iter().cloned());
485 operation
486 .securities
487 .extend(node.metadata.securities.iter().cloned());
488 let methods = if let Some(method) = &node.method {
489 vec![*method]
490 } else {
491 vec![
492 PathItemType::Get,
493 PathItemType::Post,
494 PathItemType::Put,
495 PathItemType::Patch,
496 ]
497 };
498 let not_exist_parameters = operation
499 .parameters
500 .0
501 .iter()
502 .filter(|p| {
503 p.parameter_in == ParameterIn::Path
504 && !path_parameter_names.contains(&p.name)
505 })
506 .map(|p| &p.name)
507 .collect::<Vec<_>>();
508 if !not_exist_parameters.is_empty() {
509 tracing::warn!(parameters = ?not_exist_parameters, path, handler_name = node.handler_type_name, "information for not exist parameters");
510 }
511 let meta_not_exist_parameters = path_parameter_names
512 .iter()
513 .filter(|name| {
514 !name.starts_with('*')
515 && !operation.parameters.0.iter().any(|parameter| {
516 parameter.name == **name
517 && parameter.parameter_in == ParameterIn::Path
518 })
519 })
520 .collect::<Vec<_>>();
521 #[cfg(debug_assertions)]
522 if !meta_not_exist_parameters.is_empty() {
523 tracing::warn!(parameters = ?meta_not_exist_parameters, path, handler_name = node.handler_type_name, "parameters information not provided");
524 }
525 let path_item = self.paths.entry(path.clone()).or_default();
526 for method in methods {
527 if path_item.operations.contains_key(&method) {
528 tracing::warn!(
529 "path `{}` already contains operation for method `{:?}`",
530 path,
531 method
532 );
533 } else {
534 path_item.operations.insert(method, operation.clone());
535 }
536 }
537 self.components.append(&mut components);
538 }
539 }
540 for child in &mut node.children {
541 self.merge_norm_node(child, &path);
542 }
543 }
544}
545
546#[async_trait]
547impl Handler for OpenApi {
548 async fn handle(
549 &self,
550 req: &mut salvo_core::Request,
551 _depot: &mut Depot,
552 res: &mut salvo_core::Response,
553 _ctrl: &mut FlowCtrl,
554 ) {
555 let pretty = req
556 .queries()
557 .get("pretty")
558 .map(|v| &**v != "false")
559 .unwrap_or(false);
560 let content = if pretty {
561 self.to_pretty_json().unwrap_or_default()
562 } else {
563 self.to_json().unwrap_or_default()
564 };
565 res.render(writing::Text::Json(&content));
566 }
567}
568#[derive(Serialize, Clone, PartialEq, Eq, Default, Debug)]
572pub enum OpenApiVersion {
573 #[serde(rename = "3.1.0")]
575 #[default]
576 Version3_1,
577}
578
579impl<'de> Deserialize<'de> for OpenApiVersion {
580 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
581 where
582 D: Deserializer<'de>,
583 {
584 struct VersionVisitor;
585
586 impl Visitor<'_> for VersionVisitor {
587 type Value = OpenApiVersion;
588
589 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
590 formatter.write_str("a version string in 3.1.x format")
591 }
592
593 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
594 where
595 E: Error,
596 {
597 self.visit_string(v.to_owned())
598 }
599
600 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
601 where
602 E: Error,
603 {
604 let version = v
605 .split('.')
606 .flat_map(|digit| digit.parse::<i8>())
607 .collect::<Vec<_>>();
608
609 if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
610 Ok(OpenApiVersion::Version3_1)
611 } else {
612 let expected: &dyn Expected = &"3.1.0";
613 Err(Error::invalid_value(
614 serde::de::Unexpected::Str(&v),
615 expected,
616 ))
617 }
618 }
619 }
620
621 deserializer.deserialize_string(VersionVisitor)
622 }
623}
624
625#[derive(PartialEq, Eq, Clone, Debug)]
629pub enum Deprecated {
630 True,
632 False,
634}
635impl From<bool> for Deprecated {
636 fn from(b: bool) -> Self {
637 if b { Self::True } else { Self::False }
638 }
639}
640
641impl Serialize for Deprecated {
642 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
643 where
644 S: Serializer,
645 {
646 serializer.serialize_bool(matches!(self, Self::True))
647 }
648}
649
650impl<'de> Deserialize<'de> for Deprecated {
651 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
652 where
653 D: serde::Deserializer<'de>,
654 {
655 struct BoolVisitor;
656 impl Visitor<'_> for BoolVisitor {
657 type Value = Deprecated;
658
659 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
660 formatter.write_str("a bool true or false")
661 }
662
663 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
664 where
665 E: serde::de::Error,
666 {
667 match v {
668 true => Ok(Deprecated::True),
669 false => Ok(Deprecated::False),
670 }
671 }
672 }
673 deserializer.deserialize_bool(BoolVisitor)
674 }
675}
676
677#[derive(PartialEq, Eq, Default, Clone, Debug)]
681pub enum Required {
682 True,
684 False,
686 #[default]
688 Unset,
689}
690
691impl From<bool> for Required {
692 fn from(value: bool) -> Self {
693 if value { Self::True } else { Self::False }
694 }
695}
696
697impl Serialize for Required {
698 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
699 where
700 S: Serializer,
701 {
702 serializer.serialize_bool(matches!(self, Self::True))
703 }
704}
705
706impl<'de> Deserialize<'de> for Required {
707 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
708 where
709 D: serde::Deserializer<'de>,
710 {
711 struct BoolVisitor;
712 impl Visitor<'_> for BoolVisitor {
713 type Value = Required;
714
715 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
716 formatter.write_str("a bool true or false")
717 }
718
719 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
720 where
721 E: serde::de::Error,
722 {
723 match v {
724 true => Ok(Required::True),
725 false => Ok(Required::False),
726 }
727 }
728 }
729 deserializer.deserialize_bool(BoolVisitor)
730 }
731}
732
733#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
738#[serde(untagged)]
739pub enum RefOr<T> {
740 Ref(schema::Ref),
742 Type(T),
744}
745
746#[cfg(test)]
747mod tests {
748 use std::fmt::Debug;
749 use std::str::FromStr;
750
751 use bytes::Bytes;
752 use serde_json::{Value, json};
753
754 use super::{response::Response, *};
755 use crate::{
756 ToSchema,
757 extract::*,
758 security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme},
759 server::Server,
760 };
761
762 use salvo_core::{http::ResBody, prelude::*};
763
764 #[test]
765 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
766 assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
767 Ok(())
768 }
769
770 #[test]
771 fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
772 let raw_json = r#"{
773 "openapi": "3.1.0",
774 "info": {
775 "title": "My api",
776 "description": "My api description",
777 "license": {
778 "name": "MIT",
779 "url": "http://mit.licence"
780 },
781 "version": "1.0.0",
782 "contact": {},
783 "termsOfService": "terms of service"
784 },
785 "paths": {}
786 }"#;
787 let doc: OpenApi = OpenApi::with_info(
788 Info::default()
789 .description("My api description")
790 .license(License::new("MIT").url("http://mit.licence"))
791 .title("My api")
792 .version("1.0.0")
793 .terms_of_service("terms of service")
794 .contact(Contact::default()),
795 );
796 let serialized = doc.to_json()?;
797
798 assert_eq!(
799 Value::from_str(&serialized)?,
800 Value::from_str(raw_json)?,
801 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
802 );
803 Ok(())
804 }
805
806 #[test]
807 fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
808 let doc = OpenApi::new("My big api", "1.1.0").paths(
809 Paths::new()
810 .path(
811 "/api/v1/users",
812 PathItem::new(
813 PathItemType::Get,
814 Operation::new().add_response("200", Response::new("Get users list")),
815 ),
816 )
817 .path(
818 "/api/v1/users",
819 PathItem::new(
820 PathItemType::Post,
821 Operation::new().add_response("200", Response::new("Post new user")),
822 ),
823 )
824 .path(
825 "/api/v1/users/{id}",
826 PathItem::new(
827 PathItemType::Get,
828 Operation::new().add_response("200", Response::new("Get user by id")),
829 ),
830 ),
831 );
832
833 let serialized = doc.to_json()?;
834 let expected = r#"
835 {
836 "openapi": "3.1.0",
837 "info": {
838 "title": "My big api",
839 "version": "1.1.0"
840 },
841 "paths": {
842 "/api/v1/users": {
843 "get": {
844 "responses": {
845 "200": {
846 "description": "Get users list"
847 }
848 }
849 },
850 "post": {
851 "responses": {
852 "200": {
853 "description": "Post new user"
854 }
855 }
856 }
857 },
858 "/api/v1/users/{id}": {
859 "get": {
860 "responses": {
861 "200": {
862 "description": "Get user by id"
863 }
864 }
865 }
866 }
867 }
868 }
869 "#
870 .replace("\r\n", "\n");
871
872 assert_eq!(
873 Value::from_str(&serialized)?,
874 Value::from_str(&expected)?,
875 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
876 );
877 Ok(())
878 }
879
880 #[test]
881 fn merge_2_openapi_documents() {
882 let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
883 "/api/v1/user",
884 PathItem::new(
885 PathItemType::Get,
886 Operation::new().add_response("200", Response::new("This will not get added")),
887 ),
888 ));
889
890 let api_2 = OpenApi::new("Api", "v2")
891 .paths(
892 Paths::new()
893 .path(
894 "/api/v1/user",
895 PathItem::new(
896 PathItemType::Get,
897 Operation::new().add_response("200", Response::new("Get user success")),
898 ),
899 )
900 .path(
901 "/ap/v2/user",
902 PathItem::new(
903 PathItemType::Get,
904 Operation::new()
905 .add_response("200", Response::new("Get user success 2")),
906 ),
907 )
908 .path(
909 "/api/v2/user",
910 PathItem::new(
911 PathItemType::Post,
912 Operation::new().add_response("200", Response::new("Get user success")),
913 ),
914 ),
915 )
916 .components(
917 Components::new().add_schema(
918 "User2",
919 Object::new()
920 .schema_type(BasicType::Object)
921 .property("name", Object::new().schema_type(BasicType::String)),
922 ),
923 );
924
925 api_1 = api_1.merge(api_2);
926 let value = serde_json::to_value(&api_1).unwrap();
927
928 assert_eq!(
929 value,
930 json!(
931 {
932 "openapi": "3.1.0",
933 "info": {
934 "title": "Api",
935 "version": "v1"
936 },
937 "paths": {
938 "/ap/v2/user": {
939 "get": {
940 "responses": {
941 "200": {
942 "description": "Get user success 2"
943 }
944 }
945 }
946 },
947 "/api/v1/user": {
948 "get": {
949 "responses": {
950 "200": {
951 "description": "Get user success"
952 }
953 }
954 }
955 },
956 "/api/v2/user": {
957 "post": {
958 "responses": {
959 "200": {
960 "description": "Get user success"
961 }
962 }
963 }
964 }
965 },
966 "components": {
967 "schemas": {
968 "User2": {
969 "type": "object",
970 "properties": {
971 "name": {
972 "type": "string"
973 }
974 }
975 }
976 }
977 }
978 }
979 )
980 )
981 }
982
983 #[test]
984 fn test_simple_document_with_security() {
985 #[derive(Deserialize, Serialize, ToSchema)]
986 #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
987 struct Pet {
988 id: u64,
989 name: String,
990 age: Option<i32>,
991 }
992
993 #[salvo_oapi::endpoint(
997 responses(
998 (status_code = 200, description = "Pet found successfully"),
999 (status_code = 404, description = "Pet was not found")
1000 ),
1001 parameters(
1002 ("id", description = "Pet database id to get Pet for"),
1003 ),
1004 security(
1005 (),
1006 ("my_auth" = ["read:items", "edit:items"]),
1007 ("token_jwt" = []),
1008 ("api_key1" = [], "api_key2" = []),
1009 )
1010 )]
1011 pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1012 let pet = Pet {
1013 id: pet_id.into_inner(),
1014 age: None,
1015 name: "lightning".to_owned(),
1016 };
1017 Json(pet)
1018 }
1019
1020 let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1021 Server::new("/api/bar/")
1022 .description("this is description of the server")
1023 .add_variable(
1024 "username",
1025 ServerVariable::new()
1026 .default_value("the_user")
1027 .description("this is user"),
1028 ),
1029 );
1030 doc.components.security_schemes.insert(
1031 "token_jwt".into(),
1032 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1033 );
1034
1035 let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1036 let doc = doc.merge_router(&router);
1037
1038 assert_eq!(
1039 Value::from_str(
1040 r#"{
1041 "openapi": "3.1.0",
1042 "info": {
1043 "title": "my application",
1044 "version": "0.1.0"
1045 },
1046 "servers": [
1047 {
1048 "url": "/api/bar/",
1049 "description": "this is description of the server",
1050 "variables": {
1051 "username": {
1052 "default": "the_user",
1053 "description": "this is user"
1054 }
1055 }
1056 }
1057 ],
1058 "paths": {
1059 "/pets/{id}": {
1060 "get": {
1061 "summary": "Get pet by id",
1062 "description": "Get pet from database by pet database id",
1063 "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1064 "parameters": [
1065 {
1066 "name": "pet_id",
1067 "in": "path",
1068 "description": "Get parameter `pet_id` from request url path.",
1069 "required": true,
1070 "schema": {
1071 "type": "integer",
1072 "format": "uint64",
1073 "minimum": 0.0
1074 }
1075 },
1076 {
1077 "name": "id",
1078 "in": "path",
1079 "description": "Pet database id to get Pet for",
1080 "required": false
1081 }
1082 ],
1083 "responses": {
1084 "200": {
1085 "description": "Pet found successfully"
1086 },
1087 "404": {
1088 "description": "Pet was not found"
1089 }
1090 },
1091 "security": [
1092 {},
1093 {
1094 "my_auth": [
1095 "read:items",
1096 "edit:items"
1097 ]
1098 },
1099 {
1100 "token_jwt": []
1101 },
1102 {
1103 "api_key1": [],
1104 "api_key2": []
1105 }
1106 ]
1107 }
1108 }
1109 },
1110 "components": {
1111 "schemas": {
1112 "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1113 "type": "object",
1114 "required": [
1115 "id",
1116 "name"
1117 ],
1118 "properties": {
1119 "age": {
1120 "type": ["integer", "null"],
1121 "format": "int32"
1122 },
1123 "id": {
1124 "type": "integer",
1125 "format": "uint64",
1126 "minimum": 0.0
1127 },
1128 "name": {
1129 "type": "string"
1130 }
1131 },
1132 "examples": [{
1133 "id": 1,
1134 "name": "bob the cat"
1135 }]
1136 }
1137 },
1138 "securitySchemes": {
1139 "token_jwt": {
1140 "type": "http",
1141 "scheme": "bearer",
1142 "bearerFormat": "JWT"
1143 }
1144 }
1145 }
1146 }"#
1147 )
1148 .unwrap(),
1149 Value::from_str(&doc.to_json().unwrap()).unwrap()
1150 );
1151 }
1152
1153 #[test]
1154 fn test_build_openapi() {
1155 let _doc = OpenApi::new("pet api", "0.1.0")
1156 .info(Info::new("my pet api", "0.2.0"))
1157 .servers(Servers::new())
1158 .add_path(
1159 "/api/v1",
1160 PathItem::new(PathItemType::Get, Operation::new()),
1161 )
1162 .security([SecurityRequirement::default()])
1163 .add_security_scheme(
1164 "api_key",
1165 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1166 )
1167 .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1168 .add_schema("example", Schema::object(Object::new()))
1169 .extend_schemas([("", Schema::from(Object::new()))])
1170 .response("200", Response::new("OK"))
1171 .extend_responses([("404", Response::new("Not Found"))])
1172 .tags(["tag1", "tag2"])
1173 .external_docs(ExternalDocs::default())
1174 .into_router("/openapi/doc");
1175 }
1176
1177 #[test]
1178 fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1179 let raw_json = r#"{
1180 "openapi": "3.1.0",
1181 "info": {
1182 "title": "My api",
1183 "description": "My api description",
1184 "license": {
1185 "name": "MIT",
1186 "url": "http://mit.licence"
1187 },
1188 "version": "1.0.0",
1189 "contact": {},
1190 "termsOfService": "terms of service"
1191 },
1192 "paths": {}
1193 }"#;
1194 let doc: OpenApi = OpenApi::with_info(
1195 Info::default()
1196 .description("My api description")
1197 .license(License::new("MIT").url("http://mit.licence"))
1198 .title("My api")
1199 .version("1.0.0")
1200 .terms_of_service("terms of service")
1201 .contact(Contact::default()),
1202 );
1203 let serialized = doc.to_pretty_json()?;
1204
1205 assert_eq!(
1206 Value::from_str(&serialized)?,
1207 Value::from_str(raw_json)?,
1208 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1209 );
1210 Ok(())
1211 }
1212
1213 #[test]
1214 fn test_deprecated_from_bool() {
1215 assert_eq!(Deprecated::True, Deprecated::from(true));
1216 assert_eq!(Deprecated::False, Deprecated::from(false));
1217 }
1218
1219 #[test]
1220 fn test_deprecated_deserialize() {
1221 let deserialize_result = serde_json::from_str::<Deprecated>("true");
1222 assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1223 let deserialize_result = serde_json::from_str::<Deprecated>("false");
1224 assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1225 }
1226
1227 #[test]
1228 fn test_required_from_bool() {
1229 assert_eq!(Required::True, Required::from(true));
1230 assert_eq!(Required::False, Required::from(false));
1231 }
1232
1233 #[test]
1234 fn test_required_deserialize() {
1235 let deserialize_result = serde_json::from_str::<Required>("true");
1236 assert_eq!(deserialize_result.unwrap(), Required::True);
1237 let deserialize_result = serde_json::from_str::<Required>("false");
1238 assert_eq!(deserialize_result.unwrap(), Required::False);
1239 }
1240
1241 #[tokio::test]
1242 async fn test_openapi_handle() {
1243 let doc = OpenApi::new("pet api", "0.1.0");
1244 let mut req = Request::new();
1245 let mut depot = Depot::new();
1246 let mut res = salvo_core::Response::new();
1247 let mut ctrl = FlowCtrl::default();
1248 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1249
1250 let bytes = match res.body.take() {
1251 ResBody::Once(bytes) => bytes,
1252 _ => Bytes::new(),
1253 };
1254
1255 assert_eq!(
1256 res.content_type()
1257 .expect("content type should exists")
1258 .to_string(),
1259 "application/json; charset=utf-8".to_owned()
1260 );
1261 assert_eq!(
1262 bytes,
1263 Bytes::from_static(
1264 b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1265 )
1266 );
1267 }
1268
1269 #[tokio::test]
1270 async fn test_openapi_handle_pretty() {
1271 let doc = OpenApi::new("pet api", "0.1.0");
1272
1273 let mut req = Request::new();
1274 req.queries_mut()
1275 .insert("pretty".to_owned(), "true".to_owned());
1276
1277 let mut depot = Depot::new();
1278 let mut res = salvo_core::Response::new();
1279 let mut ctrl = FlowCtrl::default();
1280 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1281
1282 let bytes = match res.body.take() {
1283 ResBody::Once(bytes) => bytes,
1284 _ => Bytes::new(),
1285 };
1286
1287 assert_eq!(
1288 res.content_type()
1289 .expect("content type should exists")
1290 .to_string(),
1291 "application/json; charset=utf-8".to_owned()
1292 );
1293 assert_eq!(
1294 bytes,
1295 Bytes::from_static(b"{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"pet api\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {}\n}")
1296 );
1297 }
1298
1299 #[test]
1300 fn test_openapi_schema_work_with_generics() {
1301 #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1302 #[salvo(schema(name = City))]
1303 pub(crate) struct CityDTO {
1304 #[salvo(schema(rename = "id"))]
1305 pub(crate) id: String,
1306 #[salvo(schema(rename = "name"))]
1307 pub(crate) name: String,
1308 }
1309
1310 #[derive(Serialize, Deserialize, Debug, ToSchema)]
1311 #[salvo(schema(name = Response))]
1312 pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1313 #[salvo(schema(rename = "status"))]
1314 pub(crate) status: String,
1316 #[salvo(schema(rename = "msg"))]
1317 pub(crate) message: String,
1319 #[salvo(schema(rename = "data"))]
1320 pub(crate) data: T,
1322 }
1323
1324 #[salvo_oapi::endpoint(
1325 operation_id = "get_all_cities",
1326 tags("city"),
1327 status_codes(200, 400, 401, 403, 500)
1328 )]
1329 pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1330 Ok(Json(ApiResponse {
1331 status: "200".to_owned(),
1332 message: "OK".to_owned(),
1333 data: vec![CityDTO {
1334 id: "1".to_owned(),
1335 name: "Beijing".to_owned(),
1336 }],
1337 }))
1338 }
1339
1340 let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1341 .add_server(Server::new("/api/bar/").description("this is description of the server"));
1342
1343 let router = Router::with_path("/cities").get(get_all_cities);
1344 let doc = doc.merge_router(&router);
1345
1346 assert_eq!(
1347 json! {{
1348 "openapi": "3.1.0",
1349 "info": {
1350 "title": "my application",
1351 "version": "0.1.0"
1352 },
1353 "servers": [
1354 {
1355 "url": "/api/bar/",
1356 "description": "this is description of the server"
1357 }
1358 ],
1359 "paths": {
1360 "/cities": {
1361 "get": {
1362 "tags": [
1363 "city"
1364 ],
1365 "operationId": "get_all_cities",
1366 "responses": {
1367 "200": {
1368 "description": "Response with json format data",
1369 "content": {
1370 "application/json": {
1371 "schema": {
1372 "$ref": "#/components/schemas/Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>"
1373 }
1374 }
1375 }
1376 },
1377 "400": {
1378 "description": "The request could not be understood by the server due to malformed syntax.",
1379 "content": {
1380 "application/json": {
1381 "schema": {
1382 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1383 }
1384 }
1385 }
1386 },
1387 "401": {
1388 "description": "The request requires user authentication.",
1389 "content": {
1390 "application/json": {
1391 "schema": {
1392 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1393 }
1394 }
1395 }
1396 },
1397 "403": {
1398 "description": "The server refused to authorize the request.",
1399 "content": {
1400 "application/json": {
1401 "schema": {
1402 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1403 }
1404 }
1405 }
1406 },
1407 "500": {
1408 "description": "The server encountered an internal error while processing this request.",
1409 "content": {
1410 "application/json": {
1411 "schema": {
1412 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1413 }
1414 }
1415 }
1416 }
1417 }
1418 }
1419 }
1420 },
1421 "components": {
1422 "schemas": {
1423 "City": {
1424 "type": "object",
1425 "required": [
1426 "id",
1427 "name"
1428 ],
1429 "properties": {
1430 "id": {
1431 "type": "string"
1432 },
1433 "name": {
1434 "type": "string"
1435 }
1436 }
1437 },
1438 "Response<alloc.vec.Vec<salvo_oapi.openapi.tests.test_openapi_schema_work_with_generics.CityDTO>>": {
1439 "type": "object",
1440 "required": [
1441 "status",
1442 "msg",
1443 "data"
1444 ],
1445 "properties": {
1446 "data": {
1447 "type": "array",
1448 "items": {
1449 "$ref": "#/components/schemas/City"
1450 }
1451 },
1452 "msg": {
1453 "type": "string",
1454 "description": "Status msg"
1455 },
1456 "status": {
1457 "type": "string",
1458 "description": "status code"
1459 }
1460 }
1461 },
1462 "salvo_core.http.errors.status_error.StatusError": {
1463 "type": "object",
1464 "required": [
1465 "code",
1466 "name",
1467 "brief",
1468 "detail"
1469 ],
1470 "properties": {
1471 "brief": {
1472 "type": "string"
1473 },
1474 "cause": {
1475 "type": "string"
1476 },
1477 "code": {
1478 "type": "integer",
1479 "format": "uint16",
1480 "minimum": 0.0
1481 },
1482 "detail": {
1483 "type": "string"
1484 },
1485 "name": {
1486 "type": "string"
1487 }
1488 }
1489 }
1490 }
1491 }
1492 }},
1493 Value::from_str(&doc.to_json().unwrap()).unwrap()
1494 );
1495 }
1496}