Support logical when expressions
This commit is contained in:
parent
d969cc9205
commit
8130be049a
4 changed files with 162 additions and 0 deletions
|
|
@ -222,6 +222,7 @@ The `evaluateWhen` method processes `when:` clauses. It supports:
|
||||||
|
|
||||||
- Boolean literals: `true`, `false`, `True`, `False`
|
- Boolean literals: `true`, `false`, `True`, `False`
|
||||||
- Negation: `not <condition>`
|
- Negation: `not <condition>`
|
||||||
|
- Inline boolean expressions with `and`, `or`, and parentheses
|
||||||
- Registered variable checks: `result is defined`, `result is success`, `result is failed`, `result is changed`, `result is skipped`
|
- Registered variable checks: `result is defined`, `result is success`, `result is failed`, `result is changed`, `result is skipped`
|
||||||
- Variable truthiness: checks `vars` map for the condition as a key, evaluating booleans, non-empty strings, and non-zero integers
|
- Variable truthiness: checks `vars` map for the condition as a key, evaluating booleans, non-empty strings, and non-zero integers
|
||||||
- Default filter handling: `var | default(value)` always evaluates to true (permissive)
|
- Default filter handling: `var | default(value)` always evaluates to true (permissive)
|
||||||
|
|
|
||||||
139
executor.go
139
executor.go
|
|
@ -1343,6 +1343,20 @@ func (e *Executor) evalCondition(cond string, host string) bool {
|
||||||
|
|
||||||
func (e *Executor) evalConditionWithLocals(cond string, host string, task *Task, locals map[string]any) bool {
|
func (e *Executor) evalConditionWithLocals(cond string, host string, task *Task, locals map[string]any) bool {
|
||||||
cond = corexTrimSpace(cond)
|
cond = corexTrimSpace(cond)
|
||||||
|
if cond == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if inner, ok := stripOuterParens(cond); ok {
|
||||||
|
return e.evalConditionWithLocals(inner, host, task, locals)
|
||||||
|
}
|
||||||
|
|
||||||
|
if left, right, ok := splitLogicalCondition(cond, "or"); ok {
|
||||||
|
return e.evalConditionWithLocals(left, host, task, locals) || e.evalConditionWithLocals(right, host, task, locals)
|
||||||
|
}
|
||||||
|
if left, right, ok := splitLogicalCondition(cond, "and"); ok {
|
||||||
|
return e.evalConditionWithLocals(left, host, task, locals) && e.evalConditionWithLocals(right, host, task, locals)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle negation
|
// Handle negation
|
||||||
if corexHasPrefix(cond, "not ") {
|
if corexHasPrefix(cond, "not ") {
|
||||||
|
|
@ -1476,6 +1490,131 @@ func (e *Executor) evalConditionWithLocals(cond string, host string, task *Task,
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripOuterParens(cond string) (string, bool) {
|
||||||
|
cond = corexTrimSpace(cond)
|
||||||
|
if len(cond) < 2 || cond[0] != '(' || cond[len(cond)-1] != ')' {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
depth := 0
|
||||||
|
inSingle := false
|
||||||
|
inDouble := false
|
||||||
|
escaped := false
|
||||||
|
for i := 0; i < len(cond); i++ {
|
||||||
|
ch := cond[i]
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\\' && (inSingle || inDouble) {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\'' && !inDouble {
|
||||||
|
inSingle = !inSingle
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' && !inSingle {
|
||||||
|
inDouble = !inDouble
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inSingle || inDouble {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch ch {
|
||||||
|
case '(':
|
||||||
|
depth++
|
||||||
|
case ')':
|
||||||
|
depth--
|
||||||
|
if depth == 0 && i != len(cond)-1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if depth < 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if depth != 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return corexTrimSpace(cond[1 : len(cond)-1]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitLogicalCondition(cond, op string) (string, string, bool) {
|
||||||
|
depth := 0
|
||||||
|
inSingle := false
|
||||||
|
inDouble := false
|
||||||
|
escaped := false
|
||||||
|
for i := 0; i <= len(cond)-len(op); i++ {
|
||||||
|
ch := cond[i]
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\\' && (inSingle || inDouble) {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\'' && !inDouble {
|
||||||
|
inSingle = !inSingle
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' && !inSingle {
|
||||||
|
inDouble = !inDouble
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inSingle || inDouble {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch ch {
|
||||||
|
case '(':
|
||||||
|
depth++
|
||||||
|
continue
|
||||||
|
case ')':
|
||||||
|
if depth > 0 {
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if depth != 0 || !strings.HasPrefix(cond[i:], op) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
prev := cond[i-1]
|
||||||
|
if !isConditionBoundary(prev) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if end := i + len(op); end < len(cond) {
|
||||||
|
if !isConditionBoundary(cond[end]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
left := corexTrimSpace(cond[:i])
|
||||||
|
right := corexTrimSpace(cond[i+len(op):])
|
||||||
|
if left == "" || right == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return left, right, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isConditionBoundary(ch byte) bool {
|
||||||
|
switch ch {
|
||||||
|
case ' ', '\t', '\n', '\r', '(', ')':
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Executor) lookupConditionValue(name string, host string, task *Task, locals map[string]any) (any, bool) {
|
func (e *Executor) lookupConditionValue(name string, host string, task *Task, locals map[string]any) (any, bool) {
|
||||||
name = corexTrimSpace(name)
|
name = corexTrimSpace(name)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1024,6 +1024,27 @@ func TestExecutor_EvaluateWhen_Good_MultipleConditions(t *testing.T) {
|
||||||
assert.False(t, e.evaluateWhen([]any{"true", "false"}, "host1", nil))
|
assert.False(t, e.evaluateWhen([]any{"true", "false"}, "host1", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecutor_EvaluateWhen_Good_LogicalAndOrExpressions(t *testing.T) {
|
||||||
|
e := NewExecutor("/tmp")
|
||||||
|
e.vars["enabled"] = true
|
||||||
|
e.vars["maintenance"] = false
|
||||||
|
|
||||||
|
assert.True(t, e.evaluateWhen("enabled and not maintenance", "host1", nil))
|
||||||
|
assert.False(t, e.evaluateWhen("enabled and maintenance", "host1", nil))
|
||||||
|
assert.True(t, e.evaluateWhen("enabled or maintenance", "host1", nil))
|
||||||
|
assert.False(t, e.evaluateWhen("maintenance or false", "host1", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecutor_EvaluateWhen_Good_LogicalExpressionParentheses(t *testing.T) {
|
||||||
|
e := NewExecutor("/tmp")
|
||||||
|
e.vars["enabled"] = true
|
||||||
|
e.vars["maintenance"] = false
|
||||||
|
e.vars["deployed"] = true
|
||||||
|
|
||||||
|
assert.True(t, e.evaluateWhen("(enabled and not maintenance) or deployed", "host1", nil))
|
||||||
|
assert.False(t, e.evaluateWhen("enabled and (maintenance or false)", "host1", nil))
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecutor_ApplyTaskResultConditions_Good_ChangedWhen(t *testing.T) {
|
func TestExecutor_ApplyTaskResultConditions_Good_ChangedWhen(t *testing.T) {
|
||||||
e := NewExecutor("/tmp")
|
e := NewExecutor("/tmp")
|
||||||
task := &Task{
|
task := &Task{
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ Jinja2-like `{{ var }}` syntax is supported:
|
||||||
`when` supports:
|
`when` supports:
|
||||||
|
|
||||||
- Boolean literals: `true`, `false`
|
- Boolean literals: `true`, `false`
|
||||||
|
- Inline boolean expressions with `and`, `or`, and parentheses
|
||||||
- Registered variable checks: `result is success`, `result is failed`, `result is changed`, `result is defined`
|
- Registered variable checks: `result is success`, `result is failed`, `result is changed`, `result is defined`
|
||||||
- Negation: `not condition`
|
- Negation: `not condition`
|
||||||
- Variable truthiness checks
|
- Variable truthiness checks
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue