allframe_core/router/
docs.rs1use crate::router::Router;
8
9#[derive(Debug, Clone)]
13pub struct DocsConfig {
14 pub path: String,
16 pub title: String,
18 pub version: String,
20 pub description: Option<String>,
22}
23
24impl DocsConfig {
25 pub fn new(
27 path: impl Into<String>,
28 title: impl Into<String>,
29 version: impl Into<String>,
30 ) -> Self {
31 Self {
32 path: path.into(),
33 title: title.into(),
34 version: version.into(),
35 description: None,
36 }
37 }
38
39 pub fn with_description(mut self, description: impl Into<String>) -> Self {
41 self.description = Some(description.into());
42 self
43 }
44
45 pub fn openapi_path(&self) -> String {
47 format!("{}/openapi.json", self.path.trim_end_matches('/'))
48 }
49}
50
51impl Router {
52 pub fn docs_config(&self, path: &str, title: &str, version: &str) -> DocsConfig {
56 DocsConfig::new(path, title, version)
57 }
58
59 pub fn openapi_json(&self, title: &str, version: &str) -> String {
64 let spec = self.to_openapi(title, version);
65 serde_json::to_string_pretty(&spec).unwrap_or_else(|_| "{}".to_string())
66 }
67
68 pub fn openapi_json_with_description(
70 &self,
71 title: &str,
72 version: &str,
73 description: &str,
74 ) -> String {
75 let spec = self.to_openapi_with_description(title, version, description);
76 serde_json::to_string_pretty(&spec).unwrap_or_else(|_| "{}".to_string())
77 }
78
79 pub fn docs_html(&self, config: &DocsConfig) -> String {
85 let openapi_path = config.openapi_path();
86
87 format!(
88 r#"<!DOCTYPE html>
89<html lang="en">
90<head>
91 <meta charset="UTF-8">
92 <meta name="viewport" content="width=device-width, initial-scale=1.0">
93 <title>{title} - API Documentation</title>
94 <style>
95 body {{
96 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
97 max-width: 800px;
98 margin: 40px auto;
99 padding: 20px;
100 line-height: 1.6;
101 }}
102 h1 {{ color: #333; }}
103 .info {{ background: #f5f5f5; padding: 20px; border-radius: 8px; }}
104 .links {{ margin-top: 30px; }}
105 .links a {{
106 display: inline-block;
107 margin: 10px 10px 10px 0;
108 padding: 10px 20px;
109 background: #007bff;
110 color: white;
111 text-decoration: none;
112 border-radius: 4px;
113 }}
114 .links a:hover {{ background: #0056b3; }}
115 </style>
116</head>
117<body>
118 <h1>{title}</h1>
119 <div class="info">
120 <p><strong>Version:</strong> {version}</p>
121 {description}
122 </div>
123 <div class="links">
124 <a href="{openapi_path}">OpenAPI Specification</a>
125 </div>
126 <p>
127 <small>Built with <a href="https://github.com/all-source-os/all-frame">AllFrame</a></small>
128 </p>
129</body>
130</html>"#,
131 title = config.title,
132 version = config.version,
133 description = config
134 .description
135 .as_ref()
136 .map(|d| format!("<p>{}</p>", d))
137 .unwrap_or_default(),
138 openapi_path = openapi_path
139 )
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use serde_json::Value;
146
147 use super::*;
148
149 #[test]
150 fn test_docs_config_creation() {
151 let config = DocsConfig::new("/docs", "My API", "1.0.0");
152
153 assert_eq!(config.path, "/docs");
154 assert_eq!(config.title, "My API");
155 assert_eq!(config.version, "1.0.0");
156 assert_eq!(config.description, None);
157 }
158
159 #[test]
160 fn test_docs_config_with_description() {
161 let config = DocsConfig::new("/docs", "My API", "1.0.0").with_description("A great API");
162
163 assert_eq!(config.description, Some("A great API".to_string()));
164 }
165
166 #[test]
167 fn test_openapi_path() {
168 let config = DocsConfig::new("/docs", "API", "1.0");
169 assert_eq!(config.openapi_path(), "/docs/openapi.json");
170 }
171
172 #[test]
173 fn test_openapi_path_with_trailing_slash() {
174 let config = DocsConfig::new("/docs/", "API", "1.0");
175 assert_eq!(config.openapi_path(), "/docs/openapi.json");
176 }
177
178 #[tokio::test]
179 async fn test_router_docs_config() {
180 let router = Router::new();
181 let config = router.docs_config("/api-docs", "Test API", "2.0.0");
182
183 assert_eq!(config.path, "/api-docs");
184 assert_eq!(config.title, "Test API");
185 assert_eq!(config.version, "2.0.0");
186 }
187
188 #[tokio::test]
189 async fn test_openapi_json() {
190 let mut router = Router::new();
191 router.get("/users", || async { "Users".to_string() });
192
193 let json = router.openapi_json("Test API", "1.0.0");
194
195 assert!(json.contains("\"openapi\": \"3.1.0\""));
196 assert!(json.contains("\"title\": \"Test API\""));
197 assert!(json.contains("\"/users\""));
198 }
199
200 #[tokio::test]
201 async fn test_openapi_json_with_description() {
202 let router = Router::new();
203 let json = router.openapi_json_with_description("Test API", "1.0.0", "A test API");
204
205 assert!(json.contains("\"description\": \"A test API\""));
206 }
207
208 #[tokio::test]
209 async fn test_docs_html() {
210 let router = Router::new();
211 let config = DocsConfig::new("/docs", "My API", "1.0.0");
212 let html = router.docs_html(&config);
213
214 assert!(html.contains("<title>My API - API Documentation</title>"));
215 assert!(html.contains("Version:</strong> 1.0.0"));
216 assert!(html.contains("href=\"/docs/openapi.json\""));
217 }
218
219 #[tokio::test]
220 async fn test_docs_html_with_description() {
221 let router = Router::new();
222 let config = DocsConfig::new("/docs", "My API", "1.0.0").with_description("A great API");
223 let html = router.docs_html(&config);
224
225 assert!(html.contains("A great API"));
226 }
227
228 #[tokio::test]
229 async fn test_docs_html_contains_allframe_link() {
230 let router = Router::new();
231 let config = DocsConfig::new("/docs", "API", "1.0");
232 let html = router.docs_html(&config);
233
234 assert!(html.contains("AllFrame"));
235 assert!(html.contains("github.com/all-source-os/all-frame"));
236 }
237
238 #[tokio::test]
239 async fn test_openapi_json_is_valid_json() {
240 let mut router = Router::new();
241 router.get("/test", || async { "Test".to_string() });
242
243 let json = router.openapi_json("API", "1.0");
244 let parsed: Value = serde_json::from_str(&json).expect("Should be valid JSON");
245
246 assert_eq!(parsed["openapi"], "3.1.0");
247 assert_eq!(parsed["info"]["title"], "API");
248 }
249}