Skip to content

Commit cede073

Browse files
authored
feat(from_env): option for mapping flag to env name (#528)
Signed-off-by: Igor Morozov <[email protected]>
1 parent c3973a6 commit cede073

File tree

4 files changed

+144
-24
lines changed

4 files changed

+144
-24
lines changed

providers/from-env/README.md

+33-15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Environment Variable JSON Flag Provider
22

3-
This repository contains a very simple environment variable based feature flag provider.
4-
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.
3+
This repository contains a very simple environment variable based feature flag provider.
4+
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.
55

6+
## Flag Configuration Structure
67

7-
## Flag Configuration Structure.
8+
Flag configurations are stored as JSON strings, with one configuration per flag key. An example configuration is described below.
89

9-
Flag configurations are stored as JSON strings, with one configuration per flag key. An example configuration is described below.
1010
```json
1111
{
1212
"defaultVariant": "not-yellow",
@@ -43,10 +43,11 @@ Flag configurations are stored as JSON strings, with one configuration per flag
4343
}
4444
```
4545

46-
## Example Usage
47-
Below is a simple example of using this `Provider`, in this example the above flag configuration is saved with the key `AM_I_YELLOW.`
46+
## Example Usage
4847

49-
```
48+
Below is a simple example of using this `Provider`, in this example the above flag configuration is saved with the key `AM_I_YELLOW.`
49+
50+
```sh
5051
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}]}'
5152
```
5253

@@ -107,20 +108,37 @@ func main() {
107108
)
108109
fmt.Println(resS, err)
109110
}
110-
111111
```
112+
112113
Console output:
113-
```
114+
115+
```console
114116
{AM_I_YELLOW 0 {true TARGETING_MATCH yellow}} <nil>
115117
{AM_I_YELLOW 0 {true TARGETING_MATCH yellow-with-key}} <nil>
116118
{i am a default value {AM_I_YELLOW string { ERROR TYPE_MISMATCH }}} error code: TYPE_MISMATCH
117119
```
118120

119-
## Common Error Response Types
121+
### Name Mapping
122+
123+
To transform the flag name into an environment variable name at runtime, you can use the option `WithFlagToEnvMapper`.
120124

121-
Error Value | Error Reason
122-
------------- | -------------
123-
PARSE_ERROR | A required `DefaultVariant` does not exist, or, the stored flag configuration cannot be parsed into the `StoredFlag` struct
124-
TYPE_MISMATCH | The responses value type does not match that of the request.
125-
FLAG_NOT_FOUND | The requested flag key does not have an associated environment variable.
125+
For example:
126+
127+
```go
128+
mapper := func(flagKey string) string {
129+
return fmt.Sprintf("MY_%s", strings.ToUpper(strings.ReplaceAll(flagKey, "-", "_")))
130+
}
131+
132+
p := fromEnv.NewProvider(fromEnv.WithFlagToEnvMapper(mapper))
133+
134+
// This will look up MY_SOME_FLAG env variable
135+
res := p.BooleanEvaluation(context.Background(), "some-flag", false, evalCtx)
136+
```
137+
138+
## Common Error Response Types
126139

140+
| Error Value | Error Reason |
141+
| -------------- | --------------------------------------------------------------------------------------------------------------------------- |
142+
| PARSE_ERROR | A required `DefaultVariant` does not exist, or, the stored flag configuration cannot be parsed into the `StoredFlag` struct |
143+
| TYPE_MISMATCH | The responses value type does not match that of the request. |
144+
| FLAG_NOT_FOUND | The requested flag key does not have an associated environment variable. |

providers/from-env/pkg/env.go

+17-4
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,32 @@ package from_env
22

33
import (
44
"encoding/json"
5-
"github.com/open-feature/go-sdk/openfeature"
5+
"fmt"
66
"os"
7+
8+
"github.com/open-feature/go-sdk/openfeature"
79
)
810

9-
type envFetch struct{}
11+
type envFetch struct {
12+
mapper FlagToEnvMapper
13+
}
1014

1115
func (ef *envFetch) fetchStoredFlag(key string) (StoredFlag, error) {
1216
v := StoredFlag{}
13-
if val := os.Getenv(key); val != "" {
17+
mappedKey := key
18+
19+
if ef.mapper != nil {
20+
mappedKey = ef.mapper(key)
21+
}
22+
23+
if val := os.Getenv(mappedKey); val != "" {
1424
if err := json.Unmarshal([]byte(val), &v); err != nil {
1525
return v, openfeature.NewParseErrorResolutionError(err.Error())
1626
}
1727
return v, nil
1828
}
19-
return v, openfeature.NewFlagNotFoundResolutionError("")
29+
30+
msg := fmt.Sprintf("key %s not found in environment variables", mappedKey)
31+
32+
return v, openfeature.NewFlagNotFoundResolutionError(msg)
2033
}

providers/from-env/pkg/provider.go

+26-5
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@ package from_env
33
import (
44
"context"
55
"errors"
6+
67
"github.com/open-feature/go-sdk/openfeature"
78
)
89

9-
// FromEnvProvider implements the FeatureProvider interface and provides functions for evaluating flags
10-
type FromEnvProvider struct {
11-
envFetch envFetch
12-
}
13-
1410
const (
1511
ReasonStatic = "static"
1612

@@ -19,6 +15,31 @@ const (
1915
ErrorFlagNotFound = "flag not found"
2016
)
2117

18+
// FromEnvProvider implements the FeatureProvider interface and provides functions for evaluating flags
19+
type FromEnvProvider struct {
20+
envFetch envFetch
21+
}
22+
23+
type ProviderOption func(*FromEnvProvider)
24+
25+
type FlagToEnvMapper func(string) string
26+
27+
func WithFlagToEnvMapper(mapper FlagToEnvMapper) ProviderOption {
28+
return func(p *FromEnvProvider) {
29+
p.envFetch.mapper = mapper
30+
}
31+
}
32+
33+
func NewProvider(opts ...ProviderOption) *FromEnvProvider {
34+
p := &FromEnvProvider{}
35+
36+
for _, opt := range opts {
37+
opt(p)
38+
}
39+
40+
return p
41+
}
42+
2243
// Metadata returns the metadata of the provider
2344
func (p *FromEnvProvider) Metadata() openfeature.Metadata {
2445
return openfeature.Metadata{

providers/from-env/pkg/provider_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package from_env_test
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"reflect"
8+
"strings"
79
"testing"
810

911
fromEnv "github.com/open-feature/go-sdk-contrib/providers/from-env/pkg"
@@ -13,6 +15,72 @@ import (
1315
// this line will fail linting if this provider is no longer compatible with the openfeature sdk
1416
var _ openfeature.FeatureProvider = &fromEnv.FromEnvProvider{}
1517

18+
func TestNewProvider(t *testing.T) {
19+
p := fromEnv.NewProvider()
20+
21+
if reflect.TypeOf(p) != reflect.TypeOf(&fromEnv.FromEnvProvider{}) {
22+
t.Fatalf("expected NewProvider to return a &from_env.FromEnvProvider, got %T", p)
23+
}
24+
}
25+
26+
func TestWithFlagToEnvMapper(t *testing.T) {
27+
mapper := func(flagKey string) string {
28+
return fmt.Sprintf("MY_%s", strings.ToUpper(strings.ReplaceAll(flagKey, "-", "_")))
29+
}
30+
31+
p := fromEnv.NewProvider(fromEnv.WithFlagToEnvMapper(mapper))
32+
testFlag := "some-flag-enabled"
33+
flagValue := fromEnv.StoredFlag{
34+
DefaultVariant: "not-yellow",
35+
Variants: []fromEnv.Variant{
36+
{
37+
Name: "yellow",
38+
TargetingKey: "",
39+
Value: true,
40+
Criteria: []fromEnv.Criteria{
41+
{
42+
Key: "color",
43+
Value: "yellow",
44+
},
45+
},
46+
},
47+
{
48+
Name: "not-yellow",
49+
TargetingKey: "",
50+
Value: false,
51+
Criteria: []fromEnv.Criteria{
52+
{
53+
Key: "color",
54+
Value: "not yellow",
55+
},
56+
},
57+
},
58+
},
59+
}
60+
evalCtx := map[string]interface{}{
61+
"color": "yellow",
62+
openfeature.TargetingKey: "user1",
63+
}
64+
65+
flagM, _ := json.Marshal(flagValue)
66+
67+
t.Setenv(mapper(testFlag), string(flagM))
68+
69+
res := p.BooleanEvaluation(context.Background(), testFlag, false, evalCtx)
70+
71+
if res.Error() != nil {
72+
t.Fatalf("expected no error, got %s", res.Error())
73+
}
74+
75+
if !res.Value {
76+
t.Fatalf("expected true value, got false")
77+
}
78+
79+
if res.Variant != "yellow" {
80+
t.Fatalf("expected yellow variant, got %s", res.Variant)
81+
}
82+
}
83+
1684
func TestBoolFromEnv(t *testing.T) {
1785
tests := map[string]struct {
1886
flagKey string

0 commit comments

Comments
 (0)