forked from heetch/regula
-
Notifications
You must be signed in to change notification settings - Fork 0
/
engine.go
267 lines (219 loc) · 7.1 KB
/
engine.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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
package regula
import (
"context"
"strconv"
"sync"
"github.com/heetch/confita"
"github.com/heetch/confita/backend"
"github.com/heetch/regula/rule"
"github.com/pkg/errors"
)
// Engine is used to evaluate a ruleset against a group of parameters.
// It provides a list of type safe methods to evaluate a ruleset and always returns the expected type to the caller.
// The engine is stateless and relies on the given evaluator to evaluate a ruleset.
// It is safe for concurrent use.
type Engine struct {
evaluator Evaluator
}
// NewEngine creates an Engine using the given evaluator.
func NewEngine(evaluator Evaluator) *Engine {
return &Engine{
evaluator: evaluator,
}
}
// Get evaluates a ruleset and returns the result.
func (e *Engine) get(ctx context.Context, typ, path string, params rule.Params, opts ...Option) (*EvalResult, error) {
var cfg engineConfig
for _, opt := range opts {
opt(&cfg)
}
var (
result *EvalResult
err error
)
if cfg.Version != "" {
result, err = e.evaluator.EvalVersion(ctx, path, cfg.Version, params)
} else {
result, err = e.evaluator.Eval(ctx, path, params)
}
if err != nil {
if err == ErrRulesetNotFound || err == rule.ErrNoMatch {
return nil, err
}
return nil, errors.Wrap(err, "failed to evaluate ruleset")
}
if result.Value.Type != typ {
return nil, ErrTypeMismatch
}
return result, nil
}
// GetString evaluates a ruleset and returns the result as a string.
func (e *Engine) GetString(ctx context.Context, path string, params rule.Params, opts ...Option) (string, *EvalResult, error) {
res, err := e.get(ctx, "string", path, params, opts...)
if err != nil {
return "", nil, err
}
return res.Value.Data, res, nil
}
// GetBool evaluates a ruleset and returns the result as a bool.
func (e *Engine) GetBool(ctx context.Context, path string, params rule.Params, opts ...Option) (bool, *EvalResult, error) {
res, err := e.get(ctx, "bool", path, params, opts...)
if err != nil {
return false, nil, err
}
b, err := strconv.ParseBool(res.Value.Data)
return b, res, err
}
// GetInt64 evaluates a ruleset and returns the result as an int64.
func (e *Engine) GetInt64(ctx context.Context, path string, params rule.Params, opts ...Option) (int64, *EvalResult, error) {
res, err := e.get(ctx, "int64", path, params, opts...)
if err != nil {
return 0, nil, err
}
i, err := strconv.ParseInt(res.Value.Data, 10, 64)
return i, res, err
}
// GetFloat64 evaluates a ruleset and returns the result as a float64.
func (e *Engine) GetFloat64(ctx context.Context, path string, params rule.Params, opts ...Option) (float64, *EvalResult, error) {
res, err := e.get(ctx, "float64", path, params, opts...)
if err != nil {
return 0, nil, err
}
f, err := strconv.ParseFloat(res.Value.Data, 64)
return f, res, err
}
// LoadStruct takes a pointer to struct and params and loads rulesets into fields
// tagged with the "ruleset" struct tag.
func (e *Engine) LoadStruct(ctx context.Context, to interface{}, params rule.Params) error {
b := backend.Func("regula", func(ctx context.Context, path string) ([]byte, error) {
res, err := e.evaluator.Eval(ctx, path, params)
if err != nil {
if err == ErrRulesetNotFound {
return nil, backend.ErrNotFound
}
return nil, err
}
return []byte(res.Value.Data), nil
})
l := confita.NewLoader(b)
l.Tag = "ruleset"
return l.Load(ctx, to)
}
type engineConfig struct {
Version string
}
// Option is used to customize the engine behaviour.
type Option func(cfg *engineConfig)
// Version is an option used to describe which ruleset version the engine should return.
func Version(version string) Option {
return func(cfg *engineConfig) {
cfg.Version = version
}
}
// An Evaluator provides methods to evaluate rulesets from any location.
// Long running implementations must listen to the given context for timeout and cancelation.
type Evaluator interface {
// Eval evaluates a ruleset using the given params.
// If no ruleset is found for a given path, the implementation must return ErrRulesetNotFound.
Eval(ctx context.Context, path string, params rule.Params) (*EvalResult, error)
// EvalVersion evaluates a specific version of a ruleset using the given params.
// If no ruleset is found for a given path, the implementation must return ErrRulesetNotFound.
EvalVersion(ctx context.Context, path string, version string, params rule.Params) (*EvalResult, error)
}
// EvalResult is the product of an evaluation. It contains the value generated as long as some metadata.
type EvalResult struct {
// Result of the evaluation
Value *rule.Value
// Version of the ruleset that generated this value
Version string
}
// RulesetBuffer can hold a group of rulesets in memory and can be used as an evaluator.
// It is safe for concurrent use.
type RulesetBuffer struct {
rw sync.RWMutex
rulesets map[string][]*rulesetInfo
}
// NewRulesetBuffer creates a ready to use RulesetBuffer.
func NewRulesetBuffer() *RulesetBuffer {
return &RulesetBuffer{
rulesets: make(map[string][]*rulesetInfo),
}
}
type rulesetInfo struct {
path, version string
r *Ruleset
}
// Add adds the given ruleset version to a list for a specific path.
// The last added ruleset is treated as the latest version.
func (b *RulesetBuffer) Add(path, version string, r *Ruleset) {
b.rw.Lock()
b.rulesets[path] = append(b.rulesets[path], &rulesetInfo{path, version, r})
b.rw.Unlock()
}
// Latest returns the latest version of a ruleset.
func (b *RulesetBuffer) Latest(path string) (*Ruleset, string, error) {
b.rw.RLock()
defer b.rw.RUnlock()
l, ok := b.rulesets[path]
if !ok || len(l) == 0 {
return nil, "", ErrRulesetNotFound
}
return l[len(l)-1].r, l[len(l)-1].version, nil
}
// GetVersion returns a ruleset associated with the given path and version.
func (b *RulesetBuffer) GetVersion(path, version string) (*Ruleset, error) {
b.rw.RLock()
defer b.rw.RUnlock()
ri, err := b.getVersion(path, version)
if err != nil {
return nil, err
}
return ri.r, nil
}
// Eval evaluates the latest added ruleset or returns ErrRulesetNotFound if not found.
func (b *RulesetBuffer) Eval(ctx context.Context, path string, params rule.Params) (*EvalResult, error) {
b.rw.RLock()
defer b.rw.RUnlock()
l, ok := b.rulesets[path]
if !ok || len(l) == 0 {
return nil, ErrRulesetNotFound
}
ri := l[len(l)-1]
v, err := ri.r.Eval(params)
if err != nil {
return nil, err
}
return &EvalResult{
Value: v,
Version: ri.version,
}, nil
}
func (b *RulesetBuffer) getVersion(path, version string) (*rulesetInfo, error) {
l, ok := b.rulesets[path]
if !ok || len(l) == 0 {
return nil, ErrRulesetNotFound
}
for _, ri := range l {
if ri.version == version {
return ri, nil
}
}
return nil, ErrRulesetNotFound
}
// EvalVersion evaluates the selected ruleset version or returns ErrRulesetNotFound if not found.
func (b *RulesetBuffer) EvalVersion(ctx context.Context, path, version string, params rule.Params) (*EvalResult, error) {
b.rw.RLock()
defer b.rw.RUnlock()
ri, err := b.getVersion(path, version)
if err != nil {
return nil, err
}
v, err := ri.r.Eval(params)
if err != nil {
return nil, err
}
return &EvalResult{
Value: v,
Version: ri.version,
}, nil
}