1use crate::Workflow;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11#[cfg(feature = "openapi")]
12use utoipa::ToSchema;
13
14pub type TemplateId = Uuid;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(ToSchema))]
20pub struct WorkflowTemplate {
21 #[cfg_attr(feature = "openapi", schema(value_type = String))]
23 pub id: TemplateId,
24
25 pub name: String,
27
28 pub description: Option<String>,
30
31 pub category: Option<String>,
33
34 #[serde(default)]
36 pub tags: Vec<String>,
37
38 pub version: String,
40
41 pub parameters: Vec<TemplateParameter>,
43
44 pub workflow_json: String,
47
48 pub author: Option<String>,
50
51 #[cfg_attr(feature = "openapi", schema(value_type = String))]
53 pub created_at: DateTime<Utc>,
54
55 #[cfg_attr(feature = "openapi", schema(value_type = String))]
57 pub updated_at: DateTime<Utc>,
58
59 #[serde(default)]
61 pub usage_count: u64,
62
63 #[serde(default)]
65 pub is_public: bool,
66
67 #[cfg_attr(feature = "openapi", schema(value_type = String))]
69 pub owner_id: Option<Uuid>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[cfg_attr(feature = "openapi", derive(ToSchema))]
75pub struct TemplateParameter {
76 pub name: String,
78
79 pub label: String,
81
82 pub description: Option<String>,
84
85 pub param_type: ParameterType,
87
88 pub default_value: Option<serde_json::Value>,
90
91 #[serde(default)]
93 pub required: bool,
94
95 #[serde(default)]
97 pub validation: Option<ParameterValidation>,
98
99 #[serde(default)]
101 pub allowed_values: Vec<serde_json::Value>,
102
103 pub group: Option<String>,
105
106 #[serde(default)]
108 pub order: u32,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
113#[cfg_attr(feature = "openapi", derive(ToSchema))]
114pub enum ParameterType {
115 String,
117
118 Integer,
120
121 Float,
123
124 Boolean,
126
127 Object,
129
130 Array,
132
133 Enum,
135
136 Secret,
138
139 Model,
141
142 Collection,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, Default)]
148#[cfg_attr(feature = "openapi", derive(ToSchema))]
149pub struct ParameterValidation {
150 pub min: Option<f64>,
152
153 pub max: Option<f64>,
155
156 pub min_length: Option<usize>,
158
159 pub max_length: Option<usize>,
161
162 pub pattern: Option<String>,
164
165 pub message: Option<String>,
167}
168
169impl WorkflowTemplate {
170 pub fn new(name: String, workflow_json: String) -> Self {
172 let now = Utc::now();
173 Self {
174 id: Uuid::new_v4(),
175 name,
176 description: None,
177 category: None,
178 tags: Vec::new(),
179 version: "1.0.0".to_string(),
180 parameters: Vec::new(),
181 workflow_json,
182 author: None,
183 created_at: now,
184 updated_at: now,
185 usage_count: 0,
186 is_public: false,
187 owner_id: None,
188 }
189 }
190
191 pub fn add_parameter(&mut self, param: TemplateParameter) {
193 self.parameters.push(param);
194 }
195
196 pub fn validate_parameters(
198 &self,
199 values: &HashMap<String, serde_json::Value>,
200 ) -> Result<(), Vec<String>> {
201 let mut errors = Vec::new();
202
203 for param in &self.parameters {
204 if param.required && !values.contains_key(¶m.name) {
205 errors.push(format!("Required parameter '{}' is missing", param.name));
206 continue;
207 }
208
209 if let Some(value) = values.get(¶m.name) {
210 if !self.validate_type(¶m.param_type, value) {
212 errors.push(format!(
213 "Parameter '{}' has invalid type, expected {:?}",
214 param.name, param.param_type
215 ));
216 }
217
218 if let Some(ref validation) = param.validation {
220 if let Some(err) = self.validate_value(value, validation, ¶m.name) {
221 errors.push(err);
222 }
223 }
224
225 if param.param_type == ParameterType::Enum
227 && !param.allowed_values.is_empty()
228 && !param.allowed_values.contains(value)
229 {
230 errors.push(format!(
231 "Parameter '{}' must be one of: {:?}",
232 param.name, param.allowed_values
233 ));
234 }
235 }
236 }
237
238 if errors.is_empty() {
239 Ok(())
240 } else {
241 Err(errors)
242 }
243 }
244
245 fn validate_type(&self, param_type: &ParameterType, value: &serde_json::Value) -> bool {
246 match param_type {
247 ParameterType::String
248 | ParameterType::Secret
249 | ParameterType::Model
250 | ParameterType::Collection => value.is_string(),
251 ParameterType::Integer => value.is_i64() || value.is_u64(),
252 ParameterType::Float => value.is_f64() || value.is_i64() || value.is_u64(),
253 ParameterType::Boolean => value.is_boolean(),
254 ParameterType::Object => value.is_object(),
255 ParameterType::Array => value.is_array(),
256 ParameterType::Enum => true, }
258 }
259
260 fn validate_value(
261 &self,
262 value: &serde_json::Value,
263 validation: &ParameterValidation,
264 name: &str,
265 ) -> Option<String> {
266 if let Some(num) = value.as_f64() {
268 if let Some(min) = validation.min {
269 if num < min {
270 return Some(format!("Parameter '{}' must be >= {}", name, min));
271 }
272 }
273 if let Some(max) = validation.max {
274 if num > max {
275 return Some(format!("Parameter '{}' must be <= {}", name, max));
276 }
277 }
278 }
279
280 if let Some(s) = value.as_str() {
282 if let Some(min_len) = validation.min_length {
283 if s.len() < min_len {
284 return Some(format!(
285 "Parameter '{}' must be at least {} characters",
286 name, min_len
287 ));
288 }
289 }
290 if let Some(max_len) = validation.max_length {
291 if s.len() > max_len {
292 return Some(format!(
293 "Parameter '{}' must be at most {} characters",
294 name, max_len
295 ));
296 }
297 }
298 if let Some(ref pattern) = validation.pattern {
299 if !pattern.is_empty() {
302 }
304 }
305 }
306
307 None
308 }
309
310 pub fn instantiate(
312 &self,
313 values: &HashMap<String, serde_json::Value>,
314 ) -> Result<Workflow, String> {
315 if let Err(errors) = self.validate_parameters(values) {
317 return Err(format!(
318 "Parameter validation failed: {}",
319 errors.join(", ")
320 ));
321 }
322
323 let mut workflow_str = self.workflow_json.clone();
325
326 for param in &self.parameters {
327 let placeholder = format!("{{{{{}}}}}", param.name);
328 let value = values
329 .get(¶m.name)
330 .or(param.default_value.as_ref())
331 .map(|v| {
332 if v.is_string() {
333 v.as_str().unwrap_or("").to_string()
334 } else {
335 v.to_string()
336 }
337 })
338 .unwrap_or_default();
339
340 workflow_str = workflow_str.replace(&placeholder, &value);
341 }
342
343 let workflow: Workflow = serde_json::from_str(&workflow_str)
345 .map_err(|e| format!("Failed to parse instantiated workflow: {}", e))?;
346
347 Ok(workflow)
348 }
349
350 pub fn from_workflow(workflow: &Workflow, name: String) -> Result<Self, String> {
352 let workflow_json = serde_json::to_string_pretty(workflow)
353 .map_err(|e| format!("Failed to serialize workflow: {}", e))?;
354
355 Ok(Self::new(name, workflow_json))
356 }
357
358 pub fn extract_placeholders(&self) -> Vec<String> {
360 let mut placeholders = Vec::new();
361
362 let chars: Vec<char> = self.workflow_json.chars().collect();
364 let mut i = 0;
365 while i < chars.len().saturating_sub(3) {
366 if chars[i] == '{' && chars[i + 1] == '{' {
367 let start = i + 2;
368 let mut end = start;
369 while end < chars.len().saturating_sub(1)
370 && !(chars[end] == '}' && chars[end + 1] == '}')
371 {
372 end += 1;
373 }
374 if end < chars.len().saturating_sub(1) {
375 let name: String = chars[start..end].iter().collect();
376 let trimmed = name.trim().to_string();
377 if !trimmed.is_empty() && !placeholders.contains(&trimmed) {
378 placeholders.push(trimmed);
379 }
380 }
381 i = end + 2;
382 } else {
383 i += 1;
384 }
385 }
386
387 placeholders
388 }
389}
390
391impl TemplateParameter {
392 pub fn string(name: &str, label: &str) -> Self {
394 Self {
395 name: name.to_string(),
396 label: label.to_string(),
397 description: None,
398 param_type: ParameterType::String,
399 default_value: None,
400 required: false,
401 validation: None,
402 allowed_values: Vec::new(),
403 group: None,
404 order: 0,
405 }
406 }
407
408 pub fn integer(name: &str, label: &str) -> Self {
410 Self {
411 name: name.to_string(),
412 label: label.to_string(),
413 description: None,
414 param_type: ParameterType::Integer,
415 default_value: None,
416 required: false,
417 validation: None,
418 allowed_values: Vec::new(),
419 group: None,
420 order: 0,
421 }
422 }
423
424 pub fn boolean(name: &str, label: &str) -> Self {
426 Self {
427 name: name.to_string(),
428 label: label.to_string(),
429 description: None,
430 param_type: ParameterType::Boolean,
431 default_value: None,
432 required: false,
433 validation: None,
434 allowed_values: Vec::new(),
435 group: None,
436 order: 0,
437 }
438 }
439
440 pub fn enumeration(name: &str, label: &str, allowed: Vec<&str>) -> Self {
442 Self {
443 name: name.to_string(),
444 label: label.to_string(),
445 description: None,
446 param_type: ParameterType::Enum,
447 default_value: None,
448 required: false,
449 validation: None,
450 allowed_values: allowed
451 .into_iter()
452 .map(|s| serde_json::Value::String(s.to_string()))
453 .collect(),
454 group: None,
455 order: 0,
456 }
457 }
458
459 pub fn model(name: &str, label: &str) -> Self {
461 Self {
462 name: name.to_string(),
463 label: label.to_string(),
464 description: None,
465 param_type: ParameterType::Model,
466 default_value: None,
467 required: false,
468 validation: None,
469 allowed_values: Vec::new(),
470 group: None,
471 order: 0,
472 }
473 }
474
475 pub fn required(mut self) -> Self {
477 self.required = true;
478 self
479 }
480
481 pub fn with_default(mut self, value: serde_json::Value) -> Self {
483 self.default_value = Some(value);
484 self
485 }
486
487 pub fn with_description(mut self, desc: &str) -> Self {
489 self.description = Some(desc.to_string());
490 self
491 }
492
493 pub fn with_validation(mut self, validation: ParameterValidation) -> Self {
495 self.validation = Some(validation);
496 self
497 }
498
499 pub fn in_group(mut self, group: &str) -> Self {
501 self.group = Some(group.to_string());
502 self
503 }
504
505 pub fn with_order(mut self, order: u32) -> Self {
507 self.order = order;
508 self
509 }
510}
511
512#[derive(Debug, Serialize, Deserialize)]
514#[cfg_attr(feature = "openapi", derive(ToSchema))]
515pub struct InstantiateTemplateRequest {
516 pub workflow_name: String,
518
519 pub parameters: HashMap<String, serde_json::Value>,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
525#[cfg_attr(feature = "openapi", derive(ToSchema))]
526pub struct TemplateListItem {
527 #[cfg_attr(feature = "openapi", schema(value_type = String))]
528 pub id: TemplateId,
529 pub name: String,
530 pub description: Option<String>,
531 pub category: Option<String>,
532 pub tags: Vec<String>,
533 pub version: String,
534 pub author: Option<String>,
535 pub usage_count: u64,
536 pub is_public: bool,
537 #[cfg_attr(feature = "openapi", schema(value_type = String))]
538 pub created_at: DateTime<Utc>,
539}
540
541impl From<&WorkflowTemplate> for TemplateListItem {
542 fn from(template: &WorkflowTemplate) -> Self {
543 Self {
544 id: template.id,
545 name: template.name.clone(),
546 description: template.description.clone(),
547 category: template.category.clone(),
548 tags: template.tags.clone(),
549 version: template.version.clone(),
550 author: template.author.clone(),
551 usage_count: template.usage_count,
552 is_public: template.is_public,
553 created_at: template.created_at,
554 }
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn test_template_creation() {
564 let workflow_json = r#"{"metadata": {"name": "{{workflow_name}}"}}"#;
565 let mut template =
566 WorkflowTemplate::new("Test Template".to_string(), workflow_json.to_string());
567
568 template.add_parameter(
569 TemplateParameter::string("workflow_name", "Workflow Name")
570 .required()
571 .with_description("Name of the workflow"),
572 );
573
574 assert_eq!(template.name, "Test Template");
575 assert_eq!(template.parameters.len(), 1);
576 assert!(template.parameters[0].required);
577 }
578
579 #[test]
580 fn test_placeholder_extraction() {
581 let workflow_json =
582 r#"{"name": "{{name}}", "model": "{{model}}", "temp": {{temperature}}}"#;
583 let template = WorkflowTemplate::new("Test".to_string(), workflow_json.to_string());
584
585 let placeholders = template.extract_placeholders();
586
587 assert_eq!(placeholders.len(), 3);
588 assert!(placeholders.contains(&"name".to_string()));
589 assert!(placeholders.contains(&"model".to_string()));
590 assert!(placeholders.contains(&"temperature".to_string()));
591 }
592
593 #[test]
594 fn test_parameter_validation() {
595 let mut template = WorkflowTemplate::new("Test".to_string(), "{}".to_string());
596 template.add_parameter(
597 TemplateParameter::integer("count", "Count")
598 .required()
599 .with_validation(ParameterValidation {
600 min: Some(1.0),
601 max: Some(100.0),
602 ..Default::default()
603 }),
604 );
605
606 let values = HashMap::new();
608 assert!(template.validate_parameters(&values).is_err());
609
610 let mut values = HashMap::new();
612 values.insert("count".to_string(), serde_json::json!(0));
613 assert!(template.validate_parameters(&values).is_err());
614
615 let mut values = HashMap::new();
617 values.insert("count".to_string(), serde_json::json!(50));
618 assert!(template.validate_parameters(&values).is_ok());
619 }
620
621 #[test]
622 fn test_enum_parameter() {
623 let param = TemplateParameter::enumeration(
624 "provider",
625 "LLM Provider",
626 vec!["openai", "anthropic", "ollama"],
627 )
628 .required();
629
630 assert_eq!(param.param_type, ParameterType::Enum);
631 assert_eq!(param.allowed_values.len(), 3);
632 assert!(param.required);
633 }
634}