graphcal_compiler/
dag_id.rs1use std::fmt;
12use std::sync::Arc;
13
14use thiserror::Error;
15
16#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct DagId {
29 head: Arc<str>,
31 tail: Arc<[Arc<str>]>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Error)]
38pub enum DagIdPathError {
39 #[error("path has no components")]
41 Empty,
42 #[error("path contains a non-UTF-8 component")]
44 NonUtf8Component,
45 #[error("path must end with `.gcl`")]
47 MissingGclExtension,
48}
49
50impl DagId {
51 pub fn new(
56 head: impl Into<Arc<str>>,
57 tail: impl IntoIterator<Item = impl Into<Arc<str>>>,
58 ) -> Self {
59 Self {
60 head: head.into(),
61 tail: tail.into_iter().map(Into::into).collect(),
62 }
63 }
64
65 pub fn root(name: impl Into<Arc<str>>) -> Self {
67 Self {
68 head: name.into(),
69 tail: Arc::from([] as [Arc<str>; 0]),
70 }
71 }
72
73 #[must_use]
75 pub fn child(&self, name: impl Into<Arc<str>>) -> Self {
76 let mut tail: Vec<Arc<str>> = self.tail.to_vec();
77 tail.push(name.into());
78 Self {
79 head: Arc::clone(&self.head),
80 tail: tail.into(),
81 }
82 }
83
84 #[must_use]
87 pub fn parent(&self) -> Option<Self> {
88 if self.tail.is_empty() {
89 return None;
90 }
91 Some(Self {
92 head: Arc::clone(&self.head),
93 tail: self.tail[..self.tail.len() - 1].into(),
94 })
95 }
96
97 pub fn segments(&self) -> impl Iterator<Item = &Arc<str>> {
99 std::iter::once(&self.head).chain(self.tail.iter())
100 }
101
102 #[must_use]
104 pub fn segment_count(&self) -> usize {
105 1 + self.tail.len()
106 }
107
108 #[must_use]
110 pub fn name(&self) -> &str {
111 self.tail.last().map_or(&self.head, |s| s)
112 }
113
114 #[must_use]
117 pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
118 if self.segment_count() <= ancestor.segment_count() {
119 return false;
120 }
121 self.segments()
122 .zip(ancestor.segments())
123 .all(|(a, b)| a == b)
124 }
125
126 pub fn from_relative_path(path: &std::path::Path) -> Result<Self, DagIdPathError> {
136 let mut segments: Vec<Arc<str>> = path
137 .components()
138 .map(|c| {
139 c.as_os_str()
140 .to_str()
141 .map(Arc::<str>::from)
142 .ok_or(DagIdPathError::NonUtf8Component)
143 })
144 .collect::<Result<_, _>>()?;
145
146 let last = segments.last_mut().ok_or(DagIdPathError::Empty)?;
147 *last = last
148 .strip_suffix(".gcl")
149 .map(Arc::<str>::from)
150 .ok_or(DagIdPathError::MissingGclExtension)?;
151
152 let mut segments = segments.into_iter();
153 let head = segments.next().ok_or(DagIdPathError::Empty)?;
154 let tail: Arc<[Arc<str>]> = segments.collect::<Vec<_>>().into();
155 Ok(Self { head, tail })
156 }
157}
158
159impl fmt::Display for DagId {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.write_str(&self.head)?;
162 for seg in self.tail.iter() {
163 f.write_str(".")?;
164 f.write_str(seg)?;
165 }
166 Ok(())
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn from_relative_path_strips_gcl() {
176 let id = DagId::from_relative_path(std::path::Path::new("helpers/math.gcl")).unwrap();
177 let segs: Vec<&str> = id.segments().map(|s| &**s).collect();
178 assert_eq!(segs, ["helpers", "math"]);
179 assert_eq!(id.to_string(), "helpers.math");
180 }
181
182 #[test]
183 fn from_relative_path_rejects_empty_path() {
184 let err = DagId::from_relative_path(std::path::Path::new("")).unwrap_err();
185 assert_eq!(err, DagIdPathError::Empty);
186 }
187
188 #[test]
189 fn from_relative_path_rejects_path_without_gcl_extension() {
190 let err = DagId::from_relative_path(std::path::Path::new("helpers/math")).unwrap_err();
191 assert_eq!(err, DagIdPathError::MissingGclExtension);
192 }
193
194 #[test]
195 fn child_appends_segment() {
196 let parent = DagId::new("helpers", ["math"]);
197 let child = parent.child("double_speed");
198 assert_eq!(child.to_string(), "helpers.math.double_speed");
199 }
200
201 #[test]
202 fn parent_drops_last_segment() {
203 let id = DagId::new("helpers", ["math", "double_speed"]);
204 let parent = id.parent().unwrap();
205 assert_eq!(parent.to_string(), "helpers.math");
206 }
207
208 #[test]
209 fn parent_of_root_is_none() {
210 let id = DagId::root("main");
211 assert!(id.parent().is_none());
212 }
213
214 #[test]
215 fn is_descendant_of_matches_nested_blocks_only() {
216 let file = DagId::new("helpers", ["math"]);
217 let child = file.child("double_speed");
218 let grandchild = child.child("inner");
219 assert!(child.is_descendant_of(&file));
220 assert!(grandchild.is_descendant_of(&file));
221 assert!(!file.is_descendant_of(&file));
222 assert!(!file.is_descendant_of(&child));
223 assert!(!DagId::new("helpers", ["other"]).is_descendant_of(&file));
224 }
225
226 #[test]
227 fn name_returns_last_segment() {
228 let id = DagId::new("helpers", ["math", "double_speed"]);
229 assert_eq!(id.name(), "double_speed");
230 }
231
232 #[test]
233 fn name_of_root_returns_head() {
234 let id = DagId::root("main");
235 assert_eq!(id.name(), "main");
236 }
237
238 #[test]
239 fn display_joins_with_dot() {
240 let id = DagId::new("a", ["b", "c"]);
241 assert_eq!(id.to_string(), "a.b.c");
242 }
243}