Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
116 lines
3.3 KiB
Rust
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,
|
|
})
|
|
}
|
|
}
|