Skip to main content

mk_lib/schema/
task_dependency.rs

1use schemars::JsonSchema;
2use serde::Deserialize;
3
4use super::TaskContext;
5
6/// This struct represents a task dependency. A task can depend on other tasks.
7/// If a task depends on another task, the dependent task must be executed before
8/// the dependent task.
9#[derive(Debug, Deserialize, JsonSchema)]
10pub struct TaskDependencyArgs {
11  /// The name of the task to depend on
12  pub name: String,
13}
14
15#[derive(Debug, Deserialize, JsonSchema)]
16#[serde(untagged)]
17/// A task dependency: either a task name string or an object with a `name` field.
18pub enum TaskDependency {
19  String(String),
20  TaskDependency(Box<TaskDependencyArgs>),
21}
22
23impl TaskDependency {
24  pub fn run(&self, context: &TaskContext) -> anyhow::Result<()> {
25    run_task_by_name(context, self.resolve_name())
26  }
27
28  pub fn resolve_name(&self) -> &str {
29    match self {
30      TaskDependency::String(name) => name,
31      TaskDependency::TaskDependency(args) => &args.name,
32    }
33  }
34}
35
36impl TaskDependencyArgs {
37  pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
38    run_task_by_name(context, &self.name)
39  }
40}
41
42pub fn run_task_by_name(context: &TaskContext, task_name: &str) -> anyhow::Result<()> {
43  assert!(!task_name.is_empty());
44
45  if context.is_task_completed(task_name)? {
46    log::trace!("Skipping completed task: {}", task_name);
47    return Ok(());
48  }
49
50  if context.is_task_active(task_name)? {
51    anyhow::bail!("Circular dependency detected - {}", task_name);
52  }
53
54  let task = context.task_root.tasks.get(task_name).ok_or_else(|| {
55    anyhow::anyhow!(
56      "Task '{}' not found. Run 'mk list' to see available tasks.",
57      task_name
58    )
59  })?;
60
61  log::trace!("Task: {:?}", task);
62
63  context.mark_task_active(task_name)?;
64
65  let result = {
66    let mut child_context = TaskContext::from_context(context);
67    child_context.set_current_task_name(task_name);
68    child_context.emit_event(&serde_json::json!({
69      "event": "task_started",
70      "task": task_name,
71    }))?;
72    task.run(&mut child_context)
73  };
74
75  context.unmark_task_active(task_name)?;
76
77  if result.is_ok() {
78    context.mark_task_complete(task_name)?;
79    context.emit_event(&serde_json::json!({
80      "event": "task_finished",
81      "task": task_name,
82      "success": true,
83    }))?;
84  } else {
85    context.emit_event(&serde_json::json!({
86      "event": "task_finished",
87      "task": task_name,
88      "success": false,
89    }))?;
90  }
91
92  result
93}
94
95#[cfg(test)]
96mod test {
97  use hashbrown::HashMap;
98  use std::sync::Arc;
99
100  use crate::schema::{
101    Task,
102    TaskRoot,
103  };
104
105  use super::*;
106
107  #[test]
108  fn test_task_dependency_1() -> anyhow::Result<()> {
109    let yaml = "
110      name: task1
111    ";
112
113    let task_dependency = serde_yaml::from_str::<TaskDependency>(yaml)?;
114    if let TaskDependency::TaskDependency(args) = task_dependency {
115      assert_eq!(args.name, "task1");
116    } else {
117      panic!("Expected TaskDependency::TaskDependency");
118    }
119
120    Ok(())
121  }
122
123  #[test]
124  fn test_task_dependency_2() -> anyhow::Result<()> {
125    let yaml = "\"task1\"";
126
127    let task_dependency = serde_yaml::from_str::<TaskDependency>(yaml)?;
128    if let TaskDependency::String(name) = task_dependency {
129      assert_eq!(name, "task1");
130    } else {
131      panic!("Expected TaskDependency::TaskString");
132    }
133
134    Ok(())
135  }
136
137  #[test]
138  fn test_task_dependency_3() -> anyhow::Result<()> {
139    let yaml = "\"\"";
140
141    let task_dependency = serde_yaml::from_str::<TaskDependency>(yaml)?;
142    if let TaskDependency::String(name) = task_dependency {
143      assert_eq!(name, "");
144    } else {
145      panic!("Expected TaskDependency::TaskString");
146    }
147
148    Ok(())
149  }
150
151  #[test]
152  fn test_task_dependency_4() -> anyhow::Result<()> {
153    let yaml = "
154      name:
155    ";
156
157    let task_dependency = serde_yaml::from_str::<TaskDependencyArgs>(yaml)?;
158    assert_eq!(task_dependency.name, "");
159
160    Ok(())
161  }
162
163  #[test]
164  fn test_task_dependency_5() -> anyhow::Result<()> {
165    let yaml = "
166      name: task_a
167    ";
168
169    let task_yaml = "
170      commands:
171        - command: echo 1
172          verbose: false
173    ";
174
175    let task = serde_yaml::from_str::<Task>(task_yaml)?;
176    let mut hm = HashMap::new();
177    hm.insert("task_a".into(), task);
178
179    let root = Arc::new(TaskRoot::from_hashmap(hm));
180    let task_dependency = serde_yaml::from_str::<TaskDependencyArgs>(yaml)?;
181    let result = task_dependency.execute(&TaskContext::empty_with_root(root));
182    assert!(result.is_ok());
183
184    Ok(())
185  }
186
187  #[test]
188  fn test_task_dependency_6_shared_dependency_is_not_a_cycle() -> anyhow::Result<()> {
189    let root_yaml = "
190      tasks:
191        root:
192          commands:
193            - command: echo root
194              verbose: false
195          depends_on:
196            - left
197            - right
198        left:
199          commands:
200            - command: echo left
201              verbose: false
202          depends_on:
203            - shared
204        right:
205          commands:
206            - command: echo right
207              verbose: false
208          depends_on:
209            - shared
210        shared:
211          commands:
212            - command: echo shared
213              verbose: false
214    ";
215
216    let root = Arc::new(serde_yaml::from_str::<TaskRoot>(root_yaml)?);
217    let context = TaskContext::empty_with_root(root);
218    let result = run_task_by_name(&context, "root");
219    assert!(result.is_ok());
220    assert!(context.is_task_completed("shared")?);
221
222    Ok(())
223  }
224}