-
Notifications
You must be signed in to change notification settings - Fork 67
/
json_evaluator.go
235 lines (199 loc) · 6.19 KB
/
json_evaluator.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
package eval
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"github.com/diegoholiveira/jsonlogic/v3"
"github.com/open-feature/flagd/pkg/model"
schema "github.com/open-feature/schemas/json"
log "github.com/sirupsen/logrus"
"github.com/xeipuuv/gojsonschema"
"google.golang.org/protobuf/types/known/structpb"
)
type JSONEvaluator struct {
state Flags
Logger *log.Entry
}
type constraints interface {
bool | string | map[string]any | float64
}
const (
Disabled = "DISABLED"
)
func (je *JSONEvaluator) GetState() (string, error) {
data, err := json.Marshal(&je.state)
if err != nil {
return "", err
}
return string(data), nil
}
func (je *JSONEvaluator) SetState(source string, state string) ([]StateChangeNotification, error) {
schemaLoader := gojsonschema.NewStringLoader(schema.FlagdDefinitions)
flagStringLoader := gojsonschema.NewStringLoader(state)
result, err := gojsonschema.Validate(schemaLoader, flagStringLoader)
if err != nil {
return nil, err
} else if !result.Valid() {
err := errors.New("invalid JSON file")
return nil, err
}
state, err = je.transposeEvaluators(state)
if err != nil {
return nil, fmt.Errorf("transpose evaluators: %w", err)
}
var newFlags Flags
err = json.Unmarshal([]byte(state), &newFlags)
if err != nil {
return nil, fmt.Errorf("unmarshal new state: %w", err)
}
if err := validateDefaultVariants(newFlags); err != nil {
return nil, err
}
s, notifications := je.state.Merge(source, newFlags)
je.state = s
return notifications, nil
}
func resolve[T constraints](key string, context *structpb.Struct,
variantEval func(string, *structpb.Struct) (string, string, error),
variants map[string]any) (
value T,
variant string,
reason string,
err error,
) {
variant, reason, err = variantEval(key, context)
if err != nil {
return value, variant, reason, err
}
var ok bool
value, ok = variants[variant].(T)
if !ok {
return value, variant, model.ErrorReason, errors.New(model.TypeMismatchErrorCode)
}
return value, variant, reason, nil
}
func (je *JSONEvaluator) ResolveBooleanValue(flagKey string, context *structpb.Struct) (
value bool,
variant string,
reason string,
err error,
) {
return resolve[bool](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
}
func (je *JSONEvaluator) ResolveStringValue(flagKey string, context *structpb.Struct) (
value string,
variant string,
reason string,
err error,
) {
return resolve[string](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
}
func (je *JSONEvaluator) ResolveFloatValue(flagKey string, context *structpb.Struct) (
value float64,
variant string,
reason string,
err error,
) {
value, variant, reason, err = resolve[float64](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
return
}
func (je *JSONEvaluator) ResolveIntValue(flagKey string, context *structpb.Struct) (
value int64,
variant string,
reason string,
err error,
) {
var val float64
val, variant, reason, err = resolve[float64](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
value = int64(val)
return
}
func (je *JSONEvaluator) ResolveObjectValue(flagKey string, context *structpb.Struct) (
value map[string]any,
variant string,
reason string,
err error,
) {
return resolve[map[string]any](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
}
// runs the rules (if defined) to determine the variant, otherwise falling through to the default
func (je *JSONEvaluator) evaluateVariant(
flagKey string,
context *structpb.Struct,
) (variant string, reason string, err error) {
flag, ok := je.state.Flags[flagKey]
if !ok {
// flag not found
return "", model.ErrorReason, errors.New(model.FlagNotFoundErrorCode)
}
if flag.State == Disabled {
return "", model.ErrorReason, errors.New(model.FlagDisabledErrorCode)
}
// get the targeting logic, if any
targeting := flag.Targeting
if targeting != nil {
targetingBytes, err := targeting.MarshalJSON()
if err != nil {
je.Logger.Errorf("Error parsing rules for flag %s, %s", flagKey, err)
return "", model.ErrorReason, err
}
b, err := json.Marshal(context)
if err != nil {
je.Logger.Errorf("error parsing context for flag %s, %s, %v", flagKey, err, context)
return "", model.ErrorReason, errors.New(model.ErrorReason)
}
var result bytes.Buffer
// evaluate json-logic rules to determine the variant
err = jsonlogic.Apply(bytes.NewReader(targetingBytes), bytes.NewReader(b), &result)
if err != nil {
je.Logger.Errorf("Error applying rules %s", err)
return "", model.ErrorReason, err
}
// strip whitespace and quotes from the variant
variant = strings.ReplaceAll(strings.TrimSpace(result.String()), "\"", "")
}
// if this is a valid variant, return it
if _, ok := je.state.Flags[flagKey].Variants[variant]; ok {
return variant, model.TargetingMatchReason, nil
}
// if it's not a valid variant, use the default value
return je.state.Flags[flagKey].DefaultVariant, model.DefaultReason, nil
}
// validateDefaultVariants returns an error if any of the default variants aren't valid
func validateDefaultVariants(flags Flags) error {
for name, flag := range flags.Flags {
if _, ok := flag.Variants[flag.DefaultVariant]; !ok {
return fmt.Errorf(
"default variant '%s' isn't a valid variant of flag '%s'", flag.DefaultVariant, name,
)
}
}
return nil
}
func (je *JSONEvaluator) transposeEvaluators(state string) (string, error) {
var evaluators Evaluators
if err := json.Unmarshal([]byte(state), &evaluators); err != nil {
return "", fmt.Errorf("unmarshal: %w", err)
}
for evalName, evalRaw := range evaluators.Evaluators {
// replace any occurrences of "evaluator": "evalName"
regex, err := regexp.Compile(fmt.Sprintf(`"\$ref":(\s)*"%s"`, evalName))
if err != nil {
return "", fmt.Errorf("compile regex: %w", err)
}
marshalledEval, err := evalRaw.MarshalJSON()
if err != nil {
return "", fmt.Errorf("marshal evaluator: %w", err)
}
evalValue := string(marshalledEval)
if len(evalValue) < 3 {
return "", errors.New("evaluator object is empty")
}
evalValue = evalValue[1 : len(evalValue)-2] // remove first { and last }
state = regex.ReplaceAllString(state, evalValue)
}
return state, nil
}