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::de::DeserializeOwned;
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 typed run input.
21    ///
22    /// The run payload is deserialized into `T`, exactly like
23    /// [`input`](Self::input), and `predicate` decides the branch on it. `label`
24    /// is a human-readable name for the branch, shown in the plan; it is never
25    /// parsed nor evaluated. In plan mode the result is also recorded as
26    /// [`ConditionResult::Evaluated`] on the next planned step, so the operator
27    /// sees which branch the plan followed and why.
28    ///
29    /// # Errors
30    ///
31    /// Returns [`EngineError::Store`] when the run payload cannot be read, and
32    /// [`EngineError::Serialization`] when it does not match `T`: a misspelled
33    /// field or variant fails the branch instead of silently taking the other
34    /// one.
35    ///
36    /// # Examples
37    ///
38    /// ```no_run
39    /// use ironflow_engine::context::WorkflowContext;
40    /// use ironflow_engine::config::ShellConfig;
41    /// use ironflow_engine::error::EngineError;
42    /// use serde::Deserialize;
43    ///
44    /// #[derive(Deserialize, PartialEq)]
45    /// #[serde(rename_all = "lowercase")]
46    /// enum Env {
47    ///     Prod,
48    ///     Staging,
49    /// }
50    ///
51    /// #[derive(Deserialize)]
52    /// struct DeployInput {
53    ///     env: Env,
54    /// }
55    ///
56    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
57    /// if ctx.when("production run", |i: &DeployInput| i.env == Env::Prod).await? {
58    ///     ctx.shell("deploy-prod", ShellConfig::new("./deploy prod")).await?;
59    /// } else {
60    ///     ctx.skip("deploy-prod", "not a production run").await?;
61    /// }
62    /// # Ok(())
63    /// # }
64    /// ```
65    pub async fn when<T, F>(&mut self, label: &str, predicate: F) -> Result<bool, EngineError>
66    where
67        T: DeserializeOwned,
68        F: FnOnce(&T) -> bool,
69    {
70        let input: T = self.input().await?;
71        let value = predicate(&input);
72
73        if let Some(plan) = self.plan().cloned() {
74            lock_plan(&plan).set_condition(ConditionResult::Evaluated {
75                expression: label.to_string(),
76                value,
77            });
78        }
79
80        Ok(value)
81    }
82
83    /// Record a branch condition whose value depends on a previous step's
84    /// output.
85    ///
86    /// Returns `value` unchanged; `label` names the branch in the plan. Under
87    /// planning, step outputs are synthetic,
88    /// so the condition is recorded as [`ConditionResult::Unevaluable`]: the
89    /// plan still follows the branch the synthetic output produces, and the
90    /// operator is told the other branch may run instead.
91    ///
92    /// # Examples
93    ///
94    /// ```no_run
95    /// use ironflow_engine::context::WorkflowContext;
96    /// use ironflow_engine::config::ShellConfig;
97    /// use ironflow_engine::error::EngineError;
98    ///
99    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
100    /// let build = ctx.shell("build", ShellConfig::new("cargo build")).await?;
101    /// if ctx.when_dynamic("build succeeded", build.is_success()) {
102    ///     ctx.shell("deploy", ShellConfig::new("./deploy")).await?;
103    /// }
104    /// # Ok(())
105    /// # }
106    /// ```
107    pub fn when_dynamic(&mut self, label: &str, value: bool) -> bool {
108        if let Some(plan) = self.plan().cloned() {
109            lock_plan(&plan).set_condition(ConditionResult::Unevaluable {
110                expression: label.to_string(),
111                reason: DYNAMIC_REASON.to_string(),
112            });
113        }
114
115        value
116    }
117}