use serde::{Deserialize, Serialize};
use super::artifact::{ArtifactInput, ArtifactOutput};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellConfig {
pub command: String,
pub timeout_secs: Option<u64>,
pub dir: Option<String>,
pub env: Vec<(String, String)>,
pub clean_env: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub outputs: Vec<ArtifactOutput>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<ArtifactInput>,
#[serde(default)]
pub allow_failure: bool,
}
impl ShellConfig {
pub fn new(command: &str) -> Self {
Self {
command: command.to_string(),
timeout_secs: None,
dir: None,
env: Vec::new(),
clean_env: false,
outputs: Vec::new(),
inputs: Vec::new(),
allow_failure: false,
}
}
pub fn timeout_secs(mut self, secs: u64) -> Self {
self.timeout_secs = Some(secs);
self
}
pub fn dir(mut self, dir: &str) -> Self {
self.dir = Some(dir.to_string());
self
}
pub fn env(mut self, key: &str, value: &str) -> Self {
self.env.push((key.to_string(), value.to_string()));
self
}
pub fn clean_env(mut self) -> Self {
self.clean_env = true;
self
}
pub fn output(mut self, pattern: &str) -> Self {
self.outputs.push(ArtifactOutput::new(pattern));
self
}
pub fn output_typed(mut self, pattern: &str, content_type: &str) -> Self {
self.outputs
.push(ArtifactOutput::typed(pattern, content_type));
self
}
pub fn input(mut self, step: &str, name: &str) -> Self {
self.inputs.push(ArtifactInput::new(step, name));
self
}
pub fn allow_failure(mut self) -> Self {
self.allow_failure = true;
self
}
pub fn input_at(mut self, step: &str, name: &str, dest: &str) -> Self {
self.inputs.push(ArtifactInput::new(step, name).at(dest));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder() {
let config = ShellConfig::new("cargo test")
.timeout_secs(60)
.dir("/app")
.env("RUST_LOG", "debug")
.clean_env();
assert_eq!(config.command, "cargo test");
assert_eq!(config.timeout_secs, Some(60));
assert_eq!(config.dir, Some("/app".to_string()));
assert_eq!(
config.env,
vec![("RUST_LOG".to_string(), "debug".to_string())]
);
assert!(config.clean_env);
}
#[test]
fn a_fresh_config_declares_no_artifact() {
let config = ShellConfig::new("echo hi");
assert!(config.outputs.is_empty());
assert!(config.inputs.is_empty());
}
#[test]
fn outputs_and_inputs_accumulate_in_declaration_order() {
let config = ShellConfig::new("build")
.output("a.txt")
.output_typed("b", "text/csv")
.input("prev", "c.txt")
.input_at("prev", "d.txt", "in/d.txt");
assert_eq!(config.outputs[0].pattern, "a.txt");
assert_eq!(config.outputs[1].content_type.as_deref(), Some("text/csv"));
assert_eq!(config.inputs[0].destination(), "c.txt");
assert_eq!(config.inputs[1].destination(), "in/d.txt");
}
#[test]
fn serde_omits_empty_artifact_declarations() {
let json = serde_json::to_string(&ShellConfig::new("echo hi")).expect("serialize");
assert!(!json.contains("outputs"));
assert!(!json.contains("inputs"));
}
#[test]
fn a_config_predating_artifacts_still_deserializes() {
let config: ShellConfig = serde_json::from_str(
r#"{"command":"echo hi","timeout_secs":null,"dir":null,"env":[],"clean_env":false}"#,
)
.expect("deserialize");
assert!(config.outputs.is_empty());
assert!(config.inputs.is_empty());
}
}