goherence/internal/executor/condition.go
2026-09-11 10:17:25 +03:00

245 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package executor
import (
"fmt"
"strconv"
"strings"
)
// evalWhen вычисляет when-выражение над hostVars. Грамматика намеренно
// минимальна и совпадает с тем, что разрешает lint.WhenGrammarRule:
// сравнения (== != < > <= >=), and/or/not, переменные и литералы.
// Никаких вызовов функций — если выражение прошло линтер, сюда оно
// попадает уже гарантированно "чистым".
//
// Переменные резолвятся через hostVars сразу в момент разбора атома
// (parseAtom), а не отложенно — hostVars известен целиком заранее, так
// что откладывать резолюцию нет причины, а более ранняя версия из-за
// этого имела баг: compare() сравнивал имя переменной как литерал,
// а не её значение.
func evalWhen(expr string, hostVars map[string]interface{}) bool {
p := &condParser{tokens: tokenize(expr), vars: hostVars}
v, err := p.parseOr()
if err != nil {
// невалидное when — считаем условие ложным и не роняем весь плейбук:
// синтаксическая проверка уже была на этапе lint, здесь это
// подстраховка на случай --force поверх ошибки линтера.
return false
}
return toBool(v)
}
type condParser struct {
tokens []string
pos int
vars map[string]interface{}
}
func (p *condParser) peek() string {
if p.pos >= len(p.tokens) {
return ""
}
return p.tokens[p.pos]
}
func (p *condParser) next() string {
t := p.peek()
p.pos++
return t
}
func (p *condParser) parseOr() (interface{}, error) {
left, err := p.parseAnd()
if err != nil {
return nil, err
}
for p.peek() == "or" {
p.next()
right, err := p.parseAnd()
if err != nil {
return nil, err
}
left = toBool(left) || toBool(right)
}
return left, nil
}
func (p *condParser) parseAnd() (interface{}, error) {
left, err := p.parseNot()
if err != nil {
return nil, err
}
for p.peek() == "and" {
p.next()
right, err := p.parseNot()
if err != nil {
return nil, err
}
left = toBool(left) && toBool(right)
}
return left, nil
}
func (p *condParser) parseNot() (interface{}, error) {
if p.peek() == "not" {
p.next()
v, err := p.parseNot()
if err != nil {
return nil, err
}
return !toBool(v), nil
}
return p.parseComparison()
}
func (p *condParser) parseComparison() (interface{}, error) {
left, err := p.parseAtom()
if err != nil {
return nil, err
}
op := p.peek()
switch op {
case "==", "!=", "<", ">", "<=", ">=":
p.next()
right, err := p.parseAtom()
if err != nil {
return nil, err
}
return compare(op, left, right), nil
}
return left, nil
}
// parseAtom резолвит переменные сразу через p.vars — идентификатор,
// не являющийся числом/строкой/true/false, ищется в hostVars и
// возвращается уже как реальное значение (или nil, если переменной нет).
func (p *condParser) parseAtom() (interface{}, error) {
tok := p.next()
switch {
case tok == "(":
v, err := p.parseOr()
if err != nil {
return nil, err
}
if p.next() != ")" {
return nil, fmt.Errorf("ожидалась закрывающая скобка")
}
return v, nil
case tok == "":
return nil, fmt.Errorf("неожиданный конец выражения")
case strings.HasPrefix(tok, `"`) || strings.HasPrefix(tok, "'"):
return strings.Trim(tok, `"'`), nil
case isNumber(tok):
f, _ := strconv.ParseFloat(tok, 64)
return f, nil
case tok == "true":
return true, nil
case tok == "false":
return false, nil
default:
return p.vars[tok], nil // отсутствующая переменная — nil, не ошибка
}
}
func compare(op string, left, right interface{}) bool {
// числовое сравнение, если оба значения — float64
lf, lok := left.(float64)
rf, rok := right.(float64)
if lok && rok {
switch op {
case "==":
return lf == rf
case "!=":
return lf != rf
case "<":
return lf < rf
case ">":
return lf > rf
case "<=":
return lf <= rf
case ">=":
return lf >= rf
}
}
// булево сравнение, если оба значения — bool (частый случай:
// `when: clear_cache == true`, где true уже распознан как bool атом)
lb, lbok := left.(bool)
rb, rbok := right.(bool)
if lbok && rbok {
switch op {
case "==":
return lb == rb
case "!=":
return lb != rb
}
}
ls := fmt.Sprintf("%v", left)
rs := fmt.Sprintf("%v", right)
switch op {
case "==":
return ls == rs
case "!=":
return ls != rs
default:
return ls < rs // строковое < / > — редкий случай, оставлен для полноты
}
}
// toBool приводит уже резолвленное значение (не varRef — резолюция
// происходит раньше, в parseAtom) к bool.
func toBool(v interface{}) bool {
switch vv := v.(type) {
case bool:
return vv
case string:
return vv != ""
case float64:
return vv != 0
default:
return v != nil
}
}
func isNumber(s string) bool {
_, err := strconv.ParseFloat(s, 64)
return err == nil
}
// tokenize — примитивный токенизатор: числа/строки/идентификаторы через
// пробелы, операторы сравнения и скобки — отдельными токенами.
func tokenize(expr string) []string {
var tokens []string
i := 0
for i < len(expr) {
c := expr[i]
switch {
case c == ' ' || c == '\t':
i++
case c == '(' || c == ')':
tokens = append(tokens, string(c))
i++
case strings.HasPrefix(expr[i:], "=="), strings.HasPrefix(expr[i:], "!="),
strings.HasPrefix(expr[i:], "<="), strings.HasPrefix(expr[i:], ">="):
tokens = append(tokens, expr[i:i+2])
i += 2
case c == '<' || c == '>':
tokens = append(tokens, string(c))
i++
case c == '"' || c == '\'':
j := i + 1
for j < len(expr) && expr[j] != c {
j++
}
tokens = append(tokens, expr[i:j+1])
i = j + 1
default:
j := i
for j < len(expr) && expr[j] != ' ' && expr[j] != '(' && expr[j] != ')' {
j++
}
tokens = append(tokens, expr[i:j])
i = j
}
}
return tokens
}