1use salvo_core::cfg_feature;
4
5mod callback;
6mod components;
7mod content;
8mod encoding;
9mod example;
10mod external_docs;
11mod header;
12pub mod info;
13mod link;
14pub mod operation;
15pub mod parameter;
16pub mod path;
17pub mod request_body;
18pub mod response;
19pub mod schema;
20pub mod security;
21pub mod server;
22mod tag;
23mod xml;
24
25use std::collections::BTreeSet;
26use std::fmt::{self, Debug, Formatter};
27use std::sync::LazyLock;
28
29use regex::Regex;
30use salvo_core::{Depot, FlowCtrl, Handler, Router, async_trait, writing};
31use serde::de::{Error, Expected, Visitor};
32use serde::{Deserialize, Deserializer, Serialize, Serializer};
33
34pub use self::callback::Callback;
35pub use self::components::Components;
36pub use self::content::Content;
37pub use self::encoding::Encoding;
38pub use self::example::Example;
39pub use self::external_docs::ExternalDocs;
40pub use self::header::Header;
41pub use self::info::{Contact, Info, License};
42pub use self::link::Link;
43pub use self::operation::{Operation, Operations};
44pub use self::parameter::{Parameter, ParameterIn, ParameterStyle, Parameters};
45pub use self::path::{PathItem, PathItemType, Paths};
46pub use self::request_body::RequestBody;
47pub use self::response::{Response, Responses};
48pub use self::schema::{
49 Array, ArrayItems, BasicType, Discriminator, KnownFormat, Number, Object, Ref, Schema,
50 SchemaFormat, SchemaType, Schemas,
51};
52pub use self::security::{SecurityRequirement, SecurityScheme};
53pub use self::server::{Server, ServerVariable, ServerVariables, Servers};
54pub use self::tag::Tag;
55pub use self::xml::Xml;
56use crate::Endpoint;
57use crate::routing::NormNode;
58
59static PATH_PARAMETER_NAME_REGEX: LazyLock<Regex> =
60 LazyLock::new(|| Regex::new(r"\{([^}:]+)").expect("invalid regex"));
61
62#[cfg(not(feature = "preserve-path-order"))]
64pub type PathMap<K, V> = std::collections::BTreeMap<K, V>;
65#[cfg(feature = "preserve-path-order")]
67pub type PathMap<K, V> = indexmap::IndexMap<K, V>;
68
69#[cfg(not(feature = "preserve-prop-order"))]
71pub type PropMap<K, V> = std::collections::BTreeMap<K, V>;
72#[cfg(feature = "preserve-prop-order")]
74pub type PropMap<K, V> = indexmap::IndexMap<K, V>;
75
76#[non_exhaustive]
85#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
86#[serde(rename_all = "camelCase")]
87pub struct OpenApi {
88 pub openapi: OpenApiVersion,
90
91 pub info: Info,
95
96 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
102 pub servers: BTreeSet<Server>,
103
104 pub paths: Paths,
108
109 #[serde(skip_serializing_if = "PathMap::is_empty", default)]
116 pub webhooks: PathMap<String, RefOr<PathItem>>,
117
118 #[serde(skip_serializing_if = "Components::is_empty")]
124 pub components: Components,
125
126 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
132 pub security: BTreeSet<SecurityRequirement>,
133
134 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
138 pub tags: BTreeSet<Tag>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
144 pub external_docs: Option<ExternalDocs>,
145
146 #[serde(default, skip_serializing_if = "String::is_empty")]
152 pub json_schema_dialect: String,
153
154 #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
156 pub extensions: PropMap<String, serde_json::Value>,
157}
158
159impl OpenApi {
160 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
170 Self {
171 info: Info::new(title, version),
172 ..Default::default()
173 }
174 }
175 #[must_use]
187 pub fn with_info(info: Info) -> Self {
188 Self {
189 info,
190 ..Default::default()
191 }
192 }
193
194 pub fn to_json(&self) -> Result<String, serde_json::Error> {
197 serde_json::to_string(self)
198 }
199
200 pub fn to_pretty_json(&self) -> Result<String, serde_json::Error> {
203 serde_json::to_string_pretty(self)
204 }
205
206 cfg_feature! {
207 #![feature ="yaml"]
208 pub fn to_yaml(&self) -> Result<String, serde_norway::Error> {
210 serde_norway::to_string(self)
211 }
212 }
213
214 #[must_use]
229 pub fn merge(mut self, mut other: Self) -> Self {
230 self.servers.append(&mut other.servers);
231 self.paths.append(&mut other.paths);
232 for (name, item) in std::mem::take(&mut other.webhooks) {
233 self.webhooks.insert(name, item);
234 }
235 self.components.append(&mut other.components);
236 self.security.append(&mut other.security);
237 self.tags.append(&mut other.tags);
238 self
239 }
240
241 #[must_use]
255 pub fn nest<P: Into<String>>(self, path: P, other: Self) -> Self {
256 self.nest_with_path_composer(path, other, |base, item_path| {
257 format!(
258 "{}/{}",
259 base.trim_end_matches('/'),
260 item_path.trim_start_matches('/')
261 )
262 })
263 }
264
265 #[must_use]
273 pub fn nest_with_path_composer<P: Into<String>, F: Fn(&str, &str) -> String>(
274 mut self,
275 path: P,
276 mut other: Self,
277 composer: F,
278 ) -> Self {
279 let path: String = path.into();
280
281 let other_paths = std::mem::take(&mut other.paths);
283 for (item_path, item) in other_paths.iter() {
284 let composed = composer(&path, item_path);
285 self.paths.insert(composed, item.clone());
286 }
287
288 self.merge(other)
291 }
292
293 #[must_use]
295 pub fn info<I: Into<Info>>(mut self, info: I) -> Self {
296 self.info = info.into();
297 self
298 }
299
300 #[must_use]
302 pub fn servers<S: IntoIterator<Item = Server>>(mut self, servers: S) -> Self {
303 self.servers = servers.into_iter().collect();
304 self
305 }
306 #[must_use]
308 pub fn add_server<S>(mut self, server: S) -> Self
309 where
310 S: Into<Server>,
311 {
312 self.servers.insert(server.into());
313 self
314 }
315
316 #[must_use]
318 pub fn paths<P: Into<Paths>>(mut self, paths: P) -> Self {
319 self.paths = paths.into();
320 self
321 }
322 #[must_use]
324 pub fn add_path<P, I>(mut self, path: P, item: I) -> Self
325 where
326 P: Into<String>,
327 I: Into<PathItem>,
328 {
329 self.paths.insert(path.into(), item.into());
330 self
331 }
332
333 #[must_use]
337 pub fn webhooks<I, K, V>(mut self, webhooks: I) -> Self
338 where
339 I: IntoIterator<Item = (K, V)>,
340 K: Into<String>,
341 V: Into<RefOr<PathItem>>,
342 {
343 self.webhooks = webhooks
344 .into_iter()
345 .map(|(name, item)| (name.into(), item.into()))
346 .collect();
347 self
348 }
349
350 #[must_use]
354 pub fn add_webhook<K: Into<String>, V: Into<RefOr<PathItem>>>(
355 mut self,
356 name: K,
357 webhook: V,
358 ) -> Self {
359 self.webhooks.insert(name.into(), webhook.into());
360 self
361 }
362
363 #[must_use]
365 pub fn components(mut self, components: impl Into<Components>) -> Self {
366 self.components = components.into();
367 self
368 }
369
370 #[must_use]
372 pub fn security<S: IntoIterator<Item = SecurityRequirement>>(mut self, security: S) -> Self {
373 self.security = security.into_iter().collect();
374 self
375 }
376
377 #[must_use]
385 pub fn add_security_scheme<N: Into<String>, S: Into<SecurityScheme>>(
386 mut self,
387 name: N,
388 security_scheme: S,
389 ) -> Self {
390 self.components
391 .security_schemes
392 .insert(name.into(), security_scheme.into());
393
394 self
395 }
396
397 #[must_use]
405 pub fn extend_security_schemes<
406 I: IntoIterator<Item = (N, S)>,
407 N: Into<String>,
408 S: Into<SecurityScheme>,
409 >(
410 mut self,
411 schemas: I,
412 ) -> Self {
413 self.components.security_schemes.extend(
414 schemas
415 .into_iter()
416 .map(|(name, item)| (name.into(), item.into())),
417 );
418 self
419 }
420
421 #[must_use]
425 pub fn add_schema<S: Into<String>, I: Into<RefOr<Schema>>>(
426 mut self,
427 name: S,
428 schema: I,
429 ) -> Self {
430 self.components.schemas.insert(name, schema);
431 self
432 }
433
434 #[must_use]
449 pub fn extend_schemas<I, C, S>(mut self, schemas: I) -> Self
450 where
451 I: IntoIterator<Item = (S, C)>,
452 C: Into<RefOr<Schema>>,
453 S: Into<String>,
454 {
455 self.components.schemas.extend(
456 schemas
457 .into_iter()
458 .map(|(name, schema)| (name.into(), schema.into())),
459 );
460 self
461 }
462
463 #[must_use]
465 pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
466 mut self,
467 name: S,
468 response: R,
469 ) -> Self {
470 self.components
471 .responses
472 .insert(name.into(), response.into());
473 self
474 }
475
476 #[must_use]
478 pub fn extend_responses<
479 I: IntoIterator<Item = (S, R)>,
480 S: Into<String>,
481 R: Into<RefOr<Response>>,
482 >(
483 mut self,
484 responses: I,
485 ) -> Self {
486 self.components.responses.extend(
487 responses
488 .into_iter()
489 .map(|(name, response)| (name.into(), response.into())),
490 );
491 self
492 }
493
494 #[must_use]
496 pub fn tags<I, T>(mut self, tags: I) -> Self
497 where
498 I: IntoIterator<Item = T>,
499 T: Into<Tag>,
500 {
501 self.tags = tags.into_iter().map(Into::into).collect();
502 self
503 }
504
505 #[must_use]
507 pub fn external_docs(mut self, external_docs: ExternalDocs) -> Self {
508 self.external_docs = Some(external_docs);
509 self
510 }
511
512 #[must_use]
528 pub fn json_schema_dialect<S: Into<String>>(mut self, dialect: S) -> Self {
529 self.json_schema_dialect = dialect.into();
530 self
531 }
532
533 #[must_use]
535 pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
536 self.extensions.insert(key.into(), value);
537 self
538 }
539
540 pub fn into_router(self, path: impl Into<String>) -> Router {
542 Router::with_path(path.into()).goal(self)
543 }
544
545 #[must_use]
547 pub fn merge_router(self, router: &Router) -> Self {
548 self.merge_router_with_base(router, "/")
549 }
550
551 #[must_use]
553 pub fn merge_router_with_base(mut self, router: &Router, base: impl AsRef<str>) -> Self {
554 let mut node = NormNode::new(router, Default::default());
555 self.merge_norm_node(&mut node, base.as_ref());
556 self
557 }
558
559 fn merge_norm_node(&mut self, node: &mut NormNode, base_path: &str) {
560 fn join_path(a: &str, b: &str) -> String {
561 if a.is_empty() {
562 b.to_owned()
563 } else if b.is_empty() {
564 a.to_owned()
565 } else {
566 format!("{}/{}", a.trim_end_matches('/'), b.trim_start_matches('/'))
567 }
568 }
569
570 let path = join_path(base_path, node.path.as_deref().unwrap_or_default());
571 let path_parameter_names = PATH_PARAMETER_NAME_REGEX
572 .captures_iter(&path)
573 .filter_map(|captures| {
574 captures
575 .iter()
576 .skip(1)
577 .map(|capture| {
578 capture
579 .expect("regex captures should not be None")
580 .as_str()
581 .to_owned()
582 })
583 .next()
584 })
585 .collect::<Vec<_>>();
586
587 if let Some(handler_type_id) = &node.handler_type_id
588 && let Some(creator) = crate::EndpointRegistry::find(handler_type_id)
589 {
590 if let Some(method) = node.method {
591 let Endpoint {
592 mut operation,
593 mut components,
594 } = creator();
595 operation.tags.extend(node.metadata.tags.iter().cloned());
596 operation
597 .securities
598 .extend(node.metadata.securities.iter().cloned());
599 let not_exist_parameters = operation
600 .parameters
601 .0
602 .iter()
603 .filter(|p| {
604 p.parameter_in == ParameterIn::Path
605 && !path_parameter_names.contains(&p.name)
606 })
607 .map(|p| &p.name)
608 .collect::<Vec<_>>();
609 if !not_exist_parameters.is_empty() {
610 tracing::warn!(parameters = ?not_exist_parameters, path, handler_name = node.handler_type_name, "information for not exist parameters");
611 }
612 #[cfg(debug_assertions)]
613 {
614 let meta_not_exist_parameters = path_parameter_names
615 .iter()
616 .filter(|name| {
617 !name.starts_with('*')
618 && !operation.parameters.0.iter().any(|parameter| {
619 parameter.name == **name
620 && parameter.parameter_in == ParameterIn::Path
621 })
622 })
623 .collect::<Vec<_>>();
624
625 if !meta_not_exist_parameters.is_empty() {
626 tracing::warn!(parameters = ?meta_not_exist_parameters, path, handler_name = node.handler_type_name, "parameters information not provided");
627 }
628 }
629 let path_item = self.paths.entry(path.clone()).or_default();
630 if path_item.operations.contains_key(&method) {
631 tracing::warn!(
632 "path `{}` already contains operation for method `{:?}`",
633 path,
634 method
635 );
636 } else {
637 path_item.operations.insert(method, operation);
638 }
639 self.components.append(&mut components);
640 } else {
641 tracing::warn!(
646 path,
647 handler_name = node.handler_type_name,
648 "endpoint has no HTTP method filter; skipping in OpenAPI document. \
649 Add `.get()`, `.post()`, etc. to the router to include it"
650 );
651 }
652 }
653
654 for child in &mut node.children {
655 self.merge_norm_node(child, &path);
656 }
657 }
658}
659
660#[async_trait]
661impl Handler for OpenApi {
662 async fn handle(
663 &self,
664 req: &mut salvo_core::Request,
665 _depot: &mut Depot,
666 res: &mut salvo_core::Response,
667 _ctrl: &mut FlowCtrl,
668 ) {
669 let pretty = req
670 .queries()
671 .get("pretty")
672 .map(|v| &**v != "false")
673 .unwrap_or(false);
674 let content = if pretty {
675 self.to_pretty_json().unwrap_or_default()
676 } else {
677 self.to_json().unwrap_or_default()
678 };
679 res.render(writing::Text::Json(&content));
680 }
681}
682#[derive(Serialize, Clone, PartialEq, Eq, Default, Debug)]
686pub enum OpenApiVersion {
687 #[serde(rename = "3.1.0")]
689 #[default]
690 Version3_1,
691}
692
693impl<'de> Deserialize<'de> for OpenApiVersion {
694 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
695 where
696 D: Deserializer<'de>,
697 {
698 struct VersionVisitor;
699
700 impl Visitor<'_> for VersionVisitor {
701 type Value = OpenApiVersion;
702
703 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
704 formatter.write_str("a version string in 3.1.x format")
705 }
706
707 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
708 where
709 E: Error,
710 {
711 self.visit_string(v.to_owned())
712 }
713
714 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
715 where
716 E: Error,
717 {
718 let version = v
719 .split('.')
720 .flat_map(|digit| digit.parse::<i8>())
721 .collect::<Vec<_>>();
722
723 if version.len() == 3 && version.first() == Some(&3) && version.get(1) == Some(&1) {
724 Ok(OpenApiVersion::Version3_1)
725 } else {
726 let expected: &dyn Expected = &"3.1.0";
727 Err(Error::invalid_value(
728 serde::de::Unexpected::Str(&v),
729 expected,
730 ))
731 }
732 }
733 }
734
735 deserializer.deserialize_string(VersionVisitor)
736 }
737}
738
739#[derive(PartialEq, Eq, Clone, Debug)]
743pub enum Deprecated {
744 True,
746 False,
748}
749impl From<bool> for Deprecated {
750 fn from(b: bool) -> Self {
751 if b { Self::True } else { Self::False }
752 }
753}
754
755impl Serialize for Deprecated {
756 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
757 where
758 S: Serializer,
759 {
760 serializer.serialize_bool(matches!(self, Self::True))
761 }
762}
763
764impl<'de> Deserialize<'de> for Deprecated {
765 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
766 where
767 D: serde::Deserializer<'de>,
768 {
769 struct BoolVisitor;
770 impl Visitor<'_> for BoolVisitor {
771 type Value = Deprecated;
772
773 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
774 formatter.write_str("a bool true or false")
775 }
776
777 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
778 where
779 E: serde::de::Error,
780 {
781 match v {
782 true => Ok(Deprecated::True),
783 false => Ok(Deprecated::False),
784 }
785 }
786 }
787 deserializer.deserialize_bool(BoolVisitor)
788 }
789}
790
791#[derive(PartialEq, Eq, Default, Clone, Debug)]
795pub enum Required {
796 True,
798 False,
800 #[default]
802 Unset,
803}
804
805impl From<bool> for Required {
806 fn from(value: bool) -> Self {
807 if value { Self::True } else { Self::False }
808 }
809}
810
811impl Serialize for Required {
812 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
813 where
814 S: Serializer,
815 {
816 serializer.serialize_bool(matches!(self, Self::True))
817 }
818}
819
820impl<'de> Deserialize<'de> for Required {
821 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
822 where
823 D: serde::Deserializer<'de>,
824 {
825 struct BoolVisitor;
826 impl Visitor<'_> for BoolVisitor {
827 type Value = Required;
828
829 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
830 formatter.write_str("a bool true or false")
831 }
832
833 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
834 where
835 E: serde::de::Error,
836 {
837 match v {
838 true => Ok(Required::True),
839 false => Ok(Required::False),
840 }
841 }
842 }
843 deserializer.deserialize_bool(BoolVisitor)
844 }
845}
846
847#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
852#[serde(untagged)]
853pub enum RefOr<T> {
854 Ref(schema::Ref),
856 Type(T),
858}
859
860#[cfg(test)]
861mod tests {
862 use std::fmt::Debug;
863 use std::str::FromStr;
864
865 use bytes::Bytes;
866 use salvo_core::http::ResBody;
867 use salvo_core::prelude::*;
868 use serde_json::{Value, json};
869
870 use super::response::Response;
871 use super::*;
872 use crate::ToSchema;
873 use crate::extract::*;
874 use crate::security::{ApiKey, ApiKeyValue, Http, HttpAuthScheme};
875 use crate::server::Server;
876
877 #[test]
878 fn serialize_deserialize_openapi_version_success() -> Result<(), serde_json::Error> {
879 assert_eq!(serde_json::to_value(&OpenApiVersion::Version3_1)?, "3.1.0");
880 Ok(())
881 }
882
883 #[test]
884 fn serialize_openapi_json_minimal_success() -> Result<(), serde_json::Error> {
885 let raw_json = r#"{
886 "openapi": "3.1.0",
887 "info": {
888 "title": "My api",
889 "description": "My api description",
890 "license": {
891 "name": "MIT",
892 "url": "http://mit.licence"
893 },
894 "version": "1.0.0",
895 "contact": {},
896 "termsOfService": "terms of service"
897 },
898 "paths": {}
899 }"#;
900 let doc: OpenApi = OpenApi::with_info(
901 Info::default()
902 .description("My api description")
903 .license(License::new("MIT").url("http://mit.licence"))
904 .title("My api")
905 .version("1.0.0")
906 .terms_of_service("terms of service")
907 .contact(Contact::default()),
908 );
909 let serialized = doc.to_json()?;
910
911 assert_eq!(
912 Value::from_str(&serialized)?,
913 Value::from_str(raw_json)?,
914 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
915 );
916 Ok(())
917 }
918
919 #[test]
920 fn serialize_openapi_json_with_paths_success() -> Result<(), serde_json::Error> {
921 let doc = OpenApi::new("My big api", "1.1.0").paths(
922 Paths::new()
923 .path(
924 "/api/v1/users",
925 PathItem::new(
926 PathItemType::Get,
927 Operation::new().add_response("200", Response::new("Get users list")),
928 ),
929 )
930 .path(
931 "/api/v1/users",
932 PathItem::new(
933 PathItemType::Post,
934 Operation::new().add_response("200", Response::new("Post new user")),
935 ),
936 )
937 .path(
938 "/api/v1/users/{id}",
939 PathItem::new(
940 PathItemType::Get,
941 Operation::new().add_response("200", Response::new("Get user by id")),
942 ),
943 ),
944 );
945
946 let serialized = doc.to_json()?;
947 let expected = r#"
948 {
949 "openapi": "3.1.0",
950 "info": {
951 "title": "My big api",
952 "version": "1.1.0"
953 },
954 "paths": {
955 "/api/v1/users": {
956 "get": {
957 "responses": {
958 "200": {
959 "description": "Get users list"
960 }
961 }
962 },
963 "post": {
964 "responses": {
965 "200": {
966 "description": "Post new user"
967 }
968 }
969 }
970 },
971 "/api/v1/users/{id}": {
972 "get": {
973 "responses": {
974 "200": {
975 "description": "Get user by id"
976 }
977 }
978 }
979 }
980 }
981 }
982 "#
983 .replace("\r\n", "\n");
984
985 assert_eq!(
986 Value::from_str(&serialized)?,
987 Value::from_str(&expected)?,
988 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{expected}"
989 );
990 Ok(())
991 }
992
993 #[test]
994 fn merge_2_openapi_documents() {
995 let mut api_1 = OpenApi::new("Api", "v1").paths(Paths::new().path(
996 "/api/v1/user",
997 PathItem::new(
998 PathItemType::Get,
999 Operation::new().add_response("200", Response::new("This will not get added")),
1000 ),
1001 ));
1002
1003 let api_2 = OpenApi::new("Api", "v2")
1004 .paths(
1005 Paths::new()
1006 .path(
1007 "/api/v1/user",
1008 PathItem::new(
1009 PathItemType::Get,
1010 Operation::new().add_response("200", Response::new("Get user success")),
1011 ),
1012 )
1013 .path(
1014 "/ap/v2/user",
1015 PathItem::new(
1016 PathItemType::Get,
1017 Operation::new()
1018 .add_response("200", Response::new("Get user success 2")),
1019 ),
1020 )
1021 .path(
1022 "/api/v2/user",
1023 PathItem::new(
1024 PathItemType::Post,
1025 Operation::new().add_response("200", Response::new("Get user success")),
1026 ),
1027 ),
1028 )
1029 .components(
1030 Components::new().add_schema(
1031 "User2",
1032 Object::new()
1033 .schema_type(BasicType::Object)
1034 .property("name", Object::new().schema_type(BasicType::String)),
1035 ),
1036 );
1037
1038 api_1 = api_1.merge(api_2);
1039 let value = serde_json::to_value(&api_1).unwrap();
1040
1041 assert_eq!(
1042 value,
1043 json!(
1044 {
1045 "openapi": "3.1.0",
1046 "info": {
1047 "title": "Api",
1048 "version": "v1"
1049 },
1050 "paths": {
1051 "/ap/v2/user": {
1052 "get": {
1053 "responses": {
1054 "200": {
1055 "description": "Get user success 2"
1056 }
1057 }
1058 }
1059 },
1060 "/api/v1/user": {
1061 "get": {
1062 "responses": {
1063 "200": {
1064 "description": "Get user success"
1065 }
1066 }
1067 }
1068 },
1069 "/api/v2/user": {
1070 "post": {
1071 "responses": {
1072 "200": {
1073 "description": "Get user success"
1074 }
1075 }
1076 }
1077 }
1078 },
1079 "components": {
1080 "schemas": {
1081 "User2": {
1082 "type": "object",
1083 "properties": {
1084 "name": {
1085 "type": "string"
1086 }
1087 }
1088 }
1089 }
1090 }
1091 }
1092 )
1093 )
1094 }
1095
1096 #[test]
1097 fn test_simple_document_with_security() {
1098 #[derive(Deserialize, Serialize, ToSchema)]
1099 #[salvo(schema(examples(json!({"name": "bob the cat", "id": 1}))))]
1100 struct Pet {
1101 id: u64,
1102 name: String,
1103 age: Option<i32>,
1104 }
1105
1106 #[salvo_oapi::endpoint(
1110 responses(
1111 (status_code = 200, description = "Pet found successfully"),
1112 (status_code = 404, description = "Pet was not found")
1113 ),
1114 parameters(
1115 ("id", description = "Pet database id to get Pet for"),
1116 ),
1117 security(
1118 (),
1119 ("my_auth" = ["read:items", "edit:items"]),
1120 ("token_jwt" = []),
1121 ("api_key1" = [], "api_key2" = []),
1122 )
1123 )]
1124 pub async fn get_pet_by_id(pet_id: PathParam<u64>) -> Json<Pet> {
1125 let pet = Pet {
1126 id: pet_id.into_inner(),
1127 age: None,
1128 name: "lightning".to_owned(),
1129 };
1130 Json(pet)
1131 }
1132
1133 let mut doc = salvo_oapi::OpenApi::new("my application", "0.1.0").add_server(
1134 Server::new("/api/bar/")
1135 .description("this is description of the server")
1136 .add_variable(
1137 "username",
1138 ServerVariable::new()
1139 .default_value("the_user")
1140 .description("this is user"),
1141 ),
1142 );
1143 doc.components.security_schemes.insert(
1144 "token_jwt".into(),
1145 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer).bearer_format("JWT")),
1146 );
1147
1148 let router = Router::with_path("/pets/{id}").get(get_pet_by_id);
1149 let doc = doc.merge_router(&router);
1150
1151 assert_eq!(
1152 Value::from_str(
1153 r#"{
1154 "openapi": "3.1.0",
1155 "info": {
1156 "title": "my application",
1157 "version": "0.1.0"
1158 },
1159 "servers": [
1160 {
1161 "url": "/api/bar/",
1162 "description": "this is description of the server",
1163 "variables": {
1164 "username": {
1165 "default": "the_user",
1166 "description": "this is user"
1167 }
1168 }
1169 }
1170 ],
1171 "paths": {
1172 "/pets/{id}": {
1173 "get": {
1174 "summary": "Get pet by id",
1175 "description": "Get pet from database by pet database id",
1176 "operationId": "salvo_oapi.openapi.tests.test_simple_document_with_security.get_pet_by_id",
1177 "parameters": [
1178 {
1179 "name": "pet_id",
1180 "in": "path",
1181 "description": "Get parameter `pet_id` from request url path.",
1182 "required": true,
1183 "schema": {
1184 "type": "integer",
1185 "format": "uint64",
1186 "minimum": 0
1187 }
1188 },
1189 {
1190 "name": "id",
1191 "in": "path",
1192 "description": "Pet database id to get Pet for",
1193 "required": true
1194 }
1195 ],
1196 "responses": {
1197 "200": {
1198 "description": "Pet found successfully"
1199 },
1200 "404": {
1201 "description": "Pet was not found"
1202 }
1203 },
1204 "security": [
1205 {},
1206 {
1207 "my_auth": [
1208 "read:items",
1209 "edit:items"
1210 ]
1211 },
1212 {
1213 "token_jwt": []
1214 },
1215 {
1216 "api_key1": [],
1217 "api_key2": []
1218 }
1219 ]
1220 }
1221 }
1222 },
1223 "components": {
1224 "schemas": {
1225 "salvo_oapi.openapi.tests.test_simple_document_with_security.Pet": {
1226 "type": "object",
1227 "required": [
1228 "id",
1229 "name"
1230 ],
1231 "properties": {
1232 "age": {
1233 "type": ["integer", "null"],
1234 "format": "int32"
1235 },
1236 "id": {
1237 "type": "integer",
1238 "format": "uint64",
1239 "minimum": 0
1240 },
1241 "name": {
1242 "type": "string"
1243 }
1244 },
1245 "examples": [{
1246 "id": 1,
1247 "name": "bob the cat"
1248 }]
1249 }
1250 },
1251 "securitySchemes": {
1252 "token_jwt": {
1253 "type": "http",
1254 "scheme": "bearer",
1255 "bearerFormat": "JWT"
1256 }
1257 }
1258 }
1259 }"#
1260 )
1261 .unwrap(),
1262 Value::from_str(&doc.to_json().unwrap()).unwrap()
1263 );
1264 }
1265
1266 #[test]
1267 fn merge_router_normalizes_constrained_path_params() {
1268 #[salvo_oapi::endpoint]
1269 async fn get_post(id: PathParam<i32>) -> &'static str {
1270 let _ = id;
1271 "ok"
1272 }
1273
1274 let router = Router::with_path("/posts/{id:num}").get(get_post);
1275 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1276
1277 assert!(doc.paths.contains_key("/posts/{id}"));
1278 assert!(!doc.paths.contains_key("/posts/{id:num}"));
1279 }
1280
1281 #[test]
1282 fn to_parameters_struct_defaults_to_query_with_required() {
1283 #[derive(Deserialize, crate::ToParameters)]
1290 #[allow(dead_code)]
1291 struct ListQuery {
1292 page: i32,
1293 #[serde(rename = "pageSize")]
1294 page_size: i32,
1295 name: String,
1296 keyword: Option<String>,
1297 }
1298
1299 #[salvo_oapi::endpoint]
1300 async fn list(query: ListQuery) -> &'static str {
1301 let _ = query;
1302 "ok"
1303 }
1304
1305 let router = Router::with_path("/list").get(list);
1306 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1307
1308 let path_item = doc.paths.get("/list").expect("/list entry should exist");
1309 let operation = path_item
1310 .operations
1311 .get(&PathItemType::Get)
1312 .expect("get operation should exist");
1313
1314 let by_name = |name: &str| {
1315 operation
1316 .parameters
1317 .0
1318 .iter()
1319 .find(|p| p.name == name)
1320 .unwrap_or_else(|| panic!("parameter `{name}` should exist"))
1321 };
1322
1323 for name in ["page", "pageSize", "name", "keyword"] {
1324 assert_eq!(
1325 by_name(name).parameter_in,
1326 ParameterIn::Query,
1327 "parameter `{name}` should be located in query"
1328 );
1329 }
1330 assert_eq!(by_name("page").required, Required::True);
1331 assert_eq!(by_name("pageSize").required, Required::True);
1332 assert_eq!(by_name("name").required, Required::True);
1333 assert_eq!(by_name("keyword").required, Required::False);
1334 }
1335
1336 #[test]
1337 fn to_parameters_accepts_singular_and_plural_keys() {
1338 #[derive(Deserialize, crate::ToParameters)]
1343 #[salvo(parameter(default_parameter_in = Header))]
1345 #[allow(dead_code)]
1346 struct AliasQuery {
1347 page: i32,
1348 #[salvo(parameters(rename = "renamed"))]
1350 raw: String,
1351 }
1352
1353 #[salvo_oapi::endpoint]
1354 async fn list(query: AliasQuery) -> &'static str {
1355 let _ = query;
1356 "ok"
1357 }
1358
1359 let router = Router::with_path("/alias").get(list);
1360 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1361 let operation = doc
1362 .paths
1363 .get("/alias")
1364 .and_then(|item| item.operations.get(&PathItemType::Get))
1365 .expect("get operation should exist");
1366 let names: Vec<&str> = operation
1367 .parameters
1368 .0
1369 .iter()
1370 .map(|p| p.name.as_str())
1371 .collect();
1372
1373 for param in &operation.parameters.0 {
1375 assert_eq!(
1376 param.parameter_in,
1377 ParameterIn::Header,
1378 "parameter `{}` should inherit the container `default_parameter_in`",
1379 param.name
1380 );
1381 }
1382 assert!(
1384 names.contains(&"renamed"),
1385 "field rename alias not applied: {names:?}"
1386 );
1387 assert!(!names.contains(&"raw"));
1388 }
1389
1390 #[test]
1391 fn merge_router_skips_route_without_method_filter() {
1392 #[salvo_oapi::endpoint]
1393 async fn any_handler() -> &'static str {
1394 "ok"
1395 }
1396
1397 let router = Router::with_path("/no-method").goal(any_handler);
1401 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1402
1403 assert!(
1404 !doc.paths.contains_key("/no-method"),
1405 "expected no path entry when the route lacks a method filter; \
1406 got: {:?}",
1407 doc.paths.keys().collect::<Vec<_>>()
1408 );
1409 }
1410
1411 #[test]
1412 fn merge_router_attaches_only_to_explicit_method() {
1413 #[salvo_oapi::endpoint]
1414 async fn delete_thing() -> &'static str {
1415 "ok"
1416 }
1417
1418 let router = Router::with_path("/thing").delete(delete_thing);
1421 let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);
1422
1423 let path_item = doc.paths.get("/thing").expect("/thing entry should exist");
1424 assert!(path_item.operations.contains_key(&PathItemType::Delete));
1425 assert!(!path_item.operations.contains_key(&PathItemType::Get));
1426 assert!(!path_item.operations.contains_key(&PathItemType::Post));
1427 assert!(!path_item.operations.contains_key(&PathItemType::Put));
1428 assert!(!path_item.operations.contains_key(&PathItemType::Patch));
1429 }
1430
1431 #[test]
1432 fn test_build_openapi() {
1433 let _doc = OpenApi::new("pet api", "0.1.0")
1434 .info(Info::new("my pet api", "0.2.0"))
1435 .servers(Servers::new())
1436 .add_path(
1437 "/api/v1",
1438 PathItem::new(PathItemType::Get, Operation::new()),
1439 )
1440 .security([SecurityRequirement::default()])
1441 .add_security_scheme(
1442 "api_key",
1443 SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
1444 )
1445 .extend_security_schemes([("TLS", SecurityScheme::MutualTls { description: None })])
1446 .add_schema("example", Schema::object(Object::new()))
1447 .extend_schemas([("", Schema::from(Object::new()))])
1448 .response("200", Response::new("OK"))
1449 .extend_responses([("404", Response::new("Not Found"))])
1450 .tags(["tag1", "tag2"])
1451 .external_docs(ExternalDocs::default())
1452 .into_router("/openapi/doc");
1453 }
1454
1455 #[test]
1456 fn json_schema_dialect_serializes_under_spec_field_name() -> Result<(), serde_json::Error> {
1457 let doc = OpenApi::new("api", "0.1.0")
1458 .json_schema_dialect("https://json-schema.org/draft/2020-12/schema");
1459 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1460
1461 assert_eq!(
1462 value["jsonSchemaDialect"],
1463 Value::String("https://json-schema.org/draft/2020-12/schema".to_owned()),
1464 "expected top-level `jsonSchemaDialect` field per OpenAPI 3.1.0"
1465 );
1466 assert!(
1467 value.get("$schema").is_none(),
1468 "`$schema` is the JSON Schema keyword inside Schema Objects, not the OpenAPI \
1469 document-level field"
1470 );
1471 Ok(())
1472 }
1473
1474 #[test]
1475 fn json_schema_dialect_omits_field_when_empty() -> Result<(), serde_json::Error> {
1476 let doc = OpenApi::new("api", "0.1.0");
1477 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1478
1479 assert!(value.get("jsonSchemaDialect").is_none());
1480 assert!(value.get("$schema").is_none());
1481 Ok(())
1482 }
1483
1484 #[test]
1485 fn webhooks_omits_field_when_empty() -> Result<(), serde_json::Error> {
1486 let doc = OpenApi::new("api", "0.1.0");
1487 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1488
1489 assert!(value.get("webhooks").is_none());
1490 Ok(())
1491 }
1492
1493 #[test]
1494 fn webhooks_serializes_inline_path_item() -> Result<(), serde_json::Error> {
1495 let doc = OpenApi::new("api", "0.1.0").add_webhook(
1496 "newPet",
1497 PathItem::new(
1498 PathItemType::Post,
1499 Operation::new().add_response("200", Response::new("acknowledged")),
1500 ),
1501 );
1502 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1503
1504 assert_eq!(
1505 value["webhooks"],
1506 json!({
1507 "newPet": {
1508 "post": {
1509 "responses": {
1510 "200": { "description": "acknowledged" }
1511 }
1512 }
1513 }
1514 })
1515 );
1516 Ok(())
1517 }
1518
1519 #[test]
1520 fn webhooks_serializes_reference_object() -> Result<(), serde_json::Error> {
1521 let doc = OpenApi::new("api", "0.1.0").add_webhook(
1522 "newPet",
1523 RefOr::Ref(Ref::new("#/components/pathItems/NewPetWebhook")),
1524 );
1525 let value: Value = serde_json::from_str(&doc.to_json()?)?;
1526
1527 assert_eq!(
1528 value["webhooks"]["newPet"],
1529 json!({ "$ref": "#/components/pathItems/NewPetWebhook" })
1530 );
1531 Ok(())
1532 }
1533
1534 #[test]
1535 fn webhooks_merge_combines_entries() {
1536 let api_a = OpenApi::new("a", "1.0").add_webhook(
1537 "newPet",
1538 PathItem::new(PathItemType::Post, Operation::new()),
1539 );
1540 let api_b = OpenApi::new("b", "1.0").add_webhook(
1541 "deletedPet",
1542 PathItem::new(PathItemType::Post, Operation::new()),
1543 );
1544
1545 let merged = api_a.merge(api_b);
1546
1547 assert!(merged.webhooks.contains_key("newPet"));
1548 assert!(merged.webhooks.contains_key("deletedPet"));
1549 }
1550
1551 #[test]
1552 fn test_openapi_to_pretty_json() -> Result<(), serde_json::Error> {
1553 let raw_json = r#"{
1554 "openapi": "3.1.0",
1555 "info": {
1556 "title": "My api",
1557 "description": "My api description",
1558 "license": {
1559 "name": "MIT",
1560 "url": "http://mit.licence"
1561 },
1562 "version": "1.0.0",
1563 "contact": {},
1564 "termsOfService": "terms of service"
1565 },
1566 "paths": {}
1567 }"#;
1568 let doc: OpenApi = OpenApi::with_info(
1569 Info::default()
1570 .description("My api description")
1571 .license(License::new("MIT").url("http://mit.licence"))
1572 .title("My api")
1573 .version("1.0.0")
1574 .terms_of_service("terms of service")
1575 .contact(Contact::default()),
1576 );
1577 let serialized = doc.to_pretty_json()?;
1578
1579 assert_eq!(
1580 Value::from_str(&serialized)?,
1581 Value::from_str(raw_json)?,
1582 "expected serialized json to match raw: \nserialized: \n{serialized} \nraw: \n{raw_json}"
1583 );
1584 Ok(())
1585 }
1586
1587 #[test]
1588 fn test_deprecated_from_bool() {
1589 assert_eq!(Deprecated::True, Deprecated::from(true));
1590 assert_eq!(Deprecated::False, Deprecated::from(false));
1591 }
1592
1593 #[test]
1594 fn test_deprecated_deserialize() {
1595 let deserialize_result = serde_json::from_str::<Deprecated>("true");
1596 assert_eq!(deserialize_result.unwrap(), Deprecated::True);
1597 let deserialize_result = serde_json::from_str::<Deprecated>("false");
1598 assert_eq!(deserialize_result.unwrap(), Deprecated::False);
1599 }
1600
1601 #[test]
1602 fn test_required_from_bool() {
1603 assert_eq!(Required::True, Required::from(true));
1604 assert_eq!(Required::False, Required::from(false));
1605 }
1606
1607 #[test]
1608 fn test_required_deserialize() {
1609 let deserialize_result = serde_json::from_str::<Required>("true");
1610 assert_eq!(deserialize_result.unwrap(), Required::True);
1611 let deserialize_result = serde_json::from_str::<Required>("false");
1612 assert_eq!(deserialize_result.unwrap(), Required::False);
1613 }
1614
1615 #[tokio::test]
1616 async fn test_openapi_handle() {
1617 let doc = OpenApi::new("pet api", "0.1.0");
1618 let mut req = Request::new();
1619 let mut depot = Depot::new();
1620 let mut res = salvo_core::Response::new();
1621 let mut ctrl = FlowCtrl::default();
1622 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1623
1624 let bytes = match res.body.take() {
1625 ResBody::Once(bytes) => bytes,
1626 _ => Bytes::new(),
1627 };
1628
1629 assert_eq!(
1630 res.content_type()
1631 .expect("content type should exist")
1632 .to_string(),
1633 "application/json; charset=utf-8".to_owned()
1634 );
1635 assert_eq!(
1636 bytes,
1637 Bytes::from_static(
1638 b"{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"pet api\",\"version\":\"0.1.0\"},\"paths\":{}}"
1639 )
1640 );
1641 }
1642
1643 #[tokio::test]
1644 async fn test_openapi_handle_pretty() {
1645 let doc = OpenApi::new("pet api", "0.1.0");
1646
1647 let mut req = Request::new();
1648 req.queries_mut()
1649 .insert("pretty".to_owned(), "true".to_owned());
1650
1651 let mut depot = Depot::new();
1652 let mut res = salvo_core::Response::new();
1653 let mut ctrl = FlowCtrl::default();
1654 doc.handle(&mut req, &mut depot, &mut res, &mut ctrl).await;
1655
1656 let bytes = match res.body.take() {
1657 ResBody::Once(bytes) => bytes,
1658 _ => Bytes::new(),
1659 };
1660
1661 assert_eq!(
1662 res.content_type()
1663 .expect("content type should exist")
1664 .to_string(),
1665 "application/json; charset=utf-8".to_owned()
1666 );
1667 assert_eq!(
1668 bytes,
1669 Bytes::from_static(b"{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"pet api\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {}\n}")
1670 );
1671 }
1672
1673 #[test]
1674 fn test_openapi_schema_work_with_generics() {
1675 crate::naming::set_namer(crate::naming::FlexNamer::new());
1677
1678 #[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
1679 #[salvo(schema(name = City))]
1680 pub(crate) struct CityDTO {
1681 #[salvo(schema(rename = "id"))]
1682 pub(crate) id: String,
1683 #[salvo(schema(rename = "name"))]
1684 pub(crate) name: String,
1685 }
1686
1687 #[derive(Serialize, Deserialize, Debug, ToSchema)]
1688 #[salvo(schema(name = Response))]
1689 pub(crate) struct ApiResponse<T: Serialize + ToSchema + Send + Debug + 'static> {
1690 #[salvo(schema(rename = "status"))]
1691 pub(crate) status: String,
1693 #[salvo(schema(rename = "msg"))]
1694 pub(crate) message: String,
1696 #[salvo(schema(rename = "data"))]
1697 pub(crate) data: T,
1699 }
1700
1701 #[salvo_oapi::endpoint(
1702 operation_id = "get_all_cities",
1703 tags("city"),
1704 status_codes(200, 400, 401, 403, 500)
1705 )]
1706 pub async fn get_all_cities() -> Result<Json<ApiResponse<Vec<CityDTO>>>, StatusError> {
1707 Ok(Json(ApiResponse {
1708 status: "200".to_owned(),
1709 message: "OK".to_owned(),
1710 data: vec![CityDTO {
1711 id: "1".to_owned(),
1712 name: "Beijing".to_owned(),
1713 }],
1714 }))
1715 }
1716
1717 let doc = salvo_oapi::OpenApi::new("my application", "0.1.0")
1718 .add_server(Server::new("/api/bar/").description("this is description of the server"));
1719
1720 let router = Router::with_path("/cities").get(get_all_cities);
1721 let doc = doc.merge_router(&router);
1722
1723 assert_eq!(
1724 json! {{
1725 "openapi": "3.1.0",
1726 "info": {
1727 "title": "my application",
1728 "version": "0.1.0"
1729 },
1730 "servers": [
1731 {
1732 "url": "/api/bar/",
1733 "description": "this is description of the server"
1734 }
1735 ],
1736 "paths": {
1737 "/cities": {
1738 "get": {
1739 "tags": [
1740 "city"
1741 ],
1742 "operationId": "get_all_cities",
1743 "responses": {
1744 "200": {
1745 "description": "Response with json format data",
1746 "content": {
1747 "application/json": {
1748 "schema": {
1749 "$ref": "#/components/schemas/Response<alloc.vec.Vec<City>>"
1750 }
1751 }
1752 }
1753 },
1754 "400": {
1755 "description": "The request could not be understood by the server due to malformed syntax.",
1756 "content": {
1757 "application/json": {
1758 "schema": {
1759 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1760 }
1761 }
1762 }
1763 },
1764 "401": {
1765 "description": "The request requires user authentication.",
1766 "content": {
1767 "application/json": {
1768 "schema": {
1769 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1770 }
1771 }
1772 }
1773 },
1774 "403": {
1775 "description": "The server refused to authorize the request.",
1776 "content": {
1777 "application/json": {
1778 "schema": {
1779 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1780 }
1781 }
1782 }
1783 },
1784 "500": {
1785 "description": "The server encountered an internal error while processing this request.",
1786 "content": {
1787 "application/json": {
1788 "schema": {
1789 "$ref": "#/components/schemas/salvo_core.http.errors.status_error.StatusError"
1790 }
1791 }
1792 }
1793 }
1794 }
1795 }
1796 }
1797 },
1798 "components": {
1799 "schemas": {
1800 "City": {
1801 "type": "object",
1802 "required": [
1803 "id",
1804 "name"
1805 ],
1806 "properties": {
1807 "id": {
1808 "type": "string"
1809 },
1810 "name": {
1811 "type": "string"
1812 }
1813 }
1814 },
1815 "Response<alloc.vec.Vec<City>>": {
1816 "type": "object",
1817 "required": [
1818 "status",
1819 "msg",
1820 "data"
1821 ],
1822 "properties": {
1823 "data": {
1824 "allOf": [
1825 {
1826 "type": "array",
1827 "items": {
1828 "$ref": "#/components/schemas/City"
1829 }
1830 },
1831 {
1832 "description": "The data returned"
1833 }
1834 ]
1835 },
1836 "msg": {
1837 "type": "string",
1838 "description": "Status msg"
1839 },
1840 "status": {
1841 "type": "string",
1842 "description": "status code"
1843 }
1844 }
1845 },
1846 "salvo_core.http.errors.status_error.StatusError": {
1847 "type": "object",
1848 "required": [
1849 "code",
1850 "name",
1851 "brief",
1852 "detail"
1853 ],
1854 "properties": {
1855 "brief": {
1856 "type": "string"
1857 },
1858 "cause": {
1859 "type": "string"
1860 },
1861 "code": {
1862 "type": "integer",
1863 "format": "uint16",
1864 "minimum": 0
1865 },
1866 "detail": {
1867 "type": "string"
1868 },
1869 "name": {
1870 "type": "string"
1871 }
1872 }
1873 }
1874 }
1875 }
1876 }},
1877 Value::from_str(&doc.to_json().unwrap()).unwrap()
1878 );
1879 }
1880}