1use anyhow::Result;
12use include_dir::{Dir, include_dir};
13use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use std::time::Duration;
17use tokio::sync::RwLock;
18use tokio::time::sleep;
19use warp::{Filter, Reply};
20
21use crate::file_scanner::list_files_recursive_with_changes;
22use crate::models::Feature;
23
24static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/public");
26
27#[derive(Debug, Clone)]
29pub struct ServerConfig {
30 pub port: u16,
32 pub host: [u8; 4],
34}
35
36impl Default for ServerConfig {
37 fn default() -> Self {
38 Self {
39 port: 3000,
40 host: [127, 0, 0, 1],
41 }
42 }
43}
44
45impl ServerConfig {
46 pub fn new(port: u16) -> Self {
48 Self {
49 port,
50 ..Default::default()
51 }
52 }
53
54 #[allow(dead_code)]
56 pub fn with_host(mut self, host: [u8; 4]) -> Self {
57 self.host = host;
58 self
59 }
60}
61
62pub async fn serve_features_with_watching(
74 features: &[Feature],
75 port: u16,
76 watch_path: PathBuf,
77) -> Result<()> {
78 let config = ServerConfig::new(port);
79 serve_features_with_config_and_watching(features, config, Some(watch_path)).await
80}
81
82pub async fn serve_features_with_config_and_watching(
94 features: &[Feature],
95 config: ServerConfig,
96 watch_path: Option<PathBuf>,
97) -> Result<()> {
98 let features_data = Arc::new(RwLock::new(features.to_vec()));
100
101 if let Some(ref path) = watch_path {
103 let features_data_clone = Arc::clone(&features_data);
104 let watch_path_clone = path.clone();
105
106 tokio::spawn(async move {
107 if let Err(e) = setup_file_watcher(features_data_clone, watch_path_clone).await {
108 eprintln!("File watcher error: {}", e);
109 }
110 });
111 }
112
113 let features_data_clone = Arc::clone(&features_data);
115 let features_route = warp::path("features.json")
116 .and(warp::get())
117 .and_then(move || {
118 let features_data = Arc::clone(&features_data_clone);
119 async move {
120 let features = features_data.read().await;
121 let features_json = match serde_json::to_string_pretty(&*features) {
122 Ok(json) => json,
123 Err(e) => {
124 eprintln!("Failed to serialize features: {}", e);
125 return Err(warp::reject::custom(SerializationError));
126 }
127 };
128
129 Ok::<_, warp::Rejection>(warp::reply::with_header(
130 features_json,
131 "content-type",
132 "application/json",
133 ))
134 }
135 });
136
137 let index_route = warp::path::end().and(warp::get()).and_then(serve_index);
139
140 let static_route = warp::path::tail()
142 .and(warp::get())
143 .and_then(serve_static_file);
144
145 let routes = features_route
146 .or(index_route)
147 .or(static_route)
148 .with(warp::cors().allow_any_origin())
149 .recover(handle_rejection);
150
151 println!(
152 "Server running at http://{}:{}",
153 config
154 .host
155 .iter()
156 .map(|&b| b.to_string())
157 .collect::<Vec<_>>()
158 .join("."),
159 config.port,
160 );
161 warp::serve(routes).run((config.host, config.port)).await;
162
163 Ok(())
164}
165
166async fn setup_file_watcher(
168 features_data: Arc<RwLock<Vec<Feature>>>,
169 watch_path: PathBuf,
170) -> Result<()> {
171 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
172
173 let watch_path_clone = watch_path.clone();
175 let _watcher = tokio::task::spawn_blocking(move || -> Result<RecommendedWatcher> {
176 let mut watcher = RecommendedWatcher::new(
177 move |res: notify::Result<Event>| {
178 match res {
179 Ok(event) => {
180 if let Err(e) = tx.blocking_send(event) {
182 eprintln!("Failed to send file system event: {}", e);
183 }
184 }
185 Err(e) => eprintln!("File watcher error: {:?}", e),
186 }
187 },
188 Config::default(),
189 )?;
190
191 watcher.watch(&watch_path_clone, RecursiveMode::Recursive)?;
192 Ok(watcher)
193 })
194 .await??;
195
196 while let Some(event) = rx.recv().await {
198 let should_recompute = event.paths.iter().any(|path| {
200 path.file_name()
201 .map(|name| name == "README.md")
202 .unwrap_or(false)
203 || event.kind.is_create()
204 || event.kind.is_remove()
205 });
206
207 if should_recompute {
208 sleep(Duration::from_millis(500)).await;
210
211 match list_files_recursive_with_changes(&watch_path) {
212 Ok(new_features) => {
213 let mut features = features_data.write().await;
214 *features = new_features;
215 println!("✅ Features updated successfully");
216 }
217 Err(e) => {
218 eprintln!("❌ Failed to recompute features: {}", e);
219 }
220 }
221 }
222 }
223
224 Ok(())
225}
226
227#[derive(Debug)]
229struct SerializationError;
230impl warp::reject::Reject for SerializationError {}
231
232async fn serve_index() -> Result<impl warp::Reply, warp::Rejection> {
241 if let Some(file) = STATIC_DIR.get_file("index.html") {
242 let content = file.contents_utf8().unwrap_or("");
243 Ok(
244 warp::reply::with_header(content, "content-type", "text/html; charset=utf-8")
245 .into_response(),
246 )
247 } else {
248 let html = create_default_index_html();
249 Ok(
250 warp::reply::with_header(html, "content-type", "text/html; charset=utf-8")
251 .into_response(),
252 )
253 }
254}
255
256async fn serve_static_file(path: warp::path::Tail) -> Result<impl warp::Reply, warp::Rejection> {
266 let path_str = path.as_str();
267
268 if let Some(file) = STATIC_DIR.get_file(path_str) {
270 let content_type = get_content_type(path_str);
271
272 if let Some(contents) = file.contents_utf8() {
273 Ok(warp::reply::with_header(contents, "content-type", content_type).into_response())
275 } else {
276 Ok(
278 warp::reply::with_header(file.contents(), "content-type", content_type)
279 .into_response(),
280 )
281 }
282 } else {
283 Err(warp::reject::not_found())
284 }
285}
286
287fn get_content_type(path: &str) -> &'static str {
297 let extension = Path::new(path)
298 .extension()
299 .and_then(|ext| ext.to_str())
300 .unwrap_or("");
301
302 match extension.to_lowercase().as_str() {
303 "html" => "text/html; charset=utf-8",
304 "css" => "text/css; charset=utf-8",
305 "js" => "application/javascript; charset=utf-8",
306 "json" => "application/json; charset=utf-8",
307 "svg" => "image/svg+xml",
308 "png" => "image/png",
309 "jpg" | "jpeg" => "image/jpeg",
310 "gif" => "image/gif",
311 "ico" => "image/x-icon",
312 "txt" => "text/plain; charset=utf-8",
313 "pdf" => "application/pdf",
314 "xml" => "application/xml; charset=utf-8",
315 "woff" => "font/woff",
316 "woff2" => "font/woff2",
317 "ttf" => "font/ttf",
318 "eot" => "application/vnd.ms-fontobject",
319 _ => "application/octet-stream",
320 }
321}
322
323fn create_default_index_html() -> String {
325 r#"<!DOCTYPE html>
326<html lang="en">
327<head>
328 <meta charset="UTF-8">
329 <meta name="viewport" content="width=device-width, initial-scale=1.0">
330 <title>Features Dashboard</title>
331 <style>
332 body {
333 font-family: Arial, sans-serif;
334 margin: 40px;
335 line-height: 1.6;
336 background: #f5f5f5;
337 }
338 .container {
339 max-width: 800px;
340 margin: 0 auto;
341 background: white;
342 padding: 30px;
343 border-radius: 8px;
344 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
345 }
346 h1 { color: #333; }
347 .links { list-style: none; padding: 0; }
348 .links li { margin: 10px 0; }
349 .links a {
350 color: #007acc;
351 text-decoration: none;
352 padding: 8px 15px;
353 border: 1px solid #007acc;
354 border-radius: 4px;
355 display: inline-block;
356 }
357 .links a:hover { background: #007acc; color: white; }
358 </style>
359</head>
360<body>
361 <div class="container">
362 <h1>🏗️ Features Dashboard</h1>
363 <p>Welcome to the feature-based architecture server!</p>
364 <ul class="links">
365 <li><a href="/features.json">📊 View Features JSON</a></li>
366 </ul>
367 <p><small>This server provides features data and serves embedded static files from the binary.</small></p>
368 </div>
369</body>
370</html>"#.to_string()
371}
372
373async fn handle_rejection(
375 err: warp::Rejection,
376) -> Result<impl warp::Reply, std::convert::Infallible> {
377 let code;
378 let message;
379
380 if err.is_not_found() {
381 code = warp::http::StatusCode::NOT_FOUND;
382 message = "NOT_FOUND";
383 } else if err
384 .find::<warp::filters::body::BodyDeserializeError>()
385 .is_some()
386 {
387 code = warp::http::StatusCode::BAD_REQUEST;
388 message = "BAD_REQUEST";
389 } else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
390 code = warp::http::StatusCode::METHOD_NOT_ALLOWED;
391 message = "METHOD_NOT_ALLOWED";
392 } else {
393 eprintln!("Unhandled rejection: {:?}", err);
394 code = warp::http::StatusCode::INTERNAL_SERVER_ERROR;
395 message = "INTERNAL_SERVER_ERROR";
396 }
397
398 Ok(warp::reply::with_status(message, code))
399}