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::git_helper::get_repository_url;
23use crate::models::Feature;
24
25static STATIC_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/public");
27
28#[derive(Debug, Clone)]
30pub struct ServerConfig {
31 pub port: u16,
33 pub host: [u8; 4],
35}
36
37impl Default for ServerConfig {
38 fn default() -> Self {
39 Self {
40 port: 3000,
41 host: [127, 0, 0, 1],
42 }
43 }
44}
45
46impl ServerConfig {
47 pub fn new(port: u16) -> Self {
49 Self {
50 port,
51 ..Default::default()
52 }
53 }
54
55 #[allow(dead_code)]
57 pub fn with_host(mut self, host: [u8; 4]) -> Self {
58 self.host = host;
59 self
60 }
61}
62
63pub async fn serve_features_with_watching(
75 features: &[Feature],
76 port: u16,
77 watch_path: PathBuf,
78) -> Result<()> {
79 let config = ServerConfig::new(port);
80 serve_features_with_config_and_watching(features, config, Some(watch_path.clone())).await
81}
82
83pub async fn serve_features_with_config_and_watching(
95 features: &[Feature],
96 config: ServerConfig,
97 watch_path: Option<PathBuf>,
98) -> Result<()> {
99 let features_data = Arc::new(RwLock::new(features.to_vec()));
101
102 let repository_url = watch_path
104 .as_ref()
105 .and_then(|path| get_repository_url(path));
106
107 if let Some(ref path) = watch_path {
109 let features_data_clone = Arc::clone(&features_data);
110 let watch_path_clone = path.clone();
111
112 tokio::spawn(async move {
113 if let Err(e) = setup_file_watcher(features_data_clone, watch_path_clone).await {
114 eprintln!("File watcher error: {}", e);
115 }
116 });
117 }
118
119 let features_data_clone = Arc::clone(&features_data);
121 let features_route = warp::path("features.json")
122 .and(warp::get())
123 .and_then(move || {
124 let features_data = Arc::clone(&features_data_clone);
125 async move {
126 let features = features_data.read().await;
127 let features_json = match serde_json::to_string_pretty(&*features) {
128 Ok(json) => json,
129 Err(e) => {
130 eprintln!("Failed to serialize features: {}", e);
131 return Err(warp::reject::custom(SerializationError));
132 }
133 };
134
135 Ok::<_, warp::Rejection>(warp::reply::with_header(
136 features_json,
137 "content-type",
138 "application/json",
139 ))
140 }
141 });
142
143 let metadata_route = warp::path("metadata.json")
145 .and(warp::get())
146 .and_then(move || {
147 let repo_url = repository_url.clone();
148 async move {
149 let mut metadata = serde_json::json!({
150 "version": env!("CARGO_PKG_VERSION")
151 });
152
153 if let Some(url) = repo_url {
155 metadata["repository"] = serde_json::json!(url);
156 }
157
158 let metadata_json = match serde_json::to_string_pretty(&metadata) {
159 Ok(json) => json,
160 Err(e) => {
161 eprintln!("Failed to serialize metadata: {}", e);
162 return Err(warp::reject::custom(SerializationError));
163 }
164 };
165
166 Ok::<_, warp::Rejection>(warp::reply::with_header(
167 metadata_json,
168 "content-type",
169 "application/json",
170 ))
171 }
172 });
173
174 let index_route = warp::path::end().and(warp::get()).and_then(serve_index);
176
177 let static_route = warp::path::tail()
179 .and(warp::get())
180 .and_then(serve_static_file);
181
182 let routes = features_route
183 .or(metadata_route)
184 .or(index_route)
185 .or(static_route)
186 .with(warp::cors().allow_any_origin())
187 .recover(handle_rejection);
188
189 println!(
190 "Server running at http://{}:{}",
191 config
192 .host
193 .iter()
194 .map(|&b| b.to_string())
195 .collect::<Vec<_>>()
196 .join("."),
197 config.port,
198 );
199 warp::serve(routes).run((config.host, config.port)).await;
200
201 Ok(())
202}
203
204async fn setup_file_watcher(
206 features_data: Arc<RwLock<Vec<Feature>>>,
207 watch_path: PathBuf,
208) -> Result<()> {
209 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
210
211 let watch_path_clone = watch_path.clone();
213 let _watcher = tokio::task::spawn_blocking(move || -> Result<RecommendedWatcher> {
214 let mut watcher = RecommendedWatcher::new(
215 move |res: notify::Result<Event>| {
216 match res {
217 Ok(event) => {
218 if let Err(e) = tx.blocking_send(event) {
220 eprintln!("Failed to send file system event: {}", e);
221 }
222 }
223 Err(e) => eprintln!("File watcher error: {:?}", e),
224 }
225 },
226 Config::default(),
227 )?;
228
229 watcher.watch(&watch_path_clone, RecursiveMode::Recursive)?;
230 Ok(watcher)
231 })
232 .await??;
233
234 while let Some(event) = rx.recv().await {
236 let should_recompute = event.paths.iter().any(|path| {
238 path.file_name()
239 .map(|name| name == "README.md")
240 .unwrap_or(false)
241 || event.kind.is_create()
242 || event.kind.is_remove()
243 });
244
245 if should_recompute {
246 sleep(Duration::from_millis(500)).await;
248
249 match list_files_recursive_with_changes(&watch_path) {
250 Ok(new_features) => {
251 let mut features = features_data.write().await;
252 *features = new_features;
253 println!("â
Features updated successfully");
254 }
255 Err(e) => {
256 eprintln!("â Failed to recompute features: {}", e);
257 }
258 }
259 }
260 }
261
262 Ok(())
263}
264
265#[derive(Debug)]
267struct SerializationError;
268impl warp::reject::Reject for SerializationError {}
269
270async fn serve_index() -> Result<impl warp::Reply, warp::Rejection> {
279 if let Some(file) = STATIC_DIR.get_file("index.html") {
280 let content = file.contents_utf8().unwrap_or("");
281 Ok(
282 warp::reply::with_header(content, "content-type", "text/html; charset=utf-8")
283 .into_response(),
284 )
285 } else {
286 let html = create_default_index_html();
287 Ok(
288 warp::reply::with_header(html, "content-type", "text/html; charset=utf-8")
289 .into_response(),
290 )
291 }
292}
293
294async fn serve_static_file(path: warp::path::Tail) -> Result<impl warp::Reply, warp::Rejection> {
304 let path_str = path.as_str();
305
306 if let Some(file) = STATIC_DIR.get_file(path_str) {
308 let content_type = get_content_type(path_str);
309
310 if let Some(contents) = file.contents_utf8() {
311 Ok(warp::reply::with_header(contents, "content-type", content_type).into_response())
313 } else {
314 Ok(
316 warp::reply::with_header(file.contents(), "content-type", content_type)
317 .into_response(),
318 )
319 }
320 } else {
321 Err(warp::reject::not_found())
322 }
323}
324
325fn get_content_type(path: &str) -> &'static str {
335 let extension = Path::new(path)
336 .extension()
337 .and_then(|ext| ext.to_str())
338 .unwrap_or("");
339
340 match extension.to_lowercase().as_str() {
341 "html" => "text/html; charset=utf-8",
342 "css" => "text/css; charset=utf-8",
343 "js" => "application/javascript; charset=utf-8",
344 "json" => "application/json; charset=utf-8",
345 "svg" => "image/svg+xml",
346 "png" => "image/png",
347 "jpg" | "jpeg" => "image/jpeg",
348 "gif" => "image/gif",
349 "ico" => "image/x-icon",
350 "txt" => "text/plain; charset=utf-8",
351 "pdf" => "application/pdf",
352 "xml" => "application/xml; charset=utf-8",
353 "woff" => "font/woff",
354 "woff2" => "font/woff2",
355 "ttf" => "font/ttf",
356 "eot" => "application/vnd.ms-fontobject",
357 _ => "application/octet-stream",
358 }
359}
360
361fn create_default_index_html() -> String {
363 r#"<!DOCTYPE html>
364<html lang="en">
365<head>
366 <meta charset="UTF-8">
367 <meta name="viewport" content="width=device-width, initial-scale=1.0">
368 <title>Features Dashboard</title>
369 <style>
370 body {
371 font-family: Arial, sans-serif;
372 margin: 40px;
373 line-height: 1.6;
374 background: #f5f5f5;
375 }
376 .container {
377 max-width: 800px;
378 margin: 0 auto;
379 background: white;
380 padding: 30px;
381 border-radius: 8px;
382 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
383 }
384 h1 { color: #333; }
385 .links { list-style: none; padding: 0; }
386 .links li { margin: 10px 0; }
387 .links a {
388 color: #007acc;
389 text-decoration: none;
390 padding: 8px 15px;
391 border: 1px solid #007acc;
392 border-radius: 4px;
393 display: inline-block;
394 }
395 .links a:hover { background: #007acc; color: white; }
396 </style>
397</head>
398<body>
399 <div class="container">
400 <h1>đī¸ Features Dashboard</h1>
401 <p>Welcome to the feature-based architecture server!</p>
402 <ul class="links">
403 <li><a href="/features.json">đ View Features JSON</a></li>
404 <li><a href="/metadata.json">âšī¸ View Metadata JSON</a></li>
405 </ul>
406 <p><small>This server provides features data and serves embedded static files from the binary.</small></p>
407 </div>
408</body>
409</html>"#.to_string()
410}
411
412async fn handle_rejection(
414 err: warp::Rejection,
415) -> Result<impl warp::Reply, std::convert::Infallible> {
416 let code;
417 let message;
418
419 if err.is_not_found() {
420 code = warp::http::StatusCode::NOT_FOUND;
421 message = "NOT_FOUND";
422 } else if err
423 .find::<warp::filters::body::BodyDeserializeError>()
424 .is_some()
425 {
426 code = warp::http::StatusCode::BAD_REQUEST;
427 message = "BAD_REQUEST";
428 } else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
429 code = warp::http::StatusCode::METHOD_NOT_ALLOWED;
430 message = "METHOD_NOT_ALLOWED";
431 } else {
432 eprintln!("Unhandled rejection: {:?}", err);
433 code = warp::http::StatusCode::INTERNAL_SERVER_ERROR;
434 message = "INTERNAL_SERVER_ERROR";
435 }
436
437 Ok(warp::reply::with_status(message, code))
438}