Skip to main content

mk_lib/schema/
mod.rs

1mod command;
2mod include;
3mod plan;
4mod precondition;
5mod shell;
6mod task;
7mod task_context;
8mod task_dependency;
9mod task_root;
10mod use_cargo;
11mod use_npm;
12mod validation;
13
14use std::collections::HashSet;
15use std::process::Stdio;
16use std::sync::{
17  Arc,
18  Mutex,
19};
20
21use once_cell::sync::Lazy;
22use regex::Regex;
23
24pub type ActiveTasks = Arc<Mutex<HashSet<String>>>;
25pub type CompletedTasks = Arc<Mutex<HashSet<String>>>;
26
27pub use command::*;
28pub use include::*;
29pub use plan::*;
30pub use precondition::*;
31pub use shell::*;
32pub use task::*;
33pub use task_context::*;
34pub use task_dependency::*;
35pub use task_root::*;
36pub use use_cargo::*;
37pub use use_npm::*;
38pub use validation::*;
39
40use crate::secrets::load_secret_value;
41
42static TEMPLATE_COMMAND_RE: Lazy<Regex> =
43  Lazy::new(|| Regex::new(r"^\$\{\{.+\}\}$").expect("valid template regex"));
44static TEMPLATE_EXPR_RE: Lazy<Regex> =
45  Lazy::new(|| Regex::new(r"\$\{\{\s*(.+?)\s*\}\}").expect("valid template expression regex"));
46
47pub fn is_shell_command(value: &str) -> anyhow::Result<bool> {
48  let re = Regex::new(r"^\$\(.+\)$")?;
49  Ok(re.is_match(value))
50}
51
52pub fn is_template_command(value: &str) -> anyhow::Result<bool> {
53  Ok(TEMPLATE_COMMAND_RE.is_match(value))
54}
55
56pub fn resolve_template_command_value(value: &str, context: &TaskContext) -> anyhow::Result<String> {
57  let value = value.trim_start_matches("${{").trim_end_matches("}}").trim();
58  resolve_template_expression(value, context)
59}
60
61pub fn resolve_template_expression(value: &str, context: &TaskContext) -> anyhow::Result<String> {
62  if value.starts_with("env.") {
63    let value = value.trim_start_matches("env.");
64    let value = context
65      .env_vars
66      .get(value)
67      .ok_or_else(|| anyhow::anyhow!("Failed to find environment variable"))?;
68    Ok(value.to_string())
69  } else if value.starts_with("secrets.") {
70    let path = value.trim_start_matches("secrets.");
71    load_secret_value(
72      path,
73      &context.task_root.config_base_dir(),
74      context.secret_vault_location.as_deref(),
75      context.secret_keys_location.as_deref(),
76      context.secret_key_name.as_deref(),
77    )
78  } else if value.starts_with("outputs.") {
79    let name = value.trim_start_matches("outputs.");
80    context
81      .get_task_output(name)?
82      .ok_or_else(|| anyhow::anyhow!("Failed to find task output - {}", name))
83  } else {
84    Ok(value.to_string())
85  }
86}
87
88pub fn interpolate_template_string(value: &str, context: &TaskContext) -> anyhow::Result<String> {
89  let mut result = String::with_capacity(value.len());
90  let mut last_end = 0usize;
91  for captures in TEMPLATE_EXPR_RE.captures_iter(value) {
92    let Some(full_match) = captures.get(0) else {
93      continue;
94    };
95    let Some(expr) = captures.get(1) else {
96      continue;
97    };
98    result.push_str(&value[last_end..full_match.start()]);
99    result.push_str(&resolve_template_expression(expr.as_str().trim(), context)?);
100    last_end = full_match.end();
101  }
102  result.push_str(&value[last_end..]);
103  Ok(result)
104}
105
106pub fn extract_output_references(value: &str) -> Vec<String> {
107  TEMPLATE_EXPR_RE
108    .captures_iter(value)
109    .filter_map(|captures| captures.get(1))
110    .map(|expr| expr.as_str().trim())
111    .filter_map(|expr| expr.strip_prefix("outputs."))
112    .map(str::to_string)
113    .collect()
114}
115
116pub fn contains_output_reference(value: &str) -> bool {
117  !extract_output_references(value).is_empty()
118}
119
120pub fn get_output_handler(verbose: bool) -> Stdio {
121  if verbose {
122    Stdio::piped()
123  } else {
124    Stdio::null()
125  }
126}
127
128#[cfg(test)]
129mod test {
130  use std::sync::Arc;
131
132  use super::*;
133
134  #[test]
135  fn test_interpolate_template_string_resolves_outputs() -> anyhow::Result<()> {
136    let root = Arc::new(TaskRoot::default());
137    let context = TaskContext::empty_with_root(root);
138    context.insert_task_output("version", "v1.2.3")?;
139    assert_eq!(
140      interpolate_template_string("tag=${{ outputs.version }}", &context)?,
141      "tag=v1.2.3"
142    );
143    Ok(())
144  }
145
146  #[test]
147  fn test_extract_output_references_finds_all_output_templates() {
148    assert_eq!(
149      extract_output_references("${{ outputs.first }}-${{ outputs.second }}"),
150      vec!["first".to_string(), "second".to_string()]
151    );
152  }
153}