1pub mod api_types;
7
8use std::sync::Arc;
9use anyhow::Result;
10
11pub use api_types::*;
13
14use 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#[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#[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, enable_tracing: true,
63 }
64 }
65}
66
67pub 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
102pub 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 .route("/search", post(search_handler))
114 .route("/struct", post(struct_search_handler))
115 .route("/symbols/near", post(symbols_near_handler))
116
117 .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 .route("/v1/spi/search", post(spi_search_handler))
125 .route("/v1/spi/health", get(spi_health_handler))
126
127 .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#[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 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 info!(
164 "Request completed: {} {} -> {} in {:?} (trace: {})",
165 method, uri, status, duration, trace_id
166 );
167
168 Ok(response)
169}
170
171async 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#[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 request.validate()
202 .map_err(|e| ApiError::BadRequest(e))?;
203
204 let total_latency = start.elapsed();
205
206 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#[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 request.validate()
240 .map_err(|e| ApiError::BadRequest(e))?;
241
242 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#[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 request.validate()
274 .map_err(|e| ApiError::BadRequest(e))?;
275
276 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#[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#[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#[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
367async 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#[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 let _source = std_error.source();
516 }
517
518 #[test]
519 fn test_layer_configurations() {
520 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 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 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 let config = ServerConfig {
557 bind_address: "[::]".to_string(), port: 0, 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 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