Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

story(issue-269): config implement source interface on manager #270

Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/z5labs/bedrock.svg)](https://pkg.go.dev/github.com/z5labs/bedrock)
[![Go Report Card](https://goreportcard.com/badge/github.com/z5labs/bedrock)](https://goreportcard.com/report/github.com/z5labs/bedrock)
![Coverage](https://img.shields.io/badge/Coverage-96.8%25-brightgreen)
![Coverage](https://img.shields.io/badge/Coverage-96.7%25-brightgreen)
[![build](https://github.com/z5labs/bedrock/actions/workflows/build.yaml/badge.svg)](https://github.com/z5labs/bedrock/actions/workflows/build.yaml)

**bedrock provides a minimal, modular and composable foundation for
Expand Down
17 changes: 15 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ type Source interface {

// Manager
type Manager struct {
store inMemoryStore
store Map
}

// Read
// Subsequent sources override previous sources.
func Read(srcs ...Source) (*Manager, error) {
store := make(inMemoryStore)
if len(srcs) == 0 {
return &Manager{store: make(Map)}, nil
}

if m, ok := srcs[0].(*Manager); len(srcs) == 1 && ok {
return m, nil
}

store := make(Map)
for _, src := range srcs {
err := src.Apply(store)
if err != nil {
Expand All @@ -50,6 +58,11 @@ func Read(srcs ...Source) (*Manager, error) {
return m, nil
}

// Apply implements the [Source] interface.
func (m *Manager) Apply(store Store) error {
return m.store.Apply(store)
}

// Unmarshal
func (m *Manager) Unmarshal(v any) error {
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Expand Down
50 changes: 50 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package config
import (
"errors"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -35,9 +36,58 @@ func TestRead(t *testing.T) {
})
})

t.Run("will return empty Manager", func(t *testing.T) {
t.Run("if no sources are provided", func(t *testing.T) {
m, err := Read()
if !assert.Nil(t, err) {
return
}
if !assert.NotNil(t, m.store) {
return
}
if !assert.Len(t, m.store, 0) {
return
}
})
})

t.Run("will override config values", func(t *testing.T) {
t.Run("if multiple sources are provided", func(t *testing.T) {
m, err := Read(
FromYaml(strings.NewReader("hello: alice")),
FromYaml(strings.NewReader("hello: bob")),
)
if !assert.Nil(t, err) {
return
}

var cfg struct {
Hello string `config:"hello"`
}
err = m.Unmarshal(&cfg)
if !assert.Nil(t, err) {
return
}
if !assert.Equal(t, "bob", cfg.Hello) {
return
}
})
})

t.Run("will be idempotent", func(t *testing.T) {
t.Run("if a single Manager is used as the source", func(t *testing.T) {
m, err := Read(FromYaml(strings.NewReader("hello: world")))
if !assert.Nil(t, err) {
return
}

m2, err := Read(m)
if !assert.Nil(t, err) {
return
}
if !assert.Equal(t, m, m2) {
return
}
})
})
}
Expand Down
86 changes: 83 additions & 3 deletions pkg/config/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@

package config

import "github.com/z5labs/bedrock/pkg/config/key"
import (
"fmt"

// Map is an ordinary map[string]any but implements the Source interface.
"github.com/z5labs/bedrock/pkg/config/key"
)

// Map is an ordinary map[string]any but implements the [Store] and [Source] interfaces.
type Map map[string]any

// Apply implements the Source interface. It recursively walks the underlying
// Apply implements the [Source] interface. It recursively walks the underlying
// map to find key value pairs to set on the given store.
func (m Map) Apply(store Store) error {
return walkMap(m, store, nil)
Expand All @@ -33,3 +37,79 @@ func walkMap(m map[string]any, store Store, chain key.Chain) error {
}
return nil
}

// UnknownKeyerError
type UnknownKeyerError struct {
key key.Keyer
}

// Error implements the error interface.
func (e UnknownKeyerError) Error() string {
return fmt.Sprintf("config source tried setting config value with unknown key.Keyer: %s", e.key.Key())
}

// Set implements the [Store] interface.
func (m Map) Set(k key.Keyer, v any) error {
return set(m, k, v)
}

func set(m map[string]any, k key.Keyer, v any) error {
switch x := k.(type) {
case key.Name:
m[string(x)] = v
case key.Chain:
return setKeyChain(m, x, v)
default:
return UnknownKeyerError{key: k}
}
return nil
}

// EmptyKeyChainError
type EmptyKeyChainError struct {
Value any
}

// Error implements the error interface.
func (e EmptyKeyChainError) Error() string {
return fmt.Sprintf("attempted to set value to an empty key chain: %v", e.Value)
}

// UnexpectedKeyValueTypeError represents the situation when
// a user tries setting a key to a different type than it
// had previously been set to.
type UnexpectedKeyValueTypeError struct {
Key string
ExpectedType string
}

// Error implements the error interface.
func (e UnexpectedKeyValueTypeError) Error() string {
return fmt.Sprintf("expected key value to be a %s: %s", e.ExpectedType, e.Key)
}

func setKeyChain(m map[string]any, chain key.Chain, v any) error {
if len(chain) == 0 {
return EmptyKeyChainError{Value: v}
}

root := chain[0]
if len(chain) == 1 {
return set(m, root, v)
}

old, ok := m[root.Key()]
if !ok {
old = make(map[string]any)
m[root.Key()] = old
}

subM, ok := old.(map[string]any)
if !ok {
return UnexpectedKeyValueTypeError{
Key: root.Key(),
ExpectedType: "map[string]any",
}
}
return set(subM, chain[1:], v)
}
54 changes: 54 additions & 0 deletions pkg/config/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,57 @@ func TestMap_Apply(t *testing.T) {
})
})
}

type myKeyer string

func (myKeyer) Key() string {
return "my key"
}

func TestMap_Set(t *testing.T) {
t.Run("will return an error", func(t *testing.T) {
t.Run("if an known key.Keyer is used", func(t *testing.T) {
store := make(Map)
err := store.Set(myKeyer("hello"), "world")

var ierr UnknownKeyerError
if !assert.ErrorAs(t, err, &ierr) {
return
}
if !assert.NotEmpty(t, ierr.Error()) {
return
}
})

t.Run("if an empty key.Chain is used", func(t *testing.T) {
store := make(Map)
err := store.Set(key.Chain{}, "world")

var ierr EmptyKeyChainError
if !assert.ErrorAs(t, err, &ierr) {
return
}
if !assert.NotEmpty(t, ierr.Error()) {
return
}
})

t.Run("if the value type is attempted to be changed while overriding an existing key", func(t *testing.T) {
store := make(Map)
err := store.Set(key.Name("hello"), "world")
if !assert.Nil(t, err) {
return
}

err = store.Set(key.Chain{key.Name("hello"), key.Name("bob")}, "world")

var ierr UnexpectedKeyValueTypeError
if !assert.ErrorAs(t, err, &ierr) {
return
}
if !assert.NotEmpty(t, ierr.Error()) {
return
}
})
})
}
89 changes: 0 additions & 89 deletions pkg/config/store.go

This file was deleted.

Loading