haz-domain 0.2.0

Shared domain types for the haz task runner (identifiers, paths, errors).
Documentation
//! `OverlayName`: the stem of a `.haz/tasks/*.yml` file,
//! validated as an identifier per `CFG-023`.

use nutype::nutype;

use crate::name::validation::{NameError, validate};

/// A validated overlay-file name (the file's stem without
/// `.yml`).
///
/// Construction is via [`OverlayName::try_new`] (or the standard
/// [`std::str::FromStr`] interface). Both delegate to the shared
/// identifier validator (`ID-001`..`ID-003`) so an `OverlayName`
/// is interchangeable with the other identifier newtypes for
/// the purposes of validation.
///
/// # Examples
///
/// ```
/// use haz_domain::name::OverlayName;
///
/// let n = OverlayName::try_new("lint").unwrap();
/// assert_eq!(n.to_string(), "lint");
/// ```
#[nutype(
    validate(with = validate, error = NameError),
    derive(
        Debug,
        Clone,
        PartialEq,
        Eq,
        PartialOrd,
        Ord,
        Hash,
        Display,
        FromStr,
        AsRef,
    ),
)]
pub struct OverlayName(String);

#[cfg(test)]
mod tests {
    use crate::name::{NameError, OverlayName};

    #[test]
    fn round_trip_via_try_new_and_display() {
        let n = OverlayName::try_new("lint").unwrap();
        assert_eq!(n.to_string(), "lint");
    }

    #[test]
    fn round_trip_via_fromstr() {
        let parsed: OverlayName = "lint".parse().unwrap();
        assert_eq!(parsed.to_string(), "lint");
    }

    #[test]
    fn id_001_rejects_disallowed_alphabet_character() {
        assert!(matches!(
            OverlayName::try_new("li nt"),
            Err(NameError::InvalidChar { c: ' ' })
        ));
    }

    #[test]
    fn id_002_rejects_invalid_leading_character() {
        assert!(matches!(
            OverlayName::try_new("_lint"),
            Err(NameError::InvalidLeadingChar { c: '_' })
        ));
    }

    #[test]
    fn id_003_rejects_empty() {
        assert!(matches!(OverlayName::try_new(""), Err(NameError::Empty)));
    }

    #[test]
    fn id_003_rejects_overlong() {
        let overlong = "a".repeat(65);
        assert!(matches!(
            OverlayName::try_new(&overlong),
            Err(NameError::TooLong { length: 65 })
        ));
    }

    #[test]
    fn id_004_case_sensitive_equality() {
        let upper = OverlayName::try_new("Lint").unwrap();
        let lower = OverlayName::try_new("lint").unwrap();
        assert_ne!(upper, lower);
    }
}