//! 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 { 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, 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, 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 = known.into_iter().filter(|p| p != &path).collect(); if updated.len() == before { return Ok(()); } io::save_known_projects(&updated, store) }