An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at new-directions 90 lines 2.7 kB view raw
1use super::{ApiError, AppState}; 2use crate::project::Project; 3use axum::Json; 4use axum::extract::{Path, State}; 5use axum::http::StatusCode; 6use serde::{Deserialize, Serialize}; 7 8#[derive(Deserialize)] 9pub struct CreateProjectRequest { 10 pub name: String, 11 pub path: String, 12} 13 14#[derive(Serialize, Deserialize)] 15pub struct ProjectResponse { 16 pub id: String, 17 pub name: String, 18 pub path: String, 19 pub registered_at: String, 20} 21 22impl From<Project> for ProjectResponse { 23 fn from(p: Project) -> Self { 24 Self { 25 id: p.id, 26 name: p.name, 27 path: p.path.display().to_string(), 28 registered_at: p.registered_at.to_rfc3339(), 29 } 30 } 31} 32 33/// GET /api/projects 34pub async fn list_projects( 35 State(state): State<AppState>, 36) -> Result<Json<Vec<ProjectResponse>>, ApiError> { 37 let projects = state.project_store.list().await?; 38 Ok(Json( 39 projects.into_iter().map(ProjectResponse::from).collect(), 40 )) 41} 42 43/// POST /api/projects 44pub async fn create_project( 45 State(state): State<AppState>, 46 Json(body): Json<CreateProjectRequest>, 47) -> Result<(StatusCode, Json<ProjectResponse>), ApiError> { 48 let path = std::path::Path::new(&body.path); 49 let canonical = path 50 .canonicalize() 51 .map_err(|e| ApiError::BadRequest(format!("Invalid path '{}': {}", body.path, e)))?; 52 53 match state.project_store.add(&body.name, &canonical).await { 54 Ok(project) => Ok((StatusCode::CREATED, Json(ProjectResponse::from(project)))), 55 Err(e) if e.to_string().contains("UNIQUE constraint") => Err(ApiError::Conflict(format!( 56 "Project '{}' already exists", 57 body.name 58 ))), 59 Err(e) => Err(ApiError::Internal(e.to_string())), 60 } 61} 62 63/// GET /api/projects/:id 64pub async fn get_project( 65 State(state): State<AppState>, 66 Path(id): Path<String>, 67) -> Result<Json<ProjectResponse>, ApiError> { 68 // Try by name first, then by ID 69 let project = match state.project_store.get_by_name(&id).await? { 70 Some(p) => Some(p), 71 None => state.project_store.get_by_id(&id).await?, 72 }; 73 match project { 74 Some(p) => Ok(Json(ProjectResponse::from(p))), 75 None => Err(ApiError::NotFound(format!("Project '{}' not found", id))), 76 } 77} 78 79/// DELETE /api/projects/:id 80pub async fn delete_project( 81 State(state): State<AppState>, 82 Path(id): Path<String>, 83) -> Result<StatusCode, ApiError> { 84 let removed = state.project_store.remove(&id).await?; 85 if removed { 86 Ok(StatusCode::NO_CONTENT) 87 } else { 88 Err(ApiError::NotFound(format!("Project '{}' not found", id))) 89 } 90}