use serde::{Deserialize, Serialize};
pub const DEFAULT_MAX_ARTIFACT_SIZE: u64 = 100 * 1024 * 1024;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct ArtifactsConfig {
#[serde(default)]
pub dir: Option<String>,
#[serde(default)]
pub format: ArtifactFormat,
#[serde(default)]
pub mode: ArtifactMode,
#[serde(default)]
pub manifest: bool,
#[serde(default = "default_max_size")]
pub max_size: u64,
}
fn default_max_size() -> u64 {
DEFAULT_MAX_ARTIFACT_SIZE
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ArtifactSpec {
Enabled(bool),
Single(ArtifactOutput),
Multiple(Vec<ArtifactOutput>),
}
impl Default for ArtifactSpec {
fn default() -> Self {
Self::Enabled(false)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ArtifactOutput {
pub path: String,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub template: Option<String>,
#[serde(default)]
pub format: Option<ArtifactFormat>,
#[serde(default)]
pub mode: Option<ArtifactMode>,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactFormat {
#[default]
Text,
Json,
Yaml,
Binary,
}
impl std::fmt::Display for ArtifactFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text => write!(f, "text"),
Self::Json => write!(f, "json"),
Self::Yaml => write!(f, "yaml"),
Self::Binary => write!(f, "binary"),
}
}
}
impl ArtifactFormat {
pub fn extension(&self) -> &'static str {
match self {
Self::Text => "txt",
Self::Json => "json",
Self::Yaml => "yaml",
Self::Binary => "bin",
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactMode {
#[default]
Overwrite,
Append,
Unique,
Fail,
}
impl std::fmt::Display for ArtifactMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Overwrite => write!(f, "overwrite"),
Self::Append => write!(f, "append"),
Self::Unique => write!(f, "unique"),
Self::Fail => write!(f, "fail"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactEntry {
pub task_id: String,
pub path: String,
pub size: u64,
pub format: String,
pub checksum: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde_yaml;
#[test]
fn test_parse_artifact_enabled() {
let yaml = r#"true"#;
let spec: ArtifactSpec = serde_yaml::from_str(yaml).unwrap();
assert!(matches!(spec, ArtifactSpec::Enabled(true)));
}
#[test]
fn test_parse_artifact_disabled() {
let yaml = r#"false"#;
let spec: ArtifactSpec = serde_yaml::from_str(yaml).unwrap();
assert!(matches!(spec, ArtifactSpec::Enabled(false)));
}
#[test]
fn test_parse_artifact_single() {
let yaml = r#"
path: ./output/{{context.meta.task_id}}.json
mode: unique
"#;
let spec: ArtifactSpec = serde_yaml::from_str(yaml).unwrap();
match spec {
ArtifactSpec::Single(output) => {
assert!(output.path.contains("context.meta.task_id"));
assert_eq!(output.mode, Some(ArtifactMode::Unique));
}
_ => panic!("Expected Single artifact"),
}
}
#[test]
fn test_parse_artifact_multiple() {
let yaml = r#"
- path: ./raw.json
source: raw_data
- path: ./processed.json
format: json
"#;
let spec: ArtifactSpec = serde_yaml::from_str(yaml).unwrap();
match spec {
ArtifactSpec::Multiple(outputs) => {
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].source, Some("raw_data".to_string()));
assert_eq!(outputs[1].format, Some(ArtifactFormat::Json));
}
_ => panic!("Expected Multiple artifacts"),
}
}
#[test]
fn test_parse_artifacts_config() {
let yaml = r#"
dir: ./output/{{context.meta.date}}
format: json
mode: overwrite
manifest: true
max_size: 52428800
"#;
let config: ArtifactsConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(
config.dir,
Some("./output/{{context.meta.date}}".to_string())
);
assert_eq!(config.format, ArtifactFormat::Json);
assert_eq!(config.mode, ArtifactMode::Overwrite);
assert!(config.manifest);
assert_eq!(config.max_size, 50 * 1024 * 1024); }
#[test]
fn test_artifacts_config_defaults() {
let yaml = "{}";
let config: ArtifactsConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.dir, None);
assert_eq!(config.format, ArtifactFormat::Text);
assert_eq!(config.mode, ArtifactMode::Overwrite);
assert!(!config.manifest);
assert_eq!(config.max_size, DEFAULT_MAX_ARTIFACT_SIZE);
}
#[test]
fn test_artifact_format_display() {
assert_eq!(ArtifactFormat::Text.to_string(), "text");
assert_eq!(ArtifactFormat::Json.to_string(), "json");
assert_eq!(ArtifactFormat::Yaml.to_string(), "yaml");
assert_eq!(ArtifactFormat::Binary.to_string(), "binary");
}
#[test]
fn test_artifact_format_extension() {
assert_eq!(ArtifactFormat::Text.extension(), "txt");
assert_eq!(ArtifactFormat::Json.extension(), "json");
assert_eq!(ArtifactFormat::Yaml.extension(), "yaml");
assert_eq!(ArtifactFormat::Binary.extension(), "bin");
}
#[test]
fn test_artifact_mode_display() {
assert_eq!(ArtifactMode::Overwrite.to_string(), "overwrite");
assert_eq!(ArtifactMode::Append.to_string(), "append");
assert_eq!(ArtifactMode::Unique.to_string(), "unique");
assert_eq!(ArtifactMode::Fail.to_string(), "fail");
}
#[test]
fn test_artifact_format_binary_serde() {
let yaml = r#""binary""#;
let format: ArtifactFormat = serde_yaml::from_str(yaml).unwrap();
assert_eq!(format, ArtifactFormat::Binary);
let json = serde_json::to_string(&format).unwrap();
assert_eq!(json, r#""binary""#);
let back: ArtifactFormat = serde_json::from_str(&json).unwrap();
assert_eq!(back, ArtifactFormat::Binary);
assert_eq!(ArtifactFormat::Binary.extension(), "bin");
assert_eq!(ArtifactFormat::Binary.to_string(), "binary");
}
#[test]
fn test_artifact_format_binary_in_artifact_spec() {
let yaml = r#"
path: ./output/image.bin
format: binary
"#;
let spec: ArtifactSpec = serde_yaml::from_str(yaml).unwrap();
match spec {
ArtifactSpec::Single(output) => {
assert_eq!(output.format, Some(ArtifactFormat::Binary));
assert_eq!(output.path, "./output/image.bin");
}
_ => panic!("Expected Single artifact"),
}
}
#[test]
fn test_artifact_entry_serialization() {
let entry = ArtifactEntry {
task_id: "test_task".to_string(),
path: "/tmp/output.json".to_string(),
size: 1234,
format: "json".to_string(),
checksum: "abc123".to_string(),
created_at: chrono::Utc::now(),
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("test_task"));
assert!(json.contains("1234"));
}
}