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 metadata_route = warp::path("metadata.json")
139 .and(warp::get())
140 .and_then(|| async move {
141 let metadata = serde_json::json!({
142 "version": env!("CARGO_PKG_VERSION")
143 });
144
145 let metadata_json = match serde_json::to_string_pretty(&metadata) {
146 Ok(json) => json,
147 Err(e) => {
148 eprintln!("Failed to serialize metadata: {}", e);
149 return Err(warp::reject::custom(SerializationError));
150 }
151 };
152
153 Ok::<_, warp::Rejection>(warp::reply::with_header(
154 metadata_json,
155 "content-type",
156 "application/json",
157 ))
158 });
159
160 let index_route = warp::path::end().and(warp::get()).and_then(serve_index);
162
163 let static_route = warp::path::tail()
165 .and(warp::get())
166 .and_then(serve_static_file);
167
168 let routes = features_route
169 .or(metadata_route)
170 .or(index_route)
171 .or(static_route)
172 .with(warp::cors().allow_any_origin())
173 .recover(handle_rejection);
174
175 println!(
176 "Server running at http://{}:{}",
177 config
178 .host
179 .iter()
180 .map(|&b| b.to_string())
181 .collect::<Vec<_>>()
182 .join("."),
183 config.port,
184 );
185 warp::serve(routes).run((config.host, config.port)).await;
186
187 Ok(())
188}
189
190async fn setup_file_watcher(
192 features_data: Arc<RwLock<Vec<Feature>>>,
193 watch_path: PathBuf,
194) -> Result<()> {
195 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
196
197 let watch_path_clone = watch_path.clone();
199 let _watcher = tokio::task::spawn_blocking(move || -> Result<RecommendedWatcher> {
200 let mut watcher = RecommendedWatcher::new(
201 move |res: notify::Result<Event>| {
202 match res {
203 Ok(event) => {
204 if let Err(e) = tx.blocking_send(event) {
206 eprintln!("Failed to send file system event: {}", e);
207 }
208 }
209 Err(e) => eprintln!("File watcher error: {:?}", e),
210 }
211 },
212 Config::default(),
213 )?;
214
215 watcher.watch(&watch_path_clone, RecursiveMode::Recursive)?;
216 Ok(watcher)
217 })
218 .await??;
219
220 while let Some(event) = rx.recv().await {
222 let should_recompute = event.paths.iter().any(|path| {
224 path.file_name()
225 .map(|name| name == "README.md")
226 .unwrap_or(false)
227 || event.kind.is_create()
228 || event.kind.is_remove()
229 });
230
231 if should_recompute {
232 sleep(Duration::from_millis(500)).await;
234
235 match list_files_recursive_with_changes(&watch_path) {
236 Ok(new_features) => {
237 let mut features = features_data.write().await;
238 *features = new_features;
239 println!("â
Features updated successfully");
240 }
241 Err(e) => {
242 eprintln!("â Failed to recompute features: {}", e);
243 }
244 }
245 }
246 }
247
248 Ok(())
249}
250
251#[derive(Debug)]
253struct SerializationError;
254impl warp::reject::Reject for SerializationError {}
255
256async fn serve_index() -> Result<impl warp::Reply, warp::Rejection> {
265 if let Some(file) = STATIC_DIR.get_file("index.html") {
266 let content = file.contents_utf8().unwrap_or("");
267 Ok(
268 warp::reply::with_header(content, "content-type", "text/html; charset=utf-8")
269 .into_response(),
270 )
271 } else {
272 let html = create_default_index_html();
273 Ok(
274 warp::reply::with_header(html, "content-type", "text/html; charset=utf-8")
275 .into_response(),
276 )
277 }
278}
279
280async fn serve_static_file(path: warp::path::Tail) -> Result<impl warp::Reply, warp::Rejection> {
290 let path_str = path.as_str();
291
292 if let Some(file) = STATIC_DIR.get_file(path_str) {
294 let content_type = get_content_type(path_str);
295
296 if let Some(contents) = file.contents_utf8() {
297 Ok(warp::reply::with_header(contents, "content-type", content_type).into_response())
299 } else {
300 Ok(
302 warp::reply::with_header(file.contents(), "content-type", content_type)
303 .into_response(),
304 )
305 }
306 } else {
307 Err(warp::reject::not_found())
308 }
309}
310
311fn get_content_type(path: &str) -> &'static str {
321 let extension = Path::new(path)
322 .extension()
323 .and_then(|ext| ext.to_str())
324 .unwrap_or("");
325
326 match extension.to_lowercase().as_str() {
327 "html" => "text/html; charset=utf-8",
328 "css" => "text/css; charset=utf-8",
329 "js" => "application/javascript; charset=utf-8",
330 "json" => "application/json; charset=utf-8",
331 "svg" => "image/svg+xml",
332 "png" => "image/png",
333 "jpg" | "jpeg" => "image/jpeg",
334 "gif" => "image/gif",
335 "ico" => "image/x-icon",
336 "txt" => "text/plain; charset=utf-8",
337 "pdf" => "application/pdf",
338 "xml" => "application/xml; charset=utf-8",
339 "woff" => "font/woff",
340 "woff2" => "font/woff2",
341 "ttf" => "font/ttf",
342 "eot" => "application/vnd.ms-fontobject",
343 _ => "application/octet-stream",
344 }
345}
346
347fn create_default_index_html() -> String {
349 r#"<!DOCTYPE html>
350<html lang="en">
351<head>
352 <meta charset="UTF-8">
353 <meta name="viewport" content="width=device-width, initial-scale=1.0">
354 <title>Features Dashboard</title>
355 <style>
356 body {
357 font-family: Arial, sans-serif;
358 margin: 40px;
359 line-height: 1.6;
360 background: #f5f5f5;
361 }
362 .container {
363 max-width: 800px;
364 margin: 0 auto;
365 background: white;
366 padding: 30px;
367 border-radius: 8px;
368 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
369 }
370 h1 { color: #333; }
371 .links { list-style: none; padding: 0; }
372 .links li { margin: 10px 0; }
373 .links a {
374 color: #007acc;
375 text-decoration: none;
376 padding: 8px 15px;
377 border: 1px solid #007acc;
378 border-radius: 4px;
379 display: inline-block;
380 }
381 .links a:hover { background: #007acc; color: white; }
382 </style>
383</head>
384<body>
385 <div class="container">
386 <h1>đī¸ Features Dashboard</h1>
387 <p>Welcome to the feature-based architecture server!</p>
388 <ul class="links">
389 <li><a href="/features.json">đ View Features JSON</a></li>
390 <li><a href="/metadata.json">âšī¸ View Metadata JSON</a></li>
391 </ul>
392 <p><small>This server provides features data and serves embedded static files from the binary.</small></p>
393 </div>
394</body>
395</html>"#.to_string()
396}
397
398async fn handle_rejection(
400 err: warp::Rejection,
401) -> Result<impl warp::Reply, std::convert::Infallible> {
402 let code;
403 let message;
404
405 if err.is_not_found() {
406 code = warp::http::StatusCode::NOT_FOUND;
407 message = "NOT_FOUND";
408 } else if err
409 .find::<warp::filters::body::BodyDeserializeError>()
410 .is_some()
411 {
412 code = warp::http::StatusCode::BAD_REQUEST;
413 message = "BAD_REQUEST";
414 } else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
415 code = warp::http::StatusCode::METHOD_NOT_ALLOWED;
416 message = "METHOD_NOT_ALLOWED";
417 } else {
418 eprintln!("Unhandled rejection: {:?}", err);
419 code = warp::http::StatusCode::INTERNAL_SERVER_ERROR;
420 message = "INTERNAL_SERVER_ERROR";
421 }
422
423 Ok(warp::reply::with_status(message, code))
424}