Files
IdeA/crates/domain/src/project.rs
Blomios 307ae71857 feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
2026-06-06 01:27:01 +02:00

116 lines
3.3 KiB
Rust

//! Project aggregate root and its path value object.
use serde::{Deserialize, Serialize};
use crate::error::DomainError;
use crate::ids::ProjectId;
use crate::remote::RemoteRef;
/// A normalized, absolute project path, aware of its target platform.
///
/// Invariant: the path is **absolute** for its platform. We accept POSIX
/// absolute paths (`/...`), Windows absolute paths (`C:\...` or `\\server\...`)
/// and WSL mount paths (`/mnt/c/...`, themselves POSIX-absolute).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProjectPath(String);
impl ProjectPath {
/// Builds a validated project path.
///
/// # Errors
/// Returns [`DomainError::PathNotAbsolute`] if `raw` is not absolute, or
/// [`DomainError::EmptyField`] if it is empty.
pub fn new(raw: impl Into<String>) -> Result<Self, DomainError> {
let raw = raw.into();
crate::validation::non_empty(&raw, "project.root")?;
if Self::is_absolute(&raw) {
Ok(Self(raw))
} else {
Err(DomainError::PathNotAbsolute { path: raw })
}
}
/// Returns whether `raw` is an absolute path on any supported platform.
fn is_absolute(raw: &str) -> bool {
let bytes = raw.as_bytes();
// POSIX / WSL absolute.
if bytes.first() == Some(&b'/') {
return true;
}
// Windows UNC.
if raw.starts_with("\\\\") {
return true;
}
// Windows drive-letter absolute: `C:\` or `C:/`.
if bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/')
{
return true;
}
false
}
/// Returns the raw path string.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ProjectPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
/// Project aggregate root.
///
/// Invariants enforced here:
/// - `name` non-empty,
/// - `root` is an absolute [`ProjectPath`].
///
/// The cross-aggregate uniqueness invariant — "no two projects share the same
/// `(remote, root)`" — is a *repository* concern (it requires knowledge of all
/// projects) and is enforced by the `ProjectStore`/use case layer, not here.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Project {
/// Stable identifier.
pub id: ProjectId,
/// Display name.
pub name: String,
/// Absolute project root.
pub root: ProjectPath,
/// Where the project physically lives.
pub remote: RemoteRef,
/// Creation timestamp, as epoch milliseconds (supplied via a `Clock` port).
pub created_at: i64,
}
impl Project {
/// Builds a validated project.
///
/// # Errors
/// Returns [`DomainError::EmptyField`] if `name` is empty.
pub fn new(
id: ProjectId,
name: impl Into<String>,
root: ProjectPath,
remote: RemoteRef,
created_at: i64,
) -> Result<Self, DomainError> {
let name = name.into();
crate::validation::non_empty(&name, "project.name")?;
Ok(Self {
id,
name,
root,
remote,
created_at,
})
}
}