103 lines
3.8 KiB
Rust
103 lines
3.8 KiB
Rust
|
|
//! Project service — public API for the project domain.
|
||
|
|
//!
|
||
|
|
//! Exposes functions to open, close, query, and manage known projects.
|
||
|
|
//! HTTP handlers call these functions instead of touching `io::fs` or session
|
||
|
|
//! state directly.
|
||
|
|
//!
|
||
|
|
//! Conventions: `docs/architecture/service-modules.md`
|
||
|
|
|
||
|
|
pub(super) mod io;
|
||
|
|
pub mod selection;
|
||
|
|
|
||
|
|
use crate::state::SessionState;
|
||
|
|
use crate::store::StoreOps;
|
||
|
|
use std::path::PathBuf;
|
||
|
|
|
||
|
|
// ── Error type ────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/// Typed errors returned by `service::project` functions.
|
||
|
|
///
|
||
|
|
/// HTTP handlers map these to specific status codes:
|
||
|
|
/// - [`Error::PathNotFound`] → 404 Not Found
|
||
|
|
/// - [`Error::NotADirectory`] → 400 Bad Request
|
||
|
|
/// - [`Error::Internal`] → 500 Internal Server Error
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub enum Error {
|
||
|
|
/// The given path does not exist on the filesystem.
|
||
|
|
PathNotFound(String),
|
||
|
|
/// The given path exists but is not a directory.
|
||
|
|
NotADirectory(String),
|
||
|
|
/// An internal error occurred (lock poisoned, store I/O failure, task panic).
|
||
|
|
Internal(String),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl std::fmt::Display for Error {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
match self {
|
||
|
|
Self::PathNotFound(msg) => write!(f, "Project not found: {msg}"),
|
||
|
|
Self::NotADirectory(msg) => write!(f, "Invalid project path: {msg}"),
|
||
|
|
Self::Internal(msg) => write!(f, "Internal error: {msg}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
/// Open a project, scaffolding it when needed, and persist the selection.
|
||
|
|
///
|
||
|
|
/// Validates that `path` exists and is a directory. On success, returns the
|
||
|
|
/// canonical path string. The path is promoted to the front of the known list.
|
||
|
|
pub async fn open_project(
|
||
|
|
path: String,
|
||
|
|
state: &SessionState,
|
||
|
|
store: &dyn StoreOps,
|
||
|
|
port: u16,
|
||
|
|
) -> Result<String, Error> {
|
||
|
|
let p = PathBuf::from(&path);
|
||
|
|
io::ensure_scaffold(p.clone(), port).await?;
|
||
|
|
io::validate_path(&p).await?;
|
||
|
|
io::set_project_root(state, Some(p))?;
|
||
|
|
let known = io::read_known_projects(store);
|
||
|
|
let updated = selection::promote_to_front(known, &path);
|
||
|
|
io::persist_open_project(&path, &updated, store)?;
|
||
|
|
Ok(path)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Close the current project and remove it from the persisted selection.
|
||
|
|
pub fn close_project(state: &SessionState, store: &dyn StoreOps) -> Result<(), Error> {
|
||
|
|
io::set_project_root(state, None)?;
|
||
|
|
io::clear_project(store)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Return the currently open project path, if any.
|
||
|
|
///
|
||
|
|
/// Checks in-memory state first, then falls back to the store.
|
||
|
|
/// If the store has a valid path, restores it into state for future calls.
|
||
|
|
pub fn get_current_project(
|
||
|
|
state: &SessionState,
|
||
|
|
store: &dyn StoreOps,
|
||
|
|
) -> Result<Option<String>, Error> {
|
||
|
|
if let Some(path) = io::get_project_root_from_state(state)? {
|
||
|
|
return Ok(Some(path.to_string_lossy().to_string()));
|
||
|
|
}
|
||
|
|
io::restore_from_store(state, store)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Return all known (previously opened) project paths from the store.
|
||
|
|
pub fn get_known_projects(store: &dyn StoreOps) -> Result<Vec<String>, Error> {
|
||
|
|
Ok(io::read_known_projects(store))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Remove a path from the known-projects list.
|
||
|
|
///
|
||
|
|
/// Returns `Ok(())` whether or not the path was present (idempotent).
|
||
|
|
pub fn forget_known_project(path: String, store: &dyn StoreOps) -> Result<(), Error> {
|
||
|
|
let known = io::read_known_projects(store);
|
||
|
|
let before = known.len();
|
||
|
|
let updated: Vec<String> = known.into_iter().filter(|p| p != &path).collect();
|
||
|
|
if updated.len() == before {
|
||
|
|
return Ok(());
|
||
|
|
}
|
||
|
|
io::save_known_projects(&updated, store)
|
||
|
|
}
|