Skip to main content

helios_persistence/advisor/
server.rs

1//! HTTP server for the configuration advisor.
2//!
3//! This module provides an Axum-based HTTP server for the configuration
4//! advisor API. The server exposes endpoints for analyzing, validating,
5//! and optimizing composite storage configurations.
6//!
7//! # Endpoints
8//!
9//! | Endpoint | Method | Description |
10//! |----------|--------|-------------|
11//! | `/health` | GET | Health check |
12//! | `/backends` | GET | List available backend types |
13//! | `/backends/:kind` | GET | Get capabilities for a backend type |
14//! | `/analyze` | POST | Analyze a configuration |
15//! | `/validate` | POST | Validate a configuration |
16//! | `/suggest` | POST | Get optimization suggestions |
17//! | `/simulate` | POST | Simulate query routing |
18//!
19//! # Example
20//!
21//! ```ignore
22//! use helios_persistence::advisor::{AdvisorConfig, AdvisorServer};
23//!
24//! #[tokio::main]
25//! async fn main() {
26//!     let config = AdvisorConfig::default();
27//!     let server = AdvisorServer::new(config);
28//!     server.run().await.unwrap();
29//! }
30//! ```
31
32use std::net::SocketAddr;
33
34#[cfg(feature = "advisor")]
35use super::handlers::{
36    AnalyzeRequest, SimulateRequest, SuggestRequest, ValidateRequest, handle_analyze,
37    handle_backend_capabilities, handle_backends, handle_simulate, handle_suggest, handle_validate,
38};
39
40/// Configuration for the advisor server.
41#[derive(Debug, Clone)]
42pub struct AdvisorConfig {
43    /// Host to bind to.
44    pub host: String,
45
46    /// Port to bind to.
47    pub port: u16,
48
49    /// Enable CORS.
50    pub enable_cors: bool,
51
52    /// Request timeout in seconds.
53    pub request_timeout_secs: u64,
54}
55
56impl Default for AdvisorConfig {
57    fn default() -> Self {
58        Self {
59            host: "127.0.0.1".to_string(),
60            port: 8081,
61            enable_cors: true,
62            request_timeout_secs: 30,
63        }
64    }
65}
66
67impl AdvisorConfig {
68    /// Creates a new configuration with the given host and port.
69    pub fn new(host: impl Into<String>, port: u16) -> Self {
70        Self {
71            host: host.into(),
72            port,
73            ..Default::default()
74        }
75    }
76
77    /// Creates a configuration from environment variables.
78    pub fn from_env() -> Self {
79        Self {
80            host: std::env::var("ADVISOR_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
81            port: std::env::var("ADVISOR_PORT")
82                .ok()
83                .and_then(|p| p.parse().ok())
84                .unwrap_or(8081),
85            enable_cors: std::env::var("ADVISOR_ENABLE_CORS")
86                .map(|v| v.to_lowercase() == "true" || v == "1")
87                .unwrap_or(true),
88            request_timeout_secs: std::env::var("ADVISOR_TIMEOUT")
89                .ok()
90                .and_then(|t| t.parse().ok())
91                .unwrap_or(30),
92        }
93    }
94
95    /// Returns the socket address to bind to.
96    pub fn socket_addr(&self) -> SocketAddr {
97        format!("{}:{}", self.host, self.port)
98            .parse()
99            .expect("Invalid host:port configuration")
100    }
101}
102
103/// The configuration advisor HTTP server.
104///
105/// This server provides REST endpoints for analyzing and optimizing
106/// composite storage configurations.
107pub struct AdvisorServer {
108    /// Server configuration.
109    config: AdvisorConfig,
110}
111
112impl AdvisorServer {
113    /// Creates a new advisor server.
114    pub fn new(config: AdvisorConfig) -> Self {
115        Self { config }
116    }
117
118    /// Creates a server with default configuration.
119    pub fn with_defaults() -> Self {
120        Self::new(AdvisorConfig::default())
121    }
122
123    /// Creates a server with configuration from environment.
124    pub fn from_env() -> Self {
125        Self::new(AdvisorConfig::from_env())
126    }
127
128    /// Returns the server configuration.
129    pub fn config(&self) -> &AdvisorConfig {
130        &self.config
131    }
132
133    /// Runs the server (blocking).
134    ///
135    /// This method starts the HTTP server and blocks until it is shut down.
136    /// Use `run_with_shutdown` for graceful shutdown support.
137    #[cfg(feature = "advisor")]
138    pub async fn run(&self) -> Result<(), std::io::Error> {
139        use tower_http::cors::{Any, CorsLayer};
140        use tracing::info;
141
142        let app = self.create_router();
143
144        // Add CORS if enabled
145        let app = if self.config.enable_cors {
146            let cors = CorsLayer::new()
147                .allow_origin(Any)
148                .allow_methods(Any)
149                .allow_headers(Any);
150            app.layer(cors)
151        } else {
152            app
153        };
154
155        let addr = self.config.socket_addr();
156        info!("Starting advisor server on {}", addr);
157
158        let listener = tokio::net::TcpListener::bind(addr).await?;
159        axum::serve(listener, app).await
160    }
161
162    /// Runs the server with graceful shutdown support.
163    #[cfg(feature = "advisor")]
164    pub async fn run_with_shutdown(
165        &self,
166        shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
167    ) -> Result<(), std::io::Error> {
168        use tower_http::cors::{Any, CorsLayer};
169        use tracing::info;
170
171        let app = self.create_router();
172
173        // Add CORS if enabled
174        let app = if self.config.enable_cors {
175            let cors = CorsLayer::new()
176                .allow_origin(Any)
177                .allow_methods(Any)
178                .allow_headers(Any);
179            app.layer(cors)
180        } else {
181            app
182        };
183
184        let addr = self.config.socket_addr();
185        info!("Starting advisor server on {}", addr);
186
187        let listener = tokio::net::TcpListener::bind(addr).await?;
188        axum::serve(listener, app)
189            .with_graceful_shutdown(shutdown_signal)
190            .await
191    }
192
193    /// Creates the Axum router with all endpoints.
194    #[cfg(feature = "advisor")]
195    fn create_router(&self) -> axum::Router {
196        use axum::{
197            Router,
198            routing::{get, post},
199        };
200
201        Router::new()
202            .route("/health", get(health_handler))
203            .route("/backends", get(backends_handler))
204            .route("/backends/{kind}", get(backend_capabilities_handler))
205            .route("/analyze", post(analyze_handler))
206            .route("/validate", post(validate_handler))
207            .route("/suggest", post(suggest_handler))
208            .route("/simulate", post(simulate_handler))
209    }
210}
211
212// ============================================================================
213// Axum Handler Functions
214// ============================================================================
215
216#[cfg(feature = "advisor")]
217async fn health_handler() -> impl axum::response::IntoResponse {
218    use axum::Json;
219    use serde_json::json;
220
221    Json(json!({
222        "status": "healthy",
223        "service": "config-advisor",
224        "version": env!("CARGO_PKG_VERSION")
225    }))
226}
227
228#[cfg(feature = "advisor")]
229async fn backends_handler() -> impl axum::response::IntoResponse {
230    use axum::Json;
231    Json(handle_backends())
232}
233
234#[cfg(feature = "advisor")]
235async fn backend_capabilities_handler(
236    axum::extract::Path(kind): axum::extract::Path<String>,
237) -> impl axum::response::IntoResponse {
238    use axum::{Json, http::StatusCode};
239
240    match handle_backend_capabilities(&kind) {
241        Ok(info) => (StatusCode::OK, Json(serde_json::to_value(info).unwrap())).into_response(),
242        Err(msg) => (
243            StatusCode::NOT_FOUND,
244            Json(serde_json::json!({ "error": msg })),
245        )
246            .into_response(),
247    }
248}
249
250#[cfg(feature = "advisor")]
251async fn analyze_handler(
252    axum::extract::Json(request): axum::extract::Json<AnalyzeRequest>,
253) -> impl axum::response::IntoResponse {
254    use axum::{Json, http::StatusCode};
255
256    match handle_analyze(request) {
257        Ok(response) => (
258            StatusCode::OK,
259            Json(serde_json::to_value(response).unwrap()),
260        )
261            .into_response(),
262        Err(msg) => (
263            StatusCode::BAD_REQUEST,
264            Json(serde_json::json!({ "error": msg })),
265        )
266            .into_response(),
267    }
268}
269
270#[cfg(feature = "advisor")]
271async fn validate_handler(
272    axum::extract::Json(request): axum::extract::Json<ValidateRequest>,
273) -> impl axum::response::IntoResponse {
274    use axum::{Json, http::StatusCode};
275
276    match handle_validate(request) {
277        Ok(response) => (
278            StatusCode::OK,
279            Json(serde_json::to_value(response).unwrap()),
280        )
281            .into_response(),
282        Err(msg) => (
283            StatusCode::BAD_REQUEST,
284            Json(serde_json::json!({ "error": msg })),
285        )
286            .into_response(),
287    }
288}
289
290#[cfg(feature = "advisor")]
291async fn suggest_handler(
292    axum::extract::Json(request): axum::extract::Json<SuggestRequest>,
293) -> impl axum::response::IntoResponse {
294    use axum::{Json, http::StatusCode};
295
296    match handle_suggest(request) {
297        Ok(response) => (
298            StatusCode::OK,
299            Json(serde_json::to_value(response).unwrap()),
300        )
301            .into_response(),
302        Err(msg) => (
303            StatusCode::BAD_REQUEST,
304            Json(serde_json::json!({ "error": msg })),
305        )
306            .into_response(),
307    }
308}
309
310#[cfg(feature = "advisor")]
311async fn simulate_handler(
312    axum::extract::Json(request): axum::extract::Json<SimulateRequest>,
313) -> impl axum::response::IntoResponse {
314    use axum::{Json, http::StatusCode};
315
316    match handle_simulate(request) {
317        Ok(response) => (
318            StatusCode::OK,
319            Json(serde_json::to_value(response).unwrap()),
320        )
321            .into_response(),
322        Err(msg) => (
323            StatusCode::BAD_REQUEST,
324            Json(serde_json::json!({ "error": msg })),
325        )
326            .into_response(),
327    }
328}
329
330#[cfg(feature = "advisor")]
331use axum::response::IntoResponse;
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_advisor_config_default() {
339        let config = AdvisorConfig::default();
340        assert_eq!(config.host, "127.0.0.1");
341        assert_eq!(config.port, 8081);
342        assert!(config.enable_cors);
343    }
344
345    #[test]
346    fn test_advisor_config_new() {
347        let config = AdvisorConfig::new("0.0.0.0", 9000);
348        assert_eq!(config.host, "0.0.0.0");
349        assert_eq!(config.port, 9000);
350    }
351
352    #[test]
353    fn test_socket_addr() {
354        let config = AdvisorConfig::new("127.0.0.1", 8081);
355        let addr = config.socket_addr();
356        assert_eq!(addr.port(), 8081);
357    }
358
359    #[test]
360    fn test_server_creation() {
361        let server = AdvisorServer::with_defaults();
362        assert_eq!(server.config().port, 8081);
363    }
364}