rtask/runner/
task.rs

1//! Task execution types and logic
2//!
3//! This module contains the runtime representation of tasks and execution logic.
4
5use crate::config;
6use crate::error::{ConfigError, ConfigResult, ExecutionError, ExecutionResult};
7use crate::runner::{evaluate_when_list, execute_command, interpolate, Context};
8use std::collections::HashMap;
9
10/// Runtime task representation
11///
12/// This differs from config::Task by including computed fields needed during execution
13#[derive(Debug, Clone)]
14pub struct Task {
15    /// Task name
16    pub name: String,
17
18    /// Usage description
19    pub usage: Option<String>,
20
21    /// Longer description
22    pub description: Option<String>,
23
24    /// Whether this task is private
25    pub private: bool,
26
27    /// Whether this task should run quietly
28    pub quiet: bool,
29
30    /// Positional arguments
31    pub args: HashMap<String, Arg>,
32
33    /// Named options
34    pub options: HashMap<String, TaskOption>,
35
36    /// Run items to execute
37    pub run: Vec<Run>,
38
39    /// Finally block
40    pub finally: Vec<Run>,
41
42    /// Source files for caching
43    pub source: Vec<String>,
44
45    /// Target files for caching
46    pub target: Vec<String>,
47
48    /// Resolved variable values for this task execution
49    pub vars: HashMap<String, String>,
50}
51
52impl Task {
53    /// Create a new task from configuration
54    pub fn from_config(name: String, config: config::Task) -> ConfigResult<Self> {
55        // Validate task configuration
56        Self::validate_config(&config)?;
57
58        Ok(Task {
59            name,
60            usage: config.usage,
61            description: config.description,
62            private: config.private,
63            quiet: config.quiet,
64            args: config
65                .args
66                .into_iter()
67                .map(|(k, v)| (k.clone(), Arg::from_config(k, v)))
68                .collect(),
69            options: config
70                .options
71                .into_iter()
72                .map(|(k, v)| (k.clone(), TaskOption::from_config(k, v)))
73                .collect(),
74            run: config.run.into_iter().map(Run::from_config).collect(),
75            finally: config.finally.into_iter().map(Run::from_config).collect(),
76            source: config.source,
77            target: config.target,
78            vars: HashMap::new(),
79        })
80    }
81
82    /// Validate task configuration
83    fn validate_config(config: &config::Task) -> ConfigResult<()> {
84        // Check source/target consistency
85        if !config.source.is_empty() && config.target.is_empty() {
86            return Err(ConfigError::SourceWithoutTarget);
87        }
88        if !config.target.is_empty() && config.source.is_empty() {
89            return Err(ConfigError::TargetWithoutSource);
90        }
91
92        // Check for duplicate names between args and options
93        for (arg_name, _) in &config.args {
94            if config.options.contains_key(arg_name) {
95                return Err(ConfigError::DuplicateNames(arg_name.clone()));
96            }
97        }
98
99        Ok(())
100    }
101
102    /// Get all dependencies (options required by when conditions, etc.)
103    pub fn dependencies(&self) -> Vec<String> {
104        let mut deps = Vec::new();
105
106        // Add option dependencies
107        for option in self.options.values() {
108            deps.extend(option.dependencies());
109        }
110
111        // Add dependencies from when conditions
112        for run in self.run.iter().chain(self.finally.iter()) {
113            deps.extend(run.dependencies());
114        }
115
116        deps
117    }
118
119    /// Execute the task in the given context
120    pub fn execute(&self, ctx: &mut Context) -> ExecutionResult<()> {
121        // Check for recursion
122        if ctx.is_task_in_stack(&self.name) {
123            return Err(ExecutionError::CommandFailed(Some(1)));
124        }
125
126        // Push task onto stack
127        ctx.push_task(self.name.clone());
128
129        // Print task start
130        ctx.print_task_start(&self.name);
131
132        // Merge task vars into context
133        for (key, value) in &self.vars {
134            ctx.set_var(key.clone(), value.clone());
135        }
136
137        // Execute with finally block handling
138        let result = self.execute_run_items(ctx);
139
140        // Always run finally blocks
141        if !self.finally.is_empty() {
142            ctx.print_debug("Running finally block...");
143            if let Err(e) = self.execute_finally_items(ctx) {
144                // If run succeeded but finally failed, return finally error
145                // If run failed, keep the run error
146                if result.is_ok() {
147                    ctx.pop_task();
148                    return Err(e);
149                }
150            }
151        }
152
153        // Pop task from stack
154        ctx.pop_task();
155
156        if result.is_ok() {
157            ctx.print_task_complete(&self.name);
158        }
159
160        result
161    }
162
163    /// Execute the main run items
164    fn execute_run_items(&self, ctx: &mut Context) -> ExecutionResult<()> {
165        for run in &self.run {
166            self.execute_run_item(run, ctx)?;
167        }
168        Ok(())
169    }
170
171    /// Execute finally items
172    fn execute_finally_items(&self, ctx: &mut Context) -> ExecutionResult<()> {
173        for run in &self.finally {
174            self.execute_run_item(run, ctx)?;
175        }
176        Ok(())
177    }
178
179    /// Execute a single run item
180    fn execute_run_item(&self, run: &Run, ctx: &mut Context) -> ExecutionResult<()> {
181        // Check when conditions
182        if !run.when.is_empty() {
183            let should_run = evaluate_when_list(&run.when, ctx)?;
184            if !should_run {
185                // Skip this run item
186                return Ok(());
187            }
188        }
189
190        // Execute commands
191        for cmd in &run.commands {
192            execute_command(cmd, ctx)?;
193        }
194
195        // Execute subtasks
196        for subtask in &run.subtasks {
197            self.execute_subtask(subtask, ctx)?;
198        }
199
200        // Set environment variables
201        if !run.set_environment.is_empty() {
202            for (key, value) in &run.set_environment {
203                match value {
204                    Some(val) => {
205                        let interpolated = interpolate(val, &ctx.vars)
206                            .unwrap_or_else(|_| val.clone());
207                        std::env::set_var(key, &interpolated);
208                        ctx.set_var(key.clone(), interpolated);
209                    }
210                    None => {
211                        std::env::remove_var(key);
212                        ctx.vars.remove(key);
213                    }
214                }
215            }
216        }
217
218        Ok(())
219    }
220
221    /// Execute a subtask (placeholder - will be implemented with full task registry)
222    fn execute_subtask(&self, _subtask: &SubTask, _ctx: &mut Context) -> ExecutionResult<()> {
223        // This will be implemented when we have a task registry in the CLI
224        // For now, just skip subtasks
225        Ok(())
226    }
227}
228
229/// Runtime representation of a run item
230#[derive(Debug, Clone)]
231pub struct Run {
232    /// Conditions that must be met
233    pub when: Vec<When>,
234
235    /// Commands to execute
236    pub commands: Vec<Command>,
237
238    /// Subtasks to execute
239    pub subtasks: Vec<SubTask>,
240
241    /// Environment variables to set
242    pub set_environment: HashMap<String, Option<String>>,
243}
244
245impl Run {
246    /// Create from config
247    pub fn from_config(config: config::Run) -> Self {
248        match config {
249            config::Run::SimpleCommand(cmd) => Run {
250                when: Vec::new(),
251                commands: vec![Command::Simple(cmd)],
252                subtasks: Vec::new(),
253                set_environment: HashMap::new(),
254            },
255            config::Run::Complex(item) => Run {
256                when: item.when.into_iter().map(When::from_config).collect(),
257                commands: item
258                    .command
259                    .into_iter()
260                    .map(Command::from_config)
261                    .collect(),
262                subtasks: item
263                    .task
264                    .into_iter()
265                    .map(SubTask::from_config)
266                    .collect(),
267                set_environment: item.set_environment,
268            },
269        }
270    }
271
272    /// Get dependencies from this run item
273    pub fn dependencies(&self) -> Vec<String> {
274        let mut deps = Vec::new();
275        for when in &self.when {
276            deps.extend(when.dependencies());
277        }
278        deps
279    }
280}
281
282/// Runtime representation of a command
283#[derive(Debug, Clone)]
284pub enum Command {
285    /// Simple command string
286    Simple(String),
287
288    /// Complex command with options
289    Complex {
290        exec: String,
291        print: String,
292        quiet: bool,
293        dir: Option<String>,
294    },
295}
296
297impl Command {
298    /// Create from config
299    pub fn from_config(config: config::Command) -> Self {
300        match config {
301            config::Command::Simple(cmd) => Command::Simple(cmd),
302            config::Command::Complex(detail) => Command::Complex {
303                print: detail.print.clone().unwrap_or_else(|| detail.exec.clone()),
304                exec: detail.exec,
305                quiet: detail.quiet,
306                dir: detail.dir,
307            },
308        }
309    }
310
311    /// Get the command to execute
312    pub fn exec(&self) -> &str {
313        match self {
314            Command::Simple(cmd) => cmd,
315            Command::Complex { exec, .. } => exec,
316        }
317    }
318
319    /// Get what to print
320    pub fn print(&self) -> &str {
321        match self {
322            Command::Simple(cmd) => cmd,
323            Command::Complex { print, .. } => print,
324        }
325    }
326
327    /// Check if this command is quiet
328    pub fn is_quiet(&self) -> bool {
329        match self {
330            Command::Simple(_) => false,
331            Command::Complex { quiet, .. } => *quiet,
332        }
333    }
334
335    /// Get the working directory
336    pub fn dir(&self) -> Option<&str> {
337        match self {
338            Command::Simple(_) => None,
339            Command::Complex { dir, .. } => dir.as_deref(),
340        }
341    }
342}
343
344/// Runtime representation of a subtask reference
345#[derive(Debug, Clone)]
346pub struct SubTask {
347    pub name: String,
348    pub options: HashMap<String, String>,
349}
350
351impl SubTask {
352    pub fn from_config(config: config::SubTask) -> Self {
353        match config {
354            config::SubTask::Simple(name) => SubTask {
355                name,
356                options: HashMap::new(),
357            },
358            config::SubTask::Complex(detail) => SubTask {
359                name: detail.name,
360                options: detail.options,
361            },
362        }
363    }
364}
365
366/// Runtime representation of a when condition
367#[derive(Debug, Clone)]
368pub struct When {
369    pub condition: WhenCondition,
370}
371
372impl When {
373    pub fn from_config(config: config::When) -> Self {
374        // Determine which condition type is set
375        let condition = if let Some(eq) = config.equal {
376            WhenCondition::Equal {
377                left: eq.left,
378                right: eq.right,
379            }
380        } else if let Some(ne) = config.not_equal {
381            WhenCondition::NotEqual {
382                left: ne.left,
383                right: ne.right,
384            }
385        } else if let Some(cmd) = config.command {
386            WhenCondition::Command(cmd)
387        } else if let Some(path) = config.exists {
388            WhenCondition::Exists(path)
389        } else if let Some(var) = config.env_set {
390            WhenCondition::EnvSet(var)
391        } else if let Some(var) = config.env_not_set {
392            WhenCondition::EnvNotSet(var)
393        } else if let Some(opt) = config.option_set {
394            WhenCondition::OptionSet(opt)
395        } else if let Some(opt) = config.option_not_set {
396            WhenCondition::OptionNotSet(opt)
397        } else {
398            // Default to always true if no condition specified
399            WhenCondition::Always
400        };
401
402        When { condition }
403    }
404
405    /// Get dependencies from this condition
406    pub fn dependencies(&self) -> Vec<String> {
407        match &self.condition {
408            WhenCondition::OptionSet(name) | WhenCondition::OptionNotSet(name) => {
409                vec![name.clone()]
410            }
411            _ => Vec::new(),
412        }
413    }
414}
415
416/// Types of when conditions
417#[derive(Debug, Clone)]
418pub enum WhenCondition {
419    Equal { left: String, right: String },
420    NotEqual { left: String, right: String },
421    Command(String),
422    Exists(String),
423    EnvSet(String),
424    EnvNotSet(String),
425    OptionSet(String),
426    OptionNotSet(String),
427    Always,
428}
429
430/// Runtime representation of an option
431#[derive(Debug, Clone)]
432pub struct TaskOption {
433    pub name: String,
434    pub usage: Option<String>,
435    pub short: Option<String>,
436    pub option_type: OptionType,
437    pub default: Option<String>,
438    pub required: bool,
439    pub rewrite: Option<String>,
440    pub environment: Option<String>,
441    pub private: bool,
442}
443
444impl TaskOption {
445    pub fn from_config(name: String, config: config::TaskOption) -> Self {
446        let option_type = match config.option_type.as_str() {
447            "bool" | "boolean" => OptionType::Bool,
448            "int" | "integer" => OptionType::Integer,
449            "float" => OptionType::Float,
450            _ => OptionType::String,
451        };
452
453        TaskOption {
454            name,
455            usage: config.usage,
456            short: config.short,
457            option_type,
458            default: config.default,
459            required: config.required,
460            rewrite: config.rewrite,
461            environment: config.environment,
462            private: config.private,
463        }
464    }
465
466    pub fn dependencies(&self) -> Vec<String> {
467        // Options don't have dependencies in the basic model
468        Vec::new()
469    }
470}
471
472/// Option value types
473#[derive(Debug, Clone, PartialEq)]
474pub enum OptionType {
475    String,
476    Bool,
477    Integer,
478    Float,
479}
480
481/// Runtime representation of an argument
482#[derive(Debug, Clone)]
483pub struct Arg {
484    pub name: String,
485    pub usage: Option<String>,
486    pub default: Option<String>,
487    pub required: bool,
488    pub private: bool,
489}
490
491impl Arg {
492    pub fn from_config(name: String, config: config::Arg) -> Self {
493        Arg {
494            name,
495            usage: config.usage,
496            default: config.default,
497            required: config.required,
498            private: config.private,
499        }
500    }
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use std::collections::HashMap;
507
508    #[test]
509    fn test_task_validation_source_without_target() {
510        let config = config::Task {
511            usage: None,
512            description: None,
513            private: false,
514            quiet: false,
515            args: HashMap::new(),
516            options: HashMap::new(),
517            run: vec![],
518            finally: vec![],
519            source: vec!["src.txt".to_string()],
520            target: vec![],
521            include: None,
522        };
523
524        let result = Task::validate_config(&config);
525        assert!(result.is_err());
526        assert!(matches!(result, Err(ConfigError::SourceWithoutTarget)));
527    }
528
529    #[test]
530    fn test_task_validation_duplicate_names() {
531        let config = config::Task {
532            usage: None,
533            description: None,
534            private: false,
535            quiet: false,
536            args: {
537                let mut args = HashMap::new();
538                args.insert(
539                    "name".to_string(),
540                    config::Arg {
541                        usage: None,
542                        default: None,
543                        required: false,
544                        private: false,
545                    },
546                );
547                args
548            },
549            options: {
550                let mut opts = HashMap::new();
551                opts.insert(
552                    "name".to_string(),
553                    config::TaskOption {
554                        usage: None,
555                        short: None,
556                        option_type: "string".to_string(),
557                        default: None,
558                        required: false,
559                        rewrite: None,
560                        environment: None,
561                        private: false,
562                    },
563                );
564                opts
565            },
566            run: vec![],
567            finally: vec![],
568            source: vec![],
569            target: vec![],
570            include: None,
571        };
572
573        let result = Task::validate_config(&config);
574        assert!(result.is_err());
575    }
576}