From 799167e87f1cb9aa3d16cecaad973dc05c376e67 Mon Sep 17 00:00:00 2001 From: Richard Carson Derr Date: Sat, 14 Sep 2024 23:31:43 -0400 Subject: [PATCH 1/5] refactor(issue-269): just use map type as store and source --- pkg/config/map.go | 86 ++++++++++++++++++++++++++++++++++++-- pkg/config/map_test.go | 54 ++++++++++++++++++++++++ pkg/config/store.go | 89 ---------------------------------------- pkg/config/store_test.go | 68 ------------------------------ 4 files changed, 137 insertions(+), 160 deletions(-) delete mode 100644 pkg/config/store.go delete mode 100644 pkg/config/store_test.go diff --git a/pkg/config/map.go b/pkg/config/map.go index 1c1dfb5..c112d87 100644 --- a/pkg/config/map.go +++ b/pkg/config/map.go @@ -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) @@ -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) +} diff --git a/pkg/config/map_test.go b/pkg/config/map_test.go index 69629a1..57c441b 100644 --- a/pkg/config/map_test.go +++ b/pkg/config/map_test.go @@ -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 + } + }) + }) +} diff --git a/pkg/config/store.go b/pkg/config/store.go deleted file mode 100644 index 707241f..0000000 --- a/pkg/config/store.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2024 Z5Labs and Contributors -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -package config - -import ( - "fmt" - - "github.com/z5labs/bedrock/pkg/config/key" -) - -// 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()) -} - -type inMemoryStore map[string]any - -func (m inMemoryStore) 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) -} diff --git a/pkg/config/store_test.go b/pkg/config/store_test.go deleted file mode 100644 index c533efe..0000000 --- a/pkg/config/store_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2024 Z5Labs and Contributors -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -package config - -import ( - "testing" - - "github.com/z5labs/bedrock/pkg/config/key" - - "github.com/stretchr/testify/assert" -) - -type myKeyer string - -func (myKeyer) Key() string { - return "my key" -} - -func TestInMemoryStore_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(inMemoryStore) - 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(inMemoryStore) - 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(inMemoryStore) - 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 - } - }) - }) -} From 6be39761cab6de9334589f86dfeca4626e3a54c6 Mon Sep 17 00:00:00 2001 From: Richard Carson Derr Date: Sat, 14 Sep 2024 23:32:23 -0400 Subject: [PATCH 2/5] feat(issue-269): implement config source interface on config manager --- pkg/config/config.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index cd854ea..34fe97d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 { @@ -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{ From 787632606036f89cea7912a31e1cb042fec6a472 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 15 Sep 2024 03:33:27 +0000 Subject: [PATCH 3/5] chore(docs): updated coverage badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3db7b82..8a914ce 100644 --- a/README.md +++ b/README.md @@ -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.4%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 From 410a68a4c698536dd676f739565d99cd0687a222 Mon Sep 17 00:00:00 2001 From: Richard Carson Derr Date: Sat, 14 Sep 2024 23:40:32 -0400 Subject: [PATCH 4/5] test(issue-269): add some cases around read optimizations --- pkg/config/config_test.go | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 00878da..bb7beb2 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -8,6 +8,7 @@ package config import ( "errors" "strconv" + "strings" "testing" "time" @@ -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 + } }) }) } From 35fef6b4bb199ea0dd033edf812819de2b511698 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 15 Sep 2024 03:41:17 +0000 Subject: [PATCH 5/5] chore(docs): updated coverage badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a914ce..2ca3b1e 100644 --- a/README.md +++ b/README.md @@ -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.4%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