Skip to content

Commit 29190e4

Browse files
Support more file format for flag files (#69)
1 parent 56f29a6 commit 29190e4

19 files changed

+376
-139
lines changed

README.md

+9-7
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ First, you need to initialize the `ffclient` with the location of your backend f
3939
err := ffclient.Init(ffclient.Config{
4040
PollInterval: 3,
4141
Retriever: &ffclient.HTTPRetriever{
42-
URL: "http://example.com/test.yaml",
42+
URL: "http://example.com/flag-config.yaml",
4343
},
4444
})
4545
defer ffclient.Close()
@@ -71,7 +71,8 @@ ffclient.Init(ffclient.Config{
7171
PollInterval: 3,
7272
Logger: log.New(file, "/tmp/log", 0),
7373
Context: context.Background(),
74-
Retriever: &ffclient.FileRetriever{Path: "testdata/test.yaml"},
74+
Retriever: &ffclient.FileRetriever{Path: "testdata/flag-config.yaml"},
75+
FileFormat: "yaml",
7576
Webhooks: []ffclient.WebhookConfig{
7677
{
7778
PayloadURL: " https://example.com/hook",
@@ -89,6 +90,7 @@ ffclient.Init(ffclient.Config{
8990
|`PollInterval` | Number of seconds to wait before refreshing the flags.<br />The default value is 60 seconds.|
9091
|`Logger` | Logger used to log what `go-feature-flag` is doing.<br />If no logger is provided the module will not log anything.|
9192
|`Context` | The context used by the retriever.<br />The default value is `context.Background()`.|
93+
|`FileFormat`| Format of your configuration file. Available formats are `yaml`, `toml` and `json`, if you omit the field it will try to unmarshal the file as a `yaml` file.|
9294
|`Retriever` | The configuration retriever you want to use to get your flag file *(see [Where do I store my flags file](#where-do-i-store-my-flags-file) for the configuration details)*.|
9395
|`Webhooks` | List of webhooks to call when your flag file has changed *(see [webhook section](#webhook) for more details)*.|
9496
## Where do I store my flags file
@@ -105,7 +107,7 @@ err := ffclient.Init(ffclient.Config{
105107
Retriever: &ffclient.GithubRetriever{
106108
RepositorySlug: "thomaspoignant/go-feature-flag",
107109
Branch: "main",
108-
FilePath: "testdata/test.yaml",
110+
FilePath: "testdata/flag-config.yaml",
109111
GithubToken: "XXXX",
110112
Timeout: 2 * time.Second,
111113
},
@@ -130,7 +132,7 @@ To configure the access to your GitHub file:
130132
err := ffclient.Init(ffclient.Config{
131133
PollInterval: 3,
132134
Retriever: &ffclient.HTTPRetriever{
133-
URL: "http://example.com/test.yaml",
135+
URL: "http://example.com/flag-config.yaml",
134136
Timeout: 2 * time.Second,
135137
},
136138
})
@@ -154,7 +156,7 @@ err := ffclient.Init(ffclient.Config{
154156
PollInterval: 3,
155157
Retriever: &ffclient.S3Retriever{
156158
Bucket: "tpoi-test",
157-
Item: "test.yaml",
159+
Item: "flag-config.yaml",
158160
AwsConfig: aws.Config{
159161
Region: aws.String("eu-west-1"),
160162
},
@@ -191,7 +193,7 @@ To configure your File retriever:
191193

192194
## Flags file format
193195
`go-feature-flag` is to avoid to have to host a backend to manage your feature flags and to keep them centralized by using a file a source.
194-
Your file should be a YAML file with a list of flags *([see example](testdata/test.yaml))*.
196+
Your file should be a YAML file with a list of flags *([see example](testdata/flag-config.yaml))*.
195197

196198
A flag configuration looks like:
197199
```yaml
@@ -400,7 +402,7 @@ All of the functions that `go-feature-flag` package supports are mirrored as met
400402
### Example:
401403

402404
```go
403-
x, err := ffclient.New(Config{ Retriever: &ffclient.HTTPRetriever{{URL: "http://example.com/test.yaml",}})
405+
x, err := ffclient.New(Config{ Retriever: &ffclient.HTTPRetriever{{URL: "http://example.com/flag-config.yaml",}})
404406
defer x.Close()
405407
406408
y, err := ffclient.New(Config{ Retriever: &ffclient.HTTPRetriever{{URL: "http://example.com/test2.yaml",}})

config.go

+1-106
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@ package ffclient
33
import (
44
"context"
55
"errors"
6-
"fmt"
7-
"github.com/aws/aws-sdk-go/aws"
8-
"github.com/aws/aws-sdk-go/aws/session"
9-
"github.com/aws/aws-sdk-go/service/s3/s3manager"
106
"log"
11-
"net/http"
12-
"time"
137

148
"github.com/thomaspoignant/go-feature-flag/internal/retriever"
159
)
@@ -23,6 +17,7 @@ type Config struct {
2317
Context context.Context // default is context.Background()
2418
Retriever Retriever
2519
Webhooks []WebhookConfig // webhooks we should call when a flag create/update/delete
20+
FileFormat string
2621
}
2722

2823
// GetRetriever returns a retriever.FlagRetriever configure with the retriever available in the config.
@@ -33,106 +28,6 @@ func (c *Config) GetRetriever() (retriever.FlagRetriever, error) {
3328
return c.Retriever.getFlagRetriever()
3429
}
3530

36-
type Retriever interface {
37-
getFlagRetriever() (retriever.FlagRetriever, error)
38-
}
39-
40-
// FileRetriever is a configuration struct for a local flat file.
41-
type FileRetriever struct {
42-
Path string
43-
}
44-
45-
func (r *FileRetriever) getFlagRetriever() (retriever.FlagRetriever, error) { // nolint: unparam
46-
return retriever.NewLocalRetriever(r.Path), nil
47-
}
48-
49-
// HTTPRetriever is a configuration struct for an HTTP endpoint retriever.
50-
type HTTPRetriever struct {
51-
URL string
52-
Method string
53-
Body string
54-
Header http.Header
55-
Timeout time.Duration
56-
}
57-
58-
func (r *HTTPRetriever) getFlagRetriever() (retriever.FlagRetriever, error) {
59-
timeout := r.Timeout
60-
if timeout <= 0 {
61-
timeout = 10 * time.Second
62-
}
63-
64-
return retriever.NewHTTPRetriever(
65-
&http.Client{
66-
Timeout: timeout,
67-
},
68-
r.URL,
69-
r.Method,
70-
r.Body,
71-
r.Header,
72-
), nil
73-
}
74-
75-
// S3Retriever is a configuration struct for a S3 retriever.
76-
type S3Retriever struct {
77-
Bucket string
78-
Item string
79-
AwsConfig aws.Config
80-
}
81-
82-
func (r *S3Retriever) getFlagRetriever() (retriever.FlagRetriever, error) {
83-
// Create an AWS session
84-
sess, err := session.NewSession(&r.AwsConfig)
85-
if err != nil {
86-
return nil, err
87-
}
88-
89-
// Create a new AWS S3 downloader
90-
downloader := s3manager.NewDownloader(sess)
91-
return retriever.NewS3Retriever(
92-
downloader,
93-
r.Bucket,
94-
r.Item,
95-
), nil
96-
}
97-
98-
// GithubRetriever is a configuration struct for a GitHub retriever.
99-
type GithubRetriever struct {
100-
RepositorySlug string
101-
Branch string // default is main
102-
FilePath string
103-
GithubToken string
104-
Timeout time.Duration // default is 10 seconds
105-
}
106-
107-
func (r *GithubRetriever) getFlagRetriever() (retriever.FlagRetriever, error) {
108-
// default branch is main
109-
branch := r.Branch
110-
if branch == "" {
111-
branch = "main"
112-
}
113-
114-
// add header for Github Token if specified
115-
header := http.Header{}
116-
if r.GithubToken != "" {
117-
header.Add("Authorization", fmt.Sprintf("token %s", r.GithubToken))
118-
}
119-
120-
URL := fmt.Sprintf(
121-
"https://raw.githubusercontent.com/%s/%s/%s",
122-
r.RepositorySlug,
123-
branch,
124-
r.FilePath)
125-
126-
httpRetriever := HTTPRetriever{
127-
URL: URL,
128-
Method: http.MethodGet,
129-
Header: header,
130-
Timeout: r.Timeout,
131-
}
132-
133-
return httpRetriever.getFlagRetriever()
134-
}
135-
13631
// WebhookConfig is the configuration of your webhook.
13732
// we will call this URL with a POST request with the following format
13833
//

config_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func TestConfig_GetRetriever(t *testing.T) {
3636
PollInterval: 3,
3737
Retriever: &ffClient.S3Retriever{
3838
Bucket: "tpoi-test",
39-
Item: "test.yaml",
39+
Item: "flag-config.yaml",
4040
AwsConfig: aws.Config{
4141
Region: aws.String("eu-west-1"),
4242
},
@@ -50,7 +50,7 @@ func TestConfig_GetRetriever(t *testing.T) {
5050
fields: fields{
5151
PollInterval: 3,
5252
Retriever: &ffClient.HTTPRetriever{
53-
URL: "http://example.com/test.yaml",
53+
URL: "http://example.com/flag-config.yaml",
5454
Method: http.MethodGet,
5555
},
5656
},
@@ -63,7 +63,7 @@ func TestConfig_GetRetriever(t *testing.T) {
6363
PollInterval: 3,
6464
Retriever: &ffClient.GithubRetriever{
6565
RepositorySlug: "thomaspoignant/go-feature-flag",
66-
FilePath: "testdata/test.yaml",
66+
FilePath: "testdata/flag-config.yaml",
6767
GithubToken: "XXX",
6868
},
6969
},

feature_flag.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
// err := ffclient.Init(ffclient.Config{
1515
// PollInterval: 3,
1616
// Retriever: &ffClient.HTTPRetriever{
17-
// URL: "http://example.com/test.yaml",
17+
// URL: "http://example.com/flag-config.yaml",
1818
// },
1919
// })
2020
// defer ffclient.Close()
@@ -118,7 +118,7 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Cache) error {
118118
return err
119119
}
120120

121-
err = cache.UpdateCache(loadedFlags)
121+
err = cache.UpdateCache(loadedFlags, config.FileFormat)
122122
if err != nil {
123123
log.Printf("error: impossible to update the cache of the flags: %v", err)
124124
return err

feature_flag_test.go

+40-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestStartWithoutRetriever(t *testing.T) {
2424
func TestStartWithNegativeInterval(t *testing.T) {
2525
_, err := New(Config{
2626
PollInterval: -60,
27-
Retriever: &FileRetriever{Path: "testdata/test.yaml"},
27+
Retriever: &FileRetriever{Path: "testdata/flag-config.yaml"},
2828
Logger: log.New(os.Stdout, "", 0),
2929
})
3030
assert.Error(t, err)
@@ -35,7 +35,7 @@ func TestValidUseCase(t *testing.T) {
3535
// Valid use case
3636
err := Init(Config{
3737
PollInterval: 5,
38-
Retriever: &FileRetriever{Path: "testdata/test.yaml"},
38+
Retriever: &FileRetriever{Path: "testdata/flag-config.yaml"},
3939
Logger: log.New(os.Stdout, "", 0),
4040
})
4141
defer Close()
@@ -48,6 +48,42 @@ func TestValidUseCase(t *testing.T) {
4848
assert.False(t, hasUnknownFlag, "User should use default value if flag does not exists")
4949
}
5050

51+
func TestValidUseCaseToml(t *testing.T) {
52+
// Valid use case
53+
gffClient, err := New(Config{
54+
PollInterval: 5,
55+
Retriever: &FileRetriever{Path: "testdata/flag-config.toml"},
56+
Logger: log.New(os.Stdout, "", 0),
57+
FileFormat: "toml",
58+
})
59+
defer gffClient.Close()
60+
61+
assert.NoError(t, err)
62+
user := ffuser.NewUser("random-key")
63+
hasTestFlag, _ := gffClient.BoolVariation("test-flag", user, false)
64+
assert.True(t, hasTestFlag, "User should have test flag")
65+
hasUnknownFlag, _ := gffClient.BoolVariation("unknown-flag", user, false)
66+
assert.False(t, hasUnknownFlag, "User should use default value if flag does not exists")
67+
}
68+
69+
func TestValidUseCaseJson(t *testing.T) {
70+
// Valid use case
71+
gffClient, err := New(Config{
72+
PollInterval: 5,
73+
Retriever: &FileRetriever{Path: "testdata/flag-config.json"},
74+
Logger: log.New(os.Stdout, "", 0),
75+
FileFormat: "json",
76+
})
77+
defer gffClient.Close()
78+
79+
assert.NoError(t, err)
80+
user := ffuser.NewUser("random-key")
81+
hasTestFlag, _ := gffClient.BoolVariation("test-flag", user, false)
82+
assert.True(t, hasTestFlag, "User should have test flag")
83+
hasUnknownFlag, _ := gffClient.BoolVariation("unknown-flag", user, false)
84+
assert.False(t, hasUnknownFlag, "User should use default value if flag does not exists")
85+
}
86+
5187
func TestS3RetrieverReturnError(t *testing.T) {
5288
_, err := New(Config{
5389
Retriever: &S3Retriever{
@@ -64,7 +100,7 @@ func TestS3RetrieverReturnError(t *testing.T) {
64100
func Test2GoFeatureFlagInstance(t *testing.T) {
65101
gffClient1, err := New(Config{
66102
PollInterval: 5,
67-
Retriever: &FileRetriever{Path: "testdata/test.yaml"},
103+
Retriever: &FileRetriever{Path: "testdata/flag-config.yaml"},
68104
Logger: log.New(os.Stdout, "", 0),
69105
})
70106
defer gffClient1.Close()
@@ -165,7 +201,7 @@ func TestImpossibleToLoadfile(t *testing.T) {
165201
func TestWrongWebhookConfig(t *testing.T) {
166202
_, err := New(Config{
167203
PollInterval: 5,
168-
Retriever: &FileRetriever{Path: "testdata/test.yaml"},
204+
Retriever: &FileRetriever{Path: "testdata/flag-config.yaml"},
169205
Webhooks: []WebhookConfig{
170206
{
171207
PayloadURL: " https://example.com/hook",

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/blang/semver v3.5.1+incompatible // indirect
99
github.com/google/go-cmp v0.5.4
1010
github.com/nikunjy/rules v0.0.0-20200120082459-0b7c4dc9dc86
11+
github.com/pelletier/go-toml v1.8.1
1112
github.com/stretchr/testify v1.7.0
1213
gopkg.in/yaml.v2 v2.4.0 // indirect
1314
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
66
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
77
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
88
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
911
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
1012
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1113
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -14,6 +16,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
1416
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
1517
github.com/nikunjy/rules v0.0.0-20200120082459-0b7c4dc9dc86 h1:AdqGYsIDYgW6HTzZFd0xAuWn2JLRh9UioTjXV31TcsY=
1618
github.com/nikunjy/rules v0.0.0-20200120082459-0b7c4dc9dc86/go.mod h1:yzFCC3jL9d8E9DklzT92Kx0F9hvJq7lxXVc89nvlZPk=
19+
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
20+
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
1721
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1822
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1923
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

internal/cache/cache.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package cache
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"gopkg.in/yaml.v3"
8+
"strings"
79
"sync"
810

11+
"github.com/pelletier/go-toml"
12+
913
"github.com/thomaspoignant/go-feature-flag/internal/model"
1014
)
1115

1216
type Cache interface {
13-
UpdateCache(loadedFlags []byte) error
17+
UpdateCache(loadedFlags []byte, fileFormat string) error
1418
Close()
1519
GetFlag(key string) (model.Flag, error)
1620
}
@@ -29,9 +33,19 @@ func New(notificationService Service) Cache {
2933
}
3034
}
3135

32-
func (c *cacheImpl) UpdateCache(loadedFlags []byte) error {
36+
func (c *cacheImpl) UpdateCache(loadedFlags []byte, fileFormat string) error {
3337
var newCache FlagsCache
34-
err := yaml.Unmarshal(loadedFlags, &newCache)
38+
var err error
39+
switch strings.ToLower(fileFormat) {
40+
case "toml":
41+
err = toml.Unmarshal(loadedFlags, &newCache)
42+
case "json":
43+
err = json.Unmarshal(loadedFlags, &newCache)
44+
default:
45+
// default unmarshaller is YAML
46+
err = yaml.Unmarshal(loadedFlags, &newCache)
47+
}
48+
3549
if err != nil {
3650
return err
3751
}

0 commit comments

Comments
 (0)