use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionInput {
pub id: String,
pub input_type: InputType,
pub variables: HashMap<String, VariableValue>,
pub metadata: InputMetadata,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputType {
Empty,
Arguments {
separator: Option<String>,
},
FilePattern {
patterns: Vec<String>,
recursive: bool,
},
StructuredData {
format: DataFormat,
schema: Option<String>,
},
Environment {
prefix: Option<String>,
},
StandardInput {
format: DataFormat,
},
Generated {
generator: String,
config: serde_json::Value,
},
Composite {
sources: Vec<Box<InputType>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputMetadata {
pub source: String,
pub created_at: DateTime<Utc>,
pub size_bytes: Option<u64>,
pub checksum: Option<String>,
pub content_type: Option<String>,
pub custom_fields: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariableDefinition {
pub name: String,
pub var_type: VariableType,
pub description: String,
pub required: bool,
pub default_value: Option<String>,
pub validation_rules: Vec<ValidationRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VariableType {
String,
Number,
Boolean,
Path,
Url,
Array,
Object,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum VariableValue {
String(String),
Number(i64),
Float(f64),
Boolean(bool),
Path(PathBuf),
Url(url::Url),
Array(Vec<VariableValue>),
Object(HashMap<String, VariableValue>),
Null,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ValidationRule {
FileExists,
Range { min: Option<i64>, max: Option<i64> },
Pattern { regex: String },
OneOf { values: Vec<String> },
Custom { validator: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DataFormat {
Json,
Yaml,
Toml,
Csv,
Xml,
PlainText,
Auto,
}
impl Default for InputMetadata {
fn default() -> Self {
Self {
source: String::new(),
created_at: Utc::now(),
size_bytes: None,
checksum: None,
content_type: None,
custom_fields: HashMap::new(),
}
}
}
impl ExecutionInput {
pub fn new(id: String, input_type: InputType) -> Self {
Self {
id,
input_type,
variables: HashMap::new(),
metadata: InputMetadata::default(),
dependencies: Vec::new(),
}
}
pub fn add_variable(&mut self, name: String, value: VariableValue) -> &mut Self {
self.variables.insert(name, value);
self
}
pub fn with_metadata(&mut self, metadata: InputMetadata) -> &mut Self {
self.metadata = metadata;
self
}
pub fn substitute_in_template(&self, template: &str) -> Result<String> {
let mut result = template.to_string();
result = result.replace("{input_id}", &self.id);
result = result.replace("{input_type}", &format!("{:?}", self.input_type));
for (key, value) in &self.variables {
let placeholder = format!("{{{}}}", key);
result = result.replace(&placeholder, &value.to_string());
}
result = self.apply_helper_functions(&result)?;
Ok(result)
}
fn apply_helper_functions(&self, template: &str) -> Result<String> {
let mut result = template.to_string();
let re = regex::Regex::new(r"\{([^|}]+)\|([^}]+)\}")?;
for capture in re.captures_iter(template) {
let var_name = &capture[1];
let function = &capture[2];
let full_match = &capture[0];
if let Some(value) = self.variables.get(var_name) {
let transformed = self.apply_transformation(value, function)?;
result = result.replace(full_match, &transformed);
}
}
Ok(result)
}
fn apply_transformation(&self, value: &VariableValue, function: &str) -> Result<String> {
match function {
"basename" => Ok(PathBuf::from(value.to_string())
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()),
"dirname" => Ok(PathBuf::from(value.to_string())
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string())),
"uppercase" => Ok(value.to_string().to_uppercase()),
"lowercase" => Ok(value.to_string().to_lowercase()),
"trim" => Ok(value.to_string().trim().to_string()),
_ => Err(anyhow!("Unknown transformation function: {}", function)),
}
}
}
impl fmt::Display for VariableValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VariableValue::String(s) => write!(f, "{}", s),
VariableValue::Number(n) => write!(f, "{}", n),
VariableValue::Float(fl) => write!(f, "{}", fl),
VariableValue::Boolean(b) => write!(f, "{}", b),
VariableValue::Path(p) => write!(f, "{}", p.to_string_lossy()),
VariableValue::Url(u) => write!(f, "{}", u),
VariableValue::Array(arr) => {
write!(
f,
"[{}]",
arr.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
VariableValue::Object(obj) => {
write!(f, "{}", serde_json::to_string(obj).unwrap_or_default())
}
VariableValue::Null => write!(f, "null"),
}
}
}
impl VariableValue {
pub fn as_path(&self) -> Result<PathBuf> {
match self {
VariableValue::Path(p) => Ok(p.clone()),
VariableValue::String(s) => Ok(PathBuf::from(s)),
_ => Err(anyhow!("Cannot convert {:?} to path", self)),
}
}
pub fn as_number(&self) -> Result<i64> {
match self {
VariableValue::Number(n) => Ok(*n),
VariableValue::Float(f) => Ok(*f as i64),
VariableValue::String(s) => s.parse().map_err(anyhow::Error::from),
_ => Err(anyhow!("Cannot convert {:?} to number", self)),
}
}
}