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 let tmp = std::env::temp_dir();
292 WorkflowTemplate::new(
293 "qc-review",
294 "Automated QC analysis, report generation, and approve/reject decision",
295 TemplateCategory::QC,
296 )
297 .with_parameter(TemplateParam::required("media_path", ParamType::FilePath))
298 .with_parameter(TemplateParam::optional(
299 "qc_profile",
300 ParamType::Enum(vec![
301 "broadcast".to_string(),
302 "streaming".to_string(),
303 "cinema".to_string(),
304 ]),
305 "broadcast",
306 ))
307 .with_parameter(TemplateParam::optional(
308 "report_output",
309 ParamType::FilePath,
310 tmp.join("oximedia-qc-report.json")
311 .to_string_lossy()
312 .into_owned(),
313 ))
314 .with_parameter(TemplateParam::optional(
315 "auto_approve_threshold",
316 ParamType::Float,
317 "0.95",
318 ))
319 .with_step(
320 TemplateStep::new("analyze", ErrorAction::Fail)
321 .with_param("input", "{{media_path}}")
322 .with_param("profile", "{{qc_profile}}")
323 .with_param("checks", "loudness,black_frames,freeze,color_bars"),
324 )
325 .with_step(
326 TemplateStep::new("generate_report", ErrorAction::Notify)
327 .with_param("output", "{{report_output}}")
328 .with_param("format", "json"),
329 )
330 .with_step(
331 TemplateStep::new("approve_or_reject", ErrorAction::Fail)
332 .with_param("auto_threshold", "{{auto_approve_threshold}}")
333 .with_param("manual_review_on_fail", "true"),
334 )
335 }
336
337 #[must_use]
339 pub fn all() -> Vec<WorkflowTemplate> {
340 vec![
341 Self::ingest_and_transcode(),
342 Self::archive_workflow(),
343 Self::qc_review(),
344 ]
345 }
346
347 #[must_use]
349 pub fn find_by_name(name: &str) -> Option<WorkflowTemplate> {
350 Self::all().into_iter().find(|t| t.name == name)
351 }
352
353 #[must_use]
355 pub fn find_by_category(category: &TemplateCategory) -> Vec<WorkflowTemplate> {
356 Self::all()
357 .into_iter()
358 .filter(|t| &t.category == category)
359 .collect()
360 }
361}
362
363pub struct TemplateInstantiator;
365
366impl TemplateInstantiator {
367 pub fn instantiate(
373 template: &WorkflowTemplate,
374 params: HashMap<std::string::String, std::string::String>,
375 ) -> Result<std::string::String, std::string::String> {
376 for param in &template.parameters {
378 if param.required && !params.contains_key(¶m.name) {
379 return Err(format!("Required parameter '{}' is missing", param.name));
380 }
381 }
382
383 let mut resolved_params: HashMap<std::string::String, std::string::String> = HashMap::new();
385 for param in &template.parameters {
386 if let Some(value) = params.get(¶m.name) {
387 resolved_params.insert(param.name.clone(), value.clone());
388 } else if let Some(default) = ¶m.default_value {
389 resolved_params.insert(param.name.clone(), default.clone());
390 }
391 }
392
393 let steps_json: Vec<std::string::String> = template
395 .steps
396 .iter()
397 .map(|step| {
398 let params_json: Vec<std::string::String> = step
399 .params
400 .iter()
401 .map(|(k, v)| {
402 let resolved = substitute_params(v, &resolved_params);
403 format!("\"{k}\":\"{resolved}\"")
404 })
405 .collect();
406
407 let on_error_str = match &step.on_error {
408 ErrorAction::Fail => "\"fail\"",
409 ErrorAction::Skip => "\"skip\"",
410 ErrorAction::Notify => "\"notify\"",
411 ErrorAction::Retry(n) => {
412 return format!(
414 "{{\"step_type\":\"{}\",\"on_error\":{{\"retry\":{}}},\"params\":{{{}}}}}",
415 step.step_type,
416 n,
417 params_json.join(",")
418 );
419 }
420 };
421
422 format!(
423 "{{\"step_type\":\"{}\",\"on_error\":{},\"params\":{{{}}}}}",
424 step.step_type,
425 on_error_str,
426 params_json.join(",")
427 )
428 })
429 .collect();
430
431 let params_json: Vec<std::string::String> = resolved_params
432 .iter()
433 .map(|(k, v)| format!("\"{k}\":\"{v}\""))
434 .collect();
435
436 Ok(format!(
437 "{{\"name\":\"{}\",\"category\":\"{:?}\",\"params\":{{{}}},\"steps\":[{}]}}",
438 template.name,
439 template.category,
440 params_json.join(","),
441 steps_json.join(",")
442 ))
443 }
444}
445
446fn substitute_params(
448 value: &str,
449 params: &HashMap<std::string::String, std::string::String>,
450) -> std::string::String {
451 let mut result = value.to_string();
452 for (key, val) in params {
453 let placeholder = format!("{{{{{key}}}}}");
454 result = result.replace(&placeholder, val);
455 }
456 result
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_ingest_and_transcode_template() {
465 let template = TemplateLibrary::ingest_and_transcode();
466 assert_eq!(template.name, "ingest-and-transcode");
467 assert_eq!(template.category, TemplateCategory::Ingest);
468 assert!(!template.steps.is_empty());
469 assert!(!template.parameters.is_empty());
470 }
471
472 #[test]
473 fn test_archive_workflow_template() {
474 let template = TemplateLibrary::archive_workflow();
475 assert_eq!(template.name, "archive-workflow");
476 assert_eq!(template.category, TemplateCategory::Archive);
477 assert_eq!(template.steps.len(), 4);
478 }
479
480 #[test]
481 fn test_qc_review_template() {
482 let template = TemplateLibrary::qc_review();
483 assert_eq!(template.name, "qc-review");
484 assert_eq!(template.category, TemplateCategory::QC);
485 assert_eq!(template.steps.len(), 3);
486 }
487
488 #[test]
489 fn test_template_library_all() {
490 let all = TemplateLibrary::all();
491 assert_eq!(all.len(), 3);
492 }
493
494 #[test]
495 fn test_find_by_name() {
496 let template = TemplateLibrary::find_by_name("archive-workflow");
497 assert!(template.is_some());
498 assert_eq!(
499 template.expect("should succeed in test").name,
500 "archive-workflow"
501 );
502 }
503
504 #[test]
505 fn test_find_by_name_not_found() {
506 let template = TemplateLibrary::find_by_name("nonexistent");
507 assert!(template.is_none());
508 }
509
510 #[test]
511 fn test_find_by_category() {
512 let templates = TemplateLibrary::find_by_category(&TemplateCategory::Ingest);
513 assert!(!templates.is_empty());
514 for t in &templates {
515 assert_eq!(t.category, TemplateCategory::Ingest);
516 }
517 }
518
519 #[test]
520 fn test_instantiate_with_required_params() {
521 let template = TemplateLibrary::ingest_and_transcode();
522 let mut params = HashMap::new();
523 params.insert("watch_folder".to_string(), "/mnt/ingest".to_string());
524 params.insert("output_folder".to_string(), "/mnt/output".to_string());
525
526 let result = TemplateInstantiator::instantiate(&template, params);
527 assert!(result.is_ok());
528 let json = result.expect("should succeed in test");
529 assert!(json.contains("ingest-and-transcode"));
530 assert!(json.contains("/mnt/ingest"));
531 }
532
533 #[test]
534 fn test_instantiate_missing_required_param() {
535 let template = TemplateLibrary::ingest_and_transcode();
536 let params = HashMap::new(); let result = TemplateInstantiator::instantiate(&template, params);
539 assert!(result.is_err());
540 let err = result.unwrap_err();
541 assert!(err.contains("Required parameter"));
542 }
543
544 #[test]
545 fn test_instantiate_with_defaults() {
546 let template = TemplateLibrary::archive_workflow();
547 let mut params = HashMap::new();
548 params.insert("source_path".to_string(), "/src".to_string());
549 params.insert("archive_destination".to_string(), "/archive".to_string());
550 let result = TemplateInstantiator::instantiate(&template, params);
553 assert!(result.is_ok());
554 let json = result.expect("should succeed in test");
555 assert!(json.contains("sha256")); }
557
558 #[test]
559 fn test_param_type_enum() {
560 let param = TemplateParam::optional(
561 "preset",
562 ParamType::Enum(vec!["a".to_string(), "b".to_string()]),
563 "a",
564 );
565 assert_eq!(param.name, "preset");
566 assert!(!param.required);
567 assert_eq!(param.default_value, Some("a".to_string()));
568 }
569
570 #[test]
571 fn test_error_action_retry() {
572 let step = TemplateStep::new("transcode", ErrorAction::Retry(3));
573 assert_eq!(step.on_error, ErrorAction::Retry(3));
574 }
575
576 #[test]
577 fn test_substitute_params() {
578 let mut params = HashMap::new();
579 params.insert("folder".to_string(), "/mnt/data".to_string());
580 let result = substitute_params("input={{folder}}/file.mp4", ¶ms);
581 assert_eq!(result, "input=/mnt/data/file.mp4");
582 }
583}