1pub mod executor;
6pub mod graph;
7
8pub use executor::*;
10pub use graph::*;
11
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
18pub struct Shell {
19 pub command: Option<String>,
21 pub flag: Option<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
27pub struct Task {
28 #[serde(default)]
30 pub shell: Option<Shell>,
31
32 pub command: String,
34
35 #[serde(default)]
37 pub args: Vec<String>,
38
39 #[serde(default)]
41 pub env: HashMap<String, serde_json::Value>,
42
43 #[serde(default, rename = "dependsOn")]
45 pub depends_on: Vec<String>,
46
47 #[serde(default)]
49 pub inputs: Vec<String>,
50
51 #[serde(default)]
53 pub outputs: Vec<String>,
54
55 #[serde(default)]
57 pub description: Option<String>,
58}
59
60impl Task {
61 pub fn description(&self) -> &str {
63 self.description
64 .as_deref()
65 .unwrap_or("No description provided")
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
71#[serde(untagged)]
72pub enum TaskGroup {
73 Sequential(Vec<TaskDefinition>),
75
76 Parallel(HashMap<String, TaskDefinition>),
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
82#[serde(untagged)]
83pub enum TaskDefinition {
84 Single(Task),
86
87 Group(TaskGroup),
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
93pub struct Tasks {
94 #[serde(flatten)]
96 pub tasks: HashMap<String, TaskDefinition>,
97}
98
99impl Tasks {
100 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn get(&self, name: &str) -> Option<&TaskDefinition> {
107 self.tasks.get(name)
108 }
109
110 pub fn list_tasks(&self) -> Vec<&str> {
112 self.tasks.keys().map(|s| s.as_str()).collect()
113 }
114
115 pub fn contains(&self, name: &str) -> bool {
117 self.tasks.contains_key(name)
118 }
119}
120
121impl TaskDefinition {
122 pub fn is_single(&self) -> bool {
124 matches!(self, TaskDefinition::Single(_))
125 }
126
127 pub fn is_group(&self) -> bool {
129 matches!(self, TaskDefinition::Group(_))
130 }
131
132 pub fn as_single(&self) -> Option<&Task> {
134 match self {
135 TaskDefinition::Single(task) => Some(task),
136 _ => None,
137 }
138 }
139
140 pub fn as_group(&self) -> Option<&TaskGroup> {
142 match self {
143 TaskDefinition::Group(group) => Some(group),
144 _ => None,
145 }
146 }
147}
148
149impl TaskGroup {
150 pub fn is_sequential(&self) -> bool {
152 matches!(self, TaskGroup::Sequential(_))
153 }
154
155 pub fn is_parallel(&self) -> bool {
157 matches!(self, TaskGroup::Parallel(_))
158 }
159
160 pub fn len(&self) -> usize {
162 match self {
163 TaskGroup::Sequential(tasks) => tasks.len(),
164 TaskGroup::Parallel(tasks) => tasks.len(),
165 }
166 }
167
168 pub fn is_empty(&self) -> bool {
170 self.len() == 0
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_task_default_values() {
180 let task = Task {
181 command: "echo".to_string(),
182 shell: None,
183 args: vec![],
184 env: HashMap::new(),
185 depends_on: vec![],
186 inputs: vec![],
187 outputs: vec![],
188 description: None,
189 };
190
191 assert!(task.shell.is_none());
192 assert_eq!(task.command, "echo");
193 assert_eq!(task.description(), "No description provided");
194 assert!(task.args.is_empty());
195 }
196
197 #[test]
198 fn test_task_deserialization() {
199 let json = r#"{
200 "command": "echo",
201 "args": ["Hello", "World"]
202 }"#;
203
204 let task: Task = serde_json::from_str(json).unwrap();
205 assert_eq!(task.command, "echo");
206 assert_eq!(task.args, vec!["Hello", "World"]);
207 assert!(task.shell.is_none()); }
209
210 #[test]
211 fn test_task_group_sequential() {
212 let task1 = Task {
213 command: "echo".to_string(),
214 args: vec!["first".to_string()],
215 shell: None,
216 env: HashMap::new(),
217 depends_on: vec![],
218 inputs: vec![],
219 outputs: vec![],
220 description: Some("First task".to_string()),
221 };
222
223 let task2 = Task {
224 command: "echo".to_string(),
225 args: vec!["second".to_string()],
226 shell: None,
227 env: HashMap::new(),
228 depends_on: vec![],
229 inputs: vec![],
230 outputs: vec![],
231 description: Some("Second task".to_string()),
232 };
233
234 let group = TaskGroup::Sequential(vec![
235 TaskDefinition::Single(task1),
236 TaskDefinition::Single(task2),
237 ]);
238
239 assert!(group.is_sequential());
240 assert!(!group.is_parallel());
241 assert_eq!(group.len(), 2);
242 }
243
244 #[test]
245 fn test_task_group_parallel() {
246 let task1 = Task {
247 command: "echo".to_string(),
248 args: vec!["task1".to_string()],
249 shell: None,
250 env: HashMap::new(),
251 depends_on: vec![],
252 inputs: vec![],
253 outputs: vec![],
254 description: Some("Task 1".to_string()),
255 };
256
257 let task2 = Task {
258 command: "echo".to_string(),
259 args: vec!["task2".to_string()],
260 shell: None,
261 env: HashMap::new(),
262 depends_on: vec![],
263 inputs: vec![],
264 outputs: vec![],
265 description: Some("Task 2".to_string()),
266 };
267
268 let mut parallel_tasks = HashMap::new();
269 parallel_tasks.insert("task1".to_string(), TaskDefinition::Single(task1));
270 parallel_tasks.insert("task2".to_string(), TaskDefinition::Single(task2));
271
272 let group = TaskGroup::Parallel(parallel_tasks);
273
274 assert!(!group.is_sequential());
275 assert!(group.is_parallel());
276 assert_eq!(group.len(), 2);
277 }
278
279 #[test]
280 fn test_tasks_collection() {
281 let mut tasks = Tasks::new();
282 assert!(tasks.list_tasks().is_empty());
283
284 let task = Task {
285 command: "echo".to_string(),
286 args: vec!["hello".to_string()],
287 shell: None,
288 env: HashMap::new(),
289 depends_on: vec![],
290 inputs: vec![],
291 outputs: vec![],
292 description: Some("Hello task".to_string()),
293 };
294
295 tasks
296 .tasks
297 .insert("greet".to_string(), TaskDefinition::Single(task));
298
299 assert!(tasks.contains("greet"));
300 assert!(!tasks.contains("nonexistent"));
301 assert_eq!(tasks.list_tasks(), vec!["greet"]);
302
303 let retrieved = tasks.get("greet").unwrap();
304 assert!(retrieved.is_single());
305 }
306
307 #[test]
308 fn test_task_definition_helpers() {
309 let task = Task {
310 command: "test".to_string(),
311 shell: None,
312 args: vec![],
313 env: HashMap::new(),
314 depends_on: vec![],
315 inputs: vec![],
316 outputs: vec![],
317 description: Some("Test task".to_string()),
318 };
319
320 let single = TaskDefinition::Single(task.clone());
321 assert!(single.is_single());
322 assert!(!single.is_group());
323 assert_eq!(single.as_single().unwrap().command, "test");
324 assert!(single.as_group().is_none());
325
326 let group = TaskDefinition::Group(TaskGroup::Sequential(vec![]));
327 assert!(!group.is_single());
328 assert!(group.is_group());
329 assert!(group.as_single().is_none());
330 assert!(group.as_group().is_some());
331 }
332}