cuenv_core/tasks/
mod.rs

1//! Task execution and management module
2//!
3//! This module provides the core types for task execution, matching the CUE schema.
4
5pub mod executor;
6pub mod graph;
7
8// Re-export executor and graph modules
9pub use executor::*;
10pub use graph::*;
11
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Shell configuration for task execution
17#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
18pub struct Shell {
19    /// Shell executable name (e.g., "bash", "fish", "zsh")
20    pub command: Option<String>,
21    /// Flag for command execution (e.g., "-c", "--command")
22    pub flag: Option<String>,
23}
24
25/// A single executable task
26#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
27pub struct Task {
28    /// Shell configuration for command execution (optional)
29    #[serde(default)]
30    pub shell: Option<Shell>,
31
32    /// Command to execute
33    pub command: String,
34
35    /// Arguments for the command
36    #[serde(default)]
37    pub args: Vec<String>,
38
39    /// Environment variables for this task
40    #[serde(default)]
41    pub env: HashMap<String, serde_json::Value>,
42
43    /// Task dependencies (names of tasks that must run first)
44    #[serde(default, rename = "dependsOn")]
45    pub depends_on: Vec<String>,
46
47    /// Input files/resources
48    #[serde(default)]
49    pub inputs: Vec<String>,
50
51    /// Output files/resources
52    #[serde(default)]
53    pub outputs: Vec<String>,
54
55    /// Description of the task
56    #[serde(default)]
57    pub description: Option<String>,
58}
59
60impl Task {
61    /// Returns the description, or a default if not set.
62    pub fn description(&self) -> &str {
63        self.description
64            .as_deref()
65            .unwrap_or("No description provided")
66    }
67}
68
69/// Represents a group of tasks with execution mode
70#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
71#[serde(untagged)]
72pub enum TaskGroup {
73    /// Sequential execution: array of tasks executed in order
74    Sequential(Vec<TaskDefinition>),
75
76    /// Parallel execution: named tasks that can run concurrently
77    Parallel(HashMap<String, TaskDefinition>),
78}
79
80/// A task definition can be either a single task or a group of tasks
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
82#[serde(untagged)]
83pub enum TaskDefinition {
84    /// A single task
85    Single(Task),
86
87    /// A group of tasks
88    Group(TaskGroup),
89}
90
91/// Root tasks structure from CUE
92#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
93pub struct Tasks {
94    /// Map of task names to their definitions
95    #[serde(flatten)]
96    pub tasks: HashMap<String, TaskDefinition>,
97}
98
99impl Tasks {
100    /// Create a new empty tasks collection
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// Get a task definition by name
106    pub fn get(&self, name: &str) -> Option<&TaskDefinition> {
107        self.tasks.get(name)
108    }
109
110    /// List all task names
111    pub fn list_tasks(&self) -> Vec<&str> {
112        self.tasks.keys().map(|s| s.as_str()).collect()
113    }
114
115    /// Check if a task exists
116    pub fn contains(&self, name: &str) -> bool {
117        self.tasks.contains_key(name)
118    }
119}
120
121impl TaskDefinition {
122    /// Check if this is a single task
123    pub fn is_single(&self) -> bool {
124        matches!(self, TaskDefinition::Single(_))
125    }
126
127    /// Check if this is a task group
128    pub fn is_group(&self) -> bool {
129        matches!(self, TaskDefinition::Group(_))
130    }
131
132    /// Get as single task if it is one
133    pub fn as_single(&self) -> Option<&Task> {
134        match self {
135            TaskDefinition::Single(task) => Some(task),
136            _ => None,
137        }
138    }
139
140    /// Get as task group if it is one
141    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    /// Check if this group is sequential
151    pub fn is_sequential(&self) -> bool {
152        matches!(self, TaskGroup::Sequential(_))
153    }
154
155    /// Check if this group is parallel
156    pub fn is_parallel(&self) -> bool {
157        matches!(self, TaskGroup::Parallel(_))
158    }
159
160    /// Get the number of tasks in this group
161    pub fn len(&self) -> usize {
162        match self {
163            TaskGroup::Sequential(tasks) => tasks.len(),
164            TaskGroup::Parallel(tasks) => tasks.len(),
165        }
166    }
167
168    /// Check if the group is empty
169    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()); // default value
208    }
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}