Adds "!{{var}}" syntax for negated truthy checks in Step.Condition.
Useful for "skip this step if feature is enabled" patterns.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
// 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}}" - negated truthy check (include if var is falsy)
|
|
// - "{{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}} - negated truthy check
|
|
stepCondNegatedVarPattern = 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}}" - include if var is NOT truthy (negated)
|
|
// - "{{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 negated truthy pattern: !{{var}}
|
|
if m := stepCondNegatedVarPattern.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
|
|
}
|