Files
storkit/server/src/http/project.rs

205 lines
6.6 KiB
Rust
Raw Normal View History

2026-02-16 16:24:21 +00:00
use crate::http::context::{AppContext, OpenApiResult, bad_request};
use crate::io::fs;
2026-02-16 16:50:50 +00:00
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
2026-02-16 16:35:25 +00:00
use serde::Deserialize;
use std::sync::Arc;
2026-02-16 16:24:21 +00:00
2026-02-16 16:50:50 +00:00
#[derive(Tags)]
enum ProjectTags {
Project,
}
2026-02-16 16:35:25 +00:00
#[derive(Deserialize, Object)]
struct PathPayload {
path: String,
2026-02-16 16:24:21 +00:00
}
2026-02-16 16:35:25 +00:00
pub struct ProjectApi {
pub ctx: Arc<AppContext>,
2026-02-16 16:24:21 +00:00
}
2026-02-16 16:50:50 +00:00
#[OpenApi(tag = "ProjectTags::Project")]
2026-02-16 16:35:25 +00:00
impl ProjectApi {
2026-02-16 16:50:50 +00:00
/// Get the currently open project path (if any).
///
/// Returns null when no project is open.
2026-02-16 16:35:25 +00:00
#[oai(path = "/project", method = "get")]
async fn get_current_project(&self) -> OpenApiResult<Json<Option<String>>> {
let result = fs::get_current_project(&self.ctx.state, self.ctx.store.as_ref())
.map_err(bad_request)?;
Ok(Json(result))
}
2026-02-16 16:50:50 +00:00
/// Open a project and set it as the current project.
///
/// Persists the selected path for later sessions.
2026-02-16 16:35:25 +00:00
#[oai(path = "/project", method = "post")]
async fn open_project(&self, payload: Json<PathPayload>) -> OpenApiResult<Json<String>> {
let confirmed = fs::open_project(payload.0.path, &self.ctx.state, self.ctx.store.as_ref())
.await
.map_err(bad_request)?;
Ok(Json(confirmed))
}
2026-02-16 16:50:50 +00:00
/// Close the current project and clear the stored selection.
2026-02-16 16:35:25 +00:00
#[oai(path = "/project", method = "delete")]
async fn close_project(&self) -> OpenApiResult<Json<bool>> {
fs::close_project(&self.ctx.state, self.ctx.store.as_ref()).map_err(bad_request)?;
Ok(Json(true))
}
2026-02-16 18:57:39 +00:00
/// List known projects from the store.
#[oai(path = "/projects", method = "get")]
async fn list_known_projects(&self) -> OpenApiResult<Json<Vec<String>>> {
let projects = fs::get_known_projects(self.ctx.store.as_ref()).map_err(bad_request)?;
Ok(Json(projects))
}
2026-02-16 19:53:31 +00:00
/// Forget a known project path.
#[oai(path = "/projects/forget", method = "post")]
async fn forget_known_project(&self, payload: Json<PathPayload>) -> OpenApiResult<Json<bool>> {
fs::forget_known_project(payload.0.path, self.ctx.store.as_ref()).map_err(bad_request)?;
Ok(Json(true))
}
2026-02-16 16:24:21 +00:00
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::context::AppContext;
use tempfile::TempDir;
fn make_api(dir: &TempDir) -> ProjectApi {
ProjectApi {
ctx: Arc::new(AppContext::new_test(dir.path().to_path_buf())),
}
}
#[tokio::test]
async fn get_current_project_returns_none_when_unset() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
// Clear the project root that new_test sets
api.close_project().await.unwrap();
let result = api.get_current_project().await.unwrap();
assert!(result.0.is_none());
}
#[tokio::test]
async fn get_current_project_returns_path_from_state() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let result = api.get_current_project().await.unwrap();
assert_eq!(result.0, Some(dir.path().to_string_lossy().to_string()));
}
#[tokio::test]
async fn open_project_succeeds_with_valid_directory() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let path = dir.path().to_string_lossy().to_string();
let payload = Json(PathPayload { path: path.clone() });
let result = api.open_project(payload).await.unwrap();
assert_eq!(result.0, path);
}
#[tokio::test]
async fn open_project_fails_with_nonexistent_file_path() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
// Create a file (not a directory) to trigger validation error
let file_path = dir.path().join("not_a_dir.txt");
std::fs::write(&file_path, "content").unwrap();
let payload = Json(PathPayload {
path: file_path.to_string_lossy().to_string(),
});
let result = api.open_project(payload).await;
assert!(result.is_err());
}
#[tokio::test]
async fn close_project_returns_true() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let result = api.close_project().await.unwrap();
assert!(result.0);
}
#[tokio::test]
async fn close_project_clears_current_project() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
// Verify project is set initially
let before = api.get_current_project().await.unwrap();
assert!(before.0.is_some());
// Close the project
api.close_project().await.unwrap();
// Verify project is now None
let after = api.get_current_project().await.unwrap();
assert!(after.0.is_none());
}
#[tokio::test]
async fn list_known_projects_returns_empty_initially() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
// Close the project so the store has no known projects
api.close_project().await.unwrap();
let result = api.list_known_projects().await.unwrap();
assert!(result.0.is_empty());
}
#[tokio::test]
async fn list_known_projects_returns_project_after_open() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let path = dir.path().to_string_lossy().to_string();
api.open_project(Json(PathPayload { path: path.clone() }))
.await
.unwrap();
let result = api.list_known_projects().await.unwrap();
assert!(result.0.contains(&path));
}
#[tokio::test]
async fn forget_known_project_removes_project() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let path = dir.path().to_string_lossy().to_string();
api.open_project(Json(PathPayload { path: path.clone() }))
.await
.unwrap();
let before = api.list_known_projects().await.unwrap();
assert!(before.0.contains(&path));
let result = api
.forget_known_project(Json(PathPayload { path: path.clone() }))
.await
.unwrap();
assert!(result.0);
let after = api.list_known_projects().await.unwrap();
assert!(!after.0.contains(&path));
}
#[tokio::test]
async fn forget_known_project_returns_true_for_nonexistent_path() {
let dir = TempDir::new().unwrap();
let api = make_api(&dir);
let result = api
.forget_known_project(Json(PathPayload {
path: "/some/unknown/path".to_string(),
}))
.await
.unwrap();
assert!(result.0);
}
}