use crate::error::{Result, WorkflowError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ParameterType {
String,
Integer,
Float,
Boolean,
Array,
Object,
FilePath,
Url,
Enum {
allowed_values: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Parameter {
pub name: String,
pub param_type: ParameterType,
pub description: String,
pub required: bool,
pub default_value: Option<ParameterValue>,
pub constraints: Option<ParameterConstraints>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ParameterValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<ParameterValue>),
Object(HashMap<String, ParameterValue>),
}
impl ParameterValue {
pub fn to_json(&self) -> serde_json::Value {
match self {
Self::String(s) => serde_json::Value::String(s.clone()),
Self::Integer(i) => serde_json::Value::Number((*i).into()),
Self::Float(f) => serde_json::Number::from_f64(*f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
Self::Boolean(b) => serde_json::Value::Bool(*b),
Self::Array(arr) => serde_json::Value::Array(arr.iter().map(|v| v.to_json()).collect()),
Self::Object(obj) => {
let mut map = serde_json::Map::new();
for (k, v) in obj {
map.insert(k.clone(), v.to_json());
}
serde_json::Value::Object(map)
}
}
}
pub fn as_string(&self) -> Option<&str> {
if let Self::String(s) = self {
Some(s)
} else {
None
}
}
pub fn as_integer(&self) -> Option<i64> {
if let Self::Integer(i) = self {
Some(*i)
} else {
None
}
}
pub fn as_float(&self) -> Option<f64> {
if let Self::Float(f) = self {
Some(*f)
} else {
None
}
}
pub fn as_boolean(&self) -> Option<bool> {
if let Self::Boolean(b) = self {
Some(*b)
} else {
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterConstraints {
pub min: Option<f64>,
pub max: Option<f64>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
}
pub struct TemplateParameterizer {
placeholder_prefix: String,
placeholder_suffix: String,
}
impl TemplateParameterizer {
pub fn new() -> Self {
Self {
placeholder_prefix: "{{".to_string(),
placeholder_suffix: "}}".to_string(),
}
}
pub fn with_markers<S: Into<String>>(prefix: S, suffix: S) -> Self {
Self {
placeholder_prefix: prefix.into(),
placeholder_suffix: suffix.into(),
}
}
pub fn apply_parameters(
&self,
template: &str,
params: &HashMap<String, ParameterValue>,
) -> Result<String> {
let mut result = template.to_string();
for (name, value) in params {
let placeholder = format!(
"{}{}{}",
self.placeholder_prefix, name, self.placeholder_suffix
);
let replacement = match value {
ParameterValue::String(s) => s.clone(),
ParameterValue::Integer(i) => i.to_string(),
ParameterValue::Float(f) => f.to_string(),
ParameterValue::Boolean(b) => b.to_string(),
ParameterValue::Array(_) | ParameterValue::Object(_) => {
serde_json::to_string(&value.to_json()).map_err(|e| {
WorkflowError::template(format!("Failed to serialize value: {}", e))
})?
}
};
result = result.replace(&placeholder, &replacement);
}
if result.contains(&self.placeholder_prefix) && result.contains(&self.placeholder_suffix) {
return Err(WorkflowError::template(
"Template contains unreplaced placeholders",
));
}
Ok(result)
}
pub fn extract_placeholders(&self, template: &str) -> Vec<String> {
let mut placeholders = Vec::new();
let mut start_pos = 0;
while let Some(start) = template[start_pos..].find(&self.placeholder_prefix) {
let absolute_start = start_pos + start + self.placeholder_prefix.len();
if let Some(end) = template[absolute_start..].find(&self.placeholder_suffix) {
let placeholder = template[absolute_start..absolute_start + end].to_string();
if !placeholders.contains(&placeholder) {
placeholders.push(placeholder);
}
start_pos = absolute_start + end + self.placeholder_suffix.len();
} else {
break;
}
}
placeholders
}
pub fn validate_coverage(
&self,
template: &str,
params: &HashMap<String, ParameterValue>,
) -> Result<()> {
let placeholders = self.extract_placeholders(template);
for placeholder in placeholders {
if !params.contains_key(&placeholder) {
return Err(WorkflowError::template(format!(
"Missing parameter value for placeholder '{}'",
placeholder
)));
}
}
Ok(())
}
}
impl Default for TemplateParameterizer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parameter_value_conversions() {
let string_val = ParameterValue::String("test".to_string());
assert_eq!(string_val.as_string(), Some("test"));
let int_val = ParameterValue::Integer(42);
assert_eq!(int_val.as_integer(), Some(42));
let bool_val = ParameterValue::Boolean(true);
assert_eq!(bool_val.as_boolean(), Some(true));
}
#[test]
fn test_parameterizer_apply() {
let parameterizer = TemplateParameterizer::new();
let template = r#"{"name": "{{workflow_name}}", "version": "{{version}}"}"#;
let mut params = HashMap::new();
params.insert(
"workflow_name".to_string(),
ParameterValue::String("test-workflow".to_string()),
);
params.insert(
"version".to_string(),
ParameterValue::String("1.0.0".to_string()),
);
let result = parameterizer
.apply_parameters(template, ¶ms)
.expect("Failed to apply parameters");
assert!(result.contains("test-workflow"));
assert!(result.contains("1.0.0"));
}
#[test]
fn test_extract_placeholders() {
let parameterizer = TemplateParameterizer::new();
let template = "Hello {{name}}, your age is {{age}}";
let placeholders = parameterizer.extract_placeholders(template);
assert_eq!(placeholders.len(), 2);
assert!(placeholders.contains(&"name".to_string()));
assert!(placeholders.contains(&"age".to_string()));
}
#[test]
fn test_validate_coverage() {
let parameterizer = TemplateParameterizer::new();
let template = "{{param1}} and {{param2}}";
let mut params = HashMap::new();
params.insert(
"param1".to_string(),
ParameterValue::String("value1".to_string()),
);
assert!(parameterizer.validate_coverage(template, ¶ms).is_err());
params.insert(
"param2".to_string(),
ParameterValue::String("value2".to_string()),
);
assert!(parameterizer.validate_coverage(template, ¶ms).is_ok());
}
#[test]
fn test_custom_markers() {
let parameterizer = TemplateParameterizer::with_markers("${", "}");
let template = "Hello ${name}";
let mut params = HashMap::new();
params.insert(
"name".to_string(),
ParameterValue::String("World".to_string()),
);
let result = parameterizer
.apply_parameters(template, ¶ms)
.expect("Failed to apply");
assert_eq!(result, "Hello World");
}
#[test]
fn test_parameter_value_to_json() {
let value = ParameterValue::Integer(42);
let json = value.to_json();
assert_eq!(json, serde_json::json!(42));
let value = ParameterValue::Boolean(true);
let json = value.to_json();
assert_eq!(json, serde_json::json!(true));
}
}