1use super::config::{DEFAULT_SPEC_PATH, OpenApiSpec};
4
5const SWAGGER_UI_CDN_BASE: &str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.31.2";
6
7pub fn ui_html(specs: &[OpenApiSpec], ui_title: &str) -> String {
9 let config_js = if specs.len() <= 1 {
10 let url = specs
11 .first()
12 .map(|s| s.spec_path.as_str())
13 .unwrap_or(DEFAULT_SPEC_PATH);
14
15 format!(r#"url: {},"#, js_string(url))
16 } else {
17 let urls = specs
19 .iter()
20 .map(|s| {
21 format!(
22 r#"{{ url: {}, name: {} }}"#,
23 js_string(&s.spec_path),
24 js_string(&s.name),
25 )
26 })
27 .collect::<Vec<_>>()
28 .join(",\n ");
29
30 let primary = js_string(&specs[0].name);
31
32 format!(
33 r#"urls: [
34 {urls}
35 ],
36 "urls.primaryName": {primary},"#
37 )
38 };
39
40 format!(
41 r##"<!doctype html>
42<html lang="en">
43 <head>
44 <meta charset="utf-8" />
45 <meta name="viewport" content="width=device-width, initial-scale=1" />
46 <title>{}</title>
47 <link rel="stylesheet" href="{}/swagger-ui.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
48 </head>
49 <body>
50 <div id="swagger-ui"></div>
51
52 <script src="{}/swagger-ui-bundle.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
53 <script src="{}/swagger-ui-standalone-preset.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
54
55 <script>
56 window.onload = function() {{
57 SwaggerUIBundle({{
58 {config_js}
59 dom_id: "#swagger-ui",
60 presets: [
61 SwaggerUIBundle.presets.apis,
62 SwaggerUIStandalonePreset
63 ],
64 layout: "StandaloneLayout"
65 }});
66 }};
67 </script>
68 </body>
69</html>"##,
70 html_escape(ui_title),
71 SWAGGER_UI_CDN_BASE,
72 SWAGGER_UI_CDN_BASE,
73 SWAGGER_UI_CDN_BASE,
74 )
75}
76
77#[inline]
78fn js_string(s: &str) -> String {
79 serde_json::to_string(s).unwrap_or_else(|_| "\"\"".to_string())
81}
82
83#[inline]
84fn html_escape(s: &str) -> String {
85 let mut out = String::with_capacity(s.len());
86 for ch in s.chars() {
87 match ch {
88 '&' => out.push_str("&"),
89 '<' => out.push_str("<"),
90 '>' => out.push_str(">"),
91 '"' => out.push_str("""),
92 '\'' => out.push_str("'"),
93 _ => out.push(ch),
94 }
95 }
96 out
97}
98
99#[cfg(test)]
100mod tests {
101 use super::{SWAGGER_UI_CDN_BASE, ui_html};
102 use crate::config::OpenApiSpec;
103
104 #[test]
105 fn ui_html_uses_single_url_config_for_one_spec() {
106 let html = ui_html(&[OpenApiSpec::new("v1")], "Docs");
107
108 assert!(html.contains("url: \"/v1/openapi.json\""));
109 assert!(!html.contains("urls.primaryName"));
110 assert!(html.contains("<title>Docs</title>"));
111 assert!(html.contains(SWAGGER_UI_CDN_BASE));
112 assert!(html.contains("crossorigin=\"anonymous\""));
113 assert!(html.contains("referrerpolicy=\"no-referrer\""));
114 }
115
116 #[test]
117 fn ui_html_uses_urls_config_for_multiple_specs() {
118 let html = ui_html(&[OpenApiSpec::new("v1"), OpenApiSpec::new("admin")], "Docs");
119
120 assert!(html.contains("urls: ["));
121 assert!(html.contains("{ url: \"/v1/openapi.json\", name: \"v1\" }"));
122 assert!(html.contains("{ url: \"/admin/openapi.json\", name: \"admin\" }"));
123 assert!(html.contains("\"urls.primaryName\": \"v1\""));
124 }
125
126 #[test]
127 fn ui_html_falls_back_to_default_path_for_empty_specs() {
128 let html = ui_html(&[], "Docs");
129 assert!(html.contains("url: \"/openapi.json\""));
130 }
131
132 #[test]
133 fn ui_html_escapes_title_html() {
134 let html = ui_html(
135 &[OpenApiSpec::new("v1")],
136 r#"</title><script>alert(1)</script>"#,
137 );
138
139 assert!(
140 html.contains("<title></title><script>alert(1)</script></title>")
141 );
142 assert!(!html.contains("</title><script>alert(1)</script>"));
143 }
144
145 #[test]
146 fn ui_html_pins_exact_swagger_ui_version() {
147 let html = ui_html(&[OpenApiSpec::new("v1")], "Docs");
148
149 assert!(html.contains("/npm/swagger-ui-dist@5.31.2/swagger-ui.css"));
150 assert!(html.contains("/npm/swagger-ui-dist@5.31.2/swagger-ui-bundle.js"));
151 assert!(html.contains("/npm/swagger-ui-dist@5.31.2/swagger-ui-standalone-preset.js"));
152 assert!(!html.contains("unpkg.com/swagger-ui-dist@5/"));
153 }
154}