use std::fmt;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DagId {
head: Arc<str>,
tail: Arc<[Arc<str>]>,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum DagIdPathError {
#[error("path has no components")]
Empty,
#[error("path contains a non-UTF-8 component")]
NonUtf8Component,
#[error("path must end with `.gcl`")]
MissingGclExtension,
}
impl DagId {
pub fn new(
head: impl Into<Arc<str>>,
tail: impl IntoIterator<Item = impl Into<Arc<str>>>,
) -> Self {
Self {
head: head.into(),
tail: tail.into_iter().map(Into::into).collect(),
}
}
pub fn root(name: impl Into<Arc<str>>) -> Self {
Self {
head: name.into(),
tail: Arc::from([] as [Arc<str>; 0]),
}
}
#[must_use]
pub fn child(&self, name: impl Into<Arc<str>>) -> Self {
let mut tail: Vec<Arc<str>> = self.tail.to_vec();
tail.push(name.into());
Self {
head: Arc::clone(&self.head),
tail: tail.into(),
}
}
#[must_use]
pub fn parent(&self) -> Option<Self> {
if self.tail.is_empty() {
return None;
}
Some(Self {
head: Arc::clone(&self.head),
tail: self.tail[..self.tail.len() - 1].into(),
})
}
pub fn segments(&self) -> impl Iterator<Item = &Arc<str>> {
std::iter::once(&self.head).chain(self.tail.iter())
}
#[must_use]
pub fn segment_count(&self) -> usize {
1 + self.tail.len()
}
#[must_use]
pub fn name(&self) -> &str {
self.tail.last().map_or(&self.head, |s| s)
}
#[must_use]
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
if self.segment_count() <= ancestor.segment_count() {
return false;
}
self.segments()
.zip(ancestor.segments())
.all(|(a, b)| a == b)
}
pub fn from_relative_path(path: &std::path::Path) -> Result<Self, DagIdPathError> {
let mut segments: Vec<Arc<str>> = path
.components()
.map(|c| {
c.as_os_str()
.to_str()
.map(Arc::<str>::from)
.ok_or(DagIdPathError::NonUtf8Component)
})
.collect::<Result<_, _>>()?;
let last = segments.last_mut().ok_or(DagIdPathError::Empty)?;
*last = last
.strip_suffix(".gcl")
.map(Arc::<str>::from)
.ok_or(DagIdPathError::MissingGclExtension)?;
let mut segments = segments.into_iter();
let head = segments.next().ok_or(DagIdPathError::Empty)?;
let tail: Arc<[Arc<str>]> = segments.collect::<Vec<_>>().into();
Ok(Self { head, tail })
}
}
impl fmt::Display for DagId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.head)?;
for seg in self.tail.iter() {
f.write_str(".")?;
f.write_str(seg)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_relative_path_strips_gcl() {
let id = DagId::from_relative_path(std::path::Path::new("helpers/math.gcl")).unwrap();
let segs: Vec<&str> = id.segments().map(|s| &**s).collect();
assert_eq!(segs, ["helpers", "math"]);
assert_eq!(id.to_string(), "helpers.math");
}
#[test]
fn from_relative_path_rejects_empty_path() {
let err = DagId::from_relative_path(std::path::Path::new("")).unwrap_err();
assert_eq!(err, DagIdPathError::Empty);
}
#[test]
fn from_relative_path_rejects_path_without_gcl_extension() {
let err = DagId::from_relative_path(std::path::Path::new("helpers/math")).unwrap_err();
assert_eq!(err, DagIdPathError::MissingGclExtension);
}
#[test]
fn child_appends_segment() {
let parent = DagId::new("helpers", ["math"]);
let child = parent.child("double_speed");
assert_eq!(child.to_string(), "helpers.math.double_speed");
}
#[test]
fn parent_drops_last_segment() {
let id = DagId::new("helpers", ["math", "double_speed"]);
let parent = id.parent().unwrap();
assert_eq!(parent.to_string(), "helpers.math");
}
#[test]
fn parent_of_root_is_none() {
let id = DagId::root("main");
assert!(id.parent().is_none());
}
#[test]
fn is_descendant_of_matches_nested_blocks_only() {
let file = DagId::new("helpers", ["math"]);
let child = file.child("double_speed");
let grandchild = child.child("inner");
assert!(child.is_descendant_of(&file));
assert!(grandchild.is_descendant_of(&file));
assert!(!file.is_descendant_of(&file));
assert!(!file.is_descendant_of(&child));
assert!(!DagId::new("helpers", ["other"]).is_descendant_of(&file));
}
#[test]
fn name_returns_last_segment() {
let id = DagId::new("helpers", ["math", "double_speed"]);
assert_eq!(id.name(), "double_speed");
}
#[test]
fn name_of_root_returns_head() {
let id = DagId::root("main");
assert_eq!(id.name(), "main");
}
#[test]
fn display_joins_with_dot() {
let id = DagId::new("a", ["b", "c"]);
assert_eq!(id.to_string(), "a.b.c");
}
}