haz-domain 0.2.0

Shared domain types for the haz task runner (identifiers, paths, errors).
Documentation
//! [`TaskId`]: the workspace-unique identity of a single task.
//!
//! A `TaskId` is the *resolved* form of a [`TaskRef`](crate::task_ref::TaskRef):
//! a pair of validated [`ProjectName`] and [`TaskName`] that
//! uniquely identifies one task within a workspace. Reference
//! resolution (see [`crate::resolution`]) produces sets of these,
//! and downstream layers use them as DAG node identities per
//! `DAG-007`.

use std::fmt;

use crate::name::{ProjectName, TaskName};

/// A fully-qualified task identifier within a workspace.
///
/// Sort order is lexicographic on `(project, task)`: a `BTreeSet`
/// of `TaskId`s iterates by project name, then by task name within
/// each project.
///
/// The [`fmt::Display`] form is `<project>:<task>`, which matches
/// the project-reference syntax defined by `REF-003`. This is the
/// canonical printed form of a resolved task.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// use haz_domain::name::{ProjectName, TaskName};
/// use haz_domain::task_id::TaskId;
///
/// let id = TaskId {
///     project: ProjectName::from_str("lib_core").unwrap(),
///     task: TaskName::from_str("compile").unwrap(),
/// };
/// assert_eq!(id.to_string(), "lib_core:compile");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TaskId {
    /// The owning project's name.
    pub project: ProjectName,
    /// The task's name within its project.
    pub task: TaskName,
}

impl fmt::Display for TaskId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}:{}", self.project, self.task)
    }
}

#[cfg(test)]
mod tests {
    use std::cmp::Ordering;
    use std::collections::BTreeSet;
    use std::str::FromStr;

    use crate::name::{ProjectName, TaskName};
    use crate::task_id::TaskId;

    fn id(project: &str, task: &str) -> TaskId {
        TaskId {
            project: ProjectName::from_str(project).unwrap(),
            task: TaskName::from_str(task).unwrap(),
        }
    }

    #[test]
    fn display_renders_project_colon_task() {
        assert_eq!(id("lib_core", "compile").to_string(), "lib_core:compile");
        assert_eq!(
            id("web-frontend", "bundle").to_string(),
            "web-frontend:bundle"
        );
    }

    #[test]
    fn equality_is_structural() {
        assert_eq!(id("a", "x"), id("a", "x"));
        assert_ne!(id("a", "x"), id("a", "y"));
        assert_ne!(id("a", "x"), id("b", "x"));
    }

    #[test]
    fn ord_is_lexicographic_project_then_task() {
        // Same project, different task: task name decides.
        assert_eq!(id("p", "a").cmp(&id("p", "b")), Ordering::Less);
        // Different project: project name decides regardless of task.
        assert_eq!(id("a", "z").cmp(&id("b", "a")), Ordering::Less);
    }

    #[test]
    fn btreeset_iterates_in_canonical_order() {
        let mut set = BTreeSet::new();
        set.insert(id("z", "build"));
        set.insert(id("a", "test"));
        set.insert(id("a", "build"));
        set.insert(id("m", "build"));

        let collected: Vec<_> = set.iter().map(ToString::to_string).collect();
        assert_eq!(collected, vec!["a:build", "a:test", "m:build", "z:build"],);
    }

    #[test]
    fn clone_preserves_value() {
        let original = id("lib_core", "compile");
        let cloned = original.clone();
        assert_eq!(original, cloned);
    }
}