jacquardRefs

← snapshot

2666 bytes
//! Named refs: the mutable edge of an immutable DAG.

use std::collections::BTreeMap;

use jac_core::SnapshotId;
use thiserror::Error;

/// A validated ref name (`main`, `feature/retry`).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RefName(String);

impl RefName {
    /// Parses and validates a ref name.
    ///
    /// # Errors
    ///
    /// Returns [`RefError::InvalidName`] if empty, or containing empty
    /// segments, leading/trailing `/`, or `.`/`..` segments.
    pub fn parse(s: &str) -> Result<Self, RefError> {
        if s.is_empty() || s.starts_with('/') || s.ends_with('/') {
            return Err(RefError::InvalidName);
        }
        for seg in s.split('/') {
            if seg.is_empty() || seg == "." || seg == ".." {
                return Err(RefError::InvalidName);
            }
        }
        Ok(Self(s.to_owned()))
    }

    /// The name as a string slice.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

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

/// Failures naming refs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum RefError {
    /// The name was empty or malformed.
    #[error("invalid ref name")]
    InvalidName,
}

/// Where refs live.
pub trait RefStore {
    /// Points `name` at `to`, creating it if absent.
    fn set(&mut self, name: &RefName, to: SnapshotId);

    /// Where `name` points, if anywhere.
    fn get(&self, name: &RefName) -> Option<SnapshotId>;
}

/// In-memory ref store for tests and the demo.
#[derive(Debug, Default)]
pub struct MemoryRefStore {
    refs: BTreeMap<RefName, SnapshotId>,
}

impl MemoryRefStore {
    /// An empty store.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            refs: BTreeMap::new(),
        }
    }

    /// All refs and their heads, in name order.
    pub fn iter(&self) -> impl Iterator<Item = (&RefName, SnapshotId)> {
        self.refs.iter().map(|(name, id)| (name, *id))
    }
}

impl RefStore for MemoryRefStore {
    fn set(&mut self, name: &RefName, to: SnapshotId) {
        self.refs.insert(name.clone(), to);
    }

    fn get(&self, name: &RefName) -> Option<SnapshotId> {
        self.refs.get(name).copied()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn names_are_validated() {
        for bad in ["", "/x", "x/", "a//b", "a/./b", "a/../b"] {
            assert!(RefName::parse(bad).is_err(), "{bad:?} should be rejected");
        }
        assert!(RefName::parse("feature/retry").is_ok());
    }
}