use crate::config::config_file::mise_toml::EnvList;
use crate::config::config_file::toml::deserialize_arr;
use crate::task::task_sources::TaskOutputs;
use crate::task::{RunEntry, Silent, Task, TaskDep};
use indexmap::IndexMap;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct TaskTemplate {
#[serde(default)]
pub description: String,
#[serde(default, rename = "alias", deserialize_with = "deserialize_arr")]
pub aliases: Vec<String>,
#[serde(default)]
pub confirm: Option<String>,
#[serde(default, deserialize_with = "deserialize_arr")]
pub depends: Vec<TaskDep>,
#[serde(default, deserialize_with = "deserialize_arr")]
pub depends_post: Vec<TaskDep>,
#[serde(default, deserialize_with = "deserialize_arr")]
pub wait_for: Vec<TaskDep>,
#[serde(default)]
pub env: EnvList,
#[serde(default)]
pub vars: EnvList,
#[serde(default)]
pub dir: Option<String>,
#[serde(default)]
pub hide: Option<bool>,
#[serde(default)]
pub raw: Option<bool>,
#[serde(default)]
pub sources: Vec<String>,
#[serde(default)]
pub outputs: TaskOutputs,
#[serde(default)]
pub shell: Option<String>,
#[serde(default)]
pub quiet: Option<bool>,
#[serde(default)]
pub silent: Option<Silent>,
#[serde(default)]
pub tools: IndexMap<String, String>,
#[serde(default)]
pub usage: String,
#[serde(default)]
pub timeout: Option<String>,
#[serde(default, deserialize_with = "deserialize_arr")]
pub run: Vec<RunEntry>,
#[serde(default, deserialize_with = "deserialize_arr")]
pub run_windows: Vec<RunEntry>,
#[serde(default)]
pub file: Option<String>,
#[serde(default)]
pub deny_all: bool,
#[serde(default)]
pub deny_read: bool,
#[serde(default)]
pub deny_write: bool,
#[serde(default)]
pub deny_net: bool,
#[serde(default)]
pub deny_env: bool,
#[serde(default)]
pub allow_read: Vec<std::path::PathBuf>,
#[serde(default)]
pub allow_write: Vec<std::path::PathBuf>,
#[serde(default)]
pub allow_net: Vec<String>,
#[serde(default)]
pub allow_env: Vec<String>,
}
impl Task {
pub fn merge_template(&mut self, template: &TaskTemplate) {
if self.run.is_empty() {
self.run = template.run.clone();
}
if self.run_windows.is_empty() {
self.run_windows = template.run_windows.clone();
}
let mut merged_tools = template.tools.clone();
for (tool, version) in &self.tools {
merged_tools.insert(tool.clone(), version.clone());
}
self.tools = merged_tools;
let mut merged_env = template.env.clone();
merged_env.0.extend(self.env.0.clone());
self.env = merged_env;
let mut merged_vars = template.vars.clone();
merged_vars.0.extend(self.vars.0.clone());
self.vars = merged_vars;
if self.depends.is_empty() && !template.depends.is_empty() {
self.depends = template.depends.clone();
}
if self.depends_post.is_empty() && !template.depends_post.is_empty() {
self.depends_post = template.depends_post.clone();
}
if self.wait_for.is_empty() && !template.wait_for.is_empty() {
self.wait_for = template.wait_for.clone();
}
if self.dir.is_none() {
self.dir = template.dir.clone();
}
if self.description.is_empty() && !template.description.is_empty() {
self.description = template.description.clone();
}
if self.aliases.is_empty() && !template.aliases.is_empty() {
self.aliases = template.aliases.clone();
}
if self.confirm.is_none() {
self.confirm = template.confirm.clone();
}
if self.sources.is_empty() && !template.sources.is_empty() {
self.sources = template.sources.clone();
}
if self.outputs == TaskOutputs::default() && template.outputs != TaskOutputs::default() {
self.outputs = template.outputs.clone();
}
if self.shell.is_none() {
self.shell = template.shell.clone();
}
if matches!(self.silent, Silent::Off)
&& let Some(ref silent) = template.silent
{
self.silent = silent.clone();
}
if self.usage.is_empty() && !template.usage.is_empty() {
self.usage = template.usage.clone();
}
if self.timeout.is_none() {
self.timeout = template.timeout.clone();
}
if self.file.is_none()
&& let Some(ref file) = template.file
{
self.file = Some(file.into());
}
self.deny_all |= template.deny_all;
self.deny_read |= template.deny_read;
self.deny_write |= template.deny_write;
self.deny_net |= template.deny_net;
self.deny_env |= template.deny_env;
self.allow_read.splice(0..0, template.allow_read.clone());
self.allow_write.splice(0..0, template.allow_write.clone());
self.allow_net.splice(0..0, template.allow_net.clone());
self.allow_env.splice(0..0, template.allow_env.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_template_run_override() {
let mut task = Task {
run: vec![RunEntry::Script("local command".to_string())],
..Default::default()
};
let template = TaskTemplate {
run: vec![RunEntry::Script("template command".to_string())],
..Default::default()
};
task.merge_template(&template);
assert_eq!(task.run.len(), 1);
assert!(matches!(&task.run[0], RunEntry::Script(s) if s == "local command"));
}
#[test]
fn test_merge_template_run_from_template() {
let mut task = Task::default();
let template = TaskTemplate {
run: vec![RunEntry::Script("template command".to_string())],
..Default::default()
};
task.merge_template(&template);
assert_eq!(task.run.len(), 1);
assert!(matches!(&task.run[0], RunEntry::Script(s) if s == "template command"));
}
#[test]
fn test_merge_template_tools_deep_merge() {
let mut task = Task {
tools: IndexMap::from([("node".to_string(), "20".to_string())]),
..Default::default()
};
let template = TaskTemplate {
tools: IndexMap::from([
("python".to_string(), "3.12".to_string()),
("node".to_string(), "18".to_string()), ]),
..Default::default()
};
task.merge_template(&template);
assert_eq!(task.tools.len(), 2);
assert_eq!(task.tools.get("node"), Some(&"20".to_string()));
assert_eq!(task.tools.get("python"), Some(&"3.12".to_string()));
}
#[test]
fn test_merge_template_description() {
let mut task = Task::default();
let template = TaskTemplate {
description: "Template description".to_string(),
..Default::default()
};
task.merge_template(&template);
assert_eq!(task.description, "Template description");
let mut task2 = Task {
description: "Local description".to_string(),
..Default::default()
};
task2.merge_template(&template);
assert_eq!(task2.description, "Local description");
}
#[test]
fn test_merge_template_depends_override() {
let mut task = Task {
depends: vec![TaskDep {
task: "local-dep".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
let template = TaskTemplate {
depends: vec![TaskDep {
task: "template-dep".to_string(),
args: vec![],
env: Default::default(),
}],
..Default::default()
};
task.merge_template(&template);
assert_eq!(task.depends.len(), 1);
assert_eq!(task.depends[0].task, "local-dep");
}
#[test]
fn test_merge_template_vars_deep_merge() {
let mut task = Task {
vars: EnvList(vec![crate::config::env_directive::EnvDirective::Val(
"target".to_string(),
"linux".to_string(),
Default::default(),
)]),
..Default::default()
};
let template = TaskTemplate {
vars: EnvList(vec![crate::config::env_directive::EnvDirective::Val(
"profile".to_string(),
"release".to_string(),
Default::default(),
)]),
..Default::default()
};
task.merge_template(&template);
assert_eq!(task.vars.0.len(), 2);
}
#[test]
fn test_merge_template_vars_override() {
let mut task = Task {
vars: EnvList(vec![
crate::config::env_directive::EnvDirective::Val(
"target".to_string(),
"linux".to_string(),
Default::default(),
),
crate::config::env_directive::EnvDirective::Val(
"shared".to_string(),
"task_value".to_string(),
Default::default(),
),
]),
..Default::default()
};
let template = TaskTemplate {
vars: EnvList(vec![
crate::config::env_directive::EnvDirective::Val(
"profile".to_string(),
"release".to_string(),
Default::default(),
),
crate::config::env_directive::EnvDirective::Val(
"shared".to_string(),
"template_value".to_string(),
Default::default(),
),
]),
..Default::default()
};
task.merge_template(&template);
let shared_val = task.vars.0.iter().rev().find_map(|d| match d {
crate::config::env_directive::EnvDirective::Val(name, value, _) if name == "shared" => {
Some(value.as_str())
}
_ => None,
});
assert_eq!(shared_val, Some("task_value"));
}
#[test]
fn test_merge_template_sandbox_config() {
let mut task = Task {
deny_net: true,
allow_read: vec!["task-read".into()],
allow_env: vec!["TASK_*".to_string()],
..Default::default()
};
let template = TaskTemplate {
deny_all: true,
deny_read: true,
deny_write: true,
deny_env: true,
allow_read: vec!["template-read".into()],
allow_write: vec!["template-write".into()],
allow_net: vec!["example.com".to_string()],
allow_env: vec!["TEMPLATE_*".to_string()],
..Default::default()
};
task.merge_template(&template);
assert!(task.deny_all);
assert!(task.deny_read);
assert!(task.deny_write);
assert!(task.deny_net);
assert!(task.deny_env);
assert_eq!(
task.allow_read,
vec![
std::path::PathBuf::from("template-read"),
std::path::PathBuf::from("task-read")
]
);
assert_eq!(
task.allow_write,
vec![std::path::PathBuf::from("template-write")]
);
assert_eq!(task.allow_net, vec!["example.com".to_string()]);
assert_eq!(
task.allow_env,
vec!["TEMPLATE_*".to_string(), "TASK_*".to_string()]
);
}
}