diff --git a/providers/from-env/README.md b/providers/from-env/README.md index fda6cdff8..b58f6593f 100644 --- a/providers/from-env/README.md +++ b/providers/from-env/README.md @@ -1,12 +1,12 @@ # Environment Variable JSON Flag Provider -This repository contains a very simple environment variable based feature flag provider. -This provider uses a JSON evaluation for matching a flag `Variant` to a provided `EvaluationContext`. Each flag `Variant` contains a slice of `Criteria`, if all `Criteria` match then the flags value is returned. Each `Variant` is evaluated starting at index 0, therefore the first matching `Variant` is returned. Each variant also has a `TargetingKey`, when set it must match the `TargetingKey` provided in the `EvaluationContext` for the `Variant` to be returned. +This repository contains a very simple environment variable based feature flag provider. +This provider uses a JSON evaluation for matching a flag `Variant` to a provided `EvaluationContext`. Each flag `Variant` contains a slice of `Criteria`, if all `Criteria` match then the flags value is returned. Each `Variant` is evaluated starting at index 0, therefore the first matching `Variant` is returned. Each variant also has a `TargetingKey`, when set it must match the `TargetingKey` provided in the `EvaluationContext` for the `Variant` to be returned. +## Flag Configuration Structure -## Flag Configuration Structure. +Flag configurations are stored as JSON strings, with one configuration per flag key. An example configuration is described below. -Flag configurations are stored as JSON strings, with one configuration per flag key. An example configuration is described below. ```json { "defaultVariant": "not-yellow", @@ -43,10 +43,11 @@ Flag configurations are stored as JSON strings, with one configuration per flag } ``` -## Example Usage -Below is a simple example of using this `Provider`, in this example the above flag configuration is saved with the key `AM_I_YELLOW.` +## Example Usage -``` +Below is a simple example of using this `Provider`, in this example the above flag configuration is saved with the key `AM_I_YELLOW.` + +```sh export AM_I_YELLOW='{"defaultVariant":"not-yellow","variants":[{"name":"yellow-with-key","targetingKey":"user","criteria":[{"key":"color","value":"yellow"}],"value":true},{"name":"yellow","targetingKey":"","criteria":[{"key":"color","value":"yellow"}],"value":true},{"name":"not-yellow","targetingKey":"","criteria": [],"value":false}]}' ``` @@ -107,20 +108,37 @@ func main() { ) fmt.Println(resS, err) } - ``` + Console output: -``` + +```console {AM_I_YELLOW 0 {true TARGETING_MATCH yellow}} {AM_I_YELLOW 0 {true TARGETING_MATCH yellow-with-key}} {i am a default value {AM_I_YELLOW string { ERROR TYPE_MISMATCH }}} error code: TYPE_MISMATCH ``` -## Common Error Response Types +### Name Mapping + +To transform the flag name into an environment variable name at runtime, you can use the option `WithFlagToEnvMapper`. -Error Value | Error Reason -------------- | ------------- -PARSE_ERROR | A required `DefaultVariant` does not exist, or, the stored flag configuration cannot be parsed into the `StoredFlag` struct -TYPE_MISMATCH | The responses value type does not match that of the request. -FLAG_NOT_FOUND | The requested flag key does not have an associated environment variable. +For example: + +```go +mapper := func(flagKey string) string { + return fmt.Sprintf("MY_%s", strings.ToUpper(strings.ReplaceAll(flagKey, "-", "_"))) +} + +p := fromEnv.NewProvider(fromEnv.WithFlagToEnvMapper(mapper)) + +// This will look up MY_SOME_FLAG env variable +res := p.BooleanEvaluation(context.Background(), "some-flag", false, evalCtx) +``` + +## Common Error Response Types +| Error Value | Error Reason | +| -------------- | --------------------------------------------------------------------------------------------------------------------------- | +| PARSE_ERROR | A required `DefaultVariant` does not exist, or, the stored flag configuration cannot be parsed into the `StoredFlag` struct | +| TYPE_MISMATCH | The responses value type does not match that of the request. | +| FLAG_NOT_FOUND | The requested flag key does not have an associated environment variable. | diff --git a/providers/from-env/pkg/env.go b/providers/from-env/pkg/env.go index 757e1185b..16f314a84 100644 --- a/providers/from-env/pkg/env.go +++ b/providers/from-env/pkg/env.go @@ -2,19 +2,32 @@ package from_env import ( "encoding/json" - "github.com/open-feature/go-sdk/openfeature" + "fmt" "os" + + "github.com/open-feature/go-sdk/openfeature" ) -type envFetch struct{} +type envFetch struct { + mapper FlagToEnvMapper +} func (ef *envFetch) fetchStoredFlag(key string) (StoredFlag, error) { v := StoredFlag{} - if val := os.Getenv(key); val != "" { + mappedKey := key + + if ef.mapper != nil { + mappedKey = ef.mapper(key) + } + + if val := os.Getenv(mappedKey); val != "" { if err := json.Unmarshal([]byte(val), &v); err != nil { return v, openfeature.NewParseErrorResolutionError(err.Error()) } return v, nil } - return v, openfeature.NewFlagNotFoundResolutionError("") + + msg := fmt.Sprintf("key %s not found in environment variables", mappedKey) + + return v, openfeature.NewFlagNotFoundResolutionError(msg) } diff --git a/providers/from-env/pkg/provider.go b/providers/from-env/pkg/provider.go index 86277cde0..3b4a3646b 100644 --- a/providers/from-env/pkg/provider.go +++ b/providers/from-env/pkg/provider.go @@ -3,14 +3,10 @@ package from_env import ( "context" "errors" + "github.com/open-feature/go-sdk/openfeature" ) -// FromEnvProvider implements the FeatureProvider interface and provides functions for evaluating flags -type FromEnvProvider struct { - envFetch envFetch -} - const ( ReasonStatic = "static" @@ -19,6 +15,31 @@ const ( ErrorFlagNotFound = "flag not found" ) +// FromEnvProvider implements the FeatureProvider interface and provides functions for evaluating flags +type FromEnvProvider struct { + envFetch envFetch +} + +type ProviderOption func(*FromEnvProvider) + +type FlagToEnvMapper func(string) string + +func WithFlagToEnvMapper(mapper FlagToEnvMapper) ProviderOption { + return func(p *FromEnvProvider) { + p.envFetch.mapper = mapper + } +} + +func NewProvider(opts ...ProviderOption) *FromEnvProvider { + p := &FromEnvProvider{} + + for _, opt := range opts { + opt(p) + } + + return p +} + // Metadata returns the metadata of the provider func (p *FromEnvProvider) Metadata() openfeature.Metadata { return openfeature.Metadata{ diff --git a/providers/from-env/pkg/provider_test.go b/providers/from-env/pkg/provider_test.go index 14d4438fc..fafdac701 100644 --- a/providers/from-env/pkg/provider_test.go +++ b/providers/from-env/pkg/provider_test.go @@ -3,7 +3,9 @@ package from_env_test import ( "context" "encoding/json" + "fmt" "reflect" + "strings" "testing" fromEnv "github.com/open-feature/go-sdk-contrib/providers/from-env/pkg" @@ -13,6 +15,72 @@ import ( // this line will fail linting if this provider is no longer compatible with the openfeature sdk var _ openfeature.FeatureProvider = &fromEnv.FromEnvProvider{} +func TestNewProvider(t *testing.T) { + p := fromEnv.NewProvider() + + if reflect.TypeOf(p) != reflect.TypeOf(&fromEnv.FromEnvProvider{}) { + t.Fatalf("expected NewProvider to return a &from_env.FromEnvProvider, got %T", p) + } +} + +func TestWithFlagToEnvMapper(t *testing.T) { + mapper := func(flagKey string) string { + return fmt.Sprintf("MY_%s", strings.ToUpper(strings.ReplaceAll(flagKey, "-", "_"))) + } + + p := fromEnv.NewProvider(fromEnv.WithFlagToEnvMapper(mapper)) + testFlag := "some-flag-enabled" + flagValue := fromEnv.StoredFlag{ + DefaultVariant: "not-yellow", + Variants: []fromEnv.Variant{ + { + Name: "yellow", + TargetingKey: "", + Value: true, + Criteria: []fromEnv.Criteria{ + { + Key: "color", + Value: "yellow", + }, + }, + }, + { + Name: "not-yellow", + TargetingKey: "", + Value: false, + Criteria: []fromEnv.Criteria{ + { + Key: "color", + Value: "not yellow", + }, + }, + }, + }, + } + evalCtx := map[string]interface{}{ + "color": "yellow", + openfeature.TargetingKey: "user1", + } + + flagM, _ := json.Marshal(flagValue) + + t.Setenv(mapper(testFlag), string(flagM)) + + res := p.BooleanEvaluation(context.Background(), testFlag, false, evalCtx) + + if res.Error() != nil { + t.Fatalf("expected no error, got %s", res.Error()) + } + + if !res.Value { + t.Fatalf("expected true value, got false") + } + + if res.Variant != "yellow" { + t.Fatalf("expected yellow variant, got %s", res.Variant) + } +} + func TestBoolFromEnv(t *testing.T) { tests := map[string]struct { flagKey string