use axum::{
Router,
extract::Json,
routing::{get, post},
};
use converge_core::{Context, ContextKey, Engine};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use tokio::task;
use tracing::{info, info_span};
use utoipa::ToSchema;
use crate::error::RuntimeError;
#[derive(Debug, Deserialize, ToSchema)]
pub struct JobRequest {
#[schema(example = json!({}))]
pub context: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct JobResponse {
pub metadata: JobMetadata,
pub cycles: u32,
pub converged: bool,
pub context_summary: ContextSummary,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ContextSummary {
pub fact_counts: std::collections::HashMap<String, usize>,
pub version: u64,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct JobMetadata {
pub cycles: u32,
pub converged: bool,
pub duration_ms: u64,
}
#[utoipa::path(
get,
path = "/health",
tag = "health",
responses(
(status = 200, description = "Server is healthy", body = String)
)
)]
pub async fn health() -> &'static str {
"ok"
}
#[utoipa::path(
get,
path = "/ready",
tag = "health",
responses(
(status = 200, description = "Server is ready", body = serde_json::Value),
(status = 503, description = "Server is not ready")
)
)]
pub async fn ready() -> Result<Json<serde_json::Value>, RuntimeError> {
Ok(Json(serde_json::json!({
"status": "ready",
"services": {
"engine": "ok"
}
})))
}
#[utoipa::path(
post,
path = "/api/v1/jobs",
tag = "jobs",
request_body = JobRequest,
responses(
(status = 200, description = "Job completed successfully", body = JobResponse),
(status = 400, description = "Invalid request", body = RuntimeError),
(status = 422, description = "Invariant violation", body = RuntimeError),
(status = 413, description = "Budget exhausted", body = RuntimeError),
(status = 409, description = "Conflict detected", body = RuntimeError),
(status = 500, description = "Internal server error", body = RuntimeError)
)
)]
#[axum::debug_handler]
pub async fn handle_job(
Json(request): Json<JobRequest>,
) -> Result<Json<JobResponse>, RuntimeError> {
let _span = info_span!("handle_job");
let _guard = _span.enter();
info!("Received job request");
let start = std::time::Instant::now();
let context_data = request.context.clone();
drop(_guard);
let result = task::spawn_blocking(move || {
let mut engine = Engine::new();
let _context_data = context_data;
let context = Context::new();
engine.run(context)
})
.await
.map_err(|e| RuntimeError::Config(format!("Task join error: {e}")))?
.map_err(RuntimeError::Converge)?;
let duration = start.elapsed();
let fact_counts: std::collections::HashMap<String, usize> = ContextKey::iter()
.map(|key| {
let count = result.context.get(key).len();
(format!("{key:?}"), count)
})
.collect();
let context_summary = ContextSummary {
fact_counts,
version: result.context.version(),
};
info!(
cycles = result.cycles,
converged = result.converged,
duration_ms = duration.as_millis(),
"Job completed"
);
Ok(Json(JobResponse {
metadata: JobMetadata {
cycles: result.cycles,
converged: result.converged,
duration_ms: duration.as_millis() as u64,
},
cycles: result.cycles,
converged: result.converged,
context_summary,
}))
}
pub fn router() -> Router<()> {
Router::new()
.route("/health", get(health))
.route("/ready", get(ready))
.route("/api/v1/jobs", post(handle_job))
}