Skip to main content

lens_core/server/
mod.rs

1//! HTTP Server Module
2//! 
3//! Provides the complete HTTP REST API server for Lens,
4//! including type definitions and integration adapters.
5
6pub mod api_types;
7
8use std::sync::Arc;
9use anyhow::Result;
10
11// Re-export the main server functions and types
12pub use api_types::*;
13
14// Server implementation
15use std::net::SocketAddr;
16use std::time::{Duration, Instant, SystemTime};
17use uuid::Uuid;
18
19use axum::{
20    extract::{Query, State},
21    http::{Method, StatusCode},
22    middleware::{self, Next},
23    response::{Json, Response, IntoResponse},
24    routing::{get, post},
25    Router,
26};
27use tower_http::{
28    cors::{CorsLayer, Any},
29    trace::TraceLayer,
30};
31use tracing::{info, instrument};
32
33/// Application state shared across handlers
34#[derive(Clone)]
35pub struct AppState {
36    pub search_engine: Arc<crate::search::SearchEngine>,
37    pub metrics: Arc<crate::metrics::MetricsCollector>,
38    pub attestation: Arc<crate::attestation::AttestationManager>,
39    pub benchmark_runner: Arc<crate::benchmark::BenchmarkRunner>,
40    pub start_time: SystemTime,
41}
42
43/// Configuration for the HTTP server
44#[derive(Debug, Clone)]
45pub struct ServerConfig {
46    pub bind_address: String,
47    pub port: u16,
48    pub enable_cors: bool,
49    pub request_timeout: Duration,
50    pub max_request_size: usize,
51    pub enable_tracing: bool,
52}
53
54impl Default for ServerConfig {
55    fn default() -> Self {
56        Self {
57            bind_address: "127.0.0.1".to_string(),
58            port: 3000,
59            enable_cors: true,
60            request_timeout: Duration::from_millis(5000),
61            max_request_size: 1024 * 1024, // 1MB
62            enable_tracing: true,
63        }
64    }
65}
66
67/// Create the main HTTP server
68pub async fn create_server(
69    config: ServerConfig,
70    search_engine: Arc<crate::search::SearchEngine>,
71    metrics: Arc<crate::metrics::MetricsCollector>,
72    attestation: Arc<crate::attestation::AttestationManager>,
73    benchmark_runner: Arc<crate::benchmark::BenchmarkRunner>,
74) -> anyhow::Result<()> {
75    let app_state = AppState {
76        search_engine,
77        metrics,
78        attestation,
79        benchmark_runner,
80        start_time: SystemTime::now(),
81    };
82
83    let app = create_app(app_state).await?;
84
85    let addr: SocketAddr = format!("{}:{}", config.bind_address, config.port)
86        .parse()
87        .map_err(|e| anyhow::anyhow!("Invalid bind address: {}", e))?;
88
89    info!("🚀 Starting Rust HTTP API server on {}", addr);
90
91    let listener = tokio::net::TcpListener::bind(&addr)
92        .await
93        .map_err(|e| anyhow::anyhow!("Failed to bind to {}: {}", addr, e))?;
94
95    axum::serve(listener, app)
96        .await
97        .map_err(|e| anyhow::anyhow!("Server error: {}", e))?;
98
99    Ok(())
100}
101
102/// Create the axum app with all routes and middleware
103pub async fn create_app(state: AppState) -> anyhow::Result<Router> {
104    let cors = CorsLayer::new()
105        .allow_origin(Any)
106        .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
107        .allow_headers(Any);
108
109    let trace_layer = TraceLayer::new_for_http();
110
111    let app = Router::new()
112        // Core search endpoints
113        .route("/search", post(search_handler))
114        .route("/struct", post(struct_search_handler))  
115        .route("/symbols/near", post(symbols_near_handler))
116        
117        // System health and compatibility
118        .route("/health", get(health_handler))
119        .route("/manifest", get(manifest_handler))
120        .route("/compat/check", get(compat_check_handler))
121        .route("/compat/bundles", get(compat_bundles_handler))
122        
123        // SPI v1 endpoints (LSP interface)
124        .route("/v1/spi/search", post(spi_search_handler))
125        .route("/v1/spi/health", get(spi_health_handler))
126        
127        // Add middleware layers individually
128        .layer(cors)
129        .layer(trace_layer)
130        .layer(middleware::from_fn_with_state(state.clone(), request_tracing_middleware))
131        .layer(middleware::from_fn(timeout_middleware))
132        .with_state(state);
133
134    Ok(app)
135}
136
137/// Request tracing and metrics middleware
138#[instrument(skip_all)]
139async fn request_tracing_middleware(
140    State(_state): State<AppState>,
141    request: axum::http::Request<axum::body::Body>,
142    next: Next,
143) -> Result<Response, StatusCode> {
144    let start = Instant::now();
145    let method = request.method().clone();
146    let uri = request.uri().clone();
147    let trace_id = Uuid::new_v4().to_string();
148    
149    // Add trace ID to request extensions
150    let mut request = request;
151    request.extensions_mut().insert(trace_id.clone());
152    
153    info!("Request started: {} {} (trace: {})", method, uri, trace_id);
154    
155    let response = next.run(request).await;
156    
157    let duration = start.elapsed();
158    let status = response.status();
159    
160    // TODO: Record metrics when MetricsCollector API is available
161    // state.metrics.record_request(...).await;
162    
163    info!(
164        "Request completed: {} {} -> {} in {:?} (trace: {})",
165        method, uri, status, duration, trace_id
166    );
167    
168    Ok(response)
169}
170
171/// Request timeout middleware
172async fn timeout_middleware(
173    request: axum::http::Request<axum::body::Body>,
174    next: Next,
175) -> Result<Response, StatusCode> {
176    match tokio::time::timeout(Duration::from_secs(30), next.run(request)).await {
177        Ok(response) => Ok(response),
178        Err(_) => {
179            info!("Request timed out");
180            Err(StatusCode::REQUEST_TIMEOUT)
181        }
182    }
183}
184
185//
186// Handler implementations
187//
188
189/// POST /search - Main search endpoint
190#[instrument(skip_all)]
191async fn search_handler(
192    State(_state): State<AppState>,
193    Json(request): Json<SearchRequest>,
194) -> Result<Json<SearchResponse>, ApiError> {
195    let start = Instant::now();
196    
197    info!("Search request: repo={}, query='{}', mode={:?}", 
198          request.repo_sha, request.q, request.mode);
199    
200    // Validate request
201    request.validate()
202        .map_err(|e| ApiError::BadRequest(e))?;
203    
204    let total_latency = start.elapsed();
205    
206    // Create mock response for now
207    let response = SearchResponse {
208        hits: vec![],
209        total: 0,
210        latency_ms: LatencyBreakdown {
211            stage_a: 10,
212            stage_b: 5,
213            stage_c: None,
214            total: std::cmp::max(1, total_latency.as_millis() as u32),
215        },
216        trace_id: Uuid::new_v4().to_string(),
217        api_version: "v1".to_string(),
218        index_version: "v1".to_string(),
219        policy_version: "v1".to_string(),
220        error: None,
221        message: None,
222    };
223    
224    info!("Search completed: {} hits in {:?}", response.hits.len(), total_latency);
225    
226    Ok(Json(response))
227}
228
229/// POST /struct - Structural search endpoint
230#[instrument(skip_all)]
231async fn struct_search_handler(
232    State(_state): State<AppState>,
233    Json(request): Json<StructRequest>,
234) -> Result<Json<SearchResponse>, ApiError> {
235    info!("Structural search: repo={}, pattern='{}', lang={:?}", 
236          request.repo_sha, request.pattern, request.lang);
237    
238    // Validate request
239    request.validate()
240        .map_err(|e| ApiError::BadRequest(e))?;
241    
242    // Create mock response for now
243    let response = SearchResponse {
244        hits: vec![],
245        total: 0,
246        latency_ms: LatencyBreakdown {
247            stage_a: 15,
248            stage_b: 8,
249            stage_c: None,
250            total: 23,
251        },
252        trace_id: Uuid::new_v4().to_string(),
253        api_version: "v1".to_string(),
254        index_version: "v1".to_string(),
255        policy_version: "v1".to_string(),
256        error: None,
257        message: None,
258    };
259    
260    Ok(Json(response))
261}
262
263/// POST /symbols/near - Find symbols near a location
264#[instrument(skip_all)]
265async fn symbols_near_handler(
266    State(_state): State<AppState>,
267    Json(request): Json<SymbolsNearRequest>,
268) -> Result<Json<SearchResponse>, ApiError> {
269    info!("Symbols near: file={}, line={}, radius={:?}", 
270          request.file, request.line, request.radius);
271    
272    // Validate request
273    request.validate()
274        .map_err(|e| ApiError::BadRequest(e))?;
275    
276    // Create mock response for now
277    let response = SearchResponse {
278        hits: vec![],
279        total: 0,
280        latency_ms: LatencyBreakdown {
281            stage_a: 8,
282            stage_b: 12,
283            stage_c: None,
284            total: 20,
285        },
286        trace_id: Uuid::new_v4().to_string(),
287        api_version: "v1".to_string(),
288        index_version: "v1".to_string(),
289        policy_version: "v1".to_string(),
290        error: None,
291        message: None,
292    };
293    
294    Ok(Json(response))
295}
296
297/// GET /health - System health check
298#[instrument(skip_all)]
299async fn health_handler(
300    State(_state): State<AppState>,
301) -> Result<Json<HealthResponse>, ApiError> {
302    let response = HealthResponse {
303        status: "ok".to_string(),
304        timestamp: chrono::Utc::now().to_rfc3339(),
305        shards_healthy: 1,
306    };
307    
308    Ok(Json(response))
309}
310
311/// GET /manifest - API manifest information
312#[instrument(skip_all)]
313async fn manifest_handler(
314    State(_state): State<AppState>,
315) -> Result<Json<serde_json::Value>, ApiError> {
316    let manifest = serde_json::json!({
317        "name": "lens-core",
318        "version": env!("CARGO_PKG_VERSION"),
319        "api_version": "v1",
320        "index_version": "v1", 
321        "policy_version": "v1",
322        "build_info": {
323            "version": env!("CARGO_PKG_VERSION"),
324            "build_timestamp": env!("BUILD_TIMESTAMP"),
325            "profile": if cfg!(debug_assertions) { "debug" } else { "release" }
326        },
327        "capabilities": {
328            "search_modes": ["lex", "struct", "hybrid"],
329            "languages": ["typescript", "python", "rust", "go", "java"],
330            "lsp_integration": true,
331            "semantic_search": true,
332        }
333    });
334    
335    Ok(Json(manifest))
336}
337
338/// GET /compat/check - Compatibility check
339#[instrument(skip_all)]
340async fn compat_check_handler(
341    State(_state): State<AppState>,
342    Query(request): Query<CompatibilityCheckRequest>,
343) -> Result<Json<CompatibilityCheckResponse>, ApiError> {
344    let server_versions = ("v1", "v1", "v1");
345    
346    let compatible = request.api_version == "v1" && 
347                    request.index_version == "v1" &&
348                    request.policy_version.as_deref().unwrap_or("v1") == "v1";
349    
350    let response = CompatibilityCheckResponse {
351        compatible,
352        api_version: request.api_version,
353        index_version: request.index_version,
354        policy_version: request.policy_version,
355        server_api_version: server_versions.0.to_string(),
356        server_index_version: server_versions.1.to_string(),
357        server_policy_version: server_versions.2.to_string(),
358        warnings: None,
359        errors: if compatible { None } else { 
360            Some(vec!["Version mismatch detected".to_string()]) 
361        },
362    };
363    
364    Ok(Json(response))
365}
366
367// Placeholder handlers for additional endpoints
368async fn compat_bundles_handler(State(_): State<AppState>) -> Json<serde_json::Value> {
369    Json(serde_json::json!({"bundles": [], "status": "ok"}))
370}
371
372async fn spi_search_handler(State(_): State<AppState>) -> Json<serde_json::Value> {
373    Json(serde_json::json!({"hits": [], "total": 0}))
374}
375
376async fn spi_health_handler(State(_): State<AppState>) -> Json<serde_json::Value> {
377    Json(serde_json::json!({"status": "ok"}))
378}
379
380/// Error handling for API responses
381#[derive(Debug)]
382pub enum ApiError {
383    BadRequest(String),
384    Unauthorized(String),
385    NotFound(String),
386    InternalError(String),
387    ServiceUnavailable(String),
388}
389
390impl IntoResponse for ApiError {
391    fn into_response(self) -> Response {
392        let (status, error_message) = match self {
393            ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
394            ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
395            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
396            ApiError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
397            ApiError::ServiceUnavailable(msg) => (StatusCode::SERVICE_UNAVAILABLE, msg),
398        };
399
400        let body = Json(serde_json::json!({
401            "error": error_message,
402            "timestamp": chrono::Utc::now().to_rfc3339(),
403            "trace_id": Uuid::new_v4().to_string(),
404        }));
405
406        (status, body).into_response()
407    }
408}
409
410impl std::fmt::Display for ApiError {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        match self {
413            ApiError::BadRequest(msg) => write!(f, "Bad Request: {}", msg),
414            ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
415            ApiError::NotFound(msg) => write!(f, "Not Found: {}", msg),
416            ApiError::InternalError(msg) => write!(f, "Internal Error: {}", msg),
417            ApiError::ServiceUnavailable(msg) => write!(f, "Service Unavailable: {}", msg),
418        }
419    }
420}
421
422impl std::error::Error for ApiError {}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use axum::http::header::CONTENT_TYPE;
428    use tower::timeout::TimeoutLayer;
429    use tower_http::limit::RequestBodyLimitLayer;
430
431    #[test]
432    fn test_server_config_default() {
433        let config = ServerConfig::default();
434        
435        assert_eq!(config.bind_address, "127.0.0.1");
436        assert_eq!(config.port, 3000);
437        assert_eq!(config.enable_cors, true);
438        assert_eq!(config.request_timeout, Duration::from_millis(5000));
439        assert_eq!(config.max_request_size, 1024 * 1024);
440        assert_eq!(config.enable_tracing, true);
441    }
442
443    #[test]
444    fn test_server_config_custom() {
445        let config = ServerConfig {
446            bind_address: "0.0.0.0".to_string(),
447            port: 8080,
448            enable_cors: false,
449            request_timeout: Duration::from_millis(10000),
450            max_request_size: 2048 * 1024,
451            enable_tracing: false,
452        };
453        
454        assert_eq!(config.bind_address, "0.0.0.0");
455        assert_eq!(config.port, 8080);
456        assert_eq!(config.enable_cors, false);
457        assert_eq!(config.request_timeout, Duration::from_millis(10000));
458        assert_eq!(config.max_request_size, 2048 * 1024);
459        assert_eq!(config.enable_tracing, false);
460    }
461
462    #[test]
463    fn test_server_config_debug() {
464        let config = ServerConfig::default();
465        let debug_output = format!("{:?}", config);
466        
467        assert!(debug_output.contains("ServerConfig"));
468        assert!(debug_output.contains("127.0.0.1"));
469        assert!(debug_output.contains("3000"));
470    }
471
472    #[test]
473    fn test_server_config_clone() {
474        let config1 = ServerConfig::default();
475        let config2 = config1.clone();
476        
477        assert_eq!(config1.bind_address, config2.bind_address);
478        assert_eq!(config1.port, config2.port);
479        assert_eq!(config1.enable_cors, config2.enable_cors);
480        assert_eq!(config1.request_timeout, config2.request_timeout);
481        assert_eq!(config1.max_request_size, config2.max_request_size);
482        assert_eq!(config1.enable_tracing, config2.enable_tracing);
483    }
484
485    #[test]
486    fn test_api_error_display() {
487        let errors = vec![
488            ApiError::BadRequest("Invalid input".to_string()),
489            ApiError::NotFound("Resource not found".to_string()),
490            ApiError::InternalError("Server error".to_string()),
491            ApiError::ServiceUnavailable("Service down".to_string()),
492        ];
493        
494        for error in errors {
495            let display = format!("{}", error);
496            assert!(!display.is_empty());
497            assert!(display.len() > 5);
498        }
499    }
500
501    #[test]
502    fn test_api_error_debug() {
503        let error = ApiError::BadRequest("test".to_string());
504        let debug = format!("{:?}", error);
505        assert!(debug.contains("BadRequest"));
506        assert!(debug.contains("test"));
507    }
508
509    #[test]
510    fn test_api_error_as_std_error() {
511        let error = ApiError::InternalError("test error".to_string());
512        let std_error: &dyn std::error::Error = &error;
513        
514        // Should not panic
515        let _source = std_error.source();
516    }
517
518    #[test]
519    fn test_layer_configurations() {
520        // Test basic layer configurations that don't require external dependencies
521        let timeout_duration = Duration::from_secs(30);
522        assert_eq!(timeout_duration.as_secs(), 30);
523        
524        let max_request_size = 1024 * 1024;
525        assert_eq!(max_request_size, 1048576);
526    }
527
528    #[test]
529    fn test_server_config_fields() {
530        let config = ServerConfig::default();
531        
532        // Test individual fields are accessible
533        assert!(!config.bind_address.is_empty());
534        assert!(config.port > 0);
535        assert!(config.request_timeout.as_millis() > 0);
536        assert!(config.max_request_size > 0);
537    }
538
539    #[test]
540    fn test_api_error_chains() {
541        let error1 = ApiError::BadRequest("First error".to_string());
542        let error2 = ApiError::InternalError("Second error".to_string());
543        let error3 = ApiError::NotFound("Third error".to_string());
544        
545        // Test error display chains work
546        let errors = vec![error1, error2, error3];
547        for error in errors {
548            let display_str = format!("{}", error);
549            assert!(!display_str.is_empty());
550        }
551    }
552
553    #[test]
554    fn test_server_config_edge_cases() {
555        // Test edge case configurations
556        let config = ServerConfig {
557            bind_address: "[::]".to_string(), // IPv6
558            port: 0, // Any available port
559            enable_cors: false,
560            request_timeout: Duration::from_millis(1),
561            max_request_size: 0,
562            enable_tracing: false,
563        };
564        
565        assert_eq!(config.bind_address, "[::]");
566        assert_eq!(config.port, 0);
567        assert_eq!(config.max_request_size, 0);
568    }
569
570    #[test]
571    fn test_server_config_comparison() {
572        let config1 = ServerConfig::default();
573        let config2 = ServerConfig::default();
574        let config3 = ServerConfig {
575            port: 8080,
576            ..ServerConfig::default()
577        };
578        
579        // Test field-level comparisons since PartialEq is not derived
580        assert_eq!(config1.bind_address, config2.bind_address);
581        assert_eq!(config1.port, config2.port);
582        assert_ne!(config1.port, config3.port);
583    }
584}
585