1pub mod executor;
6pub mod graph;
7pub mod index;
8pub mod io;
9
10pub use executor::*;
12pub use graph::*;
13pub use index::{IndexedTask, TaskIndex, TaskPath};
14
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
21pub struct Shell {
22 pub command: Option<String>,
24 pub flag: Option<String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
30pub struct Mapping {
31 pub from: String,
33 pub to: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
39pub struct ExternalInput {
40 pub project: String,
42 pub task: String,
44 pub map: Vec<Mapping>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
50pub struct Task {
51 #[serde(default)]
53 pub shell: Option<Shell>,
54
55 pub command: String,
57
58 #[serde(default)]
60 pub args: Vec<String>,
61
62 #[serde(default)]
64 pub env: HashMap<String, serde_json::Value>,
65
66 #[serde(default, rename = "dependsOn")]
68 pub depends_on: Vec<String>,
69
70 #[serde(default)]
72 pub inputs: Vec<String>,
73
74 #[serde(default)]
76 pub outputs: Vec<String>,
77
78 #[serde(default, rename = "externalInputs")]
80 pub external_inputs: Option<Vec<ExternalInput>>,
81
82 #[serde(default)]
84 pub workspaces: Vec<String>,
85
86 #[serde(default)]
88 pub description: Option<String>,
89}
90
91impl Task {
92 pub fn description(&self) -> &str {
94 self.description
95 .as_deref()
96 .unwrap_or("No description provided")
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
102#[serde(untagged)]
103pub enum TaskGroup {
104 Sequential(Vec<TaskDefinition>),
106
107 Parallel(HashMap<String, TaskDefinition>),
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
113#[serde(untagged)]
114pub enum TaskDefinition {
115 Single(Box<Task>),
117
118 Group(TaskGroup),
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
124pub struct Tasks {
125 #[serde(flatten)]
127 pub tasks: HashMap<String, TaskDefinition>,
128}
129
130impl Tasks {
131 pub fn new() -> Self {
133 Self::default()
134 }
135
136 pub fn get(&self, name: &str) -> Option<&TaskDefinition> {
138 self.tasks.get(name)
139 }
140
141 pub fn list_tasks(&self) -> Vec<&str> {
143 self.tasks.keys().map(|s| s.as_str()).collect()
144 }
145
146 pub fn contains(&self, name: &str) -> bool {
148 self.tasks.contains_key(name)
149 }
150}
151
152impl TaskDefinition {
153 pub fn is_single(&self) -> bool {
155 matches!(self, TaskDefinition::Single(_))
156 }
157
158 pub fn is_group(&self) -> bool {
160 matches!(self, TaskDefinition::Group(_))
161 }
162
163 pub fn as_single(&self) -> Option<&Task> {
165 match self {
166 TaskDefinition::Single(task) => Some(task.as_ref()),
167 _ => None,
168 }
169 }
170
171 pub fn as_group(&self) -> Option<&TaskGroup> {
173 match self {
174 TaskDefinition::Group(group) => Some(group),
175 _ => None,
176 }
177 }
178}
179
180impl TaskGroup {
181 pub fn is_sequential(&self) -> bool {
183 matches!(self, TaskGroup::Sequential(_))
184 }
185
186 pub fn is_parallel(&self) -> bool {
188 matches!(self, TaskGroup::Parallel(_))
189 }
190
191 pub fn len(&self) -> usize {
193 match self {
194 TaskGroup::Sequential(tasks) => tasks.len(),
195 TaskGroup::Parallel(tasks) => tasks.len(),
196 }
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.len() == 0
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_task_default_values() {
211 let task = Task {
212 command: "echo".to_string(),
213 shell: None,
214 args: vec![],
215 env: HashMap::new(),
216 depends_on: vec![],
217 inputs: vec![],
218 outputs: vec![],
219 external_inputs: None,
220 workspaces: vec![],
221 description: None,
222 };
223
224 assert!(task.shell.is_none());
225 assert_eq!(task.command, "echo");
226 assert_eq!(task.description(), "No description provided");
227 assert!(task.args.is_empty());
228 }
229
230 #[test]
231 fn test_task_deserialization() {
232 let json = r#"{
233 "command": "echo",
234 "args": ["Hello", "World"]
235 }"#;
236
237 let task: Task = serde_json::from_str(json).unwrap();
238 assert_eq!(task.command, "echo");
239 assert_eq!(task.args, vec!["Hello", "World"]);
240 assert!(task.shell.is_none()); }
242
243 #[test]
244 fn test_task_group_sequential() {
245 let task1 = Task {
246 command: "echo".to_string(),
247 args: vec!["first".to_string()],
248 shell: None,
249 env: HashMap::new(),
250 depends_on: vec![],
251 inputs: vec![],
252 outputs: vec![],
253 external_inputs: None,
254 workspaces: vec![],
255 description: Some("First task".to_string()),
256 };
257
258 let task2 = Task {
259 command: "echo".to_string(),
260 args: vec!["second".to_string()],
261 shell: None,
262 env: HashMap::new(),
263 depends_on: vec![],
264 inputs: vec![],
265 outputs: vec![],
266 external_inputs: None,
267 workspaces: vec![],
268 description: Some("Second task".to_string()),
269 };
270
271 let group = TaskGroup::Sequential(vec![
272 TaskDefinition::Single(Box::new(task1)),
273 TaskDefinition::Single(Box::new(task2)),
274 ]);
275
276 assert!(group.is_sequential());
277 assert!(!group.is_parallel());
278 assert_eq!(group.len(), 2);
279 }
280
281 #[test]
282 fn test_task_group_parallel() {
283 let task1 = Task {
284 command: "echo".to_string(),
285 args: vec!["task1".to_string()],
286 shell: None,
287 env: HashMap::new(),
288 depends_on: vec![],
289 inputs: vec![],
290 outputs: vec![],
291 external_inputs: None,
292 workspaces: vec![],
293 description: Some("Task 1".to_string()),
294 };
295
296 let task2 = Task {
297 command: "echo".to_string(),
298 args: vec!["task2".to_string()],
299 shell: None,
300 env: HashMap::new(),
301 depends_on: vec![],
302 inputs: vec![],
303 outputs: vec![],
304 external_inputs: None,
305 workspaces: vec![],
306 description: Some("Task 2".to_string()),
307 };
308
309 let mut parallel_tasks = HashMap::new();
310 parallel_tasks.insert("task1".to_string(), TaskDefinition::Single(Box::new(task1)));
311 parallel_tasks.insert("task2".to_string(), TaskDefinition::Single(Box::new(task2)));
312
313 let group = TaskGroup::Parallel(parallel_tasks);
314
315 assert!(!group.is_sequential());
316 assert!(group.is_parallel());
317 assert_eq!(group.len(), 2);
318 }
319
320 #[test]
321 fn test_tasks_collection() {
322 let mut tasks = Tasks::new();
323 assert!(tasks.list_tasks().is_empty());
324
325 let task = Task {
326 command: "echo".to_string(),
327 args: vec!["hello".to_string()],
328 shell: None,
329 env: HashMap::new(),
330 depends_on: vec![],
331 inputs: vec![],
332 outputs: vec![],
333 external_inputs: None,
334 workspaces: vec![],
335 description: Some("Hello task".to_string()),
336 };
337
338 tasks
339 .tasks
340 .insert("greet".to_string(), TaskDefinition::Single(Box::new(task)));
341
342 assert!(tasks.contains("greet"));
343 assert!(!tasks.contains("nonexistent"));
344 assert_eq!(tasks.list_tasks(), vec!["greet"]);
345
346 let retrieved = tasks.get("greet").unwrap();
347 assert!(retrieved.is_single());
348 }
349
350 #[test]
351 fn test_task_definition_helpers() {
352 let task = Task {
353 command: "test".to_string(),
354 shell: None,
355 args: vec![],
356 env: HashMap::new(),
357 depends_on: vec![],
358 inputs: vec![],
359 outputs: vec![],
360 external_inputs: None,
361 workspaces: vec![],
362 description: Some("Test task".to_string()),
363 };
364
365 let single = TaskDefinition::Single(Box::new(task.clone()));
366 assert!(single.is_single());
367 assert!(!single.is_group());
368 assert_eq!(single.as_single().unwrap().command, "test");
369 assert!(single.as_group().is_none());
370
371 let group = TaskDefinition::Group(TaskGroup::Sequential(vec![]));
372 assert!(!group.is_single());
373 assert!(group.is_group());
374 assert!(group.as_single().is_none());
375 assert!(group.as_group().is_some());
376 }
377}