An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
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}