Skip to main content

ironflow_engine/context/steps/
conditions.rs

1//! Branch conditions for [`WorkflowContext`].
2//!
3//! A handler branches with plain Rust `if`/`else`, which the planner cannot
4//! see. These two methods make a branch visible to
5//! [`Engine::plan_handler`](crate::engine::Engine::plan_handler) without
6//! changing what the handler does at run time.
7
8use serde_json::Value;
9
10use crate::context::WorkflowContext;
11use crate::error::EngineError;
12use crate::plan::{ConditionResult, lock_plan};
13
14/// Why a condition computed from a step output cannot be resolved by the
15/// planner.
16const DYNAMIC_REASON: &str =
17    "depends on a previous step's output, which is synthetic while planning";
18
19impl WorkflowContext {
20    /// Evaluate a named branch condition against the run input.
21    ///
22    /// Outside plan mode this simply applies `predicate` to
23    /// [`payload`](Self::payload). In plan mode the result is also recorded as
24    /// [`ConditionResult::Evaluated`] on the next planned step, so the operator
25    /// sees which branch the plan followed and why.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`EngineError::Store`] when the run payload cannot be read.
30    ///
31    /// # Examples
32    ///
33    /// ```no_run
34    /// use ironflow_engine::context::WorkflowContext;
35    /// use ironflow_engine::config::ShellConfig;
36    /// use ironflow_engine::error::EngineError;
37    ///
38    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
39    /// if ctx.when("input.env == 'prod'", |p| p["env"] == "prod").await? {
40    ///     ctx.shell("deploy-prod", ShellConfig::new("./deploy prod")).await?;
41    /// } else {
42    ///     ctx.skip("deploy-prod", "not a production run").await?;
43    /// }
44    /// # Ok(())
45    /// # }
46    /// ```
47    pub async fn when<F>(&mut self, expression: &str, predicate: F) -> Result<bool, EngineError>
48    where
49        F: FnOnce(&Value) -> bool,
50    {
51        let payload = self.payload().await?;
52        let value = predicate(&payload);
53
54        if let Some(plan) = self.plan().cloned() {
55            lock_plan(&plan).set_condition(ConditionResult::Evaluated {
56                expression: expression.to_string(),
57                value,
58            });
59        }
60
61        Ok(value)
62    }
63
64    /// Record a branch condition whose value depends on a previous step's
65    /// output.
66    ///
67    /// Returns `value` unchanged. Under planning, step outputs are synthetic,
68    /// so the condition is recorded as [`ConditionResult::Unevaluable`]: the
69    /// plan still follows the branch the synthetic output produces, and the
70    /// operator is told the other branch may run instead.
71    ///
72    /// # Examples
73    ///
74    /// ```no_run
75    /// use ironflow_engine::context::WorkflowContext;
76    /// use ironflow_engine::config::ShellConfig;
77    /// use ironflow_engine::error::EngineError;
78    ///
79    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
80    /// let build = ctx.shell("build", ShellConfig::new("cargo build")).await?;
81    /// if ctx.when_dynamic("build succeeded", build.is_success()) {
82    ///     ctx.shell("deploy", ShellConfig::new("./deploy")).await?;
83    /// }
84    /// # Ok(())
85    /// # }
86    /// ```
87    pub fn when_dynamic(&mut self, expression: &str, value: bool) -> bool {
88        if let Some(plan) = self.plan().cloned() {
89            lock_plan(&plan).set_condition(ConditionResult::Unevaluable {
90                expression: expression.to_string(),
91                reason: DYNAMIC_REASON.to_string(),
92            });
93        }
94
95        value
96    }
97}