Skip to main content

noetl_executor/
condition.rs

1//! Condition evaluation — simple template-style equality / contains
2//! / truthy checks (`evaluate_condition`) and full Rhai expression
3//! evaluation (`evaluate_rhai_condition`).
4//!
5//! Extracted from `repos/cli/src/playbook_runner.rs` lines 771-911
6//! in R-1.1 PR-2b per § H.10.3 of Appendix H of the global hybrid
7//! cloud blueprint.  Both the CLI's tree walker and the worker's
8//! NATS-mode runner evaluate `when` / `case` conditions the same
9//! way; this module is the shared implementation.
10
11use anyhow::Result;
12use rhai::{Dynamic, Engine, Map, Scope};
13use std::collections::HashMap;
14
15/// Evaluate a simple template-style condition.  Supports:
16///
17/// - `{{ var == "value" }}`
18/// - `{{ var != "value" }}`
19/// - `{{ 'value' in var }}`
20/// - `{{ var }}` (truthy check)
21///
22/// Variable references are substituted from the supplied `variables`
23/// map before the comparison is run.
24pub fn evaluate_condition(
25    condition: &str,
26    variables: &HashMap<String, String>,
27) -> Result<bool> {
28    // Extract content from {{ ... }} if present.
29    let expression = if condition.trim().starts_with("{{") && condition.trim().ends_with("}}") {
30        condition
31            .trim()
32            .strip_prefix("{{")
33            .unwrap()
34            .strip_suffix("}}")
35            .unwrap()
36            .trim()
37    } else {
38        condition.trim()
39    };
40
41    // Replace variables within the expression.
42    let mut rendered = expression.to_string();
43    for (key, value) in variables {
44        rendered = rendered.replace(key, value);
45    }
46
47    fn strip_quotes(s: &str) -> String {
48        let s = s.trim();
49        if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
50            s[1..s.len() - 1].to_string()
51        } else {
52            s.to_string()
53        }
54    }
55
56    if rendered.contains("==") {
57        let parts: Vec<&str> = rendered.split("==").map(|s| s.trim()).collect();
58        if parts.len() == 2 {
59            return Ok(strip_quotes(parts[0]) == strip_quotes(parts[1]));
60        }
61    }
62
63    if rendered.contains("!=") {
64        let parts: Vec<&str> = rendered.split("!=").map(|s| s.trim()).collect();
65        if parts.len() == 2 {
66            return Ok(strip_quotes(parts[0]) != strip_quotes(parts[1]));
67        }
68    }
69
70    // 'in' operator (e.g., "'value' in var" or "var in list").
71    if rendered.contains(" in ") {
72        let parts: Vec<&str> = rendered.split(" in ").map(|s| s.trim()).collect();
73        if parts.len() == 2 {
74            let needle = strip_quotes(parts[0]);
75            let haystack = strip_quotes(parts[1]);
76            return Ok(haystack.contains(&needle));
77        }
78    }
79
80    // Truthy check — not empty, not "false", not "0".
81    let value = strip_quotes(&rendered);
82    Ok(!value.is_empty() && value != "false" && value != "0")
83}
84
85/// Evaluate a Rhai expression that returns a boolean condition.
86///
87/// The scope is populated with `workload.*`, `vars.*`, and
88/// `<step>.<field>` maps derived from the supplied `variables` map.
89/// Three helper functions are registered: `eq(a, b)`, `ne(a, b)`,
90/// `contains(haystack, needle)`.
91pub fn evaluate_rhai_condition(
92    code: &str,
93    variables: &HashMap<String, String>,
94) -> Result<bool> {
95    let mut engine = Engine::new();
96    let mut scope = Scope::new();
97
98    // workload.* -> scope.workload map.
99    let mut workload_map = Map::new();
100    for (key, value) in variables {
101        if key.starts_with("workload.") {
102            let short_key = key.strip_prefix("workload.").unwrap_or(key);
103            workload_map.insert(short_key.to_string().into(), Dynamic::from(value.clone()));
104        }
105    }
106    scope.push("workload", workload_map);
107
108    // vars.* -> scope.vars map.
109    let mut vars_map = Map::new();
110    for (key, value) in variables {
111        if key.starts_with("vars.") {
112            let short_key = key.strip_prefix("vars.").unwrap_or(key);
113            vars_map.insert(short_key.to_string().into(), Dynamic::from(value.clone()));
114        }
115    }
116    scope.push("vars", vars_map);
117
118    // <step>.<field> -> scope.<step> map.
119    for (key, value) in variables {
120        if !key.starts_with("workload.") && !key.starts_with("vars.") && key.contains('.') {
121            let parts: Vec<&str> = key.splitn(2, '.').collect();
122            if parts.len() == 2 {
123                let step_name = parts[0];
124                let field_name = parts[1];
125
126                if !scope.contains(step_name) {
127                    scope.push(step_name.to_string(), Map::new());
128                }
129
130                if let Some(step_map) = scope.get_mut(step_name) {
131                    if let Some(map) = step_map.clone().try_cast::<Map>() {
132                        let mut map = map;
133                        map.insert(field_name.to_string().into(), Dynamic::from(value.clone()));
134                        *step_map = Dynamic::from(map);
135                    }
136                }
137            }
138        }
139    }
140
141    // Comparison helpers.
142    engine.register_fn("eq", |a: &str, b: &str| -> bool { a == b });
143    engine.register_fn("ne", |a: &str, b: &str| -> bool { a != b });
144    engine.register_fn("contains", |haystack: &str, needle: &str| -> bool {
145        haystack.contains(needle)
146    });
147
148    let result = engine
149        .eval_with_scope::<Dynamic>(&mut scope, code)
150        .map_err(|e| anyhow::anyhow!("Rhai condition error: {}", e))?;
151
152    if result.is_bool() {
153        Ok(result.as_bool().unwrap_or(false))
154    } else if result.is_int() {
155        Ok(result.as_int().unwrap_or(0) != 0)
156    } else if result.is_string() {
157        let s = result.into_string().unwrap_or_default();
158        Ok(!s.is_empty() && s != "false" && s != "0")
159    } else {
160        Ok(!result.is_unit())
161    }
162}
163
164// ===========================================================================
165// R-1.2 PR-2b — structured condition surface
166//
167// The CLI's `evaluate_condition` / `evaluate_rhai_condition` above work
168// on template-style **strings** with a flat `HashMap<String, String>`
169// variable map.  That matches how the CLI's tree walker calls into the
170// YAML's `when:` / `if:` blocks.
171//
172// The worker (R-1.2 PR-2c/d) receives commands from NATS that carry
173// **structured JSON** `case` / `when` blocks: each condition is a
174// `{ left, op, right }` triple, and the worker evaluates them against
175// `noetl_tools::context::ExecutionContext`.  The worker's pre-PR-2b
176// inline implementation lived in `repos/worker/src/executor/case_evaluator.rs`
177// (~437 LoC).  This module exposes the condition primitive so both
178// binaries agree on operator semantics.
179//
180// The wrapper struct + Case/CaseAction control-flow types stay in the
181// worker — they're tied to the worker's pull-loop dispatch semantics
182// per § H.10.
183// ===========================================================================
184
185use noetl_tools::context::ExecutionContext as ToolsExecutionContext;
186use noetl_tools::template::TemplateEngine;
187use serde::{Deserialize, Serialize};
188
189/// Operator for [`evaluate_structured_condition`].
190///
191/// Twelve variants matching the worker's pre-PR-2b inline operator
192/// set.  Wire format: lowercase snake-case (`"eq"`, `"not_in"`, etc.).
193#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
194#[serde(rename_all = "snake_case")]
195pub enum Operator {
196    /// Equality (`left == right`).
197    #[default]
198    Eq,
199    /// Inequality (`left != right`).
200    Ne,
201    /// Greater than (numeric).
202    Gt,
203    /// Less than (numeric).
204    Lt,
205    /// Greater than or equal (numeric).
206    Gte,
207    /// Less than or equal (numeric).
208    Lte,
209    /// `left` (string) contains `right` (string).
210    Contains,
211    /// `left` (string) matches `right` (regex).
212    Matches,
213    /// `left` is truthy (right ignored).
214    Truthy,
215    /// `left` is falsy (right ignored).
216    Falsy,
217    /// `left` is an element of `right` (array).
218    In,
219    /// `left` is NOT an element of `right` (array).
220    NotIn,
221}
222
223/// Structured condition the worker carries on its NATS command
224/// envelopes.  Lifted from the worker's pre-PR-2b
225/// `executor::case_evaluator::Condition`.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct Condition {
228    /// Left-hand side value or variable reference.  Resolved against
229    /// the context (variable lookup), against `result.<path>`
230    /// (JSON-path navigation of the tool result), or as a literal
231    /// string after template substitution.
232    pub left: String,
233
234    /// Operator.
235    #[serde(default)]
236    pub op: Operator,
237
238    /// Right-hand side value.  May contain templates;
239    /// [`evaluate_structured_condition`] renders them against the
240    /// supplied context before applying the operator.
241    #[serde(default)]
242    pub right: Option<serde_json::Value>,
243}
244
245/// Evaluate a structured condition against `ctx` + optional tool
246/// `result`.
247///
248/// Behaviour matches the worker's pre-PR-2b
249/// `CaseEvaluator::evaluate_condition`:
250///
251/// - `condition.left` resolution order:
252///   1. `"result"` → the supplied tool result (or `Null` if `None`).
253///   2. `"result.<path>"` → JSON path navigation of the result.
254///   3. Variable lookup via `ctx.get_variable(&left)`.
255///   4. Template rendering via `TemplateEngine::render`.
256///   5. Literal string.
257/// - `condition.right` is template-rendered against `ctx` before use.
258/// - Operator semantics match the worker's inline implementation.
259///
260/// This function is pure and synchronous — no I/O, no async.
261pub fn evaluate_structured_condition(
262    condition: &Condition,
263    ctx: &ToolsExecutionContext,
264    result: Option<&serde_json::Value>,
265) -> Result<bool> {
266    let template_engine = TemplateEngine::new();
267    let left = resolve_value(&condition.left, ctx, result, &template_engine)?;
268    let right = condition
269        .right
270        .as_ref()
271        .map(|r| resolve_json_value(r, ctx, &template_engine))
272        .transpose()?;
273
274    match condition.op {
275        Operator::Eq => Ok(left == right.unwrap_or(serde_json::Value::Null)),
276        Operator::Ne => Ok(left != right.unwrap_or(serde_json::Value::Null)),
277        Operator::Gt => compare_numeric(&left, &right, |a, b| a > b),
278        Operator::Lt => compare_numeric(&left, &right, |a, b| a < b),
279        Operator::Gte => compare_numeric(&left, &right, |a, b| a >= b),
280        Operator::Lte => compare_numeric(&left, &right, |a, b| a <= b),
281        Operator::Contains => {
282            let left_str = left.as_str().unwrap_or("");
283            let right_str = right.as_ref().and_then(|r| r.as_str()).unwrap_or("");
284            Ok(left_str.contains(right_str))
285        }
286        Operator::Matches => {
287            let left_str = left.as_str().unwrap_or("");
288            let pattern = right.as_ref().and_then(|r| r.as_str()).unwrap_or("");
289            let re = regex::Regex::new(pattern)
290                .map_err(|e| anyhow::anyhow!("Invalid regex: {}", e))?;
291            Ok(re.is_match(left_str))
292        }
293        Operator::Truthy => Ok(is_truthy(&left)),
294        Operator::Falsy => Ok(!is_truthy(&left)),
295        Operator::In => {
296            if let Some(serde_json::Value::Array(arr)) = &right {
297                Ok(arr.contains(&left))
298            } else {
299                Ok(false)
300            }
301        }
302        Operator::NotIn => {
303            if let Some(serde_json::Value::Array(arr)) = &right {
304                Ok(!arr.contains(&left))
305            } else {
306                Ok(true)
307            }
308        }
309    }
310}
311
312/// Resolve a value reference to a JSON value.  See
313/// [`evaluate_structured_condition`] for the resolution order.
314fn resolve_value(
315    value: &str,
316    ctx: &ToolsExecutionContext,
317    result: Option<&serde_json::Value>,
318    template_engine: &TemplateEngine,
319) -> Result<serde_json::Value> {
320    if let Some(path) = value.strip_prefix("result.") {
321        if let Some(res) = result {
322            return Ok(json_path(res, path)
323                .cloned()
324                .unwrap_or(serde_json::Value::Null));
325        }
326        return Ok(serde_json::Value::Null);
327    }
328
329    if value == "result" {
330        return Ok(result.cloned().unwrap_or(serde_json::Value::Null));
331    }
332
333    if let Some(var) = ctx.get_variable(value) {
334        return Ok(var.clone());
335    }
336
337    if TemplateEngine::is_template(value) {
338        let template_ctx = ctx.to_template_context();
339        let rendered = template_engine
340            .render(value, &template_ctx)
341            .map_err(|e| anyhow::anyhow!(e))?;
342        return Ok(serde_json::from_str(&rendered).unwrap_or(serde_json::json!(rendered)));
343    }
344
345    Ok(serde_json::json!(value))
346}
347
348/// Resolve a JSON value that might contain templates.
349fn resolve_json_value(
350    value: &serde_json::Value,
351    ctx: &ToolsExecutionContext,
352    template_engine: &TemplateEngine,
353) -> Result<serde_json::Value> {
354    let template_ctx = ctx.to_template_context();
355    template_engine
356        .render_value(value, &template_ctx)
357        .map_err(|e| anyhow::anyhow!(e))
358}
359
360/// Navigate a JSON path (dot-delimited keys + optional numeric
361/// indices for arrays).
362fn json_path<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
363    let mut current = value;
364    for segment in path.split('.') {
365        match current {
366            serde_json::Value::Object(obj) => {
367                current = obj.get(segment)?;
368            }
369            serde_json::Value::Array(arr) => {
370                let idx: usize = segment.parse().ok()?;
371                current = arr.get(idx)?;
372            }
373            _ => return None,
374        }
375    }
376    Some(current)
377}
378
379/// Compare two JSON values numerically.
380fn compare_numeric<F>(
381    left: &serde_json::Value,
382    right: &Option<serde_json::Value>,
383    cmp: F,
384) -> Result<bool>
385where
386    F: Fn(f64, f64) -> bool,
387{
388    let left_num = value_to_f64(left)?;
389    let right_num = value_to_f64(right.as_ref().unwrap_or(&serde_json::Value::Null))?;
390    Ok(cmp(left_num, right_num))
391}
392
393/// Check if a JSON value is truthy (empty / zero / false → falsy).
394fn is_truthy(value: &serde_json::Value) -> bool {
395    match value {
396        serde_json::Value::Null => false,
397        serde_json::Value::Bool(b) => *b,
398        serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
399        serde_json::Value::String(s) => !s.is_empty(),
400        serde_json::Value::Array(a) => !a.is_empty(),
401        serde_json::Value::Object(o) => !o.is_empty(),
402    }
403}
404
405/// Convert a JSON value to f64.  Booleans become 0.0 / 1.0; nulls
406/// become 0.0; strings parse via `FromStr`.
407fn value_to_f64(value: &serde_json::Value) -> Result<f64> {
408    match value {
409        serde_json::Value::Number(n) => n
410            .as_f64()
411            .ok_or_else(|| anyhow::anyhow!("Invalid number")),
412        serde_json::Value::String(s) => s
413            .parse()
414            .map_err(|_| anyhow::anyhow!("Cannot parse '{s}' as number")),
415        serde_json::Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
416        serde_json::Value::Null => Ok(0.0),
417        _ => Err(anyhow::anyhow!("Cannot convert {value:?} to number")),
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    fn vars(pairs: &[(&str, &str)]) -> HashMap<String, String> {
426        pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
427    }
428
429    #[test]
430    fn evaluate_condition_equality() {
431        let v = HashMap::new();
432        assert!(evaluate_condition("'test' == 'test'", &v).unwrap());
433        assert!(!evaluate_condition("'test' == 'other'", &v).unwrap());
434    }
435
436    #[test]
437    fn evaluate_condition_inequality() {
438        let v = HashMap::new();
439        assert!(evaluate_condition("'test' != 'other'", &v).unwrap());
440        assert!(!evaluate_condition("'test' != 'test'", &v).unwrap());
441    }
442
443    #[test]
444    fn evaluate_condition_in_operator() {
445        let v = HashMap::new();
446        assert!(evaluate_condition("'foo' in 'foobar'", &v).unwrap());
447        assert!(!evaluate_condition("'baz' in 'foobar'", &v).unwrap());
448    }
449
450    #[test]
451    fn evaluate_condition_substitutes_variables() {
452        let v = vars(&[("workload.action", "deploy")]);
453        assert!(evaluate_condition("workload.action == 'deploy'", &v).unwrap());
454        assert!(!evaluate_condition("workload.action == 'undeploy'", &v).unwrap());
455    }
456
457    #[test]
458    fn evaluate_rhai_condition_workload_field() {
459        let v = vars(&[("workload.count", "5")]);
460        assert!(evaluate_rhai_condition("workload.count == \"5\"", &v).unwrap());
461        assert!(!evaluate_rhai_condition("workload.count == \"6\"", &v).unwrap());
462    }
463
464    #[test]
465    fn evaluate_rhai_condition_helpers() {
466        let v = vars(&[("workload.action", "DEPLOY")]);
467        assert!(evaluate_rhai_condition("eq(workload.action, \"DEPLOY\")", &v).unwrap());
468        assert!(evaluate_rhai_condition(
469            "contains(workload.action, \"DEP\")",
470            &v
471        )
472        .unwrap());
473    }
474
475    // ---- R-1.2 PR-2b — structured condition tests --------------------
476
477    fn tools_ctx_with(pairs: &[(&str, serde_json::Value)]) -> ToolsExecutionContext {
478        let mut ctx = ToolsExecutionContext::default();
479        for (k, v) in pairs {
480            ctx.set_variable(*k, v.clone());
481        }
482        ctx
483    }
484
485    #[test]
486    fn structured_eq_against_variable() {
487        let ctx = tools_ctx_with(&[("status", serde_json::json!("success"))]);
488        let cond = Condition {
489            left: "status".into(),
490            op: Operator::Eq,
491            right: Some(serde_json::json!("success")),
492        };
493        assert!(evaluate_structured_condition(&cond, &ctx, None).unwrap());
494        let cond_fail = Condition {
495            left: "status".into(),
496            op: Operator::Eq,
497            right: Some(serde_json::json!("failed")),
498        };
499        assert!(!evaluate_structured_condition(&cond_fail, &ctx, None).unwrap());
500    }
501
502    #[test]
503    fn structured_ne_inverts_eq() {
504        let ctx = tools_ctx_with(&[("status", serde_json::json!("ok"))]);
505        let cond = Condition {
506            left: "status".into(),
507            op: Operator::Ne,
508            right: Some(serde_json::json!("error")),
509        };
510        assert!(evaluate_structured_condition(&cond, &ctx, None).unwrap());
511    }
512
513    #[test]
514    fn structured_numeric_comparisons() {
515        let ctx = tools_ctx_with(&[("count", serde_json::json!(10))]);
516        for (op, rhs, expected) in [
517            (Operator::Gt, 5, true),
518            (Operator::Gt, 10, false),
519            (Operator::Gte, 10, true),
520            (Operator::Lt, 100, true),
521            (Operator::Lte, 10, true),
522        ] {
523            let cond = Condition {
524                left: "count".into(),
525                op,
526                right: Some(serde_json::json!(rhs)),
527            };
528            assert_eq!(
529                evaluate_structured_condition(&cond, &ctx, None).unwrap(),
530                expected,
531                "op {:?} vs {} expected {}",
532                cond.op,
533                rhs,
534                expected
535            );
536        }
537    }
538
539    #[test]
540    fn structured_contains_matches_strings() {
541        let ctx = tools_ctx_with(&[("msg", serde_json::json!("hello world"))]);
542        let cond = Condition {
543            left: "msg".into(),
544            op: Operator::Contains,
545            right: Some(serde_json::json!("world")),
546        };
547        assert!(evaluate_structured_condition(&cond, &ctx, None).unwrap());
548        let cond_no = Condition {
549            left: "msg".into(),
550            op: Operator::Contains,
551            right: Some(serde_json::json!("zzz")),
552        };
553        assert!(!evaluate_structured_condition(&cond_no, &ctx, None).unwrap());
554    }
555
556    #[test]
557    fn structured_matches_regex() {
558        let ctx = tools_ctx_with(&[("user", serde_json::json!("alice@example.com"))]);
559        let cond = Condition {
560            left: "user".into(),
561            op: Operator::Matches,
562            right: Some(serde_json::json!(r"^\w+@\w+\.com$")),
563        };
564        assert!(evaluate_structured_condition(&cond, &ctx, None).unwrap());
565    }
566
567    #[test]
568    fn structured_truthy_falsy() {
569        let ctx = tools_ctx_with(&[
570            ("on", serde_json::json!(true)),
571            ("zero", serde_json::json!(0)),
572            ("empty", serde_json::json!("")),
573            ("nonempty", serde_json::json!("x")),
574        ]);
575        let truthy_on = Condition {
576            left: "on".into(),
577            op: Operator::Truthy,
578            right: None,
579        };
580        assert!(evaluate_structured_condition(&truthy_on, &ctx, None).unwrap());
581        let falsy_zero = Condition {
582            left: "zero".into(),
583            op: Operator::Falsy,
584            right: None,
585        };
586        assert!(evaluate_structured_condition(&falsy_zero, &ctx, None).unwrap());
587        let falsy_empty = Condition {
588            left: "empty".into(),
589            op: Operator::Falsy,
590            right: None,
591        };
592        assert!(evaluate_structured_condition(&falsy_empty, &ctx, None).unwrap());
593        let truthy_x = Condition {
594            left: "nonempty".into(),
595            op: Operator::Truthy,
596            right: None,
597        };
598        assert!(evaluate_structured_condition(&truthy_x, &ctx, None).unwrap());
599    }
600
601    #[test]
602    fn structured_in_and_not_in() {
603        let ctx = tools_ctx_with(&[("role", serde_json::json!("admin"))]);
604        let in_cond = Condition {
605            left: "role".into(),
606            op: Operator::In,
607            right: Some(serde_json::json!(["admin", "ops", "dev"])),
608        };
609        assert!(evaluate_structured_condition(&in_cond, &ctx, None).unwrap());
610        let not_in_cond = Condition {
611            left: "role".into(),
612            op: Operator::NotIn,
613            right: Some(serde_json::json!(["guest", "viewer"])),
614        };
615        assert!(evaluate_structured_condition(&not_in_cond, &ctx, None).unwrap());
616    }
617
618    #[test]
619    fn structured_left_resolves_result_path() {
620        let ctx = ToolsExecutionContext::default();
621        let result = serde_json::json!({
622            "status": "ok",
623            "data": {"count": 42}
624        });
625        let cond = Condition {
626            left: "result.data.count".into(),
627            op: Operator::Eq,
628            right: Some(serde_json::json!(42)),
629        };
630        assert!(evaluate_structured_condition(&cond, &ctx, Some(&result)).unwrap());
631    }
632
633    #[test]
634    fn structured_left_resolves_bare_result() {
635        let ctx = ToolsExecutionContext::default();
636        let result = serde_json::json!("hello");
637        let cond = Condition {
638            left: "result".into(),
639            op: Operator::Eq,
640            right: Some(serde_json::json!("hello")),
641        };
642        assert!(evaluate_structured_condition(&cond, &ctx, Some(&result)).unwrap());
643    }
644
645    #[test]
646    fn structured_operator_serializes_snake_case() {
647        let cond = Condition {
648            left: "x".into(),
649            op: Operator::NotIn,
650            right: None,
651        };
652        let s = serde_json::to_string(&cond).unwrap();
653        assert!(s.contains("\"not_in\""), "got: {s}");
654        let parsed: Condition = serde_json::from_str(&s).unwrap();
655        assert!(matches!(parsed.op, Operator::NotIn));
656    }
657
658    #[test]
659    fn structured_in_returns_false_when_right_not_array() {
660        let ctx = tools_ctx_with(&[("x", serde_json::json!(1))]);
661        let cond = Condition {
662            left: "x".into(),
663            op: Operator::In,
664            right: Some(serde_json::json!("not an array")),
665        };
666        assert!(!evaluate_structured_condition(&cond, &ctx, None).unwrap());
667    }
668}