Skip to content

Commit

Permalink
feat(from_env): option for mapping flag to env name (#528)
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Morozov <morozov.ig.s@gmail.com>
  • Loading branch information
morigs authored Jun 14, 2024
1 parent c3973a6 commit cede073
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 24 deletions.
48 changes: 33 additions & 15 deletions providers/from-env/README.md
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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}]}'
```

Expand Down Expand Up @@ -107,20 +108,37 @@ func main() {
)
fmt.Println(resS, err)
}

```

Console output:
```

```console
{AM_I_YELLOW 0 {true TARGETING_MATCH yellow}} <nil>
{AM_I_YELLOW 0 {true TARGETING_MATCH yellow-with-key}} <nil>
{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. |
21 changes: 17 additions & 4 deletions providers/from-env/pkg/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
31 changes: 26 additions & 5 deletions providers/from-env/pkg/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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{
Expand Down
68 changes: 68 additions & 0 deletions providers/from-env/pkg/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down

0 comments on commit cede073

Please sign in to comment.