diff --git a/change-handler.go b/change-handler.go index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d6e66fcd0d06a074bbfabab8e1438f6c47d58aa2 100644 --- a/change-handler.go +++ b/change-handler.go @@ -0,0 +1,50 @@ +// Copyright 2022 schukai GmbH +// SPDX-License-Identifier: AGPL-3.0 + +package configuration + +import ( + "github.com/r3labs/diff/v3" +) + +type ChangeEvent struct { + Changlog diff.Changelog +} + +type ChangeHook interface { + Handle(event ChangeEvent) +} + +// OnChange registers a hook that is called when the configuration changes. +func (s *Settings[C]) OnChange(hook ChangeHook) *Settings[C] { + s.hooks.change = append(s.hooks.change, hook) + return s +} + +// HasOnChangeHook returns true if there are registered hooks. +func (s *Settings[C]) HasOnChangeHook(hook ChangeHook) *Settings[C] { + for _, h := range s.hooks.change { + if h == hook { + break + } + } + return s +} + +// RemoveOnChangeHook removes a change hook from the list of hooks. +func (s *Settings[C]) RemoveOnChangeHook(hook ChangeHook) *Settings[C] { + for i, h := range s.hooks.change { + if h == hook { + s.hooks.change = append(s.hooks.change[:i], s.hooks.change[i+1:]...) + break + } + } + return s +} + +func (s *Settings[C]) notifyChangeHooks(changelog diff.Changelog) *Settings[C] { + for _, h := range s.hooks.change { + go h.Handle(ChangeEvent{Changlog: changelog}) + } + return s +} diff --git a/change.go b/change.go index cb083d7804b799c6dfd85ade5439a7b893d69643..c3000e42ffdbf355b4462560201707f3ff04bf93 100644 --- a/change.go +++ b/change.go @@ -7,48 +7,6 @@ import ( "github.com/r3labs/diff/v3" ) -type ChangeEvent struct { - Changlog diff.Changelog -} - -type EventHook interface { - Handle(event ChangeEvent) -} - -// OnChange registers a hook that is called when the configuration changes. -func (s *Settings[C]) OnChange(hook EventHook) *Settings[C] { - s.hooks.change = append(s.hooks.change, hook) - return s -} - -// HasOnChangeHook returns true if there are registered hooks. -func (s *Settings[C]) HasOnChangeHook(hook EventHook) *Settings[C] { - for _, h := range s.hooks.change { - if h == hook { - break - } - } - return s -} - -// RemoveOnChangeHook removes a change hook from the list of hooks. -func (s *Settings[C]) RemoveOnChangeHook(hook EventHook) *Settings[C] { - for i, h := range s.hooks.change { - if h == hook { - s.hooks.change = append(s.hooks.change[:i], s.hooks.change[i+1:]...) - break - } - } - return s -} - -func (s *Settings[C]) notifyChangeHooks(changelog diff.Changelog) *Settings[C] { - for _, h := range s.hooks.change { - h.Handle(ChangeEvent{Changlog: changelog}) - } - return s -} - func (s *Settings[C]) setConfigInternal(config C, lock bool) *Settings[C] { var ( @@ -64,9 +22,12 @@ func (s *Settings[C]) setConfigInternal(config C, lock bool) *Settings[C] { if lock { s.Unlock() } + }() + + defer func() { if len(changelog) > 0 { - go s.notifyChangeHooks(changelog) + s.notifyChangeHooks(changelog) } }() diff --git a/change_test.go b/change_test.go index 0e8616fd12e36b3847e7693bd7b7244d92293536..6dea9f6ff847e7bd7bd13abbf4cf8d9e6f5d268d 100644 --- a/change_test.go +++ b/change_test.go @@ -12,7 +12,7 @@ import ( ) type mockTestEventHandler struct { - EventHook + ChangeHook } func (m *mockTestEventHandler) Handle(event ChangeEvent) { @@ -30,7 +30,7 @@ func TestAddRemoveHook(t *testing.T) { s := New(config) - var h EventHook + var h ChangeHook h = &mockTestEventHandler{} s.OnChange(h) @@ -60,7 +60,7 @@ func TestReadmeExample(t *testing.T) { msg := "" - var h EventHook + var h ChangeHook h = &ChangeEventHandler{ Callback: func(event ChangeEvent) { log := event.Changlog @@ -126,7 +126,7 @@ func TestCangeOnChange(t *testing.T) { counter := 0 - var h EventHook + var h ChangeHook h = &ChangeEventHandler{ Callback: func(event ChangeEvent) { counter++ diff --git a/error-handler.go b/error-handler.go index 91f3ae462a4f001e9815cf4378e1a53adadfafc0..5eb3752cd00c401595b2de66442a98b12ef7a699 100644 --- a/error-handler.go +++ b/error-handler.go @@ -3,28 +3,22 @@ package configuration -import ( - "fmt" - "github.com/r3labs/diff/v3" -) - -type ChangeEvent struct { - Changlog diff.Changelog +type ErrorEvent struct { } -type EventHook interface { - Handle(event ChangeEvent) +type ErrorHook interface { + Handle(event ErrorEvent) } // OnChange registers a hook that is called when the configuration changes. -func (s *Settings[C]) OnChange(hook EventHook) *Settings[C] { - s.hooks.change = append(s.hooks.change, hook) +func (s *Settings[C]) OnError(hook ErrorHook) *Settings[C] { + s.hooks.error = append(s.hooks.error, hook) return s } // HasOnChangeHook returns true if there are registered hooks. -func (s *Settings[C]) HasOnChangeHook(hook EventHook) *Settings[C] { - for _, h := range s.hooks.change { +func (s *Settings[C]) HasOnErrorHook(hook ErrorHook) *Settings[C] { + for _, h := range s.hooks.error { if h == hook { break } @@ -33,73 +27,19 @@ func (s *Settings[C]) HasOnChangeHook(hook EventHook) *Settings[C] { } // RemoveOnChangeHook removes a change hook from the list of hooks. -func (s *Settings[C]) RemoveOnChangeHook(hook EventHook) *Settings[C] { - for i, h := range s.hooks.change { +func (s *Settings[C]) RemoveOnErrorHook(hook ErrorHook) *Settings[C] { + for i, h := range s.hooks.error { if h == hook { - s.hooks.change = append(s.hooks.change[:i], s.hooks.change[i+1:]...) + s.hooks.error = append(s.hooks.error[:i], s.hooks.error[i+1:]...) break } } return s } -func (s *Settings[C]) notifyChangeHooks(changelog diff.Changelog) *Settings[C] { - for _, h := range s.hooks.change { - h.Handle(ChangeEvent{Changlog: changelog}) +func (s *Settings[C]) notifyErrorHooks() *Settings[C] { + for _, h := range s.hooks.error { + go h.Handle(ErrorEvent{}) } return s } - -func (s *Settings[C]) setConfigInternal(config C, lock bool) *Settings[C] { - - var ( - changelog diff.Changelog - err error - ) - - if lock { - s.Lock() - } - - defer func() { - if lock { - s.Unlock() - } - - if len(changelog) > 0 { - go s.notifyChangeHooks(changelog) - } - - fmt.Println(">>>>>>>>> setConfigInternal", len(changelog)) - fmt.Println(changelog) - fmt.Println(s.errors) - - }() - - if err := validateConfig[C](config); err != nil { - s.errors = append(s.errors, err) - return s - } - - d, err := diff.NewDiffer() - if err != nil { - s.errors = append(s.errors, err) - return s - } - - d.ConvertCompatibleTypes = true - d.AllowTypeMismatch = true - - changelog, err = d.Diff(s.config, config) - if err != nil { - s.errors = append(s.errors, err) - return s - } - - s.config = config - return s -} - -func (s *Settings[C]) SetConfig(config C) *Settings[C] { - return s.setConfigInternal(config, true) -} diff --git a/error-handler_test.go b/error-handler_test.go index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..96279ff9983f1a18b9dedc8af21769f55201e236 100644 --- a/error-handler_test.go +++ b/error-handler_test.go @@ -0,0 +1,68 @@ +package configuration + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +type mockTestEventErrorHandler struct { + ErrorHook + ch chan bool +} + +func (m *mockTestEventErrorHandler) Handle(event ErrorEvent) { + // do nothing + m.ch <- true + +} + +func TestErrorHandling(t *testing.T) { + + defaults := ConfigStruct2{ + A: "Hello!", + } + + c := New(defaults) + var h ErrorHook + h = &mockTestEventErrorHandler{ + ch: make(chan bool), + } + + c.OnError(h) + c.SetDefaultDirectories() + + select { + case <-h.(*mockTestEventErrorHandler).ch: + assert.True(t, true) + case <-time.After(11 * time.Second): + t.Error("Timeout, expected error event") + } + +} + +func TestAddRemoveErrorHook(t *testing.T) { + + config := struct { + Host string + }{ + Host: "localhost", + } + + s := New(config) + + var h ErrorHook + h = &mockTestEventErrorHandler{} + s.OnError(h) + + if len(s.hooks.error) != 1 { + t.Error("Expected 1 got ", len(s.hooks.change)) + } + + s.RemoveOnErrorHook(h) + + if len(s.hooks.error) != 0 { + t.Error("Expected 0 got ", len(s.hooks.change)) + } + +} diff --git a/export.go b/export.go index 0c57483150b4a4d704d844a4006abf4f183900bc..e1d9aba2f0fb3252e2f4ac1278de0519b68867d1 100644 --- a/export.go +++ b/export.go @@ -50,6 +50,7 @@ func (s *Settings[C]) writeProperties(writer io.Writer) error { p := properties.LoadMap(m) _, err := p.Write(writer, properties.UTF8) + return err } @@ -79,6 +80,7 @@ func (s *Settings[C]) WriteFile(fn string, format Format) *Settings[C] { defer file.Close() s.Write(io.Writer(file), format) + return s } diff --git a/file.go b/file.go index 252529e71ad892f1e6e9bfa70a3cda00334d075e..633ac3779c61814dfe6aafce3745e0a53cf3deb4 100644 --- a/file.go +++ b/file.go @@ -227,9 +227,6 @@ func (s *Settings[C]) AddUserConfigDirectory() *Settings[C] { return s } - s.Lock() - defer s.Unlock() - current, err := os.UserConfigDir() if err != nil { s.errors = append(s.errors, err) diff --git a/handler.go b/handler.go index eb9becefdb1c1196aee002e15fe9b88f7d144303..20a59e25c443da79f89d0e08e728f9ad01ef5eca 100644 --- a/handler.go +++ b/handler.go @@ -7,3 +7,11 @@ type ChangeEventHandler struct { func (c *ChangeEventHandler) Handle(event ChangeEvent) { c.Callback(event) } + +type ErrorEventHandler struct { + Callback func(event ErrorEvent) +} + +func (c *ErrorEventHandler) Handle(event ErrorEvent) { + c.Callback(event) +} diff --git a/http-handler_test.go b/http-handler_test.go index 61b46f6704ea4a890b865853ea23654c6bca008f..12981d01cbf42f6d8306b7229c742ff4351a35f1 100644 --- a/http-handler_test.go +++ b/http-handler_test.go @@ -272,7 +272,7 @@ func TestConfigurationServePostJson(t *testing.T) { closeChan := make(chan bool) counter := 0 - var h EventHook + var h ChangeHook h = &ChangeEventHandler{ Callback: func(event ChangeEvent) { counter++ diff --git a/import.go b/import.go index 074a4132819db11f2f0c52f17e859034f3407574..576b508bb2e08b9477f3a253fae6cef0b94e0fa6 100644 --- a/import.go +++ b/import.go @@ -6,6 +6,7 @@ package configuration import ( "bytes" "encoding/json" + "github.com/imdario/mergo" "github.com/magiconair/properties" "github.com/pelletier/go-toml/v2" diff --git a/settings.go b/settings.go index 12d57bdbd1edf116ac65b3bc06e572aab26bacfa..15f66f6a3877f14fd9214e033e99b52c60179f0a 100644 --- a/settings.go +++ b/settings.go @@ -28,7 +28,8 @@ type Settings[C any] struct { mnemonic string importCounter int hooks struct { - change []EventHook + change []ChangeHook + error []ErrorHook } fileWatch fileWatch diff --git a/watch_test.go b/watch_test.go index e796abde5549639452ed32c4ef03674843765740..494970bf736c336d8dfee0437b459cffd451634c 100644 --- a/watch_test.go +++ b/watch_test.go @@ -10,6 +10,135 @@ import ( "time" ) +func runTestFilesChange(fn string, data []testHostTimeout, t *testing.T) { + + for _, x := range data { + + h := x.host + + b := []byte("Host: \"" + h + "\"") + err := os.WriteFile(fn, b, 0644) + if err != nil { + t.Error(err) + return + } + + time.Sleep(x.timeout) + + } + +} + +func createTestFileForWatch1() (string, error) { + f, err := os.CreateTemp("", "watch_test") + if err != nil { + return "", err + } + + f.WriteString("Host: \"127.0.0.1\"") + f.Close() + + return f.Name(), nil + +} + +type testHostTimeout struct { + host string + timeout time.Duration +} + +func TestMultiChange(t *testing.T) { + + tmpFn, err := createTestFileForWatch1() + if err != nil { + t.Error(err) + return + } + + defer os.Remove(tmpFn) + + config := struct { + Host string `yaml:"Host"` + }{ + Host: "localhost", + } + + c := New(config) + c.SetMnemonic("my-app") + assert.Equal(t, c.Config().Host, "localhost") + + result := []string{} + signal := make(chan string) + + var h ChangeHook + h = &ChangeEventHandler{ + Callback: func(event ChangeEvent) { + result = append(result, event.Changlog[0].To.(string)) + signal <- event.Changlog[0].To.(string) + }, + } + + var e ErrorHook + e = &ErrorEventHandler{ + Callback: func(event ErrorEvent) { + //for _, err := range c.Errors() { + // t.Error(err) + //} + }, + } + + c.AddFile(tmpFn, Yaml) + c.Import() + + if c.HasErrors() { + t.Error(c.Errors()) + } + + data := []testHostTimeout{ + + { + host: "1.org", + timeout: time.Millisecond * 100, + }, + { + host: "2.org", + timeout: time.Millisecond * 10, + }, + { + host: "3.org", + timeout: time.Millisecond * 2, + }, + { + host: "4.org", + timeout: time.Millisecond * 100, + }, + { + host: "9.org", + timeout: time.Millisecond * 100, + }, + } + + c.OnChange(h).OnError(e).Watch() + + go runTestFilesChange(tmpFn, data, t) + + for loop := true; loop; { + select { + case <-signal: + if len(result) == len(data) { + loop = false + break + } + case <-time.After(time.Second * 10): + t.Log(result) + t.Fatalf("Timeout") + } + } + + assert.Equal(t, "9.org", c.Config().Host) + +} + func TestWatch(t *testing.T) { f, err := os.CreateTemp("", "watch_test") @@ -35,7 +164,7 @@ func TestWatch(t *testing.T) { signal := make(chan bool) - var h EventHook + var h ChangeHook h = &ChangeEventHandler{ Callback: func(event ChangeEvent) { assert.Equal(t, event.Changlog[0].From, "localhost")