omni_agent/routes/
index.rs

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/// Route information structure for API documentation
15#[derive(Serialize, Clone)]
16pub struct RouteInfo {
17    /// Path of the route
18    path: String,
19    /// HTTP methods supported by the route
20    methods: Vec<String>,
21}
22
23/// Response structure for routes listing endpoint
24#[derive(Serialize)]
25pub struct RoutesResponse {
26    /// List of all available routes and their methods
27    routes: Vec<RouteInfo>,
28}
29
30/// Routes collection that will be populated during startup
31#[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        // Check if the route already exists
43        if let Some(route) = self.routes.iter_mut().find(|r| r.path == path) {
44            // Add method if it doesn't exist
45            if !route.methods.contains(&method) {
46                route.methods.push(method);
47            }
48        } else {
49            // Add new route info
50            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
62/// Global singleton instance of the routes collection
63/// Stores information about all registered API routes
64lazy_static! {
65    static ref ROUTES_COLLECTION: Arc<Mutex<RoutesCollection>> = Arc::new(Mutex::new(RoutesCollection::new()));
66}
67
68/// Decodes any encoded characters in route paths and preserves parameter notation
69fn decode_route_path(path: &str) -> String {
70    let mut result = path.to_string();
71    
72    // Handle common Unicode escape sequences
73    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("<", "&lt;").replace(">", "&gt;")
90}
91
92// Keep your existing collect_routes function
93pub fn collect_routes(rocket: &Rocket<Build>) {
94    let mut routes_collection = ROUTES_COLLECTION.lock().unwrap();
95    
96    for route in rocket.routes() {
97        // Get the path and decode any escaped characters
98        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/// Routes listing endpoint providing HTML representation of routes
108#[get("/")]
109pub fn index() -> content::RawHtml<String> {
110    let routes_collection = ROUTES_COLLECTION.lock().unwrap();
111    let routes = routes_collection.get_routes();
112
113    // Collect unique versions dynamically
114    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    // Add "All Versions" and "Unversioned" options
137    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    // Add detected versions dynamically
145    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    // Start building the HTML for the table
153    let mut route_rows = String::new();
154
155    // Sort routes for better presentation
156    let mut sorted_routes = routes.clone();
157    sorted_routes.sort_by(|a, b| a.path.cmp(&b.path));
158
159    // Create table rows
160    for route in sorted_routes {
161        // Sort methods for consistency
162        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    // Complete HTML with search, method, and version filters
193    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}