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 let missing: Vec<String> = self
204 .extract_placeholders(template)
205 .into_iter()
206 .filter(|name| !params.contains_key(name))
207 .collect();
208 if !missing.is_empty() {
209 return Err(WorkflowError::template(format!(
210 "Template contains unreplaced placeholders: {}",
211 missing.join(", ")
212 )));
213 }
214
215 Ok(result)
216 }
217
218 pub fn extract_placeholders(&self, template: &str) -> Vec<String> {
220 let mut placeholders = Vec::new();
221 let mut start_pos = 0;
222
223 while let Some(start) = template[start_pos..].find(&self.placeholder_prefix) {
224 let absolute_start = start_pos + start + self.placeholder_prefix.len();
225
226 if let Some(end) = template[absolute_start..].find(&self.placeholder_suffix) {
227 let placeholder = template[absolute_start..absolute_start + end].to_string();
228 if !placeholders.contains(&placeholder) {
229 placeholders.push(placeholder);
230 }
231 start_pos = absolute_start + end + self.placeholder_suffix.len();
232 } else {
233 break;
234 }
235 }
236
237 placeholders
238 }
239
240 pub fn validate_coverage(
242 &self,
243 template: &str,
244 params: &HashMap<String, ParameterValue>,
245 ) -> Result<()> {
246 let placeholders = self.extract_placeholders(template);
247
248 for placeholder in placeholders {
249 if !params.contains_key(&placeholder) {
250 return Err(WorkflowError::template(format!(
251 "Missing parameter value for placeholder '{}'",
252 placeholder
253 )));
254 }
255 }
256
257 Ok(())
258 }
259}
260
261impl Default for TemplateParameterizer {
262 fn default() -> Self {
263 Self::new()
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_parameter_value_conversions() {
273 let string_val = ParameterValue::String("test".to_string());
274 assert_eq!(string_val.as_string(), Some("test"));
275
276 let int_val = ParameterValue::Integer(42);
277 assert_eq!(int_val.as_integer(), Some(42));
278
279 let bool_val = ParameterValue::Boolean(true);
280 assert_eq!(bool_val.as_boolean(), Some(true));
281 }
282
283 #[test]
284 fn test_parameterizer_apply() {
285 let parameterizer = TemplateParameterizer::new();
286 let template = r#"{"name": "{{workflow_name}}", "version": "{{version}}"}"#;
287
288 let mut params = HashMap::new();
289 params.insert(
290 "workflow_name".to_string(),
291 ParameterValue::String("test-workflow".to_string()),
292 );
293 params.insert(
294 "version".to_string(),
295 ParameterValue::String("1.0.0".to_string()),
296 );
297
298 let result = parameterizer
299 .apply_parameters(template, ¶ms)
300 .expect("Failed to apply parameters");
301
302 assert!(result.contains("test-workflow"));
303 assert!(result.contains("1.0.0"));
304 }
305
306 #[test]
307 fn test_extract_placeholders() {
308 let parameterizer = TemplateParameterizer::new();
309 let template = "Hello {{name}}, your age is {{age}}";
310
311 let placeholders = parameterizer.extract_placeholders(template);
312
313 assert_eq!(placeholders.len(), 2);
314 assert!(placeholders.contains(&"name".to_string()));
315 assert!(placeholders.contains(&"age".to_string()));
316 }
317
318 #[test]
319 fn test_validate_coverage() {
320 let parameterizer = TemplateParameterizer::new();
321 let template = "{{param1}} and {{param2}}";
322
323 let mut params = HashMap::new();
324 params.insert(
325 "param1".to_string(),
326 ParameterValue::String("value1".to_string()),
327 );
328
329 assert!(parameterizer.validate_coverage(template, ¶ms).is_err());
331
332 params.insert(
333 "param2".to_string(),
334 ParameterValue::String("value2".to_string()),
335 );
336
337 assert!(parameterizer.validate_coverage(template, ¶ms).is_ok());
339 }
340
341 #[test]
342 fn test_custom_markers() {
343 let parameterizer = TemplateParameterizer::with_markers("${", "}");
344 let template = "Hello ${name}";
345
346 let mut params = HashMap::new();
347 params.insert(
348 "name".to_string(),
349 ParameterValue::String("World".to_string()),
350 );
351
352 let result = parameterizer
353 .apply_parameters(template, ¶ms)
354 .expect("Failed to apply");
355
356 assert_eq!(result, "Hello World");
357 }
358
359 #[test]
360 fn test_apply_allows_value_with_literal_braces() {
361 let parameterizer = TemplateParameterizer::new();
364 let template = "value = {{payload}}";
365
366 let mut params = HashMap::new();
367 params.insert(
368 "payload".to_string(),
369 ParameterValue::String("literal {{brace}} text".to_string()),
370 );
371
372 let result = parameterizer
373 .apply_parameters(template, ¶ms)
374 .expect("value containing braces should not be rejected");
375 assert_eq!(result, "value = literal {{brace}} text");
376 }
377
378 #[test]
379 fn test_apply_detects_missing_placeholder() {
380 let parameterizer = TemplateParameterizer::new();
382 let template = "{{provided}} and {{missing}}";
383
384 let mut params = HashMap::new();
385 params.insert(
386 "provided".to_string(),
387 ParameterValue::String("ok".to_string()),
388 );
389
390 let err = parameterizer
391 .apply_parameters(template, ¶ms)
392 .expect_err("missing placeholder should error");
393 assert!(err.to_string().contains("missing"));
394 }
395
396 #[test]
397 fn test_apply_allows_array_value_with_braces() {
398 let parameterizer = TemplateParameterizer::new();
400 let template = r#"{"items": {{items}}}"#;
401
402 let mut params = HashMap::new();
403 params.insert(
404 "items".to_string(),
405 ParameterValue::Array(vec![ParameterValue::Integer(1), ParameterValue::Integer(2)]),
406 );
407
408 let result = parameterizer
409 .apply_parameters(template, ¶ms)
410 .expect("array value should not be rejected");
411 assert!(result.contains("[1,2]"));
412 }
413
414 #[test]
415 fn test_parameter_value_to_json() {
416 let value = ParameterValue::Integer(42);
417 let json = value.to_json();
418 assert_eq!(json, serde_json::json!(42));
419
420 let value = ParameterValue::Boolean(true);
421 let json = value.to_json();
422 assert_eq!(json, serde_json::json!(true));
423 }
424}