1use crate::client::CrateClient;
4use crate::config::AppConfig;
5use crate::error::{CrateCheckerError, Result};
6use crate::types::*;
7use crate::utils::validate_batch_input;
8use axum::{
9 extract::{Path, Query, State},
10 http::{Method, StatusCode},
11 response::Json,
12 routing::{get, post},
13 Router,
14};
15use chrono::Utc;
16use dashmap::DashMap;
17use serde_json::Value;
18use std::collections::HashMap;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::Arc;
21use std::time::{Duration, Instant};
22use tower::ServiceBuilder;
23use tower_http::{
24 cors::{Any, CorsLayer},
25 trace::TraceLayer,
26};
27use tracing::{error, info};
28
29#[derive(Clone)]
31pub struct AppState {
32 pub client: CrateClient,
33 pub config: AppConfig,
34 pub metrics: Arc<ServerMetrics>,
35 pub cache: Arc<DashMap<String, CacheEntry>>,
36 pub start_time: Instant,
37}
38
39#[derive(Clone)]
41pub struct CacheEntry {
42 pub data: Value,
43 pub expires_at: Instant,
44}
45
46#[derive(Default)]
48pub struct ServerMetrics {
49 pub requests_total: AtomicU64,
50 pub requests_successful: AtomicU64,
51 pub requests_failed: AtomicU64,
52 pub cache_hits: AtomicU64,
53 pub cache_misses: AtomicU64,
54 pub total_response_time_ms: AtomicU64,
55}
56
57impl ServerMetrics {
58 pub fn record_request(&self, success: bool, response_time_ms: u64) {
59 self.requests_total.fetch_add(1, Ordering::Relaxed);
60 self.total_response_time_ms
61 .fetch_add(response_time_ms, Ordering::Relaxed);
62
63 if success {
64 self.requests_successful.fetch_add(1, Ordering::Relaxed);
65 } else {
66 self.requests_failed.fetch_add(1, Ordering::Relaxed);
67 }
68 }
69
70 pub fn record_cache_hit(&self) {
71 self.cache_hits.fetch_add(1, Ordering::Relaxed);
72 }
73
74 pub fn record_cache_miss(&self) {
75 self.cache_misses.fetch_add(1, Ordering::Relaxed);
76 }
77
78 pub fn get_metrics(&self) -> MetricsResponse {
79 let total = self.requests_total.load(Ordering::Relaxed);
80 let total_time = self.total_response_time_ms.load(Ordering::Relaxed);
81
82 MetricsResponse {
83 requests_total: total,
84 requests_successful: self.requests_successful.load(Ordering::Relaxed),
85 requests_failed: self.requests_failed.load(Ordering::Relaxed),
86 average_response_time_ms: if total > 0 {
87 total_time as f64 / total as f64
88 } else {
89 0.0
90 },
91 cache_hits: self.cache_hits.load(Ordering::Relaxed),
92 cache_misses: self.cache_misses.load(Ordering::Relaxed),
93 uptime_seconds: 0, }
95 }
96}
97
98pub async fn start_server(config: AppConfig) -> Result<()> {
100 info!("Starting server on {}", config.bind_address());
101
102 config.validate().map_err(CrateCheckerError::validation)?;
104
105 let client = CrateClient::builder()
107 .base_url(&config.crates_io.api_url)
108 .user_agent(&config.crates_io.user_agent)
109 .timeout(Duration::from_secs(config.crates_io.timeout_seconds))
110 .build()?;
111
112 let state = AppState {
114 client,
115 config: config.clone(),
116 metrics: Arc::new(ServerMetrics::default()),
117 cache: Arc::new(DashMap::new()),
118 start_time: Instant::now(),
119 };
120
121 let app = create_router(state);
123
124 let listener = tokio::net::TcpListener::bind(&config.bind_address()).await?;
126
127 info!("Server listening on {}", config.bind_address());
128 info!("Health check: http://{}/health", config.bind_address());
129 info!("API docs: http://{}/", config.bind_address());
130
131 axum::serve(listener, app).await?;
133
134 Ok(())
135}
136
137fn create_router(state: AppState) -> Router {
139 let mut app = Router::new()
140 .route("/health", get(health_check))
142 .route("/", get(api_docs))
144 .route("/api/crates/:name", get(get_crate))
146 .route("/api/crates/:name/:version", get(get_crate_version))
147 .route(
148 "/api/crates/:name/:version/deps",
149 get(get_crate_dependencies),
150 )
151 .route("/api/crates/:name/stats", get(get_crate_stats))
152 .route("/api/search", get(search_crates))
153 .route("/api/batch", post(handle_batch))
154 .route("/metrics", get(get_metrics))
156 .with_state(state.clone());
158
159 let service = ServiceBuilder::new().layer(TraceLayer::new_for_http());
161
162 app = app.layer(service);
163
164 if state.config.server.enable_cors {
166 app = app.layer(
167 CorsLayer::new()
168 .allow_methods([Method::GET, Method::POST])
169 .allow_headers(Any)
170 .allow_origin(Any),
171 );
172 }
173
174 app
175}
176
177async fn health_check(State(state): State<AppState>) -> Json<HealthResponse> {
179 Json(HealthResponse {
180 status: "healthy".to_string(),
181 timestamp: Utc::now(),
182 version: "1.0.0".to_string(),
183 uptime_seconds: state.start_time.elapsed().as_secs(),
184 })
185}
186
187async fn api_docs() -> &'static str {
189 r#"# Crate Checker API
190
191## Available Endpoints
192
193### Health Check
194- `GET /health` - Server health status
195
196### Crate Information
197- `GET /api/crates/{name}` - Get crate information
198- `GET /api/crates/{name}/{version}` - Check specific version
199- `GET /api/crates/{name}/{version}/deps` - Get dependencies
200- `GET /api/crates/{name}/stats` - Get download statistics
201
202### Search
203- `GET /api/search?q={query}&limit={limit}` - Search crates
204
205### Batch Operations
206- `POST /api/batch` - Process multiple crates
207
208### Monitoring
209- `GET /metrics` - Server metrics
210
211## Examples
212
213```bash
214# Check if crate exists
215curl http://localhost:3000/api/crates/serde
216
217# Search for crates
218curl "http://localhost:3000/api/search?q=http%20client&limit=5"
219
220# Batch processing
221curl -X POST http://localhost:3000/api/batch \
222 -H "Content-Type: application/json" \
223 -d '{"serde": "1.0.0", "tokio": "latest"}'
224```
225"#
226}
227
228async fn get_crate(
230 State(state): State<AppState>,
231 Path(name): Path<String>,
232) -> std::result::Result<Json<CrateInfo>, AppError> {
233 let start_time = Instant::now();
234
235 let cache_key = format!("crate:{}", name);
237 if let Some(cached) = get_from_cache(&state, &cache_key) {
238 state.metrics.record_cache_hit();
239 state
240 .metrics
241 .record_request(true, start_time.elapsed().as_millis() as u64);
242 return Ok(Json(serde_json::from_value(cached.data)?));
243 }
244
245 state.metrics.record_cache_miss();
246
247 match state.client.get_crate_info(&name).await {
248 Ok(info) => {
249 if state.config.cache.enabled {
251 set_cache(&state, &cache_key, serde_json::to_value(&info)?);
252 }
253
254 state
255 .metrics
256 .record_request(true, start_time.elapsed().as_millis() as u64);
257 Ok(Json(info))
258 }
259 Err(e) => {
260 error!("Failed to get crate info for '{}': {}", name, e);
261 state
262 .metrics
263 .record_request(false, start_time.elapsed().as_millis() as u64);
264 Err(AppError::from(e))
265 }
266 }
267}
268
269async fn get_crate_version(
271 State(state): State<AppState>,
272 Path((name, version)): Path<(String, String)>,
273) -> std::result::Result<Json<CrateCheckResult>, AppError> {
274 let start_time = Instant::now();
275
276 let cache_key = format!("crate:{}:{}", name, version);
277 if let Some(cached) = get_from_cache(&state, &cache_key) {
278 state.metrics.record_cache_hit();
279 state
280 .metrics
281 .record_request(true, start_time.elapsed().as_millis() as u64);
282 return Ok(Json(serde_json::from_value(cached.data)?));
283 }
284
285 state.metrics.record_cache_miss();
286
287 let result = if version == "latest" {
288 match state.client.get_crate_info(&name).await {
289 Ok(info) => CrateCheckResult {
290 crate_name: name.clone(),
291 exists: true,
292 latest_version: Some(info.newest_version.clone()),
293 requested_version: Some("latest".to_string()),
294 version_exists: Some(true),
295 error: None,
296 info: Some(info),
297 },
298 Err(e) => CrateCheckResult {
299 crate_name: name.clone(),
300 exists: false,
301 latest_version: None,
302 requested_version: Some(version),
303 version_exists: None,
304 error: Some(e.to_string()),
305 info: None,
306 },
307 }
308 } else {
309 match state.client.get_all_versions(&name).await {
311 Ok(versions) => {
312 let version_exists = versions.iter().any(|v| v.num == version);
313 let info = if version_exists {
314 state.client.get_crate_info(&name).await.ok()
315 } else {
316 None
317 };
318
319 CrateCheckResult {
320 crate_name: name.clone(),
321 exists: true,
322 latest_version: info.as_ref().map(|i| i.newest_version.clone()),
323 requested_version: Some(version),
324 version_exists: Some(version_exists),
325 error: None,
326 info,
327 }
328 }
329 Err(e) => CrateCheckResult {
330 crate_name: name.clone(),
331 exists: false,
332 latest_version: None,
333 requested_version: Some(version),
334 version_exists: None,
335 error: Some(e.to_string()),
336 info: None,
337 },
338 }
339 };
340
341 if state.config.cache.enabled {
343 set_cache(&state, &cache_key, serde_json::to_value(&result)?);
344 }
345
346 state
347 .metrics
348 .record_request(true, start_time.elapsed().as_millis() as u64);
349 Ok(Json(result))
350}
351
352async fn get_crate_dependencies(
354 State(state): State<AppState>,
355 Path((name, version)): Path<(String, String)>,
356) -> std::result::Result<Json<Vec<Dependency>>, AppError> {
357 let start_time = Instant::now();
358
359 let actual_version = if version == "latest" {
360 match state.client.get_latest_version(&name).await {
361 Ok(v) => v,
362 Err(e) => {
363 state
364 .metrics
365 .record_request(false, start_time.elapsed().as_millis() as u64);
366 return Err(AppError::from(e));
367 }
368 }
369 } else {
370 version
371 };
372
373 match state
374 .client
375 .get_crate_dependencies(&name, &actual_version)
376 .await
377 {
378 Ok(deps) => {
379 state
380 .metrics
381 .record_request(true, start_time.elapsed().as_millis() as u64);
382 Ok(Json(deps))
383 }
384 Err(e) => {
385 error!(
386 "Failed to get dependencies for '{}:{}': {}",
387 name, actual_version, e
388 );
389 state
390 .metrics
391 .record_request(false, start_time.elapsed().as_millis() as u64);
392 Err(AppError::from(e))
393 }
394 }
395}
396
397async fn get_crate_stats(
399 State(state): State<AppState>,
400 Path(name): Path<String>,
401) -> std::result::Result<Json<DownloadStats>, AppError> {
402 let start_time = Instant::now();
403
404 match state.client.get_download_stats(&name).await {
405 Ok(stats) => {
406 state
407 .metrics
408 .record_request(true, start_time.elapsed().as_millis() as u64);
409 Ok(Json(stats))
410 }
411 Err(e) => {
412 error!("Failed to get stats for '{}': {}", name, e);
413 state
414 .metrics
415 .record_request(false, start_time.elapsed().as_millis() as u64);
416 Err(AppError::from(e))
417 }
418 }
419}
420
421async fn search_crates(
423 State(state): State<AppState>,
424 Query(params): Query<HashMap<String, String>>,
425) -> std::result::Result<Json<Vec<CrateSearchResult>>, AppError> {
426 let start_time = Instant::now();
427
428 let query = params
429 .get("q")
430 .ok_or_else(|| AppError::BadRequest("Missing 'q' parameter".to_string()))?;
431
432 let limit = params
433 .get("limit")
434 .and_then(|l| l.parse().ok())
435 .unwrap_or(10);
436
437 match state.client.search_crates(query, Some(limit)).await {
438 Ok(results) => {
439 state
440 .metrics
441 .record_request(true, start_time.elapsed().as_millis() as u64);
442 Ok(Json(results))
443 }
444 Err(e) => {
445 error!("Failed to search for '{}': {}", query, e);
446 state
447 .metrics
448 .record_request(false, start_time.elapsed().as_millis() as u64);
449 Err(AppError::from(e))
450 }
451 }
452}
453
454async fn handle_batch(
456 State(state): State<AppState>,
457 Json(request): Json<BatchRequest>,
458) -> std::result::Result<Json<BatchResponse>, AppError> {
459 let start_time = Instant::now();
460
461 validate_batch_input(&request.input).map_err(AppError::from)?;
462
463 let result = match request.input {
464 BatchInput::CrateVersionMap(map) => state.client.process_crate_version_map(map).await?,
465 BatchInput::CrateList { crates } => {
466 let results = state.client.process_crate_list(crates).await?;
467 let successful = results.iter().filter(|r| r.error.is_none()).count();
468 let failed = results.len() - successful;
469 let total_processed = results.len();
470
471 BatchResult {
472 results,
473 total_processed,
474 successful,
475 failed,
476 processing_time_ms: start_time.elapsed().as_millis() as u64,
477 }
478 }
479 BatchInput::Operations { operations } => {
480 state
481 .client
482 .process_batch_operations(operations)
483 .await?
484 .result
485 }
486 };
487
488 let response = BatchResponse {
489 request_id: uuid::Uuid::new_v4().to_string(),
490 status: "completed".to_string(),
491 result,
492 };
493
494 state
495 .metrics
496 .record_request(true, start_time.elapsed().as_millis() as u64);
497 Ok(Json(response))
498}
499
500async fn get_metrics(State(state): State<AppState>) -> Json<MetricsResponse> {
502 let mut metrics = state.metrics.get_metrics();
503 metrics.uptime_seconds = state.start_time.elapsed().as_secs();
504 Json(metrics)
505}
506
507fn get_from_cache(state: &AppState, key: &str) -> Option<CacheEntry> {
509 if !state.config.cache.enabled {
510 return None;
511 }
512
513 if let Some(entry) = state.cache.get(key) {
514 if entry.expires_at > Instant::now() {
515 return Some(entry.clone());
516 } else {
517 state.cache.remove(key);
519 }
520 }
521
522 None
523}
524
525fn set_cache(state: &AppState, key: &str, data: Value) {
527 if !state.config.cache.enabled {
528 return;
529 }
530
531 if state.cache.len() > state.config.cache.max_entries {
533 let now = Instant::now();
534 state.cache.retain(|_, entry| entry.expires_at > now);
535 }
536
537 let entry = CacheEntry {
538 data,
539 expires_at: Instant::now() + Duration::from_secs(state.config.cache.ttl_seconds),
540 };
541
542 state.cache.insert(key.to_string(), entry);
543}
544
545#[derive(Debug)]
547pub enum AppError {
548 Internal(CrateCheckerError),
549 BadRequest(String),
550 NotFound(String),
551}
552
553impl From<CrateCheckerError> for AppError {
554 fn from(err: CrateCheckerError) -> Self {
555 match err {
556 CrateCheckerError::CrateNotFound(_) | CrateCheckerError::VersionNotFound { .. } => {
557 Self::NotFound(err.to_string())
558 }
559 CrateCheckerError::ValidationError(_) | CrateCheckerError::InvalidBatchInput(_) => {
560 Self::BadRequest(err.to_string())
561 }
562 _ => Self::Internal(err),
563 }
564 }
565}
566
567impl From<serde_json::Error> for AppError {
568 fn from(err: serde_json::Error) -> Self {
569 Self::BadRequest(format!("JSON error: {}", err))
570 }
571}
572
573impl axum::response::IntoResponse for AppError {
575 fn into_response(self) -> axum::response::Response {
576 let (status, message) = match self {
577 AppError::Internal(e) => {
578 error!("Internal error: {}", e);
579 (
580 StatusCode::INTERNAL_SERVER_ERROR,
581 "Internal server error".to_string(),
582 )
583 }
584 AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
585 AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
586 };
587
588 let body = serde_json::json!({
589 "error": message,
590 "timestamp": Utc::now().to_rfc3339()
591 });
592
593 (status, Json(body)).into_response()
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use axum::{
601 body::Body,
602 http::{Request, StatusCode},
603 };
604 use tower::ServiceExt;
605
606 async fn create_test_app() -> Router {
607 let client = CrateClient::new();
608 let config = AppConfig::default();
609 let state = AppState {
610 client,
611 config,
612 metrics: Arc::new(ServerMetrics::default()),
613 cache: Arc::new(DashMap::new()),
614 start_time: Instant::now(),
615 };
616
617 create_router(state)
618 }
619
620 #[tokio::test]
621 async fn test_health_check() {
622 let app = create_test_app().await;
623
624 let request = Request::builder()
625 .uri("/health")
626 .body(Body::empty())
627 .unwrap();
628
629 let response = app.oneshot(request).await.unwrap();
630 assert_eq!(response.status(), StatusCode::OK);
631 }
632
633 #[tokio::test]
634 async fn test_api_docs() {
635 let app = create_test_app().await;
636
637 let request = Request::builder().uri("/").body(Body::empty()).unwrap();
638
639 let response = app.oneshot(request).await.unwrap();
640 assert_eq!(response.status(), StatusCode::OK);
641 }
642
643 #[tokio::test]
644 async fn test_metrics_endpoint() {
645 let app = create_test_app().await;
646
647 let request = Request::builder()
648 .uri("/metrics")
649 .body(Body::empty())
650 .unwrap();
651
652 let response = app.oneshot(request).await.unwrap();
653 assert_eq!(response.status(), StatusCode::OK);
654 }
655}