Skip to main content

oxigdal_workflow/templates/
parameterization.rs

1//! Template parameterization system.
2
3use crate::error::{Result, WorkflowError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Parameter type enumeration.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum ParameterType {
10    /// String parameter.
11    String,
12    /// Integer parameter.
13    Integer,
14    /// Float parameter.
15    Float,
16    /// Boolean parameter.
17    Boolean,
18    /// Array parameter.
19    Array,
20    /// Object parameter.
21    Object,
22    /// File path parameter.
23    FilePath,
24    /// URL parameter.
25    Url,
26    /// Enum parameter (with allowed values).
27    Enum {
28        /// List of allowed values for this enum.
29        allowed_values: Vec<String>,
30    },
31}
32
33/// Parameter definition.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Parameter {
36    /// Parameter name.
37    pub name: String,
38    /// Parameter type.
39    pub param_type: ParameterType,
40    /// Parameter description.
41    pub description: String,
42    /// Whether the parameter is required.
43    pub required: bool,
44    /// Default value (if not required).
45    pub default_value: Option<ParameterValue>,
46    /// Parameter constraints.
47    pub constraints: Option<ParameterConstraints>,
48}
49
50/// Parameter value.
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde(untagged)]
53pub enum ParameterValue {
54    /// String value.
55    String(String),
56    /// Integer value.
57    Integer(i64),
58    /// Float value.
59    Float(f64),
60    /// Boolean value.
61    Boolean(bool),
62    /// Array value.
63    Array(Vec<ParameterValue>),
64    /// Object value.
65    Object(HashMap<String, ParameterValue>),
66}
67
68impl ParameterValue {
69    /// Convert to JSON value.
70    pub fn to_json(&self) -> serde_json::Value {
71        match self {
72            Self::String(s) => serde_json::Value::String(s.clone()),
73            Self::Integer(i) => serde_json::Value::Number((*i).into()),
74            Self::Float(f) => serde_json::Number::from_f64(*f)
75                .map(serde_json::Value::Number)
76                .unwrap_or(serde_json::Value::Null),
77            Self::Boolean(b) => serde_json::Value::Bool(*b),
78            Self::Array(arr) => serde_json::Value::Array(arr.iter().map(|v| v.to_json()).collect()),
79            Self::Object(obj) => {
80                let mut map = serde_json::Map::new();
81                for (k, v) in obj {
82                    map.insert(k.clone(), v.to_json());
83                }
84                serde_json::Value::Object(map)
85            }
86        }
87    }
88
89    /// Get as string (if applicable).
90    pub fn as_string(&self) -> Option<&str> {
91        if let Self::String(s) = self {
92            Some(s)
93        } else {
94            None
95        }
96    }
97
98    /// Get as integer (if applicable).
99    pub fn as_integer(&self) -> Option<i64> {
100        if let Self::Integer(i) = self {
101            Some(*i)
102        } else {
103            None
104        }
105    }
106
107    /// Get as float (if applicable).
108    pub fn as_float(&self) -> Option<f64> {
109        if let Self::Float(f) = self {
110            Some(*f)
111        } else {
112            None
113        }
114    }
115
116    /// Get as boolean (if applicable).
117    pub fn as_boolean(&self) -> Option<bool> {
118        if let Self::Boolean(b) = self {
119            Some(*b)
120        } else {
121            None
122        }
123    }
124}
125
126/// Parameter constraints.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ParameterConstraints {
129    /// Minimum value (for numeric types).
130    pub min: Option<f64>,
131    /// Maximum value (for numeric types).
132    pub max: Option<f64>,
133    /// Minimum length (for strings/arrays).
134    pub min_length: Option<usize>,
135    /// Maximum length (for strings/arrays).
136    pub max_length: Option<usize>,
137    /// Regex pattern (for strings).
138    pub pattern: Option<String>,
139}
140
141/// Template parameterizer for applying parameters to templates.
142pub struct TemplateParameterizer {
143    placeholder_prefix: String,
144    placeholder_suffix: String,
145}
146
147impl TemplateParameterizer {
148    /// Create a new template parameterizer.
149    pub fn new() -> Self {
150        Self {
151            placeholder_prefix: "{{".to_string(),
152            placeholder_suffix: "}}".to_string(),
153        }
154    }
155
156    /// Create a parameterizer with custom placeholder markers.
157    pub fn with_markers<S: Into<String>>(prefix: S, suffix: S) -> Self {
158        Self {
159            placeholder_prefix: prefix.into(),
160            placeholder_suffix: suffix.into(),
161        }
162    }
163
164    /// Apply parameters to a template string.
165    pub fn apply_parameters(
166        &self,
167        template: &str,
168        params: &HashMap<String, ParameterValue>,
169    ) -> Result<String> {
170        let mut result = template.to_string();
171
172        for (name, value) in params {
173            let placeholder = format!(
174                "{}{}{}",
175                self.placeholder_prefix, name, self.placeholder_suffix
176            );
177
178            let replacement = match value {
179                ParameterValue::String(s) => s.clone(),
180                ParameterValue::Integer(i) => i.to_string(),
181                ParameterValue::Float(f) => f.to_string(),
182                ParameterValue::Boolean(b) => b.to_string(),
183                ParameterValue::Array(_) | ParameterValue::Object(_) => {
184                    serde_json::to_string(&value.to_json()).map_err(|e| {
185                        WorkflowError::template(format!("Failed to serialize value: {}", e))
186                    })?
187                }
188            };
189
190            result = result.replace(&placeholder, &replacement);
191        }
192
193        // Check for remaining placeholders
194        if result.contains(&self.placeholder_prefix) && result.contains(&self.placeholder_suffix) {
195            return Err(WorkflowError::template(
196                "Template contains unreplaced placeholders",
197            ));
198        }
199
200        Ok(result)
201    }
202
203    /// Extract placeholders from a template.
204    pub fn extract_placeholders(&self, template: &str) -> Vec<String> {
205        let mut placeholders = Vec::new();
206        let mut start_pos = 0;
207
208        while let Some(start) = template[start_pos..].find(&self.placeholder_prefix) {
209            let absolute_start = start_pos + start + self.placeholder_prefix.len();
210
211            if let Some(end) = template[absolute_start..].find(&self.placeholder_suffix) {
212                let placeholder = template[absolute_start..absolute_start + end].to_string();
213                if !placeholders.contains(&placeholder) {
214                    placeholders.push(placeholder);
215                }
216                start_pos = absolute_start + end + self.placeholder_suffix.len();
217            } else {
218                break;
219            }
220        }
221
222        placeholders
223    }
224
225    /// Validate that all placeholders can be filled.
226    pub fn validate_coverage(
227        &self,
228        template: &str,
229        params: &HashMap<String, ParameterValue>,
230    ) -> Result<()> {
231        let placeholders = self.extract_placeholders(template);
232
233        for placeholder in placeholders {
234            if !params.contains_key(&placeholder) {
235                return Err(WorkflowError::template(format!(
236                    "Missing parameter value for placeholder '{}'",
237                    placeholder
238                )));
239            }
240        }
241
242        Ok(())
243    }
244}
245
246impl Default for TemplateParameterizer {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_parameter_value_conversions() {
258        let string_val = ParameterValue::String("test".to_string());
259        assert_eq!(string_val.as_string(), Some("test"));
260
261        let int_val = ParameterValue::Integer(42);
262        assert_eq!(int_val.as_integer(), Some(42));
263
264        let bool_val = ParameterValue::Boolean(true);
265        assert_eq!(bool_val.as_boolean(), Some(true));
266    }
267
268    #[test]
269    fn test_parameterizer_apply() {
270        let parameterizer = TemplateParameterizer::new();
271        let template = r#"{"name": "{{workflow_name}}", "version": "{{version}}"}"#;
272
273        let mut params = HashMap::new();
274        params.insert(
275            "workflow_name".to_string(),
276            ParameterValue::String("test-workflow".to_string()),
277        );
278        params.insert(
279            "version".to_string(),
280            ParameterValue::String("1.0.0".to_string()),
281        );
282
283        let result = parameterizer
284            .apply_parameters(template, &params)
285            .expect("Failed to apply parameters");
286
287        assert!(result.contains("test-workflow"));
288        assert!(result.contains("1.0.0"));
289    }
290
291    #[test]
292    fn test_extract_placeholders() {
293        let parameterizer = TemplateParameterizer::new();
294        let template = "Hello {{name}}, your age is {{age}}";
295
296        let placeholders = parameterizer.extract_placeholders(template);
297
298        assert_eq!(placeholders.len(), 2);
299        assert!(placeholders.contains(&"name".to_string()));
300        assert!(placeholders.contains(&"age".to_string()));
301    }
302
303    #[test]
304    fn test_validate_coverage() {
305        let parameterizer = TemplateParameterizer::new();
306        let template = "{{param1}} and {{param2}}";
307
308        let mut params = HashMap::new();
309        params.insert(
310            "param1".to_string(),
311            ParameterValue::String("value1".to_string()),
312        );
313
314        // Missing param2
315        assert!(parameterizer.validate_coverage(template, &params).is_err());
316
317        params.insert(
318            "param2".to_string(),
319            ParameterValue::String("value2".to_string()),
320        );
321
322        // Now should be ok
323        assert!(parameterizer.validate_coverage(template, &params).is_ok());
324    }
325
326    #[test]
327    fn test_custom_markers() {
328        let parameterizer = TemplateParameterizer::with_markers("${", "}");
329        let template = "Hello ${name}";
330
331        let mut params = HashMap::new();
332        params.insert(
333            "name".to_string(),
334            ParameterValue::String("World".to_string()),
335        );
336
337        let result = parameterizer
338            .apply_parameters(template, &params)
339            .expect("Failed to apply");
340
341        assert_eq!(result, "Hello World");
342    }
343
344    #[test]
345    fn test_parameter_value_to_json() {
346        let value = ParameterValue::Integer(42);
347        let json = value.to_json();
348        assert_eq!(json, serde_json::json!(42));
349
350        let value = ParameterValue::Boolean(true);
351        let json = value.to_json();
352        assert_eq!(json, serde_json::json!(true));
353    }
354}