diff options
| author | soryu <soryu@soryu.co> | 2026-02-08 21:07:30 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-08 21:07:30 +0000 |
| commit | 3662b334dfd68cfdf00ed44ae88927c2e1b2aabe (patch) | |
| tree | bff5ae1e189fb8bcc0211d97dab3b9acb4257038 /makima/src/server/handlers/directives.rs | |
| parent | 87fa8c4af66745bd30bc84b6c5ef657dd4dec002 (diff) | |
| download | soryu-3662b334dfd68cfdf00ed44ae88927c2e1b2aabe.tar.gz soryu-3662b334dfd68cfdf00ed44ae88927c2e1b2aabe.zip | |
Remove directive mechanism
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 785 |
1 files changed, 0 insertions, 785 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs deleted file mode 100644 index 3f62a33..0000000 --- a/makima/src/server/handlers/directives.rs +++ /dev/null @@ -1,785 +0,0 @@ -//! HTTP handlers for directive CRUD operations. - -use axum::{ - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, - Json, -}; -use uuid::Uuid; - -use std::collections::HashMap; - -use crate::db::models::{ - ChainStep, ChainStepWithContract, ChainWithSteps, CreateDirectiveRequest, Directive, - DirectiveChain, DirectiveListResponse, DirectiveWithChains, EvaluationListResponse, - StepContractSummary, SubmitPlanRequest, UpdateDirectiveRequest, -}; -use crate::db::repository::{self, RepositoryError}; -use crate::orchestration; -use crate::server::auth::Authenticated; -use crate::server::messages::ApiError; -use crate::server::state::SharedState; - -/// List all directives for the authenticated user's owner. -#[utoipa::path( - get, - path = "/api/v1/directives", - responses( - (status = 200, description = "List of directives", body = DirectiveListResponse), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn list_directives( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::list_directives_for_owner(pool, auth.owner_id).await { - Ok(directives) => { - let total = directives.len() as i64; - Json(DirectiveListResponse { directives, total }).into_response() - } - Err(e) => { - tracing::error!("Failed to list directives: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Get a directive by ID with its chains. -#[utoipa::path( - get, - path = "/api/v1/directives/{id}", - params( - ("id" = Uuid, Path, description = "Directive ID") - ), - responses( - (status = 200, description = "Directive details with chains", body = DirectiveWithChains), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Directive not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn get_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(); - } - Err(e) => { - tracing::error!("Failed to get directive {}: {}", id, e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response(); - } - }; - - let chains = match repository::list_chains_for_directive(pool, id).await { - Ok(c) => c, - Err(e) => { - tracing::warn!("Failed to get chains for directive {}: {}", id, e); - Vec::new() - } - }; - - // Build chains with steps - let mut all_steps_by_chain = Vec::new(); - for chain in &chains { - let steps = match repository::list_steps_for_chain(pool, chain.id).await { - Ok(s) => s, - Err(e) => { - tracing::warn!("Failed to get steps for chain {}: {}", chain.id, e); - Vec::new() - } - }; - all_steps_by_chain.push(steps); - } - - // Collect all contract IDs (from steps + orchestrator) - let mut contract_ids: Vec<Uuid> = all_steps_by_chain - .iter() - .flat_map(|steps| steps.iter().filter_map(|s| s.contract_id)) - .collect(); - if let Some(orch_id) = directive.orchestrator_contract_id { - contract_ids.push(orch_id); - } - - // Batch fetch contract summaries - let mut summary_map: HashMap<Uuid, StepContractSummary> = if contract_ids.is_empty() { - HashMap::new() - } else { - match repository::get_contract_summaries_batch(pool, &contract_ids).await { - Ok(summaries) => summaries.into_iter().map(|s| (s.id, s)).collect(), - Err(e) => { - tracing::warn!("Failed to fetch contract summaries: {}", e); - HashMap::new() - } - } - }; - - // Build enriched chains - let chains_with_steps: Vec<ChainWithSteps> = chains - .into_iter() - .zip(all_steps_by_chain.into_iter()) - .map(|(chain, steps)| { - let enriched_steps = steps - .into_iter() - .map(|step| { - let contract_summary = - step.contract_id.and_then(|id| summary_map.remove(&id)); - ChainStepWithContract { - step, - contract_summary, - } - }) - .collect(); - ChainWithSteps { - chain, - steps: enriched_steps, - } - }) - .collect(); - - let orchestrator_contract_summary = directive - .orchestrator_contract_id - .and_then(|id| summary_map.remove(&id)); - - Json(DirectiveWithChains { - directive, - orchestrator_contract_summary, - chains: chains_with_steps, - }) - .into_response() -} - -/// Create a new directive. -#[utoipa::path( - post, - path = "/api/v1/directives", - request_body = CreateDirectiveRequest, - responses( - (status = 201, description = "Directive created", body = Directive), - (status = 400, description = "Invalid request", body = ApiError), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn create_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Json(req): Json<CreateDirectiveRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::create_directive_for_owner(pool, auth.owner_id, req).await { - Ok(directive) => (StatusCode::CREATED, Json(directive)).into_response(), - Err(e) => { - tracing::error!("Failed to create directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Update an existing directive. -#[utoipa::path( - put, - path = "/api/v1/directives/{id}", - params( - ("id" = Uuid, Path, description = "Directive ID") - ), - request_body = UpdateDirectiveRequest, - responses( - (status = 200, description = "Directive updated", body = Directive), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Directive not found", body = ApiError), - (status = 409, description = "Version conflict", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn update_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateDirectiveRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, req).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - format!( - "Version conflict: expected {}, actual {}", - expected, actual - ), - )), - ) - .into_response(), - Err(RepositoryError::Database(e)) => { - tracing::error!("Failed to update directive {}: {}", id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Delete a directive. -#[utoipa::path( - delete, - path = "/api/v1/directives/{id}", - params( - ("id" = Uuid, Path, description = "Directive ID") - ), - responses( - (status = 204, description = "Directive deleted"), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Directive not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn delete_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::delete_directive_for_owner(pool, id, auth.owner_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to delete directive {}: {}", id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// List chains for a directive. -#[utoipa::path( - get, - path = "/api/v1/directives/{id}/chains", - params( - ("id" = Uuid, Path, description = "Directive ID") - ), - responses( - (status = 200, description = "List of chains", body = Vec<DirectiveChain>), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Directive not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn list_chains( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify directive exists and belongs to owner - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(); - } - Err(e) => { - tracing::error!("Failed to get directive {}: {}", id, e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response(); - } - } - - match repository::list_chains_for_directive(pool, id).await { - Ok(chains) => Json(chains).into_response(), - Err(e) => { - tracing::error!("Failed to list chains for directive {}: {}", id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Get a chain with its steps. -#[utoipa::path( - get, - path = "/api/v1/directives/{id}/chains/{chain_id}", - params( - ("id" = Uuid, Path, description = "Directive ID"), - ("chain_id" = Uuid, Path, description = "Chain ID") - ), - responses( - (status = 200, description = "Chain with steps", body = ChainWithSteps), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Chain not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn get_chain( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, chain_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify directive exists and belongs to owner - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(); - } - Err(e) => { - tracing::error!("Failed to get directive {}: {}", id, e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response(); - } - } - - // Get the chain and verify it belongs to this directive - let chains = match repository::list_chains_for_directive(pool, id).await { - Ok(c) => c, - Err(e) => { - tracing::error!("Failed to list chains: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response(); - } - }; - - let chain = match chains.into_iter().find(|c| c.id == chain_id) { - Some(c) => c, - None => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Chain not found")), - ) - .into_response(); - } - }; - - let steps = match repository::list_steps_for_chain(pool, chain_id).await { - Ok(s) => s, - Err(e) => { - tracing::warn!("Failed to get steps for chain {}: {}", chain_id, e); - Vec::new() - } - }; - - // Collect contract IDs from steps - let contract_ids: Vec<Uuid> = steps.iter().filter_map(|s| s.contract_id).collect(); - - let mut summary_map: HashMap<Uuid, StepContractSummary> = if contract_ids.is_empty() { - HashMap::new() - } else { - match repository::get_contract_summaries_batch(pool, &contract_ids).await { - Ok(summaries) => summaries.into_iter().map(|s| (s.id, s)).collect(), - Err(e) => { - tracing::warn!("Failed to fetch contract summaries: {}", e); - HashMap::new() - } - } - }; - - let enriched_steps = steps - .into_iter() - .map(|step| { - let contract_summary = step.contract_id.and_then(|id| summary_map.remove(&id)); - ChainStepWithContract { - step, - contract_summary, - } - }) - .collect(); - - Json(ChainWithSteps { - chain, - steps: enriched_steps, - }) - .into_response() -} - -/// Start a directive: create a planning contract and begin orchestration. -#[utoipa::path( - post, - path = "/api/v1/directives/{id}/start", - params( - ("id" = Uuid, Path, description = "Directive ID") - ), - responses( - (status = 200, description = "Directive started", body = Directive), - (status = 400, description = "Directive not in draft status", body = ApiError), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Directive not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn start_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match orchestration::directive::init_directive(pool, &state, auth.owner_id, id).await { - Ok(directive) => Json(directive).into_response(), - Err(e) if e.contains("not found") => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", e)), - ) - .into_response(), - Err(e) if e.contains("must be in 'draft'") => ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("INVALID_STATUS", e)), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to start directive {}: {}", id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("START_FAILED", e)), - ) - .into_response() - } - } -} - -/// Trigger a manual evaluation for a step. -#[utoipa::path( - post, - path = "/api/v1/directives/{id}/steps/{step_id}/evaluate", - params( - ("id" = Uuid, Path, description = "Directive ID"), - ("step_id" = Uuid, Path, description = "Step ID") - ), - responses( - (status = 200, description = "Evaluation triggered", body = ChainStep), - (status = 400, description = "Step cannot be evaluated", body = ApiError), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn evaluate_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match orchestration::directive::trigger_manual_evaluation(pool, &state, auth.owner_id, id, step_id).await { - Ok(step) => Json(step).into_response(), - Err(e) if e.contains("not found") || e.contains("Not found") => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", e)), - ) - .into_response(), - Err(e) if e.contains("hasn't been executed") || e.contains("no active chain") => ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("INVALID_STATE", e)), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to trigger evaluation for step {}: {}", step_id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("EVALUATION_FAILED", e)), - ) - .into_response() - } - } -} - -/// List evaluations for a step. -#[utoipa::path( - get, - path = "/api/v1/directives/{id}/steps/{step_id}/evaluations", - params( - ("id" = Uuid, Path, description = "Directive ID"), - ("step_id" = Uuid, Path, description = "Step ID") - ), - responses( - (status = 200, description = "List of evaluations", body = EvaluationListResponse), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn list_evaluations( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify directive exists and belongs to owner - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(); - } - Err(e) => { - tracing::error!("Failed to get directive {}: {}", id, e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response(); - } - } - - match repository::list_evaluations_for_step(pool, step_id).await { - Ok(evaluations) => { - let total = evaluations.len() as i64; - Json(EvaluationListResponse { evaluations, total }).into_response() - } - Err(e) => { - tracing::error!("Failed to list evaluations for step {}: {}", step_id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", e.to_string())), - ) - .into_response() - } - } -} - -/// Submit a chain plan for a directive. -#[utoipa::path( - post, - path = "/api/v1/directives/{id}/submit-plan", - params( - ("id" = Uuid, Path, description = "Directive ID") - ), - request_body = SubmitPlanRequest, - responses( - (status = 200, description = "Plan submitted, directive active", body = Directive), - (status = 400, description = "Invalid request or status", body = ApiError), - (status = 401, description = "Unauthorized", body = ApiError), - (status = 404, description = "Directive not found", body = ApiError), - (status = 503, description = "Database not configured", body = ApiError), - (status = 500, description = "Internal server error", body = ApiError), - ), - security( - ("bearer_auth" = []), - ("api_key" = []) - ), - tag = "Directives" -)] -pub async fn submit_plan( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<SubmitPlanRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match orchestration::directive::submit_plan(pool, &state, auth.owner_id, id, &req.plan).await { - Ok(directive) => Json(directive).into_response(), - Err(e) if e.contains("not found") || e.contains("Not found") => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", e)), - ) - .into_response(), - Err(e) if e.contains("must be in 'planning'") || e.contains("no steps") => ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("INVALID_REQUEST", e)), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to submit plan for directive {}: {}", id, e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("SUBMIT_PLAN_FAILED", e)), - ) - .into_response() - } - } -} |
