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}