1use rocket::get;
2use rocket::response::content;
3use serde::{Deserialize, Serialize};
4use env_logger::{Builder, Target};
5use lazy_static::lazy_static;
6use reqwest::Client;
7use rocket::serde::json::Json;
8use rocket::Build;
9use rocket::Rocket;
10use std::{env, sync::Arc};
11use tokio::sync::RwLock;
12use std::sync::Mutex;
13
14#[derive(Serialize, Clone)]
16pub struct RouteInfo {
17 path: String,
19 methods: Vec<String>,
21}
22
23#[derive(Serialize)]
25pub struct RoutesResponse {
26 routes: Vec<RouteInfo>,
28}
29
30#[derive(Clone)]
32pub struct RoutesCollection {
33 routes: Vec<RouteInfo>,
34}
35
36impl RoutesCollection {
37 pub fn new() -> Self {
38 Self { routes: Vec::new() }
39 }
40
41 pub fn add_route(&mut self, path: String, method: String) {
42 if let Some(route) = self.routes.iter_mut().find(|r| r.path == path) {
44 if !route.methods.contains(&method) {
46 route.methods.push(method);
47 }
48 } else {
49 self.routes.push(RouteInfo {
51 path,
52 methods: vec![method],
53 });
54 }
55 }
56
57 pub fn get_routes(&self) -> Vec<RouteInfo> {
58 self.routes.clone()
59 }
60}
61
62lazy_static! {
65 static ref ROUTES_COLLECTION: Arc<Mutex<RoutesCollection>> = Arc::new(Mutex::new(RoutesCollection::new()));
66}
67
68fn decode_route_path(path: &str) -> String {
70 let mut result = path.to_string();
71
72 let replacements = [
74 ("\\u003C", "<"), ("\\u003E", ">"),
75 ("\\u003c", "<"), ("\\u003e", ">"),
76 ("\\u0026", "&"), ("\\u0027", "'"),
77 ("\\u0022", "\""), ("\\u003D", "="),
78 ("\\u003F", "?"), ("\\u002F", "/"),
79 ];
80
81 for (encoded, decoded) in replacements.iter() {
82 result = result.replace(encoded, decoded);
83 }
84
85 result
86}
87
88fn escape_html_in_path(path: &str) -> String {
89 path.replace("<", "<").replace(">", ">")
90}
91
92pub fn collect_routes(rocket: &Rocket<Build>) {
94 let mut routes_collection = ROUTES_COLLECTION.lock().unwrap();
95
96 for route in rocket.routes() {
97 let path = decode_route_path(&route.uri.to_string());
99
100 routes_collection.add_route(
101 path,
102 route.method.to_string(),
103 );
104 }
105}
106
107#[get("/")]
109pub fn index() -> content::RawHtml<String> {
110 let routes_collection = ROUTES_COLLECTION.lock().unwrap();
111 let routes = routes_collection.get_routes();
112
113 let mut versions: Vec<String> = routes
115 .iter()
116 .filter_map(|route| {
117 let path = &route.path;
118 if let Some(start) = path.find("/api/v") {
119 let rest = &path[start + 6..];
120 let end = rest.find('/').unwrap_or(rest.len());
121 let version = &rest[..end];
122 if version.chars().all(|c| c.is_numeric()) {
123 Some(format!("v{}", version))
124 } else {
125 None
126 }
127 } else {
128 None
129 }
130 })
131 .collect();
132
133 versions.sort();
134 versions.dedup();
135
136 let mut version_options = String::from(
138 r#"
139 <option value="all">All Versions</option>
140 <option value="unversioned">Unversioned</option>
141 "#,
142 );
143
144 version_options.push_str(
146 &versions
147 .iter()
148 .map(|v| format!(r#"<option value="{}">{}</option>"#, v.to_lowercase(), v))
149 .collect::<String>(),
150 );
151
152 let mut route_rows = String::new();
154
155 let mut sorted_routes = routes.clone();
157 sorted_routes.sort_by(|a, b| a.path.cmp(&b.path));
158
159 for route in sorted_routes {
161 let mut methods = route.methods.clone();
163 methods.sort();
164
165 for method in methods {
166 let method_class = method.to_lowercase();
167 let escaped_path = escape_html_in_path(&route.path);
168 let version = if let Some(start) = route.path.find("/api/v") {
169 let rest = &route.path[start + 6..];
170 let end = rest.find('/').unwrap_or(rest.len());
171 format!("v{}", &rest[..end])
172 } else {
173 "unversioned".to_string()
174 };
175
176 route_rows.push_str(&format!(
177 "<tr class=\"route-row\" data-method=\"{}\" data-path=\"{}\" data-version=\"{}\">
178 <td class=\"method-col\"><span class=\"method {}\">{}</span></td>
179 <td class=\"path-col\"><a href=\"{}\" style=\"color: white; text-decoration: none;\">{}</a></td>
180 </tr>\n",
181 method.to_lowercase(),
182 escaped_path.to_lowercase(),
183 version.to_lowercase(),
184 method_class,
185 method,
186 escaped_path,
187 escaped_path
188 ));
189 }
190 }
191
192 let html = format!(
194 r#"<!DOCTYPE html>
195<html lang="en">
196<head>
197 <meta charset="UTF-8">
198 <meta name="viewport" content="width=device-width, initial-scale=1.0">
199 <title>OmniAgent API</title>
200 <style>
201 body {{
202 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
203 line-height: 1.6;
204 color: #333;
205 max-width: 900px;
206 margin: 0 auto;
207 padding: 20px;
208 }}
209 h1 {{
210 color: #2c3e50;
211 border-bottom: 2px solid #3498db;
212 padding-bottom: 10px;
213 }}
214 .container {{
215 background-color: #fff;
216 border-radius: 8px;
217 padding: 25px;
218 box-shadow: 0 4px 6px rgba(0,0,0,0.1);
219 margin-bottom: 20px;
220 }}
221 .routes-section {{
222 background-color: #fff;
223 border-radius: 8px;
224 padding: 20px;
225 box-shadow: 0 4px 6px rgba(0,0,0,0.1);
226 }}
227 table {{
228 width: 100%;
229 border-collapse: separate;
230 border-spacing: 0;
231 }}
232 th, td {{
233 text-align: left;
234 padding: 12px 15px;
235 border-bottom: 1px solid #eee;
236 }}
237 th {{
238 background-color: #f8f9fa;
239 font-weight: 600;
240 }}
241 .method {{
242 display: inline-block;
243 padding: 6px 10px;
244 border-radius: 4px;
245 color: white;
246 font-weight: bold;
247 min-width: 60px;
248 text-align: center;
249 }}
250 .method-col {{
251 width: 100px;
252 }}
253 .get {{
254 background-color: #61affe;
255 }}
256 .post {{
257 background-color: #49cc90;
258 }}
259 .put {{
260 background-color: #fca130;
261 }}
262 .delete {{
263 background-color: #f93e3e;
264 }}
265 .patch {{
266 background-color: #9c42be;
267 }}
268 /* Search and Filter Styles */
269 .search-container {{
270 display: flex;
271 gap: 10px;
272 margin-bottom: 15px;
273 }}
274 #searchInput, #methodFilter, #versionFilter {{
275 padding: 10px;
276 width: calc(33.33% - 10px);
277 border: 1px solid #ccc;
278 border-radius: 8px;
279 background-color: #f8f9fa;
280 color: #333;
281 font-size: 14px;
282 transition: all 0.3s ease;
283 }}
284 #searchInput:focus, #methodFilter:focus, #versionFilter:focus {{
285 outline: none;
286 border-color: #3498db;
287 box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
288 }}
289 option {{
290 padding: 10px;
291 }}
292 /* Dark Mode Styles */
293 @media (prefers-color-scheme: dark) {{
294 body {{
295 background-color: #1a1a1a;
296 color: #e0e0e0;
297 }}
298 .container, .routes-section {{
299 background-color: #2d2d2d;
300 box-shadow: 0 4px 6px rgba(0,0,0,0.3);
301 }}
302 th {{
303 background-color: #3d3d3d;
304 }}
305 td {{
306 border-bottom: 1px solid #444;
307 }}
308 h1 {{
309 color: #81a1c1;
310 border-bottom-color: #5e81ac;
311 }}
312 #searchInput, #methodFilter, #versionFilter {{
313 background-color: #2d2d2d;
314 color: #e0e0e0;
315 border: 1px solid #444;
316 }}
317 #searchInput:focus, #methodFilter:focus, #versionFilter:focus {{
318 border-color: #81a1c1;
319 box-shadow: 0 0 5px rgba(94, 129, 172, 0.5);
320 }}
321 }}
322 </style>
323</head>
324<body>
325 <div class="container">
326 <h1>Welcome to OmniAgent</h1>
327 <p>OmniAgent is a distributed system for managing application instances within a given worker on the OmniCloud platform. Please refer to the API documentation below to get started!</p>
328 </div>
329
330 <div class="routes-section">
331 <h2>Available Routes</h2>
332
333 <!-- Search Bar and Filters -->
334 <div class="search-container">
335 <input type="text" id="searchInput" placeholder="Search routes by path..." onkeyup="filterRoutes()">
336 <select id="methodFilter" onchange="filterRoutes()">
337 <option value="all">All Methods</option>
338 <option value="get">GET</option>
339 <option value="post">POST</option>
340 <option value="put">PUT</option>
341 <option value="delete">DELETE</option>
342 <option value="patch">PATCH</option>
343 </select>
344 <select id="versionFilter" onchange="filterRoutes()">
345 {version_options}
346 </select>
347 </div>
348
349 <table>
350 <thead>
351 <tr>
352 <th>Method</th>
353 <th>Path</th>
354 </tr>
355 </thead>
356 <tbody id="routesTable">
357 {route_rows}
358 </tbody>
359 </table>
360 </div>
361
362 <script>
363 function filterRoutes() {{
364 let input = document.getElementById('searchInput').value.toLowerCase();
365 let methodFilter = document.getElementById('methodFilter').value.toLowerCase();
366 let versionFilter = document.getElementById('versionFilter').value.toLowerCase();
367 let rows = document.querySelectorAll('.route-row');
368
369 rows.forEach(row => {{
370 let method = row.getAttribute('data-method').toLowerCase();
371 let path = row.getAttribute('data-path').toLowerCase();
372 let version = row.getAttribute('data-version').toLowerCase();
373
374 // Match method, version, and path with filters
375 let methodMatch = methodFilter === 'all' || method === methodFilter;
376 let versionMatch =
377 versionFilter === 'all' ||
378 (versionFilter === 'unversioned' && version === 'unversioned') ||
379 version === versionFilter;
380 let pathMatch = path.includes(input);
381
382 // Show row if all conditions match
383 if (methodMatch && versionMatch && pathMatch) {{
384 row.style.display = '';
385 }} else {{
386 row.style.display = 'none';
387 }}
388 }});
389 }}
390 </script>
391</body>
392</html>"#
393 );
394
395 content::RawHtml(html)
396}