-
Notifications
You must be signed in to change notification settings - Fork 18
/
boolevator.go
133 lines (113 loc) · 3.37 KB
/
boolevator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
package boolevator
import (
"context"
"errors"
"fmt"
"github.com/PaesslerAG/gval"
"github.com/cirruslabs/cirrus-cli/pkg/parser/expander"
"strconv"
"text/scanner"
)
type Function func(arguments ...interface{}) interface{}
type Boolevator struct {
functions map[string]Function
}
var ErrInternal = errors.New("internal boolevator error")
func New(opts ...Option) *Boolevator {
boolevator := &Boolevator{
functions: make(map[string]Function),
}
// Apply options
for _, opt := range opts {
opt(boolevator)
}
return boolevator
}
func parseString(_ context.Context, parser *gval.Parser) (gval.Evaluable, error) {
unquoted := trimAllQuotes(parser.TokenText())
return parser.Const(unquoted), nil
}
func trimAllQuotes(text string) string {
//nolint:gomnd
if len(text) < 2 {
return text
}
firstCharacter := text[0]
lastCharacter := text[len(text)-1]
if firstCharacter == lastCharacter && (firstCharacter == '"' || firstCharacter == '\'') {
// return recursively to handle double quoted strings
return trimAllQuotes(text[1 : len(text)-1])
}
return text
}
func (boolevator *Boolevator) Eval(expr string, env map[string]string) (bool, error) {
// Ensure that we keep the env as is
localEnv := make(map[string]string)
for key, value := range env {
localEnv[key] = expander.ExpandEnvironmentVariables(value, env)
}
// We declare this as a closure since we need a way to pass the env map inside
expandVariable := func(ctx context.Context, parser *gval.Parser) (gval.Evaluable, error) {
var variableName string
r := parser.Scan()
if r == '{' {
/* ${VARIABLE} */
parser.Scan()
variableName = parser.TokenText()
parser.Scan()
} else {
/* $VARIABLE */
variableName = parser.TokenText()
}
// Lookup variable
expandedVariable := localEnv[variableName]
return parser.Const(expandedVariable), nil
}
languageBases := []gval.Language{
// Constants
gval.Constant("true", "true"),
gval.Constant("false", "false"),
// gval-provided prefixes and meta prefixes
gval.Parentheses(),
gval.Ident(),
// Prefixes
gval.PrefixExtension(scanner.Char, parseString),
gval.PrefixExtension(scanner.String, parseString),
gval.PrefixExtension('$', expandVariable),
// Operators
gval.PrefixOperator("!", opNot),
gval.InfixOperator("in", opIn),
gval.InfixOperator("&&", opAnd),
gval.InfixOperator("||", opOr),
gval.InfixOperator("==", opEquals),
gval.InfixOperator("!=", opNotEquals),
gval.InfixOperator("=~", opRegexEquals),
gval.InfixOperator("!=~", opRegexNotEquals),
// Operator precedence
//
// Identical to https://introcs.cs.princeton.edu/java/11precedence/
// except for the "in" and regex operators which have the same precedence
// as their non-regex counterparts.
gval.Precedence("!", 14),
gval.Precedence("in", 10),
gval.Precedence("==", 8),
gval.Precedence("!=", 8),
gval.Precedence("=~", 8),
gval.Precedence("!=~", 8),
gval.Precedence("&&", 4),
gval.Precedence("||", 3),
}
// Functions
for name, function := range boolevator.functions {
languageBases = append(languageBases, gval.Function(name, function))
}
result, err := gval.NewLanguage(languageBases...).Evaluate(expr, nil)
if err != nil {
return false, fmt.Errorf("%w: %v", ErrInternal, err)
}
booleanValue, err := strconv.ParseBool(result.(string))
if err != nil {
return false, fmt.Errorf("%w: %v", ErrInternal, err)
}
return booleanValue, nil
}