Skip to content

Commit d805690

Browse files
committed
feat!: refactor store interfaces and json parsing
Signed-off-by: Simon Schrottner <[email protected]> diff --git c/core/pkg/evaluator/fractional_test.go i/core/pkg/evaluator/fractional_test.go index e933e86..c1dfb9a 100644 --- c/core/pkg/evaluator/fractional_test.go +++ i/core/pkg/evaluator/fractional_test.go @@ -15,11 +15,12 @@ func TestFractionalEvaluation(t *testing.T) { var sources = []string{source} ctx := context.Background() - commonFlags := map[string]model.Flag{ - "headerColor": { + commonFlags := []model.Flag{ + { + Key: "headerColor", State: "ENABLED", DefaultVariant: "red", - Variants: colorVariants, + Variants: colorVariants, Targeting: []byte(`{ "if": [ { @@ -51,10 +52,11 @@ func TestFractionalEvaluation(t *testing.T) { ] }`), }, - "customSeededHeaderColor": { + { + Key: "customSeededHeaderColor", State: "ENABLED", DefaultVariant: "red", - Variants: colorVariants, + Variants: colorVariants, Targeting: []byte(`{ "if": [ { @@ -77,7 +79,7 @@ func TestFractionalEvaluation(t *testing.T) { } tests := map[string]struct { - flags map[string]model.Flag + flags []model.Flag flagKey string context map[string]any expectedValue string @@ -166,12 +168,12 @@ func TestFractionalEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "[email protected] with different flag key": { - flags: map[string]model.Flag{ - "footerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "footerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "in": ["@faas.com", { @@ -201,7 +203,7 @@ func TestFractionalEvaluation(t *testing.T) { }, null ] }`), - }, + }, }, flagKey: "footerColor", context: map[string]any{ @@ -212,12 +214,12 @@ func TestFractionalEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "non even split": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "in": ["@faas.com", { @@ -243,7 +245,7 @@ func TestFractionalEvaluation(t *testing.T) { }, null ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -254,12 +256,12 @@ func TestFractionalEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "fallback to default variant if no email provided": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "fractional": [ {"var": "email"}, [ @@ -280,7 +282,7 @@ func TestFractionalEvaluation(t *testing.T) { ] ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{}, @@ -289,12 +291,12 @@ func TestFractionalEvaluation(t *testing.T) { expectedReason: model.DefaultReason, }, "get variant for non-percentage weight values": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "fractional": [ {"var": "email"}, [ @@ -307,7 +309,7 @@ func TestFractionalEvaluation(t *testing.T) { ] ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -318,12 +320,12 @@ func TestFractionalEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "get variant for non-specified weight values": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "fractional": [ {"var": "email"}, [ @@ -334,7 +336,7 @@ func TestFractionalEvaluation(t *testing.T) { ] ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -345,12 +347,12 @@ func TestFractionalEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "default to targetingKey if no bucket key provided": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "fractional": [ [ "blue", @@ -362,7 +364,7 @@ func TestFractionalEvaluation(t *testing.T) { ] ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -373,20 +375,20 @@ func TestFractionalEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "missing email - parser should ignore nil/missing custom variables and continue": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte( - `{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte( + `{ "fractional": [ {"var": "email"}, ["red",50], ["blue",50] ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -438,12 +440,12 @@ func BenchmarkFractionalEvaluation(b *testing.B) { var sources = []string{source} ctx := context.Background() - flags := map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags := []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "in": ["@faas.com", { @@ -473,11 +475,11 @@ func BenchmarkFractionalEvaluation(b *testing.B) { }, null ] }`), - }, + }, } tests := map[string]struct { - flags map[string]model.Flag + flags []model.Flag flagKey string context map[string]any expectedValue string diff --git c/core/pkg/evaluator/json.go i/core/pkg/evaluator/json.go index 12b862b..5f8fce1 100644 --- c/core/pkg/evaluator/json.go +++ i/core/pkg/evaluator/json.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/xeipuuv/gojsonschema" "regexp" "strings" "time" @@ -177,7 +178,7 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context var reason string var metadata map[string]interface{} - for flagKey, flag := range allFlags { + for _, flag := range allFlags { if flag.State == Disabled { // ignore evaluation of disabled flag continue @@ -186,18 +187,18 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context defaultValue := flag.Variants[flag.DefaultVariant] switch defaultValue.(type) { case bool: - value, variant, reason, metadata, err = resolve[bool](ctx, reqID, flagKey, context, je.evaluateVariant) + value, variant, reason, metadata, err = resolve[bool](ctx, reqID, flag.Key, context, je.evaluateVariant) case string: - value, variant, reason, metadata, err = resolve[string](ctx, reqID, flagKey, context, je.evaluateVariant) + value, variant, reason, metadata, err = resolve[string](ctx, reqID, flag.Key, context, je.evaluateVariant) case float64: - value, variant, reason, metadata, err = resolve[float64](ctx, reqID, flagKey, context, je.evaluateVariant) + value, variant, reason, metadata, err = resolve[float64](ctx, reqID, flag.Key, context, je.evaluateVariant) case map[string]any: - value, variant, reason, metadata, err = resolve[map[string]any](ctx, reqID, flagKey, context, je.evaluateVariant) + value, variant, reason, metadata, err = resolve[map[string]any](ctx, reqID, flag.Key, context, je.evaluateVariant) } if err != nil { - je.Logger.ErrorWithID(reqID, fmt.Sprintf("bulk evaluation: key: %s returned error: %s", flagKey, err.Error())) + je.Logger.ErrorWithID(reqID, fmt.Sprintf("bulk evaluation: key: %s returned error: %s", flag.Key, err.Error())) } - values = append(values, NewAnyValue(value, variant, reason, flagKey, metadata, err)) + values = append(values, NewAnyValue(value, variant, reason, flag.Key, metadata, err)) } return values, flagSetMetadata, nil @@ -453,23 +454,32 @@ func (je *JSON) configToFlagDefinition(config string, definition *Definition) er "flag definition does not conform to the schema; validation errors: %s", err), ) } - + type JsonRawDef struct { + Flags map[string]model.Flag `json:"flags"` + Metadata map[string]interface{} `json:"metadata"` + } + // Transpose evaluators and unmarshal directly into JsonDef transposedConfig, err := transposeEvaluators(config) if err != nil { return fmt.Errorf("transposing evaluators: %w", err) } - err = json.Unmarshal([]byte(transposedConfig), &definition) + var rawDef JsonRawDef + err = json.Unmarshal([]byte(transposedConfig), &rawDef) if err != nil { return fmt.Errorf("unmarshalling provided configurations: %w", err) } - + definition.Metadata = rawDef.Metadata + for s, flag := range rawDef.Flags { + flag.Key = s + definition.Flags = append(definition.Flags, flag) + } return validateDefaultVariants(definition) } // validateDefaultVariants returns an error if any of the default variants aren't valid func validateDefaultVariants(flags *Definition) error { - for name, flag := range flags.Flags { + for _, flag := range flags.Flags { // Default Variant is not provided in the config if flag.DefaultVariant == "" { continue @@ -477,7 +487,7 @@ func validateDefaultVariants(flags *Definition) error { if _, ok := flag.Variants[flag.DefaultVariant]; !ok { return fmt.Errorf( - "default variant: '%s' isn't a valid variant of flag: '%s'", flag.DefaultVariant, name, + "default variant: '%s' isn't a valid variant of flag: '%s'", flag.DefaultVariant, flag.Key, ) } } diff --git c/core/pkg/evaluator/json_model.go i/core/pkg/evaluator/json_model.go index 0f09eec..826f390 100644 --- c/core/pkg/evaluator/json_model.go +++ i/core/pkg/evaluator/json_model.go @@ -11,7 +11,7 @@ type Evaluators struct { } type Definition struct { - Flags map[string]model.Flag `json:"flags"` + Flags []model.Flag `json:"flags"` Metadata map[string]interface{} `json:"metadata"` } diff --git c/core/pkg/evaluator/semver_test.go i/core/pkg/evaluator/semver_test.go index fbc6582..52f59a9 100644 --- c/core/pkg/evaluator/semver_test.go +++ i/core/pkg/evaluator/semver_test.go @@ -321,7 +321,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { ctx := context.Background() tests := map[string]struct { - flags map[string]model.Flag + flags []model.Flag flagKey string context map[string]any expectedValue string @@ -330,12 +330,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedError error }{ "versions and operator provided - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.0.0", ">", "0.1.0"] @@ -343,7 +343,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", null ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -354,12 +354,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "resolve target property using nested operation - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": [{"var": "version"}, ">", "1.0.0"] @@ -367,7 +367,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", null ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -378,12 +378,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions and operator provided - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.0.0", ">", "1.0.0"] @@ -391,7 +391,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -402,12 +402,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions and major-version operator provided - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.2.3", "^", "1.5.6"] @@ -415,7 +415,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -426,12 +426,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions and minor-version operator provided - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.2.3", "~", "1.2.6"] @@ -439,7 +439,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -450,12 +450,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions given as double - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": [1.2, "=", "1.2"] @@ -463,7 +463,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -474,12 +474,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions given as int - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": [1, "=", "v1.0.0"] @@ -487,7 +487,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -498,12 +498,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions and minor-version without patch version operator provided - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": [1.2, "=", "1.2"] @@ -511,7 +511,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -522,12 +522,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions with prefixed v operator provided - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": [{"var": "version"}, "<", "v1.2"] @@ -535,7 +535,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -546,12 +546,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions and major-version operator provided - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["2.2.3", "^", "1.2.3"] @@ -559,7 +559,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -570,12 +570,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "versions and minor-version operator provided - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.3.3", "~", "1.2.6"] @@ -583,7 +583,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -594,12 +594,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "resolve target property using nested operation - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": [{"var": "version"}, ">", "1.0.0"] @@ -607,7 +607,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -618,12 +618,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing (not an array) - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": "not an array" @@ -631,7 +631,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -642,12 +642,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing (wrong number of items in array) - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["not", "enough"] @@ -655,7 +655,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -666,12 +666,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing (invalid property value) - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["invalid", ">", "1.0.0"] @@ -679,8 +679,9 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, }, + }, + flagKey: "headerColor", context: map[string]any{ "email": "[email protected]", @@ -690,12 +691,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing (invalid property type) - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": [1.0, ">", "1.0.0"] @@ -703,8 +704,9 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, }, + }, + flagKey: "headerColor", context: map[string]any{ "email": "[email protected]", @@ -714,12 +716,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing (invalid operator) - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.0.0", "invalid", "1.0.0"] @@ -727,7 +729,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -738,12 +740,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing (invalid operator type) - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.0.0", 1, "1.0.0"] @@ -751,7 +753,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -762,12 +764,12 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing (invalid target version) - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "sem_ver": ["1.0.0", ">", "invalid"] @@ -775,7 +777,7 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ diff --git c/core/pkg/evaluator/string_comparison_test.go i/core/pkg/evaluator/string_comparison_test.go index 3e6163c..f22466f 100644 --- c/core/pkg/evaluator/string_comparison_test.go +++ i/core/pkg/evaluator/string_comparison_test.go @@ -18,7 +18,7 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { ctx := context.Background() tests := map[string]struct { - flags map[string]model.Flag + flags []model.Flag flagKey string context map[string]any expectedValue string @@ -27,12 +27,12 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { expectedError error }{ "two strings provided - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "starts_with": ["[email protected]", "user@faas"] @@ -40,7 +40,7 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { "red", null ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -51,12 +51,12 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "resolve target property using nested operation - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "starts_with": [{"var": "email"}, "user@faas"] @@ -64,7 +64,7 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { "red", null ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -75,12 +75,12 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "two strings provided - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "starts_with": ["[email protected]", "nope"] @@ -88,7 +88,7 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -99,12 +99,12 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "resolve target property using nested operation - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "starts_with": [{"var": "email"}, "nope"] @@ -112,7 +112,7 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -123,12 +123,12 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "starts_with": "no-array" @@ -136,7 +136,7 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -186,7 +186,7 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { ctx := context.Background() tests := map[string]struct { - flags map[string]model.Flag + flags []model.Flag flagKey string context map[string]any expectedValue string @@ -195,12 +195,12 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { expectedError error }{ "two strings provided - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "ends_with": ["[email protected]", "faas.com"] @@ -208,7 +208,7 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { "red", null ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -219,12 +219,12 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "resolve target property using nested operation - match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "ends_with": [{"var": "email"}, "faas.com"] @@ -232,7 +232,7 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { "red", null ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -243,12 +243,12 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "two strings provided - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "ends_with": ["[email protected]", "nope"] @@ -256,7 +256,7 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -267,12 +267,12 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "resolve target property using nested operation - no match": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "ends_with": [{"var": "email"}, "nope"] @@ -280,7 +280,7 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ @@ -291,12 +291,12 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { expectedReason: model.TargetingMatchReason, }, "error during parsing - return default": { - flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: colorVariants, - Targeting: []byte(`{ + flags: []model.Flag{{ + Key: "headerColor", + State: "ENABLED", + DefaultVariant: "red", + Variants: colorVariants, + Targeting: []byte(`{ "if": [ { "ends_with": "no-array" @@ -304,7 +304,7 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { "red", "green" ] }`), - }, + }, }, flagKey: "headerColor", context: map[string]any{ diff --git c/core/pkg/store/store.go i/core/pkg/store/store.go index 0c6bc16..404604a 100644 --- c/core/pkg/store/store.go +++ i/core/pkg/store/store.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "slices" + "sort" "github.com/hashicorp/go-memdb" "github.com/open-feature/flagd/core/pkg/logger" @@ -15,14 +16,14 @@ var noValidatedSources = []string{} type SelectorContextKey struct{} type FlagQueryResult struct { - Flags map[string]model.Flag + Flags []model.Flag } type IStore interface { Get(ctx context.Context, key string, selector *Selector) (model.Flag, model.Metadata, error) - GetAll(ctx context.Context, selector *Selector) (map[string]model.Flag, model.Metadata, error) + GetAll(ctx context.Context, selector *Selector) ([]model.Flag, model.Metadata, error) Watch(ctx context.Context, selector *Selector, watcher chan<- FlagQueryResult) - Update(source string, flags map[string]model.Flag, metadata model.Metadata) + Update(source string, flags []model.Flag, metadata model.Metadata) } var _ IStore = (*Store)(nil) @@ -192,8 +193,8 @@ func (s *Store) Get(_ context.Context, key string, selector *Selector) (model.Fl } // GetAll returns a copy of the store's state (copy in order to be concurrency safe) -func (s *Store) GetAll(ctx context.Context, selector *Selector) (map[string]model.Flag, model.Metadata, error) { - flags := make(map[string]model.Flag) +func (s *Store) GetAll(ctx context.Context, selector *Selector) ([]model.Flag, model.Metadata, error) { + var flags []model.Flag queryMeta := selector.ToMetadata() it, err := s.selectOrAll(selector) @@ -208,7 +209,7 @@ func (s *Store) GetAll(ctx context.Context, selector *Selector) (map[string]mode // Update the flag state with the provided flags. func (s *Store) Update( source string, - flags map[string]model.Flag, + flags []model.Flag, metadata model.Metadata, ) { if source == "" { @@ -225,32 +226,10 @@ func (s *Store) Update( priority = 0 } - txn := s.db.Txn(true) - defer txn.Abort() - - // get all flags for the source we are updating - selector := NewSelector(sourceIndex + "=" + source) - oldFlags, _, _ := s.GetAll(context.Background(), &selector) - - for key := range oldFlags { - if _, ok := flags[key]; !ok { - // flag has been deleted - s.logger.Debug(fmt.Sprintf("flag %s has been deleted from source %s", key, source)) - - count, err := txn.DeleteAll(flagsTable, keySourceCompoundIndex, key, source) - s.logger.Debug(fmt.Sprintf("deleted %d flags with key %s from source %s", count, key, source)) - - if err != nil { - s.logger.Error(fmt.Sprintf("error deleting flag: %s, %v", key, err)) - } - continue - } - } - - for key, newFlag := range flags { + newFlags := make(map[string]model.Flag) + for _, newFlag := range flags { s.logger.Debug(fmt.Sprintf("got metadata %v", metadata)) - newFlag.Key = key newFlag.Source = source newFlag.Priority = priority newFlag.Metadata = patchMetadata(metadata, newFlag.Metadata) @@ -263,10 +242,42 @@ func (s *Store) Update( flagSetId = setFlagSetId } newFlag.FlagSetId = flagSetId + newFlags[newFlag.FlagSetId+"|"+newFlag.Key] = newFlag + } - raw, err := txn.First(flagsTable, keySourceCompoundIndex, key, source) + txn := s.db.Txn(true) + defer txn.Abort() + + // get all flags for the source we are updating + selector := NewSelector(sourceIndex + "=" + source) + oldFlags, _, _ := s.GetAll(context.Background(), &selector) + + for _, oldFlag := range oldFlags { + if _, ok := newFlags[oldFlag.FlagSetId+"|"+oldFlag.Key]; !ok { + // flag has been deleted + s.logger.Debug(fmt.Sprintf("flag '%s' and flagSetId '%s' has been deleted from source '%s'", oldFlag.Key, oldFlag.FlagSetId, source)) + + count, err := txn.DeleteAll(flagsTable, flagSetIdKeySourceCompoundIndex, oldFlag.FlagSetId, oldFlag.Key, source) + s.logger.Debug(fmt.Sprintf( + "deleted %d flags with key '%s' and flagSetId '%s' from source '%s'", + count, + oldFlag.Key, + oldFlag.FlagSetId, + source, + )) + + if err != nil { + s.logger.Error(fmt.Sprintf("error deleting flag: %s, %v", oldFlag.Key, err)) + } + continue + } + } + + for _, newFlag := range newFlags { + + raw, err := txn.First(flagsTable, keySourceCompoundIndex, newFlag.Key, source) if err != nil { - s.logger.Error(fmt.Sprintf("unable to get flag %s from source %s: %v", key, source, err)) + s.logger.Error(fmt.Sprintf("unable to get flag %s from source %s: %v", newFlag.Key, source, err)) continue } oldFlag, ok := raw.(model.Flag) @@ -275,9 +286,9 @@ func (s *Store) Update( if oldFlag.FlagSetId != newFlag.FlagSetId { // If the flagSetId is different, we need to delete the entry, since flagSetId+key represents the primary index, and it's now been changed. // This is important especially for clients listening to flagSetId changes, as they expect the flag to be removed from the set in this case. - _, err = txn.DeleteAll(flagsTable, idIndex, oldFlag.FlagSetId, key) + _, err = txn.DeleteAll(flagsTable, idIndex, oldFlag.FlagSetId, newFlag.Key) if err != nil { - s.logger.Error(fmt.Sprintf("unable to delete flags with key %s and flagSetId %s: %v", key, oldFlag.FlagSetId, err)) + s.logger.Error(fmt.Sprintf("unable to delete flags with key %s and flagSetId %s: %v", newFlag.Key, oldFlag.FlagSetId, err)) continue } } @@ -286,7 +297,7 @@ func (s *Store) Update( s.logger.Debug(fmt.Sprintf("storing flag: %v", newFlag)) err = txn.Insert(flagsTable, newFlag) if err != nil { - s.logger.Error(fmt.Sprintf("unable to insert flag %s: %v", key, err)) + s.logger.Error(fmt.Sprintf("unable to insert flag %s: %v", newFlag.Key, err)) continue } } @@ -335,20 +346,32 @@ func (s *Store) selectOrAll(selector *Selector) (it memdb.ResultIterator, err er } // collects flags from an iterator, ensuring that only the highest priority flag is kept when there are duplicates -func (s *Store) collect(it memdb.ResultIterator) map[string]model.Flag { +func (s *Store) collect(it memdb.ResultIterator) []model.Flag { flags := make(map[string]model.Flag) for raw := it.Next(); raw != nil; raw = it.Next() { flag := raw.(model.Flag) - if existing, ok := flags[flag.Key]; ok { + + // checking for multiple flags with the same key, as they can be defined multiple times in different sources + if existing, ok := flags[flag.FlagSetId+"|"+flag.Key]; ok { if flag.Priority < existing.Priority { - s.logger.Debug(fmt.Sprintf("discarding duplicate flag %s from lower priority source %s in favor of flag from source %s", flag.Key, s.sources[flag.Priority], s.sources[existing.Priority])) + s.logger.Debug(fmt.Sprintf("discarding duplicate flag with key '%s' and flagSetId '%s' from lower priority source '%s' in favor of flag from source '%s'", flag.Key, flag.FlagSetId, s.sources[flag.Priority], s.sources[existing.Priority])) continue // we already have a higher priority flag } - s.logger.Debug(fmt.Sprintf("overwriting duplicate flag %s from lower priority source %s in favor of flag from source %s", flag.Key, s.sources[existing.Priority], s.sources[flag.Priority])) + s.logger.Debug(fmt.Sprintf("overwriting duplicate flag with key '%s' and flagSetId '%s' from lower priority source '%s' in favor of flag from source '%s'", flag.Key, flag.FlagSetId, s.sources[existing.Priority], s.sources[flag.Priority])) } - flags[flag.Key] = flag + + flags[flag.FlagSetId+"|"+flag.Key] = flag } - return flags + + flattenedFlags := make([]model.Flag, 0, len(flags)) + for _, value := range flags { + flattenedFlags = append(flattenedFlags, value) + } + // we should order to keep the same order all the time in our response + sort.Slice(flattenedFlags, func(i, j int) bool { + return fmt.Sprintf("%s|%s", flattenedFlags[i].FlagSetId, flattenedFlags[i].Key) < fmt.Sprintf("%s|%s", flattenedFlags[j].FlagSetId, flattenedFlags[j].Key) + }) + return flattenedFlags } func patchMetadata(original, patch model.Metadata) model.Metadata { diff --git c/core/pkg/store/store_test.go i/core/pkg/store/store_test.go index c6cf2dd..f482e6a 100644 --- c/core/pkg/store/store_test.go +++ i/core/pkg/store/store_test.go @@ -2,6 +2,7 @@ package store import ( "context" + "sort" "testing" "time" @@ -21,9 +22,9 @@ func TestUpdateFlags(t *testing.T) { tests := []struct { name string setup func(t *testing.T) IStore - newFlags map[string]model.Flag + newFlags []model.Flag source string - wantFlags map[string]model.Flag + wantFlags []model.Flag setMetadata model.Metadata }{ { @@ -37,7 +38,7 @@ func TestUpdateFlags(t *testing.T) { }, source: source1, newFlags: nil, - wantFlags: map[string]model.Flag{}, + wantFlags: []model.Flag{}, }, { name: "both empty flags", @@ -49,8 +50,8 @@ func TestUpdateFlags(t *testing.T) { return s }, source: source1, - newFlags: map[string]model.Flag{}, - wantFlags: map[string]model.Flag{}, + newFlags: []model.Flag{}, + wantFlags: []model.Flag{}, }, { name: "empty new", @@ -63,7 +64,7 @@ func TestUpdateFlags(t *testing.T) { }, source: source1, newFlags: nil, - wantFlags: map[string]model.Flag{}, + wantFlags: []model.Flag{}, }, { name: "update from source 1 (old flag removed)", @@ -72,17 +73,17 @@ func TestUpdateFlags(t *testing.T) { if err != nil { t.Fatalf("NewStore failed: %v", err) } - s.Update(source1, map[string]model.Flag{ - "waka": {DefaultVariant: "off"}, + s.Update(source1, []model.Flag{ + {Key: "waka", DefaultVariant: "off"}, }, nil) return s }, - newFlags: map[string]model.Flag{ - "paka": {DefaultVariant: "on"}, + newFlags: []model.Flag{ + {Key: "paka", DefaultVariant: "on"}, }, source: source1, - wantFlags: map[string]model.Flag{ - "paka": {Key: "paka", DefaultVariant: "on", Source: source1, FlagSetId: nilFlagSetId, Priority: 0}, + wantFlags: []model.Flag{ + {Key: "paka", DefaultVariant: "on", Source: source1, FlagSetId: nilFlagSetId, Priority: 0}, }, }, { @@ -92,18 +93,18 @@ func TestUpdateFlags(t *testing.T) { if err != nil { t.Fatalf("NewStore failed: %v", err) } - s.Update(source1, map[string]model.Flag{ - "waka": {DefaultVariant: "off"}, + s.Update(source1, []model.Flag{ + {Key: "waka", DefaultVariant: "off"}, }, nil) return s }, - newFlags: map[string]model.Flag{ - "paka": {DefaultVariant: "on"}, + newFlags: []model.Flag{ + {Key: "paka", DefaultVariant: "on"}, }, source: source2, - wantFlags: map[string]model.Flag{ - "waka": {Key: "waka", DefaultVariant: "off", Source: source1, FlagSetId: nilFlagSetId, Priority: 0}, - "paka": {Key: "paka", DefaultVariant: "on", Source: source2, FlagSetId: nilFlagSetId, Priority: 1}, + wantFlags: []model.Flag{ + {Key: "waka", DefaultVariant: "off", Source: source1, FlagSetId: nilFlagSetId, Priority: 0}, + {Key: "paka", DefaultVariant: "on", Source: source2, FlagSetId: nilFlagSetId, Priority: 1}, }, }, { @@ -113,20 +114,20 @@ func TestUpdateFlags(t *testing.T) { if err != nil { t.Fatalf("NewStore failed: %v", err) } - s.Update(source1, map[string]model.Flag{}, model.Metadata{}) + s.Update(source1, []model.Flag{}, model.Metadata{}) return s }, setMetadata: model.Metadata{ "flagSetId": "topLevelSet", // top level set metadata, including flagSetId }, - newFlags: map[string]model.Flag{ - "waka": {DefaultVariant: "on"}, - "paka": {DefaultVariant: "on", Metadata: model.Metadata{"flagSetId": "flagLevelSet"}}, // overrides set level flagSetId + newFlags: []model.Flag{ + {Key: "waka", DefaultVariant: "on"}, + {Key: "paka", DefaultVariant: "on", Metadata: model.Metadata{"flagSetId": "flagLevelSet"}}, // overrides set level flagSetId }, source: source1, - wantFlags: map[string]model.Flag{ - "waka": {Key: "waka", DefaultVariant: "on", Source: source1, FlagSetId: "topLevelSet", Priority: 0, Metadata: model.Metadata{"flagSetId": "topLevelSet"}}, - "paka": {Key: "paka", DefaultVariant: "on", Source: source1, FlagSetId: "flagLevelSet", Priority: 0, Metadata: model.Metadata{"flagSetId": "flagLevelSet"}}, + wantFlags: []model.Flag{ + {Key: "waka", DefaultVariant: "on", Source: source1, FlagSetId: "topLevelSet", Priority: 0, Metadata: model.Metadata{"flagSetId": "topLevelSet"}}, + {Key: "paka", DefaultVariant: "on", Source: source1, FlagSetId: "flagLevelSet", Priority: 0, Metadata: model.Metadata{"flagSetId": "flagLevelSet"}}, }, }, } @@ -138,8 +139,13 @@ func TestUpdateFlags(t *testing.T) { store := tt.setup(t) store.Update(tt.source, tt.newFlags, tt.setMetadata) gotFlags, _, _ := store.GetAll(context.Background(), nil) - - require.Equal(t, tt.wantFlags, gotFlags) + sort.Slice(tt.wantFlags, func(i, j int) bool { + return tt.wantFlags[i].FlagSetId+"|"+tt.wantFlags[i].Key > tt.wantFlags[j].FlagSetId+"|"+tt.wantFlags[j].Key + }) + sort.Slice(gotFlags, func(i, j int) bool { + return gotFlags[i].FlagSetId+"|"+gotFlags[i].Key > gotFlags[j].FlagSetId+"|"+gotFlags[j].Key + }) + require.EqualValues(t, tt.wantFlags, gotFlags) }) } } @@ -206,16 +212,16 @@ func TestGet(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - sourceAFlags := map[string]model.Flag{ - "flagA": {Key: "flagA", DefaultVariant: "off"}, - "dupe": {Key: "dupe", DefaultVariant: "on"}, + sourceAFlags := []model.Flag{ + {Key: "flagA", DefaultVariant: "off"}, + {Key: "dupe", DefaultVariant: "on"}, } - sourceBFlags := map[string]model.Flag{ - "flagB": {Key: "flagB", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdB}}, + sourceBFlags := []model.Flag{ + {Key: "flagB", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdB}}, } - sourceCFlags := map[string]model.Flag{ - "flagC": {Key: "flagC", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, - "dupe": {Key: "dupe", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + sourceCFlags := []model.Flag{ + {Key: "flagC", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + {Key: "dupe", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, } store, err := NewStore(logger.NewLogger(nil, false), sources) @@ -253,35 +259,36 @@ func TestGetAllNoWatcher(t *testing.T) { tests := []struct { name string selector *Selector - wantFlags map[string]model.Flag + wantFlags []model.Flag }{ { name: "nil selector", selector: nil, - wantFlags: map[string]model.Flag{ + wantFlags: []model.Flag{ // "dupe" should be overwritten by higher priority flag - "flagA": {Key: "flagA", DefaultVariant: "off", Source: sourceA, FlagSetId: nilFlagSetId, Priority: 0}, - "flagB": {Key: "flagB", DefaultVariant: "off", Source: sourceB, FlagSetId: flagSetIdB, Priority: 1, Metadata: model.Metadata{"flagSetId": flagSetIdB}}, - "flagC": {Key: "flagC", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, - "dupe": {Key: "dupe", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + {Key: "dupe", DefaultVariant: "on", Source: sourceA, FlagSetId: nilFlagSetId, Priority: 0}, + {Key: "flagA", DefaultVariant: "off", Source: sourceA, FlagSetId: nilFlagSetId, Priority: 0}, + {Key: "flagB", DefaultVariant: "off", Source: sourceB, FlagSetId: flagSetIdB, Priority: 1, Metadata: model.Metadata{"flagSetId": flagSetIdB}}, + {Key: "flagC", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + {Key: "dupe", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, }, }, { name: "source selector", selector: &sourceASelector, - wantFlags: map[string]model.Flag{ + wantFlags: []model.Flag{ // we should get the "dupe" from sourceA - "flagA": {Key: "flagA", DefaultVariant: "off", Source: sourceA, FlagSetId: nilFlagSetId, Priority: 0}, - "dupe": {Key: "dupe", DefaultVariant: "on", Source: sourceA, FlagSetId: nilFlagSetId, Priority: 0}, + {Key: "flagA", DefaultVariant: "off", Source: sourceA, FlagSetId: nilFlagSetId, Priority: 0}, + {Key: "dupe", DefaultVariant: "on", Source: sourceA, FlagSetId: nilFlagSetId, Priority: 0}, }, }, { name: "flagSetId selector", selector: &flagSetIdCSelector, - wantFlags: map[string]model.Flag{ + wantFlags: []model.Flag{ // we should get the "dupe" from flagSetIdC - "flagC": {Key: "flagC", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, - "dupe": {Key: "dupe", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + {Key: "flagC", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + {Key: "dupe", DefaultVariant: "off", Source: sourceC, FlagSetId: flagSetIdC, Priority: 2, Metadata: model.Metadata{"flagSetId": flagSetIdC}}, }, }, } @@ -291,16 +298,16 @@ func TestGetAllNoWatcher(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - sourceAFlags := map[string]model.Flag{ - "flagA": {Key: "flagA", DefaultVariant: "off"}, - "dupe": {Key: "dupe", DefaultVariant: "on"}, + sourceAFlags := []model.Flag{ + {Key: "flagA", DefaultVariant: "off"}, + {Key: "dupe", DefaultVariant: "on"}, } - sourceBFlags := map[string]model.Flag{ - "flagB": {Key: "flagB", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdB}}, + sourceBFlags := []model.Flag{ + {Key: "flagB", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdB}}, } - sourceCFlags := map[string]model.Flag{ - "flagC": {Key: "flagC", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, - "dupe": {Key: "dupe", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + sourceCFlags := []model.Flag{ + {Key: "flagC", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, + {Key: "dupe", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": flagSetIdC}}, } store, err := NewStore(logger.NewLogger(nil, false), sources) @@ -314,6 +321,12 @@ func TestGetAllNoWatcher(t *testing.T) { gotFlags, _, _ := store.GetAll(context.Background(), tt.selector) require.Equal(t, len(tt.wantFlags), len(gotFlags)) + sort.Slice(tt.wantFlags, func(i, j int) bool { + return tt.wantFlags[i].FlagSetId+"|"+tt.wantFlags[i].Key > tt.wantFlags[j].FlagSetId+"|"+tt.wantFlags[j].Key + }) + sort.Slice(gotFlags, func(i, j int) bool { + return gotFlags[i].FlagSetId+"|"+gotFlags[i].Key > gotFlags[j].FlagSetId+"|"+gotFlags[j].Key + }) require.Equal(t, tt.wantFlags, gotFlags) }) } @@ -365,14 +378,12 @@ func TestWatch(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - sourceAFlags := map[string]model.Flag{ - "flagA": {Key: "flagA", DefaultVariant: "off"}, + sourceAFlags := []model.Flag{ + {Key: "flagA", DefaultVariant: "off"}, } - sourceBFlags := map[string]model.Flag{ - "flagB": {Key: "flagB", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": myFlagSetId}}, - } - sourceCFlags := map[string]model.Flag{ - "flagC": {Key: "flagC", DefaultVariant: "off"}, + sourceBFlags := []model.Flag{{Key: "flagB", DefaultVariant: "off", Metadata: model.Metadata{"flagSetId": myFlagSetId}}} + sourceCFlags := []model.Flag{ + {Key: "flagC", DefaultVariant: "off"}, } store, err := NewStore(logger.NewLogger(nil, false), sources) @@ -396,29 +407,30 @@ func TestWatch(t *testing.T) { time.Sleep(pauseTime) // changing a flag default variant should trigger an update - store.Update(sourceA, map[string]model.Flag{ - "flagA": {Key: "flagA", DefaultVariant: "on"}, + store.Update(sourceA, []model.Flag{ + {Key: "flagA", DefaultVariant: "on"}, }, model.Metadata{}) time.Sleep(pauseTime) // changing a flag default variant should trigger an update - store.Update(sourceB, map[string]model.Flag{ - "flagB": {Key: "flagB", DefaultVariant: "on", Metadata: model.Metadata{"flagSetId": myFlagSetId}}, + store.Update(sourceB, []model.Flag{ + {Key: "flagB", DefaultVariant: "on", Metadata: model.Metadata{"flagSetId": myFlagSetId}}, }, model.Metadata{}) time.Sleep(pauseTime) // removing a flag set id should trigger an update (even for flag set id selectors; it should remove the flag from the set) - store.Update(sourceB, map[string]model.Flag{ - "flagB": {Key: "flagB", DefaultVariant: "on"}, + // TODO: challenge this test and behaviour + store.Update(sourceB, []model.Flag{ + {Key: "flagB", DefaultVariant: "on"}, }, model.Metadata{}) time.Sleep(pauseTime) // adding a flag set id should trigger an update - store.Update(sourceB, map[string]model.Flag{ - "flagB": {Key: "flagB", DefaultVariant: "on", Metadata: model.Metadata{"flagSetId": myFlagSetId}}, + store.Update(sourceB, []model.Flag{ + {Key: "flagB", DefaultVariant: "on", Metadata: model.Metadata{"flagSetId": myFlagSetId}}, }, model.Metadata{}) }() @@ -448,9 +460,9 @@ func TestQueryMetadata(t *testing.T) { otherSource := "otherSource" nonExistingFlagSetId := "nonExistingFlagSetId" var sources = []string{sourceA} - sourceAFlags := map[string]model.Flag{ - "flagA": {Key: "flagA", DefaultVariant: "off"}, - "flagB": {Key: "flagB", DefaultVariant: "on"}, + sourceAFlags := []model.Flag{ + {Key: "flagA", DefaultVariant: "off"}, + {Key: "flagB", DefaultVariant: "on"}, } store, err := NewStore(logger.NewLogger(nil, false), sources)
1 parent aa504f7 commit d805690

File tree

7 files changed

+445
-396
lines changed

7 files changed

+445
-396
lines changed

core/pkg/evaluator/fractional_test.go

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ func TestFractionalEvaluation(t *testing.T) {
1515
var sources = []string{source}
1616
ctx := context.Background()
1717

18-
commonFlags := map[string]model.Flag{
19-
"headerColor": {
18+
commonFlags := []model.Flag{
19+
{
20+
Key: "headerColor",
2021
State: "ENABLED",
2122
DefaultVariant: "red",
22-
Variants: colorVariants,
23+
Variants: colorVariants,
2324
Targeting: []byte(`{
2425
"if": [
2526
{
@@ -51,10 +52,11 @@ func TestFractionalEvaluation(t *testing.T) {
5152
]
5253
}`),
5354
},
54-
"customSeededHeaderColor": {
55+
{
56+
Key: "customSeededHeaderColor",
5557
State: "ENABLED",
5658
DefaultVariant: "red",
57-
Variants: colorVariants,
59+
Variants: colorVariants,
5860
Targeting: []byte(`{
5961
"if": [
6062
{
@@ -77,7 +79,7 @@ func TestFractionalEvaluation(t *testing.T) {
7779
}
7880

7981
tests := map[string]struct {
80-
flags map[string]model.Flag
82+
flags []model.Flag
8183
flagKey string
8284
context map[string]any
8385
expectedValue string
@@ -166,12 +168,12 @@ func TestFractionalEvaluation(t *testing.T) {
166168
expectedReason: model.TargetingMatchReason,
167169
},
168170
"[email protected] with different flag key": {
169-
flags: map[string]model.Flag{
170-
"footerColor": {
171-
State: "ENABLED",
172-
DefaultVariant: "red",
173-
Variants: colorVariants,
174-
Targeting: []byte(`{
171+
flags: []model.Flag{{
172+
Key: "footerColor",
173+
State: "ENABLED",
174+
DefaultVariant: "red",
175+
Variants: colorVariants,
176+
Targeting: []byte(`{
175177
"if": [
176178
{
177179
"in": ["@faas.com", {
@@ -201,7 +203,7 @@ func TestFractionalEvaluation(t *testing.T) {
201203
}, null
202204
]
203205
}`),
204-
},
206+
},
205207
},
206208
flagKey: "footerColor",
207209
context: map[string]any{
@@ -212,12 +214,12 @@ func TestFractionalEvaluation(t *testing.T) {
212214
expectedReason: model.TargetingMatchReason,
213215
},
214216
"non even split": {
215-
flags: map[string]model.Flag{
216-
"headerColor": {
217-
State: "ENABLED",
218-
DefaultVariant: "red",
219-
Variants: colorVariants,
220-
Targeting: []byte(`{
217+
flags: []model.Flag{{
218+
Key: "headerColor",
219+
State: "ENABLED",
220+
DefaultVariant: "red",
221+
Variants: colorVariants,
222+
Targeting: []byte(`{
221223
"if": [
222224
{
223225
"in": ["@faas.com", {
@@ -243,7 +245,7 @@ func TestFractionalEvaluation(t *testing.T) {
243245
}, null
244246
]
245247
}`),
246-
},
248+
},
247249
},
248250
flagKey: "headerColor",
249251
context: map[string]any{
@@ -254,12 +256,12 @@ func TestFractionalEvaluation(t *testing.T) {
254256
expectedReason: model.TargetingMatchReason,
255257
},
256258
"fallback to default variant if no email provided": {
257-
flags: map[string]model.Flag{
258-
"headerColor": {
259-
State: "ENABLED",
260-
DefaultVariant: "red",
261-
Variants: colorVariants,
262-
Targeting: []byte(`{
259+
flags: []model.Flag{{
260+
Key: "headerColor",
261+
State: "ENABLED",
262+
DefaultVariant: "red",
263+
Variants: colorVariants,
264+
Targeting: []byte(`{
263265
"fractional": [
264266
{"var": "email"},
265267
[
@@ -280,7 +282,7 @@ func TestFractionalEvaluation(t *testing.T) {
280282
]
281283
]
282284
}`),
283-
},
285+
},
284286
},
285287
flagKey: "headerColor",
286288
context: map[string]any{},
@@ -289,12 +291,12 @@ func TestFractionalEvaluation(t *testing.T) {
289291
expectedReason: model.DefaultReason,
290292
},
291293
"get variant for non-percentage weight values": {
292-
flags: map[string]model.Flag{
293-
"headerColor": {
294-
State: "ENABLED",
295-
DefaultVariant: "red",
296-
Variants: colorVariants,
297-
Targeting: []byte(`{
294+
flags: []model.Flag{{
295+
Key: "headerColor",
296+
State: "ENABLED",
297+
DefaultVariant: "red",
298+
Variants: colorVariants,
299+
Targeting: []byte(`{
298300
"fractional": [
299301
{"var": "email"},
300302
[
@@ -307,7 +309,7 @@ func TestFractionalEvaluation(t *testing.T) {
307309
]
308310
]
309311
}`),
310-
},
312+
},
311313
},
312314
flagKey: "headerColor",
313315
context: map[string]any{
@@ -318,12 +320,12 @@ func TestFractionalEvaluation(t *testing.T) {
318320
expectedReason: model.TargetingMatchReason,
319321
},
320322
"get variant for non-specified weight values": {
321-
flags: map[string]model.Flag{
322-
"headerColor": {
323-
State: "ENABLED",
324-
DefaultVariant: "red",
325-
Variants: colorVariants,
326-
Targeting: []byte(`{
323+
flags: []model.Flag{{
324+
Key: "headerColor",
325+
State: "ENABLED",
326+
DefaultVariant: "red",
327+
Variants: colorVariants,
328+
Targeting: []byte(`{
327329
"fractional": [
328330
{"var": "email"},
329331
[
@@ -334,7 +336,7 @@ func TestFractionalEvaluation(t *testing.T) {
334336
]
335337
]
336338
}`),
337-
},
339+
},
338340
},
339341
flagKey: "headerColor",
340342
context: map[string]any{
@@ -345,12 +347,12 @@ func TestFractionalEvaluation(t *testing.T) {
345347
expectedReason: model.TargetingMatchReason,
346348
},
347349
"default to targetingKey if no bucket key provided": {
348-
flags: map[string]model.Flag{
349-
"headerColor": {
350-
State: "ENABLED",
351-
DefaultVariant: "red",
352-
Variants: colorVariants,
353-
Targeting: []byte(`{
350+
flags: []model.Flag{{
351+
Key: "headerColor",
352+
State: "ENABLED",
353+
DefaultVariant: "red",
354+
Variants: colorVariants,
355+
Targeting: []byte(`{
354356
"fractional": [
355357
[
356358
"blue",
@@ -362,7 +364,7 @@ func TestFractionalEvaluation(t *testing.T) {
362364
]
363365
]
364366
}`),
365-
},
367+
},
366368
},
367369
flagKey: "headerColor",
368370
context: map[string]any{
@@ -373,20 +375,20 @@ func TestFractionalEvaluation(t *testing.T) {
373375
expectedReason: model.TargetingMatchReason,
374376
},
375377
"missing email - parser should ignore nil/missing custom variables and continue": {
376-
flags: map[string]model.Flag{
377-
"headerColor": {
378-
State: "ENABLED",
379-
DefaultVariant: "red",
380-
Variants: colorVariants,
381-
Targeting: []byte(
382-
`{
378+
flags: []model.Flag{{
379+
Key: "headerColor",
380+
State: "ENABLED",
381+
DefaultVariant: "red",
382+
Variants: colorVariants,
383+
Targeting: []byte(
384+
`{
383385
"fractional": [
384386
{"var": "email"},
385387
["red",50],
386388
["blue",50]
387389
]
388390
}`),
389-
},
391+
},
390392
},
391393
flagKey: "headerColor",
392394
context: map[string]any{
@@ -438,12 +440,12 @@ func BenchmarkFractionalEvaluation(b *testing.B) {
438440
var sources = []string{source}
439441
ctx := context.Background()
440442

441-
flags := map[string]model.Flag{
442-
"headerColor": {
443-
State: "ENABLED",
444-
DefaultVariant: "red",
445-
Variants: colorVariants,
446-
Targeting: []byte(`{
443+
flags := []model.Flag{{
444+
Key: "headerColor",
445+
State: "ENABLED",
446+
DefaultVariant: "red",
447+
Variants: colorVariants,
448+
Targeting: []byte(`{
447449
"if": [
448450
{
449451
"in": ["@faas.com", {
@@ -473,11 +475,11 @@ func BenchmarkFractionalEvaluation(b *testing.B) {
473475
}, null
474476
]
475477
}`),
476-
},
478+
},
477479
}
478480

479481
tests := map[string]struct {
480-
flags map[string]model.Flag
482+
flags []model.Flag
481483
flagKey string
482484
context map[string]any
483485
expectedValue string

core/pkg/evaluator/json.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"github.com/xeipuuv/gojsonschema"
910
"regexp"
1011
"strings"
1112
"time"
@@ -177,7 +178,7 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context
177178
var reason string
178179
var metadata map[string]interface{}
179180

180-
for flagKey, flag := range allFlags {
181+
for _, flag := range allFlags {
181182
if flag.State == Disabled {
182183
// ignore evaluation of disabled flag
183184
continue
@@ -186,18 +187,18 @@ func (je *Resolver) ResolveAllValues(ctx context.Context, reqID string, context
186187
defaultValue := flag.Variants[flag.DefaultVariant]
187188
switch defaultValue.(type) {
188189
case bool:
189-
value, variant, reason, metadata, err = resolve[bool](ctx, reqID, flagKey, context, je.evaluateVariant)
190+
value, variant, reason, metadata, err = resolve[bool](ctx, reqID, flag.Key, context, je.evaluateVariant)
190191
case string:
191-
value, variant, reason, metadata, err = resolve[string](ctx, reqID, flagKey, context, je.evaluateVariant)
192+
value, variant, reason, metadata, err = resolve[string](ctx, reqID, flag.Key, context, je.evaluateVariant)
192193
case float64:
193-
value, variant, reason, metadata, err = resolve[float64](ctx, reqID, flagKey, context, je.evaluateVariant)
194+
value, variant, reason, metadata, err = resolve[float64](ctx, reqID, flag.Key, context, je.evaluateVariant)
194195
case map[string]any:
195-
value, variant, reason, metadata, err = resolve[map[string]any](ctx, reqID, flagKey, context, je.evaluateVariant)
196+
value, variant, reason, metadata, err = resolve[map[string]any](ctx, reqID, flag.Key, context, je.evaluateVariant)
196197
}
197198
if err != nil {
198-
je.Logger.ErrorWithID(reqID, fmt.Sprintf("bulk evaluation: key: %s returned error: %s", flagKey, err.Error()))
199+
je.Logger.ErrorWithID(reqID, fmt.Sprintf("bulk evaluation: key: %s returned error: %s", flag.Key, err.Error()))
199200
}
200-
values = append(values, NewAnyValue(value, variant, reason, flagKey, metadata, err))
201+
values = append(values, NewAnyValue(value, variant, reason, flag.Key, metadata, err))
201202
}
202203

203204
return values, flagSetMetadata, nil
@@ -453,31 +454,40 @@ func (je *JSON) configToFlagDefinition(config string, definition *Definition) er
453454
"flag definition does not conform to the schema; validation errors: %s", err),
454455
)
455456
}
456-
457+
type JsonRawDef struct {
458+
Flags map[string]model.Flag `json:"flags"`
459+
Metadata map[string]interface{} `json:"metadata"`
460+
}
461+
// Transpose evaluators and unmarshal directly into JsonDef
457462
transposedConfig, err := transposeEvaluators(config)
458463
if err != nil {
459464
return fmt.Errorf("transposing evaluators: %w", err)
460465
}
461466

462-
err = json.Unmarshal([]byte(transposedConfig), &definition)
467+
var rawDef JsonRawDef
468+
err = json.Unmarshal([]byte(transposedConfig), &rawDef)
463469
if err != nil {
464470
return fmt.Errorf("unmarshalling provided configurations: %w", err)
465471
}
466-
472+
definition.Metadata = rawDef.Metadata
473+
for s, flag := range rawDef.Flags {
474+
flag.Key = s
475+
definition.Flags = append(definition.Flags, flag)
476+
}
467477
return validateDefaultVariants(definition)
468478
}
469479

470480
// validateDefaultVariants returns an error if any of the default variants aren't valid
471481
func validateDefaultVariants(flags *Definition) error {
472-
for name, flag := range flags.Flags {
482+
for _, flag := range flags.Flags {
473483
// Default Variant is not provided in the config
474484
if flag.DefaultVariant == "" {
475485
continue
476486
}
477487

478488
if _, ok := flag.Variants[flag.DefaultVariant]; !ok {
479489
return fmt.Errorf(
480-
"default variant: '%s' isn't a valid variant of flag: '%s'", flag.DefaultVariant, name,
490+
"default variant: '%s' isn't a valid variant of flag: '%s'", flag.DefaultVariant, flag.Key,
481491
)
482492
}
483493
}

core/pkg/evaluator/json_model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type Evaluators struct {
1111
}
1212

1313
type Definition struct {
14-
Flags map[string]model.Flag `json:"flags"`
14+
Flags []model.Flag `json:"flags"`
1515
Metadata map[string]interface{} `json:"metadata"`
1616
}
1717

0 commit comments

Comments
 (0)