1#![allow(dead_code)]
2use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum ConditionValue {
13 Str(String),
15 Int(i64),
17 Float(f64),
19 Bool(bool),
21 Null,
23}
24
25impl ConditionValue {
26 #[must_use]
28 pub fn as_bool(&self) -> Option<bool> {
29 match self {
30 Self::Bool(b) => Some(*b),
31 Self::Int(n) => Some(*n != 0),
32 Self::Str(s) => match s.to_lowercase().as_str() {
33 "true" | "yes" | "1" => Some(true),
34 "false" | "no" | "0" | "" => Some(false),
35 _ => None,
36 },
37 Self::Null => Some(false),
38 Self::Float(_) => None,
39 }
40 }
41
42 #[allow(clippy::cast_precision_loss)]
44 #[must_use]
45 pub fn as_int(&self) -> Option<i64> {
46 match self {
47 Self::Int(n) => Some(*n),
48 Self::Float(f) => Some(*f as i64),
49 Self::Str(s) => s.parse().ok(),
50 Self::Bool(b) => Some(i64::from(*b)),
51 Self::Null => None,
52 }
53 }
54
55 #[allow(clippy::cast_precision_loss)]
57 #[must_use]
58 pub fn as_float(&self) -> Option<f64> {
59 match self {
60 Self::Float(f) => Some(*f),
61 Self::Int(n) => Some(*n as f64),
62 Self::Str(s) => s.parse().ok(),
63 Self::Bool(_) | Self::Null => None,
64 }
65 }
66
67 #[must_use]
69 pub fn to_string_repr(&self) -> String {
70 match self {
71 Self::Str(s) => s.clone(),
72 Self::Int(n) => n.to_string(),
73 Self::Float(f) => f.to_string(),
74 Self::Bool(b) => b.to_string(),
75 Self::Null => "null".to_string(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq)]
82pub enum ComparisonOp {
83 Eq,
85 Neq,
87 Lt,
89 Lte,
91 Gt,
93 Gte,
95 Contains,
97 StartsWith,
99 EndsWith,
101 Matches,
103}
104
105#[derive(Debug, Clone)]
107pub enum StepCondition {
108 Always,
110 Never,
112 Compare {
114 variable: String,
116 op: ComparisonOp,
118 value: ConditionValue,
120 },
121 Exists {
123 variable: String,
125 },
126 And(Vec<StepCondition>),
128 Or(Vec<StepCondition>),
130 Not(Box<StepCondition>),
132 StepStatus {
134 step_name: String,
136 expected_status: String,
138 },
139 Expression(String),
141}
142
143#[derive(Debug, Clone)]
145pub struct ConditionContext {
146 pub variables: HashMap<String, ConditionValue>,
148 pub step_results: HashMap<String, String>,
150}
151
152impl Default for ConditionContext {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158impl ConditionContext {
159 #[must_use]
161 pub fn new() -> Self {
162 Self {
163 variables: HashMap::new(),
164 step_results: HashMap::new(),
165 }
166 }
167
168 pub fn set_variable(&mut self, name: impl Into<String>, value: ConditionValue) {
170 self.variables.insert(name.into(), value);
171 }
172
173 pub fn set_step_result(&mut self, step_name: impl Into<String>, status: impl Into<String>) {
175 self.step_results.insert(step_name.into(), status.into());
176 }
177
178 #[must_use]
180 pub fn get_variable(&self, name: &str) -> Option<&ConditionValue> {
181 self.variables.get(name)
182 }
183
184 #[must_use]
186 pub fn get_step_result(&self, step_name: &str) -> Option<&str> {
187 self.step_results
188 .get(step_name)
189 .map(std::string::String::as_str)
190 }
191
192 #[must_use]
194 pub fn variable_count(&self) -> usize {
195 self.variables.len()
196 }
197
198 #[must_use]
200 pub fn step_result_count(&self) -> usize {
201 self.step_results.len()
202 }
203}
204
205#[must_use]
207pub fn evaluate(condition: &StepCondition, ctx: &ConditionContext) -> bool {
208 match condition {
209 StepCondition::Always => true,
210 StepCondition::Never => false,
211 StepCondition::Compare {
212 variable,
213 op,
214 value,
215 } => {
216 let Some(actual) = ctx.get_variable(variable) else {
217 return false;
218 };
219 compare_values(actual, op, value)
220 }
221 StepCondition::Exists { variable } => ctx.variables.contains_key(variable),
222 StepCondition::And(conditions) => conditions.iter().all(|c| evaluate(c, ctx)),
223 StepCondition::Or(conditions) => conditions.iter().any(|c| evaluate(c, ctx)),
224 StepCondition::Not(inner) => !evaluate(inner, ctx),
225 StepCondition::StepStatus {
226 step_name,
227 expected_status,
228 } => ctx
229 .get_step_result(step_name)
230 .is_some_and(|s| s == expected_status),
231 StepCondition::Expression(expr) => evaluate_expression(expr, ctx),
232 }
233}
234
235#[allow(clippy::cast_precision_loss)]
237fn compare_values(actual: &ConditionValue, op: &ComparisonOp, expected: &ConditionValue) -> bool {
238 match op {
239 ComparisonOp::Eq => actual == expected,
240 ComparisonOp::Neq => actual != expected,
241 ComparisonOp::Lt | ComparisonOp::Lte | ComparisonOp::Gt | ComparisonOp::Gte => {
242 if let (Some(a), Some(b)) = (actual.as_float(), expected.as_float()) {
243 match op {
244 ComparisonOp::Lt => a < b,
245 ComparisonOp::Lte => a <= b,
246 ComparisonOp::Gt => a > b,
247 ComparisonOp::Gte => a >= b,
248 _ => false,
249 }
250 } else {
251 false
252 }
253 }
254 ComparisonOp::Contains => {
255 let a = actual.to_string_repr();
256 let b = expected.to_string_repr();
257 a.contains(&b)
258 }
259 ComparisonOp::StartsWith => {
260 let a = actual.to_string_repr();
261 let b = expected.to_string_repr();
262 a.starts_with(&b)
263 }
264 ComparisonOp::EndsWith => {
265 let a = actual.to_string_repr();
266 let b = expected.to_string_repr();
267 a.ends_with(&b)
268 }
269 ComparisonOp::Matches => {
270 let text = actual.to_string_repr();
271 let pattern = expected.to_string_repr();
272 simple_glob_match(&pattern, &text)
273 }
274 }
275}
276
277fn simple_glob_match(pattern: &str, text: &str) -> bool {
279 let p: Vec<char> = pattern.chars().collect();
280 let t: Vec<char> = text.chars().collect();
281 glob_match_recursive(&p, &t, 0, 0)
282}
283
284fn glob_match_recursive(p: &[char], t: &[char], pi: usize, ti: usize) -> bool {
286 if pi == p.len() && ti == t.len() {
287 return true;
288 }
289 if pi == p.len() {
290 return false;
291 }
292 if p[pi] == '*' {
293 for i in ti..=t.len() {
295 if glob_match_recursive(p, t, pi + 1, i) {
296 return true;
297 }
298 }
299 false
300 } else if ti < t.len() && (p[pi] == '?' || p[pi] == t[ti]) {
301 glob_match_recursive(p, t, pi + 1, ti + 1)
302 } else {
303 false
304 }
305}
306
307fn evaluate_expression(expr: &str, ctx: &ConditionContext) -> bool {
309 let trimmed = expr.trim();
310 if let Some(pos) = trimmed.find("==") {
312 let lhs = trimmed[..pos].trim();
313 let rhs = trimmed[pos + 2..].trim();
314 if let Some(val) = ctx.get_variable(lhs) {
315 return val.to_string_repr() == rhs;
316 }
317 }
318 if let Some(pos) = trimmed.find("!=") {
320 let lhs = trimmed[..pos].trim();
321 let rhs = trimmed[pos + 2..].trim();
322 if let Some(val) = ctx.get_variable(lhs) {
323 return val.to_string_repr() != rhs;
324 }
325 }
326 if let Some(val) = ctx.get_variable(trimmed) {
328 return val.as_bool().unwrap_or(false);
329 }
330 false
331}
332
333#[derive(Debug)]
335pub struct ConditionBuilder {
336 condition: StepCondition,
338}
339
340impl ConditionBuilder {
341 #[must_use]
343 pub fn always() -> Self {
344 Self {
345 condition: StepCondition::Always,
346 }
347 }
348
349 #[must_use]
351 pub fn never() -> Self {
352 Self {
353 condition: StepCondition::Never,
354 }
355 }
356
357 pub fn compare(variable: impl Into<String>, op: ComparisonOp, value: ConditionValue) -> Self {
359 Self {
360 condition: StepCondition::Compare {
361 variable: variable.into(),
362 op,
363 value,
364 },
365 }
366 }
367
368 pub fn exists(variable: impl Into<String>) -> Self {
370 Self {
371 condition: StepCondition::Exists {
372 variable: variable.into(),
373 },
374 }
375 }
376
377 pub fn step_status(step_name: impl Into<String>, expected: impl Into<String>) -> Self {
379 Self {
380 condition: StepCondition::StepStatus {
381 step_name: step_name.into(),
382 expected_status: expected.into(),
383 },
384 }
385 }
386
387 #[must_use]
389 pub fn and(self, other: ConditionBuilder) -> Self {
390 Self {
391 condition: StepCondition::And(vec![self.condition, other.condition]),
392 }
393 }
394
395 #[must_use]
397 pub fn or(self, other: ConditionBuilder) -> Self {
398 Self {
399 condition: StepCondition::Or(vec![self.condition, other.condition]),
400 }
401 }
402
403 #[must_use]
405 pub fn not(self) -> Self {
406 Self {
407 condition: StepCondition::Not(Box::new(self.condition)),
408 }
409 }
410
411 #[must_use]
413 pub fn build(self) -> StepCondition {
414 self.condition
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 fn ctx_with_vars() -> ConditionContext {
423 let mut ctx = ConditionContext::new();
424 ctx.set_variable("status", ConditionValue::Str("success".to_string()));
425 ctx.set_variable("count", ConditionValue::Int(42));
426 ctx.set_variable("ratio", ConditionValue::Float(0.95));
427 ctx.set_variable("enabled", ConditionValue::Bool(true));
428 ctx.set_variable("disabled", ConditionValue::Bool(false));
429 ctx.set_step_result("transcode", "completed");
430 ctx.set_step_result("qc", "failed");
431 ctx
432 }
433
434 #[test]
435 fn test_always_and_never() {
436 let ctx = ConditionContext::new();
437 assert!(evaluate(&StepCondition::Always, &ctx));
438 assert!(!evaluate(&StepCondition::Never, &ctx));
439 }
440
441 #[test]
442 fn test_compare_eq_string() {
443 let ctx = ctx_with_vars();
444 let cond = StepCondition::Compare {
445 variable: "status".to_string(),
446 op: ComparisonOp::Eq,
447 value: ConditionValue::Str("success".to_string()),
448 };
449 assert!(evaluate(&cond, &ctx));
450 }
451
452 #[test]
453 fn test_compare_neq() {
454 let ctx = ctx_with_vars();
455 let cond = StepCondition::Compare {
456 variable: "status".to_string(),
457 op: ComparisonOp::Neq,
458 value: ConditionValue::Str("failed".to_string()),
459 };
460 assert!(evaluate(&cond, &ctx));
461 }
462
463 #[test]
464 fn test_compare_gt_int() {
465 let ctx = ctx_with_vars();
466 let cond = StepCondition::Compare {
467 variable: "count".to_string(),
468 op: ComparisonOp::Gt,
469 value: ConditionValue::Int(10),
470 };
471 assert!(evaluate(&cond, &ctx));
472 }
473
474 #[test]
475 fn test_compare_lte_float() {
476 let ctx = ctx_with_vars();
477 let cond = StepCondition::Compare {
478 variable: "ratio".to_string(),
479 op: ComparisonOp::Lte,
480 value: ConditionValue::Float(1.0),
481 };
482 assert!(evaluate(&cond, &ctx));
483 }
484
485 #[test]
486 fn test_compare_contains() {
487 let ctx = ctx_with_vars();
488 let cond = StepCondition::Compare {
489 variable: "status".to_string(),
490 op: ComparisonOp::Contains,
491 value: ConditionValue::Str("ucc".to_string()),
492 };
493 assert!(evaluate(&cond, &ctx));
494 }
495
496 #[test]
497 fn test_compare_starts_with() {
498 let ctx = ctx_with_vars();
499 let cond = StepCondition::Compare {
500 variable: "status".to_string(),
501 op: ComparisonOp::StartsWith,
502 value: ConditionValue::Str("suc".to_string()),
503 };
504 assert!(evaluate(&cond, &ctx));
505 }
506
507 #[test]
508 fn test_compare_ends_with() {
509 let ctx = ctx_with_vars();
510 let cond = StepCondition::Compare {
511 variable: "status".to_string(),
512 op: ComparisonOp::EndsWith,
513 value: ConditionValue::Str("ess".to_string()),
514 };
515 assert!(evaluate(&cond, &ctx));
516 }
517
518 #[test]
519 fn test_glob_matches() {
520 let ctx = ctx_with_vars();
521 let cond = StepCondition::Compare {
522 variable: "status".to_string(),
523 op: ComparisonOp::Matches,
524 value: ConditionValue::Str("suc*".to_string()),
525 };
526 assert!(evaluate(&cond, &ctx));
527 }
528
529 #[test]
530 fn test_exists() {
531 let ctx = ctx_with_vars();
532 assert!(evaluate(
533 &StepCondition::Exists {
534 variable: "count".to_string()
535 },
536 &ctx
537 ));
538 assert!(!evaluate(
539 &StepCondition::Exists {
540 variable: "nope".to_string()
541 },
542 &ctx
543 ));
544 }
545
546 #[test]
547 fn test_and_condition() {
548 let ctx = ctx_with_vars();
549 let cond = StepCondition::And(vec![StepCondition::Always, StepCondition::Always]);
550 assert!(evaluate(&cond, &ctx));
551 let cond2 = StepCondition::And(vec![StepCondition::Always, StepCondition::Never]);
552 assert!(!evaluate(&cond2, &ctx));
553 }
554
555 #[test]
556 fn test_or_condition() {
557 let ctx = ctx_with_vars();
558 let cond = StepCondition::Or(vec![StepCondition::Never, StepCondition::Always]);
559 assert!(evaluate(&cond, &ctx));
560 }
561
562 #[test]
563 fn test_not_condition() {
564 let ctx = ctx_with_vars();
565 let cond = StepCondition::Not(Box::new(StepCondition::Never));
566 assert!(evaluate(&cond, &ctx));
567 }
568
569 #[test]
570 fn test_step_status() {
571 let ctx = ctx_with_vars();
572 let cond = StepCondition::StepStatus {
573 step_name: "transcode".to_string(),
574 expected_status: "completed".to_string(),
575 };
576 assert!(evaluate(&cond, &ctx));
577
578 let cond_fail = StepCondition::StepStatus {
579 step_name: "qc".to_string(),
580 expected_status: "completed".to_string(),
581 };
582 assert!(!evaluate(&cond_fail, &ctx));
583 }
584
585 #[test]
586 fn test_expression_eq() {
587 let ctx = ctx_with_vars();
588 let cond = StepCondition::Expression("status == success".to_string());
589 assert!(evaluate(&cond, &ctx));
590 }
591
592 #[test]
593 fn test_expression_neq() {
594 let ctx = ctx_with_vars();
595 let cond = StepCondition::Expression("status != failed".to_string());
596 assert!(evaluate(&cond, &ctx));
597 }
598
599 #[test]
600 fn test_expression_bool_variable() {
601 let ctx = ctx_with_vars();
602 assert!(evaluate(
603 &StepCondition::Expression("enabled".to_string()),
604 &ctx
605 ));
606 assert!(!evaluate(
607 &StepCondition::Expression("disabled".to_string()),
608 &ctx
609 ));
610 }
611
612 #[test]
613 fn test_condition_builder() {
614 let ctx = ctx_with_vars();
615 let cond = ConditionBuilder::compare("count", ComparisonOp::Gte, ConditionValue::Int(40))
616 .and(ConditionBuilder::step_status("transcode", "completed"))
617 .build();
618 assert!(evaluate(&cond, &ctx));
619 }
620
621 #[test]
622 fn test_condition_value_as_bool() {
623 assert_eq!(ConditionValue::Bool(true).as_bool(), Some(true));
624 assert_eq!(ConditionValue::Int(0).as_bool(), Some(false));
625 assert_eq!(ConditionValue::Str("yes".to_string()).as_bool(), Some(true));
626 assert_eq!(ConditionValue::Null.as_bool(), Some(false));
627 }
628
629 #[test]
630 fn test_condition_value_as_int() {
631 assert_eq!(ConditionValue::Int(5).as_int(), Some(5));
632 assert_eq!(ConditionValue::Float(3.7).as_int(), Some(3));
633 assert_eq!(ConditionValue::Str("10".to_string()).as_int(), Some(10));
634 assert_eq!(ConditionValue::Bool(true).as_int(), Some(1));
635 }
636
637 #[test]
638 fn test_condition_context_default() {
639 let ctx = ConditionContext::default();
640 assert_eq!(ctx.variable_count(), 0);
641 assert_eq!(ctx.step_result_count(), 0);
642 }
643
644 #[test]
645 fn test_missing_variable_compare_returns_false() {
646 let ctx = ConditionContext::new();
647 let cond = StepCondition::Compare {
648 variable: "nonexistent".to_string(),
649 op: ComparisonOp::Eq,
650 value: ConditionValue::Int(1),
651 };
652 assert!(!evaluate(&cond, &ctx));
653 }
654}