Skip to content

Commit

Permalink
Merge pull request #15 from heetch/specify-backend
Browse files Browse the repository at this point in the history
Specify backend
  • Loading branch information
Yasss authored Mar 29, 2018
2 parents a1b6a5d + 31c3859 commit 76917f3
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 46 deletions.
13 changes: 1 addition & 12 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 1 addition & 28 deletions Gopkg.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,3 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"e
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true


[[constraint]]
name = "github.com/coreos/etcd"
version = "3.0.0"
Expand All @@ -51,4 +24,4 @@

[prune]
go-tests = true
unused-packages = true
unused-packages = true
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ type Config struct {
}
```
### Backend tag
By default, Confita queries each backend one after another until a key is found. However, in order to avoid some useless processing the `backend` tag can be specified to describe in which backend this key is expected to be found.
This is especially useful when the location of the key is known beforehand.
```go
type Config struct {
Host string `config:"host,backend=env"`
Port uint32 `config:"port,required,backend=etcd"`
Timeout time.Duration `config:"timeout"`
}
```
### Loading configuration
Creating a loader:
Expand Down
12 changes: 9 additions & 3 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,27 @@ var (
// A Backend is used to fetch values from a given key.
type Backend interface {
Get(ctx context.Context, key string) ([]byte, error)
Name() string
}

// Func creates a Backend from a function.
func Func(fn func(context.Context, string) ([]byte, error)) Backend {
return &backendFunc{fn: fn}
func Func(name string, fn func(context.Context, string) ([]byte, error)) Backend {
return &backendFunc{fn: fn, name: name}
}

type backendFunc struct {
fn func(context.Context, string) ([]byte, error)
fn func(context.Context, string) ([]byte, error)
name string
}

func (b *backendFunc) Get(ctx context.Context, key string) ([]byte, error) {
return b.fn(ctx, key)
}

func (b *backendFunc) Name() string {
return b.name
}

// A ValueUnmarshaler decodes a value identified by a key into a target.
type ValueUnmarshaler interface {
UnmarshalValue(ctx context.Context, key string, to interface{}) error
Expand Down
5 changes: 5 additions & 0 deletions backend/consul/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ func (b *Backend) Get(ctx context.Context, key string) ([]byte, error) {

return kv.Value, nil
}

// Name returns the name of the backend.
func (b *Backend) Name() string {
return "consul"
}
2 changes: 1 addition & 1 deletion backend/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
// If the key is not found, this backend tries again by turning any kebabcase key to snakecase and
// lowercase letters to uppercase.
func NewBackend() backend.Backend {
return backend.Func(func(ctx context.Context, key string) ([]byte, error) {
return backend.Func("env", func(ctx context.Context, key string) ([]byte, error) {
val, ok := os.LookupEnv(key)
if ok {
return []byte(val), nil
Expand Down
5 changes: 5 additions & 0 deletions backend/etcd/etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ func (b *Backend) Get(ctx context.Context, key string) ([]byte, error) {

return resp.Kvs[0].Value, nil
}

// Name returns the name of the backend.
func (b *Backend) Name() string {
return "etcd"
}
12 changes: 12 additions & 0 deletions backend/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ import (
type Backend struct {
path string
unmarshaler backend.ValueUnmarshaler
name string
}

// NewBackend creates a configuration loader that loads from a file.
// The content will get decoded based on the file extension and cached in the backend.
func NewBackend(path string) *Backend {
name := filepath.Ext(path)
if name != "" {
name = name[1:]
}

return &Backend{
path: path,
name: name,
}
}

Expand Down Expand Up @@ -74,6 +81,11 @@ func (b *Backend) Get(ctx context.Context, key string) ([]byte, error) {
return nil, errors.New("not implemented")
}

// Name returns the type of the file.
func (b *Backend) Name() string {
return b.name
}

type jsonConfig map[string]json.RawMessage

func (j jsonConfig) UnmarshalValue(_ context.Context, key string, to interface{}) error {
Expand Down
23 changes: 21 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,37 @@ func (l *Loader) parseStruct(ctx context.Context, ref *reflect.Value) error {

key := tag
var required bool
var bcknd string

if idx := strings.Index(tag, ","); idx != -1 {
key = tag[:idx]
if tag[idx+1:] == "required" {
required = true
opts := strings.Split(tag[idx+1:], ",")

for _, o := range opts {
if o == "required" {
required = true
}

if strings.HasPrefix(o, "backend=") {
bcknd = o[len("backend="):]
}
}
}

var found bool
var backendFound bool
for _, b := range l.backends {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

if bcknd != "" && bcknd != b.Name() {
continue
}
backendFound = true

if u, ok := b.(backend.ValueUnmarshaler); ok {
err := u.UnmarshalValue(ctx, key, value.Addr().Interface())
if err != nil && err != backend.ErrNotFound {
Expand Down Expand Up @@ -140,6 +155,10 @@ func (l *Loader) parseStruct(ctx context.Context, ref *reflect.Value) error {
break
}

if bcknd != "" && !backendFound {
return fmt.Errorf("the backend: '%s' is not supported", bcknd)
}

if required && !found {
return fmt.Errorf("required key '%s' for field '%s' not found", key, field.Name)
}
Expand Down
137 changes: 137 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"

"github.com/heetch/confita"
"github.com/heetch/confita/backend"
"github.com/stretchr/testify/require"
Expand All @@ -24,6 +26,10 @@ func (s store) Get(ctx context.Context, key string) ([]byte, error) {
return []byte(data), nil
}

func (store) Name() string {
return "store"
}

type longRunningStore time.Duration

func (s longRunningStore) Get(ctx context.Context, key string) ([]byte, error) {
Expand All @@ -35,6 +41,10 @@ func (s longRunningStore) Get(ctx context.Context, key string) ([]byte, error) {
}
}

func (longRunningStore) Name() string {
return "longRunningStore"
}

type valueUnmarshaler store

func (k valueUnmarshaler) Get(ctx context.Context, key string) ([]byte, error) {
Expand All @@ -50,6 +60,10 @@ func (k valueUnmarshaler) UnmarshalValue(ctx context.Context, key string, to int
return json.Unmarshal(data, to)
}

func (valueUnmarshaler) Name() string {
return "valueUnmarshaler"
}

func TestLoad(t *testing.T) {
type nested struct {
Int int `config:"int"`
Expand Down Expand Up @@ -226,3 +240,126 @@ func TestLoadFromValueUnmarshaler(t *testing.T) {
require.Equal(t, 10, s.Age)
require.Zero(t, s.Ignored)
}

type backendMock struct {
store map[string]string
called int
name string
}

func (b *backendMock) Get(ctx context.Context, key string) ([]byte, error) {
b.called++
data, ok := b.store[key]
if !ok {
return nil, backend.ErrNotFound
}

return []byte(data), nil
}

func (b *backendMock) Name() string {
return b.name
}

func TestBackendTag(t *testing.T) {
type test struct {
Tikka string `config:"tikka,backend=store"`
Cheese string `config:"cheese,required,backend=backendCalled"`
}

backendNotCalled := &backendMock{
store: make(map[string]string),
name: "backendNotCalled",
}
backendNotCalled.store["cheese"] = "nan"

myStore := make(store)
myStore["tikka"] = "masala"

t.Run("OK", func(t *testing.T) {
backendCalled := &backendMock{
store: make(map[string]string),
name: "backendCalled",
}
backendCalled.store["cheese"] = "nan"

ldr := confita.NewLoader(myStore, backendCalled, backendNotCalled)

var cfg test
err := ldr.Load(context.Background(), &cfg)
require.NoError(t, err)

assert.Equal(t, "nan", cfg.Cheese)
assert.Equal(t, "masala", cfg.Tikka)
assert.Equal(t, 1, backendCalled.called)
assert.Equal(t, 0, backendNotCalled.called)
})

t.Run("NOK", func(t *testing.T) {
backendCalled := &backendMock{
store: make(map[string]string),
name: "backendCalled",
}

ldr := confita.NewLoader(myStore, backendCalled, backendNotCalled)

var cfg test
err := ldr.Load(context.Background(), &cfg)
require.EqualError(t, err, "required key 'cheese' for field 'Cheese' not found")

assert.Equal(t, 1, backendCalled.called)
assert.Equal(t, 0, backendNotCalled.called)
})
}

func TestTags(t *testing.T) {

t.Run("BadRequired", func(t *testing.T) {
type test struct {
Key string `config:"key,rrequiredd,backend=store"`
}

myStore := make(store)
myStore["oups"] = "value"

ldr := confita.NewLoader(myStore)

var cfg test
err := ldr.Load(context.Background(), &cfg)
require.NoError(t, err)

assert.Equal(t, "", cfg.Key)
})

t.Run("BadBackendValue", func(t *testing.T) {
type test struct {
Key string `config:"key,backend=stor"`
}

myStore := make(store)
myStore["key"] = "value"

ldr := confita.NewLoader(myStore)

var cfg test
err := ldr.Load(context.Background(), &cfg)
require.Error(t, err)
})

t.Run("BadTagsOrder", func(t *testing.T) {
type test struct {
Key string `config:"backend=store,key"`
}

myStore := make(store)
myStore["key"] = "value"

ldr := confita.NewLoader(myStore)

var cfg test
err := ldr.Load(context.Background(), &cfg)
require.NoError(t, err)

assert.Equal(t, "", cfg.Key)
})
}

0 comments on commit 76917f3

Please sign in to comment.