1#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TemplateCategory {
13 Ingest,
15 Transcode,
17 Delivery,
19 Archive,
21 QC,
23 Distribution,
25}
26
27#[derive(Debug, Clone, PartialEq)]
29pub enum ParamType {
30 String,
32 Integer,
34 Float,
36 Bool,
38 FilePath,
40 Enum(Vec<std::string::String>),
42}
43
44#[derive(Debug, Clone)]
46pub struct TemplateParam {
47 pub name: std::string::String,
49 pub param_type: ParamType,
51 pub default_value: Option<std::string::String>,
53 pub required: bool,
55}
56
57impl TemplateParam {
58 #[must_use]
60 pub fn required(name: impl Into<std::string::String>, param_type: ParamType) -> Self {
61 Self {
62 name: name.into(),
63 param_type,
64 default_value: None,
65 required: true,
66 }
67 }
68
69 #[must_use]
71 pub fn optional(
72 name: impl Into<std::string::String>,
73 param_type: ParamType,
74 default_value: impl Into<std::string::String>,
75 ) -> Self {
76 Self {
77 name: name.into(),
78 param_type,
79 default_value: Some(default_value.into()),
80 required: false,
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum ErrorAction {
88 Fail,
90 Retry(u32),
92 Skip,
94 Notify,
96}
97
98#[derive(Debug, Clone)]
100pub struct TemplateStep {
101 pub step_type: std::string::String,
103 pub params: HashMap<std::string::String, std::string::String>,
105 pub on_error: ErrorAction,
107}
108
109impl TemplateStep {
110 #[must_use]
112 pub fn new(step_type: impl Into<std::string::String>, on_error: ErrorAction) -> Self {
113 Self {
114 step_type: step_type.into(),
115 params: HashMap::new(),
116 on_error,
117 }
118 }
119
120 #[must_use]
122 pub fn with_param(
123 mut self,
124 key: impl Into<std::string::String>,
125 value: impl Into<std::string::String>,
126 ) -> Self {
127 self.params.insert(key.into(), value.into());
128 self
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct WorkflowTemplate {
135 pub name: std::string::String,
137 pub description: std::string::String,
139 pub category: TemplateCategory,
141 pub parameters: Vec<TemplateParam>,
143 pub steps: Vec<TemplateStep>,
145}
146
147impl WorkflowTemplate {
148 #[must_use]
150 pub fn new(
151 name: impl Into<std::string::String>,
152 description: impl Into<std::string::String>,
153 category: TemplateCategory,
154 ) -> Self {
155 Self {
156 name: name.into(),
157 description: description.into(),
158 category,
159 parameters: Vec::new(),
160 steps: Vec::new(),
161 }
162 }
163
164 #[must_use]
166 pub fn with_parameter(mut self, param: TemplateParam) -> Self {
167 self.parameters.push(param);
168 self
169 }
170
171 #[must_use]
173 pub fn with_step(mut self, step: TemplateStep) -> Self {
174 self.steps.push(step);
175 self
176 }
177}
178
179pub struct TemplateLibrary;
181
182impl TemplateLibrary {
183 #[must_use]
187 pub fn ingest_and_transcode() -> WorkflowTemplate {
188 WorkflowTemplate::new(
189 "ingest-and-transcode",
190 "Watch folder for incoming media, validate, transcode, and deliver",
191 TemplateCategory::Ingest,
192 )
193 .with_parameter(TemplateParam::required("watch_folder", ParamType::FilePath))
194 .with_parameter(TemplateParam::required(
195 "output_folder",
196 ParamType::FilePath,
197 ))
198 .with_parameter(TemplateParam::optional(
199 "transcode_preset",
200 ParamType::Enum(vec![
201 "broadcast_hd".to_string(),
202 "web_h264".to_string(),
203 "archive_prores".to_string(),
204 ]),
205 "web_h264",
206 ))
207 .with_parameter(TemplateParam::optional(
208 "validate_on_ingest",
209 ParamType::Bool,
210 "true",
211 ))
212 .with_step(
213 TemplateStep::new("watch_folder", ErrorAction::Fail)
214 .with_param("path", "{{watch_folder}}")
215 .with_param("pattern", "*.{mxf,mp4,mov}"),
216 )
217 .with_step(
218 TemplateStep::new("validate", ErrorAction::Retry(2))
219 .with_param("enabled", "{{validate_on_ingest}}")
220 .with_param("checks", "format,integrity,container"),
221 )
222 .with_step(
223 TemplateStep::new("transcode", ErrorAction::Retry(1))
224 .with_param("preset", "{{transcode_preset}}")
225 .with_param("output", "{{output_folder}}"),
226 )
227 .with_step(
228 TemplateStep::new("deliver", ErrorAction::Notify)
229 .with_param("destination", "{{output_folder}}")
230 .with_param("verify_checksum", "true"),
231 )
232 }
233
234 #[must_use]
238 pub fn archive_workflow() -> WorkflowTemplate {
239 WorkflowTemplate::new(
240 "archive-workflow",
241 "Validate media, compute checksums, archive to long-term storage, and notify",
242 TemplateCategory::Archive,
243 )
244 .with_parameter(TemplateParam::required("source_path", ParamType::FilePath))
245 .with_parameter(TemplateParam::required(
246 "archive_destination",
247 ParamType::FilePath,
248 ))
249 .with_parameter(TemplateParam::optional(
250 "checksum_algorithm",
251 ParamType::Enum(vec![
252 "md5".to_string(),
253 "sha256".to_string(),
254 "xxhash".to_string(),
255 ]),
256 "sha256",
257 ))
258 .with_parameter(TemplateParam::optional(
259 "notify_email",
260 ParamType::String,
261 "",
262 ))
263 .with_step(
264 TemplateStep::new("validate", ErrorAction::Fail)
265 .with_param("source", "{{source_path}}")
266 .with_param("strict", "true"),
267 )
268 .with_step(
269 TemplateStep::new("checksum", ErrorAction::Fail)
270 .with_param("algorithm", "{{checksum_algorithm}}")
271 .with_param("output_manifest", "true"),
272 )
273 .with_step(
274 TemplateStep::new("archive", ErrorAction::Retry(3))
275 .with_param("destination", "{{archive_destination}}")
276 .with_param("verify_after_copy", "true"),
277 )
278 .with_step(
279 TemplateStep::new("notify", ErrorAction::Skip)
280 .with_param("channel", "email")
281 .with_param("to", "{{notify_email}}")
282 .with_param("message", "Archive complete"),
283 )
284 }
285
286 #[must_use]
290 pub fn qc_review() -> WorkflowTemplate {
291 WorkflowTemplate::new(
292 "qc-review",
293 "Automated QC analysis, report generation, and approve/reject decision",
294 TemplateCategory::QC,
295 )
296 .with_parameter(TemplateParam::required("media_path", ParamType::FilePath))
297 .with_parameter(TemplateParam::optional(
298 "qc_profile",
299 ParamType::Enum(vec![
300 "broadcast".to_string(),
301 "streaming".to_string(),
302 "cinema".to_string(),
303 ]),
304 "broadcast",
305 ))
306 .with_parameter(TemplateParam::optional(
307 "report_output",
308 ParamType::FilePath,
309 "/tmp/qc_report.json",
310 ))
311 .with_parameter(TemplateParam::optional(
312 "auto_approve_threshold",
313 ParamType::Float,
314 "0.95",
315 ))
316 .with_step(
317 TemplateStep::new("analyze", ErrorAction::Fail)
318 .with_param("input", "{{media_path}}")
319 .with_param("profile", "{{qc_profile}}")
320 .with_param("checks", "loudness,black_frames,freeze,color_bars"),
321 )
322 .with_step(
323 TemplateStep::new("generate_report", ErrorAction::Notify)
324 .with_param("output", "{{report_output}}")
325 .with_param("format", "json"),
326 )
327 .with_step(
328 TemplateStep::new("approve_or_reject", ErrorAction::Fail)
329 .with_param("auto_threshold", "{{auto_approve_threshold}}")
330 .with_param("manual_review_on_fail", "true"),
331 )
332 }
333
334 #[must_use]
336 pub fn all() -> Vec<WorkflowTemplate> {
337 vec![
338 Self::ingest_and_transcode(),
339 Self::archive_workflow(),
340 Self::qc_review(),
341 ]
342 }
343
344 #[must_use]
346 pub fn find_by_name(name: &str) -> Option<WorkflowTemplate> {
347 Self::all().into_iter().find(|t| t.name == name)
348 }
349
350 #[must_use]
352 pub fn find_by_category(category: &TemplateCategory) -> Vec<WorkflowTemplate> {
353 Self::all()
354 .into_iter()
355 .filter(|t| &t.category == category)
356 .collect()
357 }
358}
359
360pub struct TemplateInstantiator;
362
363impl TemplateInstantiator {
364 pub fn instantiate(
370 template: &WorkflowTemplate,
371 params: HashMap<std::string::String, std::string::String>,
372 ) -> Result<std::string::String, std::string::String> {
373 for param in &template.parameters {
375 if param.required && !params.contains_key(¶m.name) {
376 return Err(format!("Required parameter '{}' is missing", param.name));
377 }
378 }
379
380 let mut resolved_params: HashMap<std::string::String, std::string::String> = HashMap::new();
382 for param in &template.parameters {
383 if let Some(value) = params.get(¶m.name) {
384 resolved_params.insert(param.name.clone(), value.clone());
385 } else if let Some(default) = ¶m.default_value {
386 resolved_params.insert(param.name.clone(), default.clone());
387 }
388 }
389
390 let steps_json: Vec<std::string::String> = template
392 .steps
393 .iter()
394 .map(|step| {
395 let params_json: Vec<std::string::String> = step
396 .params
397 .iter()
398 .map(|(k, v)| {
399 let resolved = substitute_params(v, &resolved_params);
400 format!("\"{k}\":\"{resolved}\"")
401 })
402 .collect();
403
404 let on_error_str = match &step.on_error {
405 ErrorAction::Fail => "\"fail\"",
406 ErrorAction::Skip => "\"skip\"",
407 ErrorAction::Notify => "\"notify\"",
408 ErrorAction::Retry(n) => {
409 return format!(
411 "{{\"step_type\":\"{}\",\"on_error\":{{\"retry\":{}}},\"params\":{{{}}}}}",
412 step.step_type,
413 n,
414 params_json.join(",")
415 );
416 }
417 };
418
419 format!(
420 "{{\"step_type\":\"{}\",\"on_error\":{},\"params\":{{{}}}}}",
421 step.step_type,
422 on_error_str,
423 params_json.join(",")
424 )
425 })
426 .collect();
427
428 let params_json: Vec<std::string::String> = resolved_params
429 .iter()
430 .map(|(k, v)| format!("\"{k}\":\"{v}\""))
431 .collect();
432
433 Ok(format!(
434 "{{\"name\":\"{}\",\"category\":\"{:?}\",\"params\":{{{}}},\"steps\":[{}]}}",
435 template.name,
436 template.category,
437 params_json.join(","),
438 steps_json.join(",")
439 ))
440 }
441}
442
443fn substitute_params(
445 value: &str,
446 params: &HashMap<std::string::String, std::string::String>,
447) -> std::string::String {
448 let mut result = value.to_string();
449 for (key, val) in params {
450 let placeholder = format!("{{{{{key}}}}}");
451 result = result.replace(&placeholder, val);
452 }
453 result
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_ingest_and_transcode_template() {
462 let template = TemplateLibrary::ingest_and_transcode();
463 assert_eq!(template.name, "ingest-and-transcode");
464 assert_eq!(template.category, TemplateCategory::Ingest);
465 assert!(!template.steps.is_empty());
466 assert!(!template.parameters.is_empty());
467 }
468
469 #[test]
470 fn test_archive_workflow_template() {
471 let template = TemplateLibrary::archive_workflow();
472 assert_eq!(template.name, "archive-workflow");
473 assert_eq!(template.category, TemplateCategory::Archive);
474 assert_eq!(template.steps.len(), 4);
475 }
476
477 #[test]
478 fn test_qc_review_template() {
479 let template = TemplateLibrary::qc_review();
480 assert_eq!(template.name, "qc-review");
481 assert_eq!(template.category, TemplateCategory::QC);
482 assert_eq!(template.steps.len(), 3);
483 }
484
485 #[test]
486 fn test_template_library_all() {
487 let all = TemplateLibrary::all();
488 assert_eq!(all.len(), 3);
489 }
490
491 #[test]
492 fn test_find_by_name() {
493 let template = TemplateLibrary::find_by_name("archive-workflow");
494 assert!(template.is_some());
495 assert_eq!(
496 template.expect("should succeed in test").name,
497 "archive-workflow"
498 );
499 }
500
501 #[test]
502 fn test_find_by_name_not_found() {
503 let template = TemplateLibrary::find_by_name("nonexistent");
504 assert!(template.is_none());
505 }
506
507 #[test]
508 fn test_find_by_category() {
509 let templates = TemplateLibrary::find_by_category(&TemplateCategory::Ingest);
510 assert!(!templates.is_empty());
511 for t in &templates {
512 assert_eq!(t.category, TemplateCategory::Ingest);
513 }
514 }
515
516 #[test]
517 fn test_instantiate_with_required_params() {
518 let template = TemplateLibrary::ingest_and_transcode();
519 let mut params = HashMap::new();
520 params.insert("watch_folder".to_string(), "/mnt/ingest".to_string());
521 params.insert("output_folder".to_string(), "/mnt/output".to_string());
522
523 let result = TemplateInstantiator::instantiate(&template, params);
524 assert!(result.is_ok());
525 let json = result.expect("should succeed in test");
526 assert!(json.contains("ingest-and-transcode"));
527 assert!(json.contains("/mnt/ingest"));
528 }
529
530 #[test]
531 fn test_instantiate_missing_required_param() {
532 let template = TemplateLibrary::ingest_and_transcode();
533 let params = HashMap::new(); let result = TemplateInstantiator::instantiate(&template, params);
536 assert!(result.is_err());
537 let err = result.unwrap_err();
538 assert!(err.contains("Required parameter"));
539 }
540
541 #[test]
542 fn test_instantiate_with_defaults() {
543 let template = TemplateLibrary::archive_workflow();
544 let mut params = HashMap::new();
545 params.insert("source_path".to_string(), "/src".to_string());
546 params.insert("archive_destination".to_string(), "/archive".to_string());
547 let result = TemplateInstantiator::instantiate(&template, params);
550 assert!(result.is_ok());
551 let json = result.expect("should succeed in test");
552 assert!(json.contains("sha256")); }
554
555 #[test]
556 fn test_param_type_enum() {
557 let param = TemplateParam::optional(
558 "preset",
559 ParamType::Enum(vec!["a".to_string(), "b".to_string()]),
560 "a",
561 );
562 assert_eq!(param.name, "preset");
563 assert!(!param.required);
564 assert_eq!(param.default_value, Some("a".to_string()));
565 }
566
567 #[test]
568 fn test_error_action_retry() {
569 let step = TemplateStep::new("transcode", ErrorAction::Retry(3));
570 assert_eq!(step.on_error, ErrorAction::Retry(3));
571 }
572
573 #[test]
574 fn test_substitute_params() {
575 let mut params = HashMap::new();
576 params.insert("folder".to_string(), "/mnt/data".to_string());
577 let result = substitute_params("input={{folder}}/file.mp4", ¶ms);
578 assert_eq!(result, "input=/mnt/data/file.mp4");
579 }
580}