helios_persistence/advisor/
server.rs1use 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#[derive(Debug, Clone)]
42pub struct AdvisorConfig {
43 pub host: String,
45
46 pub port: u16,
48
49 pub enable_cors: bool,
51
52 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 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 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 pub fn socket_addr(&self) -> SocketAddr {
97 format!("{}:{}", self.host, self.port)
98 .parse()
99 .expect("Invalid host:port configuration")
100 }
101}
102
103pub struct AdvisorServer {
108 config: AdvisorConfig,
110}
111
112impl AdvisorServer {
113 pub fn new(config: AdvisorConfig) -> Self {
115 Self { config }
116 }
117
118 pub fn with_defaults() -> Self {
120 Self::new(AdvisorConfig::default())
121 }
122
123 pub fn from_env() -> Self {
125 Self::new(AdvisorConfig::from_env())
126 }
127
128 pub fn config(&self) -> &AdvisorConfig {
130 &self.config
131 }
132
133 #[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 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 #[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 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 #[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#[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}