haz-domain 0.2.0

Shared domain types for the haz task runner (identifiers, paths, errors).
Documentation
//! [`ProjectRoot`]: the workspace location of a project.
//!
//! A project's root is either the workspace root itself (implicit
//! mode per `DISC-003`) or a nested directory expressed as a
//! [`CanonicalPath`] (explicit mode per `DISC-005`).
//!
//! `Project.root` uses this type so the domain can represent both
//! modes uniformly. Equality on [`ProjectRoot`] discriminates the
//! two variants and, within [`ProjectRoot::Nested`], delegates to
//! [`CanonicalPath`] equality.

use std::fmt;

use crate::path::CanonicalPath;

/// The workspace-internal location of a project.
///
/// Two values compare equal iff they address the same workspace
/// location: the singleton [`ProjectRoot::WorkspaceRoot`] equals
/// itself, and [`ProjectRoot::Nested`] equality is canonical-path
/// equality.
///
/// # Examples
///
/// ```
/// use haz_domain::path::{CanonicalPath, HazPath, ProjectRoot};
///
/// let nested = ProjectRoot::Nested(
///     CanonicalPath::from_absolute(&HazPath::parse("/lib_core").unwrap()).unwrap()
/// );
/// assert_eq!(nested.to_string(), "/lib_core");
///
/// let implicit = ProjectRoot::WorkspaceRoot;
/// assert_eq!(implicit.to_string(), "/");
///
/// assert_ne!(nested, implicit);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProjectRoot {
    /// The project lives at the workspace root itself (implicit
    /// mode per `DISC-003`).
    WorkspaceRoot,
    /// The project lives at a nested workspace-internal path
    /// (explicit mode per `DISC-005`).
    Nested(CanonicalPath),
}

impl ProjectRoot {
    /// `true` iff this is the workspace-root variant.
    #[must_use]
    pub fn is_workspace_root(&self) -> bool {
        matches!(self, Self::WorkspaceRoot)
    }

    /// Borrow the inner [`CanonicalPath`] when this is the nested
    /// variant; `None` when this is the workspace-root variant.
    #[must_use]
    pub fn as_canonical_path(&self) -> Option<&CanonicalPath> {
        match self {
            Self::WorkspaceRoot => None,
            Self::Nested(p) => Some(p),
        }
    }
}

impl fmt::Display for ProjectRoot {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::WorkspaceRoot => f.write_str("/"),
            Self::Nested(p) => p.fmt(f),
        }
    }
}

impl From<CanonicalPath> for ProjectRoot {
    fn from(p: CanonicalPath) -> Self {
        Self::Nested(p)
    }
}

#[cfg(test)]
mod tests {
    use crate::path::{CanonicalPath, HazPath, ProjectRoot};

    fn nested(s: &str) -> ProjectRoot {
        ProjectRoot::Nested(CanonicalPath::from_absolute(&HazPath::parse(s).unwrap()).unwrap())
    }

    #[test]
    fn workspace_root_display_is_slash() {
        assert_eq!(ProjectRoot::WorkspaceRoot.to_string(), "/");
    }

    #[test]
    fn nested_display_matches_canonical_path() {
        assert_eq!(nested("/lib_core").to_string(), "/lib_core");
        assert_eq!(nested("/web/frontend").to_string(), "/web/frontend");
    }

    #[test]
    fn is_workspace_root_discriminates() {
        assert!(ProjectRoot::WorkspaceRoot.is_workspace_root());
        assert!(!nested("/lib_core").is_workspace_root());
    }

    #[test]
    fn as_canonical_path_returns_none_for_workspace_root() {
        assert!(ProjectRoot::WorkspaceRoot.as_canonical_path().is_none());
    }

    #[test]
    fn as_canonical_path_returns_inner_for_nested() {
        let cp = CanonicalPath::from_absolute(&HazPath::parse("/lib_core").unwrap()).unwrap();
        let pr = ProjectRoot::Nested(cp.clone());
        assert_eq!(pr.as_canonical_path(), Some(&cp));
    }

    #[test]
    fn equality_within_nested_delegates_to_canonical_path() {
        assert_eq!(nested("/lib_core"), nested("/lib_core"));
        assert_ne!(nested("/lib_core"), nested("/web/frontend"));
    }

    #[test]
    fn workspace_root_and_nested_are_distinct() {
        assert_ne!(ProjectRoot::WorkspaceRoot, nested("/lib_core"));
    }

    #[test]
    fn from_canonical_path_yields_nested_variant() {
        let cp = CanonicalPath::from_absolute(&HazPath::parse("/lib_core").unwrap()).unwrap();
        let pr: ProjectRoot = cp.clone().into();
        assert_eq!(pr, ProjectRoot::Nested(cp));
    }
}