1use salvo_core::writing::Text;
8use salvo_core::{async_trait, Depot, FlowCtrl, Handler, Request, Response, Router};
9use std::borrow::Cow;
10
11const INDEX_TMPL: &str = r#"
12<!DOCTYPE html>
13<html>
14 <head>
15 <title>{{title}}</title>
16 {{keywords}}
17 {{description}}
18 <meta charset="utf-8">
19 <meta name="viewport" content="width=device-width, initial-scale=1">
20 <style>
21 body {
22 margin: 0;
23 padding: 0;
24 }
25 </style>
26 </head>
27
28 <body>
29 <div id="redoc-container"></div>
30 <script src="{{lib_url}}"></script>
31 <script>
32 Redoc.init(
33 "{{spec_url}}",
34 {},
35 document.getElementById("redoc-container")
36 );
37 </script>
38 </body>
39</html>
40"#;
41
42#[non_exhaustive]
44#[derive(Clone, Debug)]
45pub struct ReDoc {
46 pub title: Cow<'static, str>,
48 pub keywords: Option<Cow<'static, str>>,
50 pub description: Option<Cow<'static, str>>,
52 pub lib_url: Cow<'static, str>,
54 pub spec_url: Cow<'static, str>,
56}
57
58impl ReDoc {
59 #[must_use]
71 pub fn new(spec_url: impl Into<Cow<'static, str>>) -> Self {
72 Self {
73 title: "ReDoc".into(),
74 keywords: None,
75 description: None,
76 lib_url: "https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js".into(),
77 spec_url: spec_url.into(),
78 }
79 }
80
81 #[must_use]
83 pub fn title(mut self, title: impl Into<Cow<'static, str>>) -> Self {
84 self.title = title.into();
85 self
86 }
87
88 #[must_use]
90 pub fn keywords(mut self, keywords: impl Into<Cow<'static, str>>) -> Self {
91 self.keywords = Some(keywords.into());
92 self
93 }
94
95 #[must_use]
97 pub fn description(mut self, description: impl Into<Cow<'static, str>>) -> Self {
98 self.description = Some(description.into());
99 self
100 }
101
102 #[must_use]
104 pub fn lib_url(mut self, lib_url: impl Into<Cow<'static, str>>) -> Self {
105 self.lib_url = lib_url.into();
106 self
107 }
108
109 pub fn into_router(self, path: impl Into<String>) -> Router {
111 Router::with_path(path.into()).goal(self)
112 }
113}
114
115#[async_trait]
116impl Handler for ReDoc {
117 async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
118 let keywords = self
119 .keywords
120 .as_ref()
121 .map(|s| {
122 format!(
123 "<meta name=\"keywords\" content=\"{}\">",
124 s.split(',').map(|s| s.trim()).collect::<Vec<_>>().join(",")
125 )
126 })
127 .unwrap_or_default();
128 let description = self
129 .description
130 .as_ref()
131 .map(|s| format!("<meta name=\"description\" content=\"{s}\">"))
132 .unwrap_or_default();
133 let html = INDEX_TMPL
134 .replacen("{{spec_url}}", &self.spec_url, 1)
135 .replacen("{{lib_url}}", &self.lib_url, 1)
136 .replacen("{{description}}", &description, 1)
137 .replacen("{{keywords}}", &keywords, 1)
138 .replacen("{{title}}", &self.title, 1);
139 res.render(Text::Html(html));
140 }
141}