feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
115
crates/domain/src/project.rs
Normal file
115
crates/domain/src/project.rs
Normal file
@ -0,0 +1,115 @@
|
||||
//! 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user