diff --git a/core/pkg/eval/fractional_evaluation_test.go b/core/pkg/eval/fractional_evaluation_test.go index f0a096873..a5cb50cde 100644 --- a/core/pkg/eval/fractional_evaluation_test.go +++ b/core/pkg/eval/fractional_evaluation_test.go @@ -55,13 +55,13 @@ func TestFractionalEvaluation(t *testing.T) { } tests := map[string]struct { - flags Flags - flagKey string - context map[string]any - expectedValue string - expectedVariant string - expectedReason string - expectedError error + flags Flags + flagKey string + context map[string]any + expectedValue string + expectedVariant string + expectedReason string + expectedErrorCode string }{ "rachel@faas.com": { flags: flags, @@ -247,38 +247,6 @@ func TestFractionalEvaluation(t *testing.T) { expectedValue: "#FF0000", expectedReason: model.DefaultReason, }, - "fallback to default variant if invalid variant as result of fractional evaluation": { - flags: Flags{ - Flags: map[string]model.Flag{ - "headerColor": { - State: "ENABLED", - DefaultVariant: "red", - Variants: map[string]any{ - "red": "#FF0000", - "blue": "#0000FF", - "green": "#00FF00", - "yellow": "#FFFF00", - }, - Targeting: []byte(`{ - "fractional": [ - {"var": "email"}, - [ - "black", - 100 - ] - ] - }`), - }, - }, - }, - flagKey: "headerColor", - context: map[string]any{ - "email": "foo@foo.com", - }, - expectedVariant: "red", - expectedValue: "#FF0000", - expectedReason: model.DefaultReason, - }, "fallback to default variant if percentages don't sum to 100": { flags: Flags{ Flags: map[string]model.Flag{ @@ -379,8 +347,11 @@ func TestFractionalEvaluation(t *testing.T) { t.Errorf("expected reason '%s', got '%s'", tt.expectedReason, reason) } - if err != tt.expectedError { - t.Errorf("expected err '%v', got '%v'", tt.expectedError, err) + if err != nil { + errorCode := err.Error() + if errorCode != tt.expectedErrorCode { + t.Errorf("expected err '%v', got '%v'", tt.expectedErrorCode, err) + } } }) } @@ -433,13 +404,13 @@ func BenchmarkFractionalEvaluation(b *testing.B) { } tests := map[string]struct { - flags Flags - flagKey string - context map[string]any - expectedValue string - expectedVariant string - expectedReason string - expectedError error + flags Flags + flagKey string + context map[string]any + expectedValue string + expectedVariant string + expectedReason string + expectedErrorCode string }{ "test@faas.com": { flags: flags, @@ -509,8 +480,11 @@ func BenchmarkFractionalEvaluation(b *testing.B) { b.Errorf("expected reason '%s', got '%s'", tt.expectedReason, reason) } - if err != tt.expectedError { - b.Errorf("expected err '%v', got '%v'", tt.expectedError, err) + if err != nil { + errorCode := err.Error() + if errorCode != tt.expectedErrorCode { + b.Errorf("expected err '%v', got '%v'", tt.expectedErrorCode, err) + } } } }) diff --git a/core/pkg/eval/json_evaluator.go b/core/pkg/eval/json_evaluator.go index ac3bf60a7..30da42b9c 100644 --- a/core/pkg/eval/json_evaluator.go +++ b/core/pkg/eval/json_evaluator.go @@ -341,21 +341,25 @@ func (je *JSONEvaluator) evaluateVariant(reqID string, flagKey string, context m je.Logger.ErrorWithID(reqID, fmt.Sprintf("error applying rules: %s", err)) return "", flag.Variants, model.ErrorReason, metadata, errors.New(model.ParseErrorCode) } + + // check if string is "null" before we strip quotes, so we can differentiate between JSON null and "null" + trimmed := strings.TrimSpace(result.String()) + if trimmed == "null" { + return flag.DefaultVariant, flag.Variants, model.DefaultReason, metadata, nil + } + // strip whitespace and quotes from the variant - variant = strings.ReplaceAll(strings.TrimSpace(result.String()), "\"", "") + variant = strings.ReplaceAll(trimmed, "\"", "") // if this is a valid variant, return it if _, ok := flag.Variants[variant]; ok { return variant, flag.Variants, model.TargetingMatchReason, metadata, nil } - - je.Logger.DebugWithID(reqID, fmt.Sprintf("returning default variant for flagKey: %s, variant is not valid", flagKey)) - reason = model.DefaultReason - } else { - reason = model.StaticReason + je.Logger.ErrorWithID(reqID, + fmt.Sprintf("invalid or missing variant: %s for flagKey: %s, variant is not valid", variant, flagKey)) + return "", flag.Variants, model.ErrorReason, metadata, errors.New(model.ParseErrorCode) } - - return flag.DefaultVariant, flag.Variants, reason, metadata, nil + return flag.DefaultVariant, flag.Variants, model.StaticReason, metadata, nil } func (je *JSONEvaluator) setFlagdProperties( diff --git a/core/pkg/eval/json_evaluator_test.go b/core/pkg/eval/json_evaluator_test.go index 0ea872ea4..932365375 100644 --- a/core/pkg/eval/json_evaluator_test.go +++ b/core/pkg/eval/json_evaluator_test.go @@ -1281,3 +1281,98 @@ func TestFlagdAmbientProperties(t *testing.T) { } }) } + +func TestTargetingVariantBehavior(t *testing.T) { + t.Run("missing variant error", func(t *testing.T) { + evaluator := eval.NewJSONEvaluator(logger.NewLogger(nil, false), store.NewFlags()) + + _, _, err := evaluator.SetState(sync.DataSync{FlagData: `{ + "flags": { + "missing-variant": { + "state": "ENABLED", + "variants": { + "foo": true, + "bar": false + }, + "defaultVariant": "foo", + "targeting": { + "if": [ true, "buz", "baz"] + } + } + } + }`}) + if err != nil { + t.Fatal(err) + } + + _, _, _, _, err = evaluator.ResolveBooleanValue(context.Background(), "default", "missing-variant", nil) + if err == nil { + t.Fatal("missing variant did not result in error") + } + }) + + t.Run("null fallback", func(t *testing.T) { + evaluator := eval.NewJSONEvaluator(logger.NewLogger(nil, false), store.NewFlags()) + + _, _, err := evaluator.SetState(sync.DataSync{FlagData: `{ + "flags": { + "null-fallback": { + "state": "ENABLED", + "variants": { + "foo": true, + "bar": false + }, + "defaultVariant": "foo", + "targeting": { + "if": [ true, null, "baz"] + } + } + } + }`}) + if err != nil { + t.Fatal(err) + } + + value, variant, reason, _, err := evaluator.ResolveBooleanValue(context.Background(), "default", "null-fallback", nil) + if err != nil { + t.Fatal(err) + } + + if !value || variant != "foo" || reason != model.DefaultReason { + t.Fatal("did not fallback to defaultValue") + } + }) + + t.Run("match booleans", func(t *testing.T) { + evaluator := eval.NewJSONEvaluator(logger.NewLogger(nil, false), store.NewFlags()) + + //nolint:dupword + _, _, err := evaluator.SetState(sync.DataSync{FlagData: `{ + "flags": { + "match-boolean": { + "state": "ENABLED", + "variants": { + "false": 1, + "true": 2 + }, + "defaultVariant": "false", + "targeting": { + "if": [ true, true, false] + } + } + } + }`}) + if err != nil { + t.Fatal(err) + } + + value, variant, reason, _, err := evaluator.ResolveIntValue(context.Background(), "default", "match-boolean", nil) + if err != nil { + t.Fatal(err) + } + + if value != 2 || variant != "true" || reason != model.TargetingMatchReason { + t.Fatal("did not map to stringified boolean") + } + }) +} diff --git a/core/pkg/eval/legacy_fractional_evaluation_test.go b/core/pkg/eval/legacy_fractional_evaluation_test.go index 008aecd77..67dda2260 100644 --- a/core/pkg/eval/legacy_fractional_evaluation_test.go +++ b/core/pkg/eval/legacy_fractional_evaluation_test.go @@ -55,13 +55,13 @@ func TestLegacyFractionalEvaluation(t *testing.T) { } tests := map[string]struct { - flags Flags - flagKey string - context map[string]any - expectedValue string - expectedVariant string - expectedReason string - expectedError error + flags Flags + flagKey string + context map[string]any + expectedValue string + expectedVariant string + expectedReason string + expectedErrorCode string }{ "test@faas.com": { flags: flags, @@ -188,11 +188,12 @@ func TestLegacyFractionalEvaluation(t *testing.T) { }, }, }, - flagKey: "headerColor", - context: map[string]any{}, - expectedVariant: "red", - expectedValue: "#FF0000", - expectedReason: model.DefaultReason, + flagKey: "headerColor", + context: map[string]any{}, + expectedVariant: "", + expectedValue: "", + expectedReason: model.ErrorReason, + expectedErrorCode: model.ParseErrorCode, }, "fallback to default variant if invalid variant as result of fractional evaluation": { flags: Flags{ @@ -222,9 +223,10 @@ func TestLegacyFractionalEvaluation(t *testing.T) { context: map[string]any{ "email": "foo@foo.com", }, - expectedVariant: "red", - expectedValue: "#FF0000", - expectedReason: model.DefaultReason, + expectedVariant: "", + expectedValue: "", + expectedReason: model.ErrorReason, + expectedErrorCode: model.ParseErrorCode, }, "fallback to default variant if percentages don't sum to 100": { flags: Flags{ @@ -291,8 +293,11 @@ func TestLegacyFractionalEvaluation(t *testing.T) { t.Errorf("expected reason '%s', got '%s'", tt.expectedReason, reason) } - if err != tt.expectedError { - t.Errorf("expected err '%v', got '%v'", tt.expectedError, err) + if err != nil { + errorCode := err.Error() + if errorCode != tt.expectedErrorCode { + t.Errorf("expected err '%v', got '%v'", tt.expectedErrorCode, err) + } } }) } diff --git a/core/pkg/eval/semver_evaluation.go b/core/pkg/eval/semver_evaluation.go index 09a97240c..cd2ef4f71 100644 --- a/core/pkg/eval/semver_evaluation.go +++ b/core/pkg/eval/semver_evaluation.go @@ -85,12 +85,12 @@ func (je *SemVerComparisonEvaluator) SemVerEvaluation(values, _ interface{}) int actualVersion, targetVersion, operator, err := parseSemverEvaluationData(values) if err != nil { je.Logger.Error(fmt.Sprintf("parse sem_ver evaluation data: %v", err)) - return nil + return false } res, err := operator.compare(actualVersion, targetVersion) if err != nil { je.Logger.Error(fmt.Sprintf("sem_ver evaluation: %v", err)) - return nil + return false } return res } diff --git a/core/pkg/eval/string_comparison_evaluation.go b/core/pkg/eval/string_comparison_evaluation.go index 20e4861dd..05650e833 100644 --- a/core/pkg/eval/string_comparison_evaluation.go +++ b/core/pkg/eval/string_comparison_evaluation.go @@ -72,7 +72,7 @@ func (sce StringComparisonEvaluator) EndsWithEvaluation(values, _ interface{}) i propertyValue, target, err := parseStringComparisonEvaluationData(values) if err != nil { sce.Logger.Error(fmt.Sprintf("parse ends_with evaluation data: %v", err)) - return nil + return false } return strings.HasSuffix(propertyValue, target) } diff --git a/docs/reference/flag-definitions.md b/docs/reference/flag-definitions.md index b667ce167..831a4b666 100644 --- a/docs/reference/flag-definitions.md +++ b/docs/reference/flag-definitions.md @@ -43,11 +43,17 @@ A fully configured flag may look like this. "new-welcome-banner": { "state": "ENABLED", "variants": { - "true": true, - "false": false + "on": true, + "off": false }, "defaultVariant": "false", - "targeting": { "in": ["@example.com", { "var": "email" }] } + "targeting": { + "if": [ + { "in": ["@example.com", { "var": "email" }] }, + "on", + "off" + ] + } } } } @@ -147,10 +153,18 @@ Example of an invalid configuration: `targeting` is an **optional** property. A targeting rule **must** be valid JSON. Flagd uses a modified version of [JsonLogic](https://jsonlogic.com/), as well as some custom pre-processing, to evaluate these rules. -The output of the targeting rule **must** match the name of one of the variants defined above. -If an invalid or null value is returned by the targeting rule, the `defaultVariant` value is used. If no targeting rules are defined, the response reason will always be `STATIC`, this allows for the flag values to be cached, this behavior is described [here](specifications/rpc-providers.md#caching). +#### Variants Returned From Targeting Rules + +The output of the targeting rule **must** match the name of one of the defined variants. +One exception to the above is that rules may return `true` or `false` which will map to the variant indexed by the equivalent string (`"true"`, `"false"`). +If a null value is returned by the targeting rule, the `defaultVariant` is used. +This can be useful for conditionally "exiting" targeting rules and falling back to the default (in this case the returned reason will be `DEFAULT`). +If an invalid variant is returned (not a string, `true`, or `false`, or a string that is not in the set of variants) the evaluation is considered erroneous. + +See [Boolean Variant Shorthand](#boolean-variant-shorthand). + #### Evaluation Context Evaluation context is included as part of the evaluation request. @@ -190,46 +204,49 @@ Conditions can be used to control the logical flow and grouping of targeting rul Operations are used to take action on, or compare properties retrieved from the context. These are provided out-of-the-box by JsonLogic. - -| Operator | Description | Context type | Example | -| ---------------------- | -------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Equals | Attribute equals the specified value, with type coercion. | any | Logic: `#!json { "==" : [1, 1] }`
Result: `true`

Logic: `#!json { "==" : [1, "1"] }`
Result: `true` | -| Strict equals | Attribute equals the specified value, with strict comparison. | any | Logic: `#!json { "===" : [1, 1] }`
Result: `true`

Logic: `#!json { "===" : [1, "1"] }`
Result: `false` | -| Not equals | Attribute doesn't equal the specified value, with type coercion. | any | Logic: `#!json { "!=" : [1, 2] }`
Result: `true`

Logic: `#!json { "!=" : [1, "1"] }`
Result: `false` | -| Strict not equal | Attribute doesn't equal the specified value, with strict comparison. | any | Logic: `#!json { "!==" : [1, 2] }`
Result: `true`

Logic: `#!json { "!==" : [1, "1"] }`
Result: `true` | -| Exists | Attribute is defined | any | Logic: `#!json { "!!": [ "mike" ] }`
Result: `true`

Logic: `#!json { "!!": [ "" ] }`
Result: `false` | -| Not exists | Attribute is not defined | any | Logic: `#!json {"!": [ "mike" ] }`
Result: `false`

Logic: `#!json {"!": [ "" ] }`
Result: `true` | -| Greater than | Attribute is greater than the specified value | number | Logic: `#!json { ">" : [2, 1] }`
Result: `true`

Logic: `#!json { ">" : [1, 2] }`
Result: `false` | -| Greater than or equals | Attribute is greater or equal to the specified value | number | Logic: `#!json { ">=" : [2, 1] }`
Result: `true`

Logic: `#!json { ">=" : [1, 1] }`
Result: `true` | -| Less than | Attribute is less than the specified value | number | Logic: `#!json { "<" : [1, 2] }`
Result: `true`

Logic: `#!json { "<" : [2, 1] }`
Result: `false` | -| Less than or equals | Attribute is less or equal to the specified value | number | Logic: `#!json { "<=" : [1, 1] }`
Result: `true`

Logic: `#!json { "<=" : [2, 1] }`
Result: `false` | -| Between | Attribute between the specified values | number | Logic: `#!json { "<" : [1, 5, 10]}`
Result: `true`

Logic: `#!json { "<" : [1, 11, 10] }`
Result: `false` | -| Between inclusive | Attribute between or equal to the specified values | number | Logic: `#!json {"<=" : [1, 1, 10] }`
Result: `true`

Logic: `#!json {"<=" : [1, 11, 10] }`
Result: `false` | -| Contains | Contains string | string | Logic: `#!json { "in": ["Spring", "Springfield"] }`
Result: `true`

Logic: `#!json { "in":["Illinois", "Springfield"] }`
Result: `false` | -| Not contains | Does not contain a string | string | Logic: `#!json { "!": { "in":["Spring", "Springfield"] } }`
Result: `false`

Logic: `#!json { "!": { "in":["Illinois", "Springfield"] } }`
Result: `true` | -| In | Attribute is in an array of strings | string | Logic: `#!json { "in" : [ "Mike", ["Bob", "Mike"]] }`
Result: `true`

Logic: `#!json { "in":["Todd", ["Bob", "Mike"]] }`
Result: `false` | -| Not it | Attribute is not in an array of strings | string | Logic: `#!json { "!": { "in" : [ "Mike", ["Bob", "Mike"]] } }`
Result: `false`

Logic: `#!json { "!": { "in":["Todd", ["Bob", "Mike"]] } }`
Result: `true` | +It's worth noting that JsonLogic operators never throw exceptions or abnormally terminate due to invalid input. +As long as a JsonLogic operator is structurally valid, it will return a falsy/nullish value. + +| Operator | Description | Context attribute type | Example | +| ---------------------- | -------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Equals | Attribute equals the specified value, with type coercion. | any | Logic: `#!json { "==" : [1, 1] }`
Result: `true`

Logic: `#!json { "==" : [1, "1"] }`
Result: `true` | +| Strict equals | Attribute equals the specified value, with strict comparison. | any | Logic: `#!json { "===" : [1, 1] }`
Result: `true`

Logic: `#!json { "===" : [1, "1"] }`
Result: `false` | +| Not equals | Attribute doesn't equal the specified value, with type coercion. | any | Logic: `#!json { "!=" : [1, 2] }`
Result: `true`

Logic: `#!json { "!=" : [1, "1"] }`
Result: `false` | +| Strict not equal | Attribute doesn't equal the specified value, with strict comparison. | any | Logic: `#!json { "!==" : [1, 2] }`
Result: `true`

Logic: `#!json { "!==" : [1, "1"] }`
Result: `true` | +| Exists | Attribute is defined | any | Logic: `#!json { "!!": [ "mike" ] }`
Result: `true`

Logic: `#!json { "!!": [ "" ] }`
Result: `false` | +| Not exists | Attribute is not defined | any | Logic: `#!json {"!": [ "mike" ] }`
Result: `false`

Logic: `#!json {"!": [ "" ] }`
Result: `true` | +| Greater than | Attribute is greater than the specified value | number | Logic: `#!json { ">" : [2, 1] }`
Result: `true`

Logic: `#!json { ">" : [1, 2] }`
Result: `false` | +| Greater than or equals | Attribute is greater or equal to the specified value | number | Logic: `#!json { ">=" : [2, 1] }`
Result: `true`

Logic: `#!json { ">=" : [1, 1] }`
Result: `true` | +| Less than | Attribute is less than the specified value | number | Logic: `#!json { "<" : [1, 2] }`
Result: `true`

Logic: `#!json { "<" : [2, 1] }`
Result: `false` | +| Less than or equals | Attribute is less or equal to the specified value | number | Logic: `#!json { "<=" : [1, 1] }`
Result: `true`

Logic: `#!json { "<=" : [2, 1] }`
Result: `false` | +| Between | Attribute between the specified values | number | Logic: `#!json { "<" : [1, 5, 10]}`
Result: `true`

Logic: `#!json { "<" : [1, 11, 10] }`
Result: `false` | +| Between inclusive | Attribute between or equal to the specified values | number | Logic: `#!json {"<=" : [1, 1, 10] }`
Result: `true`

Logic: `#!json {"<=" : [1, 11, 10] }`
Result: `false` | +| Contains | Contains string | string | Logic: `#!json { "in": ["Spring", "Springfield"] }`
Result: `true`

Logic: `#!json { "in":["Illinois", "Springfield"] }`
Result: `false` | +| Not contains | Does not contain a string | string | Logic: `#!json { "!": { "in":["Spring", "Springfield"] } }`
Result: `false`

Logic: `#!json { "!": { "in":["Illinois", "Springfield"] } }`
Result: `true` | +| In | Attribute is in an array of strings | string | Logic: `#!json { "in" : [ "Mike", ["Bob", "Mike"]] }`
Result: `true`

Logic: `#!json { "in":["Todd", ["Bob", "Mike"]] }`
Result: `false` | +| Not it | Attribute is not in an array of strings | string | Logic: `#!json { "!": { "in" : [ "Mike", ["Bob", "Mike"]] } }`
Result: `false`

Logic: `#!json { "!": { "in":["Todd", ["Bob", "Mike"]] } }`
Result: `true` | #### Custom Operations These are custom operations specific to flagd and flagd providers. They are purpose built extensions to JsonLogic in order to support common feature flag use cases. +Consistent with build-in JsonLogic operators, flagd's custom operators return falsy/nullish values with invalid inputs. -| Function | Description | Example | -| ---------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `fractional` (_available v0.6.4+_) | Deterministic, pseudorandom fractional distribution | Logic: `#!json { "fractional" : [ { "var": "email" }, [ "red" , 50], [ "green" , 50 ] ] }`
Result: Pseudo randomly `red` or `green` based on the evaluation context property `email`.

Additional documentation can be found [here](./custom-operations/fractional-operation.md). | -| `starts_with` | Attribute starts with the specified value | Logic: `#!json { "starts_with" : [ "192.168.0.1", "192.168"] }`
Result: `true`

Logic: `#!json { "starts_with" : [ "10.0.0.1", "192.168"] }`
Result: `false`
Additional documentation can be found [here](./custom-operations/string-comparison-operation.md). | -| `ends_with` | Attribute ends with the specified value | Logic: `#!json { "ends_with" : [ "noreply@example.com", "@example.com"] }`
Result: `true`

Logic: `#!json { ends_with" : [ "noreply@example.com", "@test.com"] }`
Result: `false`
Additional documentation can be found [here](./custom-operations/string-comparison-operation.md).| -| `sem_ver` | Attribute matches a semantic versioning condition | Logic: `#!json {"sem_ver": ["1.1.2", ">=", "1.0.0"]}`
Result: `true`

Additional documentation can be found [here](./custom-operations/semver-operation.md). | +| Function | Description | Context attribute type | Example | +| ---------------------------------- | --------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fractional` (_available v0.6.4+_) | Deterministic, pseudorandom fractional distribution | string (bucketing value) | Logic: `#!json { "fractional" : [ { "var": "email" }, [ "red" , 50], [ "green" , 50 ] ] }`
Result: Pseudo randomly `red` or `green` based on the evaluation context property `email`.

Additional documentation can be found [here](./custom-operations/fractional-operation.md). | +| `starts_with` | Attribute starts with the specified value | string | Logic: `#!json { "starts_with" : [ "192.168.0.1", "192.168"] }`
Result: `true`

Logic: `#!json { "starts_with" : [ "10.0.0.1", "192.168"] }`
Result: `false`
Additional documentation can be found [here](./custom-operations/string-comparison-operation.md). | +| `ends_with` | Attribute ends with the specified value | string | Logic: `#!json { "ends_with" : [ "noreply@example.com", "@example.com"] }`
Result: `true`

Logic: `#!json { ends_with" : [ "noreply@example.com", "@test.com"] }`
Result: `false`
Additional documentation can be found [here](./custom-operations/string-comparison-operation.md). | +| `sem_ver` | Attribute matches a semantic versioning condition | string (valid [semver](https://semver.org/)) | Logic: `#!json {"sem_ver": ["1.1.2", ">=", "1.0.0"]}`
Result: `true`

Additional documentation can be found [here](./custom-operations/semver-operation.md). | #### $flagd properties in the evaluation context Flagd adds the following properties to the evaluation context that can be used in the targeting rules. -| Property | Description | From version | -|----------|-------------|--------------| -| `$flagd.flagKey` | the identifier for the flag being evaluated | v0.6.4 | -| `$flagd.timestamp`| a unix timestamp (in seconds) of the time of evaluation | v0.6.7 | +| Property | Description | From version | +| ------------------ | ------------------------------------------------------- | ------------ | +| `$flagd.flagKey` | the identifier for the flag being evaluated | v0.6.4 | +| `$flagd.timestamp` | a unix timestamp (in seconds) of the time of evaluation | v0.6.7 | ## Shared evaluators @@ -301,6 +318,54 @@ Example: } ``` +## Boolean Variant Shorthand + +Since rules that return `true` or `false` map to the variant indexed by the equivalent string (`"true"`, `"false"`), you can use shorthand for these cases. + +For example, this: + +```json +{ + "flags": { + "new-welcome-banner": { + "state": "ENABLED", + "variants": { + "true": true, + "false": false + }, + "defaultVariant": "false", + "targeting": { + "if": [ + { "in": ["@example.com", { "var": "email" }] }, + "true", + "false" + ] + } + } + } +} +``` + +can be shortened to this: + +```json +{ + "flags": { + "new-welcome-banner": { + "state": "ENABLED", + "variants": { + "true": true, + "false": false + }, + "defaultVariant": "false", + "targeting": { + { "in": ["@example.com", { "var": "email" }] } + } + } + } +} +``` + ## Examples Sample configurations can be found at . diff --git a/docs/reference/specifications/custom-operations/fractional-operation-spec.md b/docs/reference/specifications/custom-operations/fractional-operation-spec.md index 8e6eb5340..792c6cc1b 100644 --- a/docs/reference/specifications/custom-operations/fractional-operation-spec.md +++ b/docs/reference/specifications/custom-operations/fractional-operation-spec.md @@ -13,7 +13,7 @@ hash function should be used. This is to ensure that flag resolution requests yi regardless of which implementation of the in-process flagd provider is being used. The supplied array must contain at least two items, with the first item being an optional [json logic variable declaration](https://jsonlogic.com/operations.html#var) -specifying the bucketing property to base the distribution of values on. If not supplied, a concatenation of the +specifying the bucketing property to base the distribution of values on. If the bucketing property expression doesn't return a string, a concatenation of the `flagKey` and `targetingKey` are used: `{"cat": [{"var":"$flagd.flagKey"}, {"var":"targetingKey"}]}`. The remaining items are `arrays`, each with two values, with the first being `string` item representing the name of the variant, and the second being a `float` item representing the percentage for that variant. The percentages of all items must add up to @@ -62,8 +62,8 @@ The following flow chart depicts the logic of this evaluator: ```mermaid flowchart TD A[Parse targetingRule] --> B{Is an array containing at least one item?}; -B -- Yes --> C{Is targetingRule at index 0 a string?}; -B -- No --> D[return nil] +B -- Yes --> C{Does expression at index 0 return a string?}; +B -- No --> D[return null] C -- No --> E[bucketingPropertyValue := default to targetingKey]; C -- Yes --> F[bucketingPropertyValue := targetingRule at index 0]; E --> G[Iterate through the remaining elements of the targetingRule array and parse the variants and their percentages]; diff --git a/docs/reference/specifications/custom-operations/semver-operation-spec.md b/docs/reference/specifications/custom-operations/semver-operation-spec.md index 56c51f7af..352e44065 100644 --- a/docs/reference/specifications/custom-operations/semver-operation-spec.md +++ b/docs/reference/specifications/custom-operations/semver-operation-spec.md @@ -21,7 +21,7 @@ The 'sem_ver' evaluation rule contains exactly three items: 1. Target property value: the resolved value of the target property referenced in the targeting rule 2. Operator: One of the following: `=`, `!=`, `>`, `<`, `>=`, `<=`, `~` (match minor version), `^` (match major version) 3. Target value: this needs to resolve to a semantic versioning string. If this condition is not met, the evaluator should -log an appropriate error message and return `nil` +log an appropriate error message and return `false` The `sem_ver` evaluation returns a boolean, indicating whether the condition has been met. @@ -35,7 +35,7 @@ The following flow chart depicts the logic of this evaluator: flowchart TD A[Parse targetingRule] --> B{Is an array containing exactly three items?}; B -- Yes --> C{Is targetingRule at index 0 a semantic version string?}; -B -- No --> D[Return nil]; +B -- No --> D[Return false]; C -- Yes --> E{Is targetingRule at index 1 a supported operator?}; C -- No --> D; E -- Yes --> F{Is targetingRule at index 2 a semantic version string?}; diff --git a/docs/reference/specifications/custom-operations/string-comparison-operation-spec.md b/docs/reference/specifications/custom-operations/string-comparison-operation-spec.md index 3e9832563..8d57b3b71 100644 --- a/docs/reference/specifications/custom-operations/string-comparison-operation-spec.md +++ b/docs/reference/specifications/custom-operations/string-comparison-operation-spec.md @@ -32,7 +32,7 @@ The following flow chart depicts the logic of this evaluator: flowchart TD A[Parse targetingRule] --> B{Is an array containing exactly two items?}; B -- Yes --> C{Is targetingRule at index 0 a string?}; -B -- No --> D[Return nil]; +B -- No --> D[Return false]; C -- Yes --> E{Is targetingRule at index 1 a string?}; C -- No --> D; E -- No --> D;