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;
7pub mod index;
8pub mod io;
9
10// Re-export executor and graph modules
11pub 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/// Shell configuration for task execution
20#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
21pub struct Shell {
22    /// Shell executable name (e.g., "bash", "fish", "zsh")
23    pub command: Option<String>,
24    /// Flag for command execution (e.g., "-c", "--command")
25    pub flag: Option<String>,
26}
27
28/// Mapping of external output to local workspace path
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
30pub struct Mapping {
31    /// Path relative to external project root of a declared output from the external task
32    pub from: String,
33    /// Path inside the dependent task’s hermetic workspace where this file/dir will be materialized
34    pub to: String,
35}
36
37/// Cross-project external input declaration
38#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
39pub struct ExternalInput {
40    /// Path to project root relative to env.cue or absolute-from-repo-root
41    pub project: String,
42    /// Name of the external task in that project
43    pub task: String,
44    /// Required explicit mappings
45    pub map: Vec<Mapping>,
46}
47
48/// A single executable task
49#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
50pub struct Task {
51    /// Shell configuration for command execution (optional)
52    #[serde(default)]
53    pub shell: Option<Shell>,
54
55    /// Command to execute
56    pub command: String,
57
58    /// Arguments for the command
59    #[serde(default)]
60    pub args: Vec<String>,
61
62    /// Environment variables for this task
63    #[serde(default)]
64    pub env: HashMap<String, serde_json::Value>,
65
66    /// Task dependencies (names of tasks that must run first)
67    #[serde(default, rename = "dependsOn")]
68    pub depends_on: Vec<String>,
69
70    /// Input files/resources
71    #[serde(default)]
72    pub inputs: Vec<String>,
73
74    /// Output files/resources
75    #[serde(default)]
76    pub outputs: Vec<String>,
77
78    /// Cross-project inputs
79    #[serde(default, rename = "externalInputs")]
80    pub external_inputs: Option<Vec<ExternalInput>>,
81
82    /// Workspaces to mount/enable for this task
83    #[serde(default)]
84    pub workspaces: Vec<String>,
85
86    /// Description of the task
87    #[serde(default)]
88    pub description: Option<String>,
89}
90
91impl Task {
92    /// Returns the description, or a default if not set.
93    pub fn description(&self) -> &str {
94        self.description
95            .as_deref()
96            .unwrap_or("No description provided")
97    }
98}
99
100/// Represents a group of tasks with execution mode
101#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
102#[serde(untagged)]
103pub enum TaskGroup {
104    /// Sequential execution: array of tasks executed in order
105    Sequential(Vec<TaskDefinition>),
106
107    /// Parallel execution: named tasks that can run concurrently
108    Parallel(HashMap<String, TaskDefinition>),
109}
110
111/// A task definition can be either a single task or a group of tasks
112#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
113#[serde(untagged)]
114pub enum TaskDefinition {
115    /// A single task
116    Single(Box<Task>),
117
118    /// A group of tasks
119    Group(TaskGroup),
120}
121
122/// Root tasks structure from CUE
123#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
124pub struct Tasks {
125    /// Map of task names to their definitions
126    #[serde(flatten)]
127    pub tasks: HashMap<String, TaskDefinition>,
128}
129
130impl Tasks {
131    /// Create a new empty tasks collection
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Get a task definition by name
137    pub fn get(&self, name: &str) -> Option<&TaskDefinition> {
138        self.tasks.get(name)
139    }
140
141    /// List all task names
142    pub fn list_tasks(&self) -> Vec<&str> {
143        self.tasks.keys().map(|s| s.as_str()).collect()
144    }
145
146    /// Check if a task exists
147    pub fn contains(&self, name: &str) -> bool {
148        self.tasks.contains_key(name)
149    }
150}
151
152impl TaskDefinition {
153    /// Check if this is a single task
154    pub fn is_single(&self) -> bool {
155        matches!(self, TaskDefinition::Single(_))
156    }
157
158    /// Check if this is a task group
159    pub fn is_group(&self) -> bool {
160        matches!(self, TaskDefinition::Group(_))
161    }
162
163    /// Get as single task if it is one
164    pub fn as_single(&self) -> Option<&Task> {
165        match self {
166            TaskDefinition::Single(task) => Some(task.as_ref()),
167            _ => None,
168        }
169    }
170
171    /// Get as task group if it is one
172    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    /// Check if this group is sequential
182    pub fn is_sequential(&self) -> bool {
183        matches!(self, TaskGroup::Sequential(_))
184    }
185
186    /// Check if this group is parallel
187    pub fn is_parallel(&self) -> bool {
188        matches!(self, TaskGroup::Parallel(_))
189    }
190
191    /// Get the number of tasks in this group
192    pub fn len(&self) -> usize {
193        match self {
194            TaskGroup::Sequential(tasks) => tasks.len(),
195            TaskGroup::Parallel(tasks) => tasks.len(),
196        }
197    }
198
199    /// Check if the group is empty
200    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()); // default value
241    }
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}