oxigdal_workflow/templates/
parameterization.rs1use crate::error::{Result, WorkflowError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum ParameterType {
10 String,
12 Integer,
14 Float,
16 Boolean,
18 Array,
20 Object,
22 FilePath,
24 Url,
26 Enum {
28 allowed_values: Vec<String>,
30 },
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Parameter {
36 pub name: String,
38 pub param_type: ParameterType,
40 pub description: String,
42 pub required: bool,
44 pub default_value: Option<ParameterValue>,
46 pub constraints: Option<ParameterConstraints>,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde(untagged)]
53pub enum ParameterValue {
54 String(String),
56 Integer(i64),
58 Float(f64),
60 Boolean(bool),
62 Array(Vec<ParameterValue>),
64 Object(HashMap<String, ParameterValue>),
66}
67
68impl ParameterValue {
69 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ParameterConstraints {
129 pub min: Option<f64>,
131 pub max: Option<f64>,
133 pub min_length: Option<usize>,
135 pub max_length: Option<usize>,
137 pub pattern: Option<String>,
139}
140
141pub struct TemplateParameterizer {
143 placeholder_prefix: String,
144 placeholder_suffix: String,
145}
146
147impl TemplateParameterizer {
148 pub fn new() -> Self {
150 Self {
151 placeholder_prefix: "{{".to_string(),
152 placeholder_suffix: "}}".to_string(),
153 }
154 }
155
156 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 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 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 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 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, ¶ms)
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 assert!(parameterizer.validate_coverage(template, ¶ms).is_err());
316
317 params.insert(
318 "param2".to_string(),
319 ParameterValue::String("value2".to_string()),
320 );
321
322 assert!(parameterizer.validate_coverage(template, ¶ms).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, ¶ms)
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}