haz-domain 0.2.0

Shared domain types for the haz task runner (identifiers, paths, errors).
Documentation
//! [`Project`]: a named collection of tasks rooted at a workspace
//! address.
//!
//! A `Project` aggregates a [`ProjectName`], its [`ProjectRoot`]
//! within the workspace, a set of [`TagName`]s the project
//! carries, and a [`BTreeMap`] of tasks keyed by their
//! [`TaskName`]. Task uniqueness within a project is enforced
//! structurally by the map.

use std::collections::{BTreeMap, BTreeSet};

use crate::name::{ProjectName, TagName, TaskName};
use crate::path::ProjectRoot;
use crate::task::Task;

/// A named project: a unit of code or content that owns tasks.
///
/// The `root` field is a [`ProjectRoot`]: either the workspace
/// root itself (implicit mode per `DISC-003`) or a nested
/// [`CanonicalPath`](crate::path::CanonicalPath) (explicit mode
/// per `DISC-005`). Equality on [`ProjectRoot`] is the
/// load-bearing invariant for matching declared paths across
/// projects.
///
/// # Examples
///
/// ```
/// use std::collections::{BTreeMap, BTreeSet};
/// use std::str::FromStr;
/// use haz_domain::name::{ProjectName, TagName};
/// use haz_domain::path::{CanonicalPath, HazPath, ProjectRoot};
/// use haz_domain::project::Project;
///
/// let root = ProjectRoot::Nested(
///     CanonicalPath::from_absolute(&HazPath::parse("/lib_core").unwrap()).unwrap()
/// );
///
/// let project = Project {
///     name: ProjectName::from_str("lib_core").unwrap(),
///     root,
///     tags: BTreeSet::from([TagName::from_str("rust").unwrap()]),
///     tasks: BTreeMap::new(),
/// };
/// assert_eq!(project.name.as_ref(), "lib_core");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Project {
    /// The project's workspace-unique name.
    pub name: ProjectName,
    /// The project's location within the workspace.
    pub root: ProjectRoot,
    /// Tags this project carries. Used by tag-based task references
    /// (`[<tag>]:<task>`) and tag-filtered task overlays.
    pub tags: BTreeSet<TagName>,
    /// Tasks owned by this project, keyed by name.
    pub tasks: BTreeMap<TaskName, Task>,
}

#[cfg(test)]
mod tests {
    use std::collections::{BTreeMap, BTreeSet};
    use std::str::FromStr;

    use nonempty::NonEmpty;

    use crate::action::TaskAction;
    use crate::env::EnvSettings;
    use crate::name::{ProjectName, TagName, TaskName};
    use crate::path::{CanonicalPath, HazPath, ProjectRoot};
    use crate::project::Project;
    use crate::task::Task;

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

    fn empty_task(name: &str) -> Task {
        Task {
            name: TaskName::from_str(name).unwrap(),
            action: TaskAction::Command(NonEmpty::from_vec(vec!["true".to_owned()]).unwrap()),
            inputs: vec![],
            outputs: vec![],
            deps: vec![],
            weak_deps: vec![],
            mutex: None,
            env: EnvSettings::default(),
        }
    }

    #[test]
    fn project_with_tasks_and_tags() {
        let mut tasks = BTreeMap::new();
        tasks.insert(TaskName::from_str("build").unwrap(), empty_task("build"));
        tasks.insert(TaskName::from_str("test").unwrap(), empty_task("test"));

        let mut tags = BTreeSet::new();
        tags.insert(TagName::from_str("rust").unwrap());
        tags.insert(TagName::from_str("library").unwrap());

        let project = Project {
            name: ProjectName::from_str("lib_core").unwrap(),
            root: root_at("/lib_core"),
            tags,
            tasks,
        };

        assert_eq!(project.tasks.len(), 2);
        assert!(project.tags.contains(&TagName::from_str("rust").unwrap()));
        assert_eq!(project.root.to_string(), "/lib_core");
    }

    #[test]
    fn project_task_uniqueness_is_structural() {
        // Inserting the same TaskName twice into the BTreeMap
        // keeps only the second value: the structure enforces
        // uniqueness without an explicit check.
        let mut tasks = BTreeMap::new();
        let key = TaskName::from_str("build").unwrap();
        let first = tasks.insert(key.clone(), empty_task("build"));
        assert!(first.is_none());
        let second = tasks.insert(key.clone(), empty_task("build"));
        assert!(second.is_some(), "duplicate insert returns the prior value");
        assert_eq!(tasks.len(), 1);
    }

    #[test]
    fn project_tags_are_a_set() {
        let mut tags = BTreeSet::new();
        let inserted_first = tags.insert(TagName::from_str("rust").unwrap());
        assert!(inserted_first);
        let inserted_again = tags.insert(TagName::from_str("rust").unwrap());
        assert!(!inserted_again, "duplicate tag insert returns false");
        assert_eq!(tags.len(), 1);
    }
}