cuenv_core/tasks/
index.rs1use super::{Task, TaskDefinition, TaskGroup, Tasks};
2use crate::{Error, Result};
3use std::collections::{BTreeMap, HashMap};
4
5#[derive(Debug, Clone, Eq, PartialEq)]
7pub struct TaskPath {
8 segments: Vec<String>,
9}
10
11impl TaskPath {
12 pub fn parse(raw: &str) -> Result<Self> {
14 if raw.trim().is_empty() {
15 return Err(Error::configuration("Task name cannot be empty"));
16 }
17
18 let normalized = raw.replace(':', ".");
19 let segments: Vec<String> = normalized
20 .split('.')
21 .filter(|s| !s.is_empty())
22 .map(|s| s.trim().to_string())
23 .collect();
24
25 if segments.is_empty() {
26 return Err(Error::configuration("Task name cannot be empty"));
27 }
28
29 for segment in &segments {
30 validate_segment(segment)?;
31 }
32
33 Ok(Self { segments })
34 }
35
36 pub fn join(&self, segment: &str) -> Result<Self> {
38 validate_segment(segment)?;
39 let mut next = self.segments.clone();
40 next.push(segment.to_string());
41 Ok(Self { segments: next })
42 }
43
44 pub fn canonical(&self) -> String {
46 self.segments.join(".")
47 }
48
49 pub fn segments(&self) -> &[String] {
51 &self.segments
52 }
53}
54
55fn validate_segment(segment: &str) -> Result<()> {
56 if segment.is_empty() {
57 return Err(Error::configuration("Task name segment cannot be empty"));
58 }
59
60 if segment.contains('.') || segment.contains(':') {
61 return Err(Error::configuration(format!(
62 "Task name segment '{segment}' may not contain '.' or ':'"
63 )));
64 }
65
66 Ok(())
67}
68
69#[derive(Debug, Clone)]
70pub struct IndexedTask {
71 pub name: String,
72 pub definition: TaskDefinition,
73 pub is_group: bool,
74}
75
76#[derive(Debug, Clone, Default)]
78pub struct TaskIndex {
79 entries: BTreeMap<String, IndexedTask>,
80}
81
82impl TaskIndex {
83 pub fn build(tasks: &HashMap<String, TaskDefinition>) -> Result<Self> {
85 let mut entries = BTreeMap::new();
86
87 for (name, definition) in tasks {
88 let path = TaskPath::parse(name)?;
89 let _ = canonicalize_definition(definition, &path, &mut entries)?;
90 }
91
92 Ok(Self { entries })
93 }
94
95 pub fn resolve(&self, raw: &str) -> Result<&IndexedTask> {
97 let path = TaskPath::parse(raw)?;
98 self.entries.get(&path.canonical()).ok_or_else(|| {
99 let available: Vec<&str> = self.entries.keys().map(String::as_str).collect();
100 Error::configuration(format!(
101 "Task '{}' not found. Available tasks: {:?}",
102 path.canonical(),
103 available
104 ))
105 })
106 }
107
108 pub fn list(&self) -> Vec<&IndexedTask> {
110 self.entries.values().collect()
111 }
112
113 pub fn to_tasks(&self) -> Tasks {
115 let tasks = self
116 .entries
117 .iter()
118 .map(|(name, entry)| (name.clone(), entry.definition.clone()))
119 .collect();
120
121 Tasks { tasks }
122 }
123}
124
125fn canonicalize_definition(
126 definition: &TaskDefinition,
127 path: &TaskPath,
128 entries: &mut BTreeMap<String, IndexedTask>,
129) -> Result<TaskDefinition> {
130 match definition {
131 TaskDefinition::Single(task) => {
132 let canon_task = canonicalize_task(task.as_ref(), path)?;
133 let name = path.canonical();
134 entries.insert(
135 name.clone(),
136 IndexedTask {
137 name,
138 definition: TaskDefinition::Single(Box::new(canon_task.clone())),
139 is_group: false,
140 },
141 );
142 Ok(TaskDefinition::Single(Box::new(canon_task)))
143 }
144 TaskDefinition::Group(group) => match group {
145 TaskGroup::Parallel(children) => {
146 let mut canon_children = HashMap::new();
147 for (child_name, child_def) in children {
148 let child_path = path.join(child_name)?;
149 let canon_child = canonicalize_definition(child_def, &child_path, entries)?;
150 canon_children.insert(child_name.clone(), canon_child);
151 }
152
153 let name = path.canonical();
154 let definition = TaskDefinition::Group(TaskGroup::Parallel(canon_children));
155 entries.insert(
156 name.clone(),
157 IndexedTask {
158 name,
159 definition: definition.clone(),
160 is_group: true,
161 },
162 );
163
164 Ok(definition)
165 }
166 TaskGroup::Sequential(children) => {
167 let mut canon_children = Vec::with_capacity(children.len());
169 for child in children {
170 let canon_child = canonicalize_definition(child, path, entries)?;
173 canon_children.push(canon_child);
174 }
175
176 let name = path.canonical();
177 let definition = TaskDefinition::Group(TaskGroup::Sequential(canon_children));
178 entries.insert(
179 name.clone(),
180 IndexedTask {
181 name,
182 definition: definition.clone(),
183 is_group: true,
184 },
185 );
186
187 Ok(definition)
188 }
189 },
190 }
191}
192
193fn canonicalize_task(task: &Task, path: &TaskPath) -> Result<Task> {
194 let mut clone = task.clone();
195 let mut canonical_deps = Vec::new();
196 for dep in &task.depends_on {
197 canonical_deps.push(canonicalize_dep(dep, path)?);
198 }
199 clone.depends_on = canonical_deps;
200 Ok(clone)
201}
202
203fn canonicalize_dep(dep: &str, current_path: &TaskPath) -> Result<String> {
204 if dep.contains('.') || dep.contains(':') {
205 return Ok(TaskPath::parse(dep)?.canonical());
206 }
207
208 let mut segments: Vec<String> = current_path.segments().to_vec();
209 segments.pop(); segments.push(dep.to_string());
211
212 let rel = TaskPath { segments };
213 Ok(rel.canonical())
214}