1use 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#[derive(Debug, Clone)]
14pub struct Task {
15 pub name: String,
17
18 pub usage: Option<String>,
20
21 pub description: Option<String>,
23
24 pub private: bool,
26
27 pub quiet: bool,
29
30 pub args: HashMap<String, Arg>,
32
33 pub options: HashMap<String, TaskOption>,
35
36 pub run: Vec<Run>,
38
39 pub finally: Vec<Run>,
41
42 pub source: Vec<String>,
44
45 pub target: Vec<String>,
47
48 pub vars: HashMap<String, String>,
50}
51
52impl Task {
53 pub fn from_config(name: String, config: config::Task) -> ConfigResult<Self> {
55 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 fn validate_config(config: &config::Task) -> ConfigResult<()> {
84 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 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 pub fn dependencies(&self) -> Vec<String> {
104 let mut deps = Vec::new();
105
106 for option in self.options.values() {
108 deps.extend(option.dependencies());
109 }
110
111 for run in self.run.iter().chain(self.finally.iter()) {
113 deps.extend(run.dependencies());
114 }
115
116 deps
117 }
118
119 pub fn execute(&self, ctx: &mut Context) -> ExecutionResult<()> {
121 if ctx.is_task_in_stack(&self.name) {
123 return Err(ExecutionError::CommandFailed(Some(1)));
124 }
125
126 ctx.push_task(self.name.clone());
128
129 ctx.print_task_start(&self.name);
131
132 for (key, value) in &self.vars {
134 ctx.set_var(key.clone(), value.clone());
135 }
136
137 let result = self.execute_run_items(ctx);
139
140 if !self.finally.is_empty() {
142 ctx.print_debug("Running finally block...");
143 if let Err(e) = self.execute_finally_items(ctx) {
144 if result.is_ok() {
147 ctx.pop_task();
148 return Err(e);
149 }
150 }
151 }
152
153 ctx.pop_task();
155
156 if result.is_ok() {
157 ctx.print_task_complete(&self.name);
158 }
159
160 result
161 }
162
163 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 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 fn execute_run_item(&self, run: &Run, ctx: &mut Context) -> ExecutionResult<()> {
181 if !run.when.is_empty() {
183 let should_run = evaluate_when_list(&run.when, ctx)?;
184 if !should_run {
185 return Ok(());
187 }
188 }
189
190 for cmd in &run.commands {
192 execute_command(cmd, ctx)?;
193 }
194
195 for subtask in &run.subtasks {
197 self.execute_subtask(subtask, ctx)?;
198 }
199
200 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 fn execute_subtask(&self, _subtask: &SubTask, _ctx: &mut Context) -> ExecutionResult<()> {
223 Ok(())
226 }
227}
228
229#[derive(Debug, Clone)]
231pub struct Run {
232 pub when: Vec<When>,
234
235 pub commands: Vec<Command>,
237
238 pub subtasks: Vec<SubTask>,
240
241 pub set_environment: HashMap<String, Option<String>>,
243}
244
245impl Run {
246 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 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#[derive(Debug, Clone)]
284pub enum Command {
285 Simple(String),
287
288 Complex {
290 exec: String,
291 print: String,
292 quiet: bool,
293 dir: Option<String>,
294 },
295}
296
297impl Command {
298 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 pub fn exec(&self) -> &str {
313 match self {
314 Command::Simple(cmd) => cmd,
315 Command::Complex { exec, .. } => exec,
316 }
317 }
318
319 pub fn print(&self) -> &str {
321 match self {
322 Command::Simple(cmd) => cmd,
323 Command::Complex { print, .. } => print,
324 }
325 }
326
327 pub fn is_quiet(&self) -> bool {
329 match self {
330 Command::Simple(_) => false,
331 Command::Complex { quiet, .. } => *quiet,
332 }
333 }
334
335 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#[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#[derive(Debug, Clone)]
368pub struct When {
369 pub condition: WhenCondition,
370}
371
372impl When {
373 pub fn from_config(config: config::When) -> Self {
374 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 WhenCondition::Always
400 };
401
402 When { condition }
403 }
404
405 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#[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#[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 Vec::new()
469 }
470}
471
472#[derive(Debug, Clone, PartialEq)]
474pub enum OptionType {
475 String,
476 Bool,
477 Integer,
478 Float,
479}
480
481#[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}