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