haz-domain 0.2.0

Shared domain types for the haz task runner (identifiers, paths, errors).
Documentation
//! `InputSpec`: a path pattern declared as a task input.
//!
//! A distinct newtype wrapping [`PathPattern`] so that the type
//! system distinguishes "this pattern declares an input" from
//! "this pattern declares an output" (see [`crate::path::OutputSpec`]).
//! The two types share the same parsing logic but live in
//! different domain roles: an input pattern is a *consumption*
//! claim (files this task reads), while an output pattern is a
//! *production* claim (files this task writes). Mixing them in
//! generic code would be a domain modelling bug.

use std::fmt;

use crate::path::pattern::{PathPattern, PathPatternError};

/// A validated input declaration: a path pattern attached to a
/// task's `inputs` field.
///
/// Construction via [`InputSpec::parse`]; the [`fmt::Display`]
/// implementation produces a string that round-trips through
/// [`InputSpec::parse`].
///
/// # Examples
///
/// ```
/// use haz_domain::path::InputSpec;
///
/// let s = InputSpec::parse("src/**/*.rs").unwrap();
/// assert_eq!(s.to_string(), "src/**/*.rs");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InputSpec(PathPattern);

impl InputSpec {
    /// Parse `s` as an input declaration.
    ///
    /// # Errors
    ///
    /// Returns the same [`PathPatternError`] as
    /// [`PathPattern::parse`].
    pub fn parse(s: &str) -> Result<Self, PathPatternError> {
        PathPattern::parse(s).map(Self)
    }

    /// Borrow the underlying pattern.
    #[must_use]
    pub fn pattern(&self) -> &PathPattern {
        &self.0
    }

    /// Consume `self`, returning the underlying pattern.
    #[must_use]
    pub fn into_pattern(self) -> PathPattern {
        self.0
    }
}

impl fmt::Display for InputSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

#[cfg(test)]
mod tests {
    use crate::path::InputSpec;
    use crate::path::pattern::PathPattern;

    #[test]
    fn parses_literal_input() {
        let s = InputSpec::parse("src/lib.rs").unwrap();
        assert!(s.pattern().is_literal());
    }

    #[test]
    fn parses_glob_input() {
        let s = InputSpec::parse("src/**/*.rs").unwrap();
        assert!(s.pattern().is_glob());
    }

    #[test]
    fn rejects_invalid_input() {
        assert!(InputSpec::parse("./foo").is_err());
    }

    #[test]
    fn round_trip() {
        let original = "src/**/*.rs";
        let s = InputSpec::parse(original).unwrap();
        assert_eq!(s.to_string(), original);
        let reparsed = InputSpec::parse(&s.to_string()).unwrap();
        assert_eq!(s, reparsed);
    }

    #[test]
    fn into_pattern_round_trips_to_pattern() {
        let s = InputSpec::parse("src/lib.rs").unwrap();
        let p: PathPattern = s.into_pattern();
        assert!(p.is_literal());
    }
}