Build an `evalexpr::HashMapContext` from trigger context and step outputs. Variable names use underscores (not dots) because `evalexpr` does not support dotted identifiers: | YAML reference | evalexpr variable | |-----------------------------------|---------------------------| | `trigger.text` | `trigger_text` | | `trigger.author`
(
trigger_ctx: &TriggerContext,
step_outputs: &HashMap<String, JsonValue>,
)
| 236 | /// - `str_ends_with(s, suffix)` → bool |
| 237 | /// - `str_len(s)` → int |
| 238 | pub fn build_eval_context( |
| 239 | trigger_ctx: &TriggerContext, |
| 240 | step_outputs: &HashMap<String, JsonValue>, |
| 241 | ) -> Result<HashMapContext, WorkflowError> { |
| 242 | use evalexpr::*; |
| 243 | |
| 244 | let mut ctx = HashMapContext::new(); |
| 245 | |
| 246 | // evalexpr v11 does not ship str_contains / str_starts_with / str_ends_with. |
| 247 | // Register them as custom functions so workflow YAML can use them. |
| 248 | |
| 249 | ctx.set_function( |
| 250 | "str_contains".into(), |
| 251 | Function::new(|args| { |
| 252 | let args = args.as_fixed_len_tuple(2)?; |
| 253 | let haystack = args[0].as_string()?; |
| 254 | let needle = args[1].as_string()?; |
| 255 | Ok(Value::Boolean(haystack.contains(needle.as_str()))) |
| 256 | }), |
| 257 | ) |
| 258 | .map_err(|e| WorkflowError::ConditionError(e.to_string()))?; |
| 259 | |
| 260 | ctx.set_function( |
| 261 | "str_starts_with".into(), |
| 262 | Function::new(|args| { |
| 263 | let args = args.as_fixed_len_tuple(2)?; |
| 264 | let s = args[0].as_string()?; |
| 265 | let prefix = args[1].as_string()?; |
| 266 | Ok(Value::Boolean(s.starts_with(prefix.as_str()))) |
| 267 | }), |
| 268 | ) |
| 269 | .map_err(|e| WorkflowError::ConditionError(e.to_string()))?; |
| 270 | |
| 271 | ctx.set_function( |
| 272 | "str_ends_with".into(), |
| 273 | Function::new(|args| { |
| 274 | let args = args.as_fixed_len_tuple(2)?; |
| 275 | let s = args[0].as_string()?; |
| 276 | let suffix = args[1].as_string()?; |
| 277 | Ok(Value::Boolean(s.ends_with(suffix.as_str()))) |
| 278 | }), |
| 279 | ) |
| 280 | .map_err(|e| WorkflowError::ConditionError(e.to_string()))?; |
| 281 | |
| 282 | ctx.set_function( |
| 283 | "str_len".into(), |
| 284 | Function::new(|arg| { |
| 285 | let s = arg.as_string()?; |
| 286 | Ok(Value::Int(s.len() as i64)) |
| 287 | }), |
| 288 | ) |
| 289 | .map_err(|e| WorkflowError::ConditionError(e.to_string()))?; |
| 290 | |
| 291 | // Register webhook fields first as `trigger_FIELD` so that standard trigger |
| 292 | // fields inserted below always take precedence and cannot be spoofed. |
| 293 | for (key, val) in &trigger_ctx.webhook_fields { |
| 294 | // Skip any key that would collide with a standard trigger_ or steps_ variable. |
| 295 | if key.starts_with("trigger_") || key.starts_with("steps_") { |
no test coverage detected