feat: implement Step.Condition evaluation in bd cook (bd-7zka.1)
Add compile-time step filtering based on formula variables:
- New EvaluateStepCondition function for {{var}} truthy and equality checks
- FilterStepsByCondition to exclude steps based on conditions
- Integration into pour, wisp, and mol bond commands
- Supports: {{var}}, {{var}} == value, {{var}} != value
Steps with conditions that evaluate to false are excluded from the
cooked formula, along with their children.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
82611834df
commit
ee34a74e90
141
internal/formula/stepcondition.go
Normal file
141
internal/formula/stepcondition.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Package formula provides Step.Condition evaluation for compile-time step filtering.
|
||||
//
|
||||
// Step.Condition is simpler than the runtime condition evaluation in condition.go.
|
||||
// It evaluates at cook/pour time to include or exclude steps based on formula variables.
|
||||
//
|
||||
// Supported formats:
|
||||
// - "{{var}}" - truthy check (non-empty, non-"false", non-"0")
|
||||
// - "{{var}} == value" - equality check
|
||||
// - "{{var}} != value" - inequality check
|
||||
package formula
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Step condition patterns
|
||||
var (
|
||||
// {{var}} - simple variable reference for truthy check
|
||||
stepCondVarPattern = regexp.MustCompile(`^\{\{(\w+)\}\}$`)
|
||||
|
||||
// {{var}} == value or {{var}} != value
|
||||
stepCondComparePattern = regexp.MustCompile(`^\{\{(\w+)\}\}\s*(==|!=)\s*(.+)$`)
|
||||
)
|
||||
|
||||
// EvaluateStepCondition evaluates a step's condition against variable values.
|
||||
// Returns true if the step should be included, false if it should be skipped.
|
||||
//
|
||||
// Condition formats:
|
||||
// - "" (empty) - always include
|
||||
// - "{{var}}" - include if var is truthy (non-empty, non-"false", non-"0")
|
||||
// - "{{var}} == value" - include if var equals value
|
||||
// - "{{var}} != value" - include if var does not equal value
|
||||
func EvaluateStepCondition(condition string, vars map[string]string) (bool, error) {
|
||||
condition = strings.TrimSpace(condition)
|
||||
|
||||
// Empty condition means always include
|
||||
if condition == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Try truthy pattern: {{var}}
|
||||
if m := stepCondVarPattern.FindStringSubmatch(condition); m != nil {
|
||||
varName := m[1]
|
||||
value := vars[varName]
|
||||
return isTruthy(value), nil
|
||||
}
|
||||
|
||||
// Try comparison pattern: {{var}} == value or {{var}} != value
|
||||
if m := stepCondComparePattern.FindStringSubmatch(condition); m != nil {
|
||||
varName := m[1]
|
||||
operator := m[2]
|
||||
expected := strings.TrimSpace(m[3])
|
||||
|
||||
// Remove quotes from expected value if present
|
||||
expected = unquoteValue(expected)
|
||||
|
||||
actual := vars[varName]
|
||||
|
||||
switch operator {
|
||||
case "==":
|
||||
return actual == expected, nil
|
||||
case "!=":
|
||||
return actual != expected, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid step condition format: %q (expected {{var}} or {{var}} == value)", condition)
|
||||
}
|
||||
|
||||
// isTruthy returns true if a value is considered "truthy" for step conditions.
|
||||
// Falsy values: empty string, "false", "0", "no", "off"
|
||||
// All other values are truthy.
|
||||
func isTruthy(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(value)
|
||||
switch lower {
|
||||
case "false", "0", "no", "off":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// unquoteValue removes surrounding quotes from a value if present.
|
||||
func unquoteValue(s string) string {
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// FilterStepsByCondition filters a list of steps based on their Condition field.
|
||||
// Steps with conditions that evaluate to false are excluded from the result.
|
||||
// Children of excluded steps are also excluded.
|
||||
//
|
||||
// Parameters:
|
||||
// - steps: the steps to filter
|
||||
// - vars: variable values for condition evaluation
|
||||
//
|
||||
// Returns the filtered steps and any error encountered during evaluation.
|
||||
func FilterStepsByCondition(steps []*Step, vars map[string]string) ([]*Step, error) {
|
||||
if vars == nil {
|
||||
vars = make(map[string]string)
|
||||
}
|
||||
|
||||
result := make([]*Step, 0, len(steps))
|
||||
|
||||
for _, step := range steps {
|
||||
// Evaluate step condition
|
||||
include, err := EvaluateStepCondition(step.Condition, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("step %q: %w", step.ID, err)
|
||||
}
|
||||
|
||||
if !include {
|
||||
// Skip this step and all its children
|
||||
continue
|
||||
}
|
||||
|
||||
// Clone the step to avoid mutating input
|
||||
clone := cloneStep(step)
|
||||
|
||||
// Recursively filter children
|
||||
if len(step.Children) > 0 {
|
||||
filteredChildren, err := FilterStepsByCondition(step.Children, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clone.Children = filteredChildren
|
||||
}
|
||||
|
||||
result = append(result, clone)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user