Skip to main content

crate_checker/
server.rs

1//! HTTP server implementation for the crate checker API
2
3use 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/// Server state shared across handlers
30#[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/// Cached response entry
40#[derive(Clone)]
41pub struct CacheEntry {
42    pub data: Value,
43    pub expires_at: Instant,
44}
45
46/// Server metrics
47#[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, // Will be set by the handler
94        }
95    }
96}
97
98/// Start the HTTP server
99pub async fn start_server(config: AppConfig) -> Result<()> {
100    info!("Starting server on {}", config.bind_address());
101
102    // Validate configuration
103    config.validate().map_err(CrateCheckerError::validation)?;
104
105    // Create client with configuration
106    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    // Create shared state
113    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    // Build the application router
122    let app = create_router(state);
123
124    // Configure server
125    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    // Start server
132    axum::serve(listener, app).await?;
133
134    Ok(())
135}
136
137/// Create the application router
138fn create_router(state: AppState) -> Router {
139    let mut app = Router::new()
140        // Health check
141        .route("/health", get(health_check))
142        // API documentation
143        .route("/", get(api_docs))
144        // Core API endpoints
145        .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        // Metrics and monitoring
155        .route("/metrics", get(get_metrics))
156        // Add state
157        .with_state(state.clone());
158
159    // Add middleware
160    let service = ServiceBuilder::new().layer(TraceLayer::new_for_http());
161
162    app = app.layer(service);
163
164    // Add CORS if enabled
165    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
177/// Health check endpoint
178async 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
187/// API documentation endpoint
188async 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
228/// Get crate information
229async 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    // Check cache first
236    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            // Cache the result
250            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
269/// Get crate version information
270async 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        // Check specific version
310        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    // Cache the result
342    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
352/// Get crate dependencies
353async 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
397/// Get crate download statistics
398async 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
421/// Search crates
422async 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
454/// Handle batch operations
455async 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
500/// Get server metrics
501async 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
507/// Helper function to get from cache
508fn 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            // Entry expired, remove it
518            state.cache.remove(key);
519        }
520    }
521
522    None
523}
524
525/// Helper function to set cache
526fn set_cache(state: &AppState, key: &str, data: Value) {
527    if !state.config.cache.enabled {
528        return;
529    }
530
531    // Clean up expired entries periodically
532    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/// Application error wrapper for HTTP responses
546#[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
573/// Convert AppError to HTTP response
574impl 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}