haz-domain 0.1.0

Shared domain types for the haz task runner (identifiers, paths, errors).
Documentation
//! [`Task`]: a single named unit of work.
//!
//! A `Task` aggregates the strongly-typed primitives defined in the
//! sibling modules: a [`TaskName`] identifier, a [`TaskAction`]
//! describing what the task does, declared [`InputSpec`] /
//! [`OutputSpec`] file claims, dependency / weak-dependency
//! [`TaskRef`] sets, an optional [`Mutex`] declaration, and
//! per-task environment settings ([`EnvSettings`]).
//!
//! `Task` performs no validation beyond what its fields already
//! enforce. Cross-task invariants (existence of referenced
//! projects, uniqueness of producing tasks per output, etc.) are
//! the responsibility of the workspace-building layer that
//! consumes the loaded configuration.

use crate::action::TaskAction;
use crate::env::EnvSettings;
use crate::mutex::Mutex;
use crate::name::TaskName;
use crate::path::{InputSpec, OutputSpec};
use crate::task_ref::TaskRef;

/// A single named unit of work in a project's task graph.
///
/// Construction is via the public fields: every field is itself
/// strongly typed, so no further validation belongs at this layer.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// use nonempty::NonEmpty;
/// use haz_domain::action::TaskAction;
/// use haz_domain::env::EnvSettings;
/// use haz_domain::name::TaskName;
/// use haz_domain::path::{InputSpec, OutputSpec};
/// use haz_domain::task::Task;
/// use haz_domain::task_ref::TaskRef;
///
/// let argv = NonEmpty::from_vec(vec!["cargo".to_owned(), "build".to_owned()]).unwrap();
/// let task = Task {
///     name: TaskName::from_str("build").unwrap(),
///     action: TaskAction::Command(argv),
///     inputs: vec![InputSpec::parse("src/**/*.rs").unwrap()],
///     outputs: vec![OutputSpec::parse("target/debug/foo").unwrap()],
///     deps: vec![TaskRef::parse("^:codegen").unwrap()],
///     weak_deps: vec![],
///     mutex: None,
///     env: EnvSettings::default(),
/// };
/// assert_eq!(task.name.as_ref(), "build");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
    /// The task's name, unique within its owning project.
    pub name: TaskName,
    /// What the task does when executed.
    pub action: TaskAction,
    /// Files this task consumes.
    pub inputs: Vec<InputSpec>,
    /// Files this task produces.
    pub outputs: Vec<OutputSpec>,
    /// Tasks that MUST complete before this one runs.
    pub deps: Vec<TaskRef>,
    /// Tasks whose outputs feed this task's inputs but which are
    /// not strict ordering constraints (used to avoid choke points
    /// in the DAG).
    pub weak_deps: Vec<TaskRef>,
    /// Optional mutex declaration: scope, name, and mode. Two
    /// tasks declaring the same `(scope, name)` MUST NOT run
    /// concurrently unless both declare `Shared` mode.
    pub mutex: Option<Mutex>,
    /// Per-task environment-variable settings: host-forwarded
    /// names and per-task overrides.
    pub env: EnvSettings,
}

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

    use nonempty::NonEmpty;

    use crate::action::{ShellType, TaskAction};
    use crate::env::EnvSettings;
    use crate::mutex::{Mutex, MutexMode, MutexScope};
    use crate::name::{MutexName, TaskName};
    use crate::path::{InputSpec, OutputSpec};
    use crate::task::Task;
    use crate::task_ref::TaskRef;

    fn name(s: &str) -> TaskName {
        TaskName::from_str(s).unwrap()
    }

    fn argv(parts: &[&str]) -> NonEmpty<String> {
        NonEmpty::from_vec(parts.iter().map(|s| (*s).to_owned()).collect()).unwrap()
    }

    #[test]
    fn task_with_command_action() {
        let task = Task {
            name: name("build"),
            action: TaskAction::Command(argv(&["cargo", "build"])),
            inputs: vec![InputSpec::parse("src/**/*.rs").unwrap()],
            outputs: vec![OutputSpec::parse("target/debug/foo").unwrap()],
            deps: vec![],
            weak_deps: vec![],
            mutex: None,
            env: EnvSettings::default(),
        };
        assert_eq!(task.name.as_ref(), "build");
        assert_eq!(task.inputs.len(), 1);
        assert_eq!(task.outputs.len(), 1);
        assert!(matches!(task.action, TaskAction::Command(_)));
    }

    #[test]
    fn task_with_shell_action_and_mutex() {
        let task = Task {
            name: name("publish"),
            action: TaskAction::Shell {
                script: "echo publishing".to_owned(),
                shell: ShellType::default(),
            },
            inputs: vec![],
            outputs: vec![],
            deps: vec![TaskRef::parse("~:build").unwrap()],
            weak_deps: vec![],
            mutex: Some(Mutex {
                scope: MutexScope::Workspace,
                name: MutexName::from_str("registry").unwrap(),
                mode: MutexMode::Exclusive,
            }),
            env: EnvSettings::default(),
        };
        assert!(matches!(task.action, TaskAction::Shell { .. }));
        assert_eq!(task.deps.len(), 1);
        assert!(task.mutex.is_some());
    }

    #[test]
    fn task_distinguishes_deps_from_weak_deps_by_field() {
        let strong = vec![TaskRef::parse("~:codegen").unwrap()];
        let weak = vec![TaskRef::parse("^:lint").unwrap()];
        let task = Task {
            name: name("compile"),
            action: TaskAction::Command(argv(&["cargo", "check"])),
            inputs: vec![],
            outputs: vec![],
            deps: strong.clone(),
            weak_deps: weak.clone(),
            mutex: None,
            env: EnvSettings::default(),
        };
        assert_eq!(task.deps, strong);
        assert_eq!(task.weak_deps, weak);
        assert_ne!(task.deps, task.weak_deps);
    }
}