From b081facea58e9798ba5117f2d2a969f356c5f0e3 Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Sun, 23 Oct 2022 09:44:08 +0200
Subject: [PATCH] feat inform enventhandler about an error that has occurred

---
 api.go                |  20 +++++++-
 change-handler.go     |   0
 change.go             |   7 +++
 copyable.go           |   7 +++
 env.go                |   7 +++
 error-handler.go      | 105 ++++++++++++++++++++++++++++++++++++++++++
 error-handler_test.go |   0
 export.go             |  21 +++++++++
 file.go               |  53 +++++++++++++++++++++
 flags.go              |  14 ++++++
 import.go             |  14 ++++++
 settings.go           |   7 +++
 watch.go              |  21 +++++++++
 13 files changed, 275 insertions(+), 1 deletion(-)
 create mode 100644 change-handler.go
 create mode 100644 error-handler.go
 create mode 100644 error-handler_test.go

diff --git a/api.go b/api.go
index 809b9e7..9dcbb03 100644
--- a/api.go
+++ b/api.go
@@ -3,13 +3,23 @@
 
 package configuration
 
-import "github.com/imdario/mergo"
+import (
+	"github.com/imdario/mergo"
+)
 
 // NewSetting creates a new configuration setting
 // with the given defaults.
 func New[C any](defaults C) *Settings[C] {
 
 	s := &Settings[C]{}
+
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	s.initDefaults()
 
 	if err := mergo.Merge(&defaults, s.config); err != nil {
@@ -32,6 +42,14 @@ func New[C any](defaults C) *Settings[C] {
 // Set the mnemonic
 // The mnemonic is used to identify the configuration in the configuration file
 func (s *Settings[C]) SetMnemonic(mnemonic string) *Settings[C] {
+
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if mnemonic == "" {
 		s.errors = append(s.errors, MnemonicEmptyError)
 	} else {
diff --git a/change-handler.go b/change-handler.go
new file mode 100644
index 0000000..e69de29
diff --git a/change.go b/change.go
index f513670..cb083d7 100644
--- a/change.go
+++ b/change.go
@@ -71,6 +71,13 @@ func (s *Settings[C]) setConfigInternal(config C, lock bool) *Settings[C] {
 
 	}()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if err := validateConfig[C](config); err != nil {
 		s.errors = append(s.errors, err)
 		return s
diff --git a/copyable.go b/copyable.go
index 1fde765..df98baf 100644
--- a/copyable.go
+++ b/copyable.go
@@ -12,6 +12,13 @@ func (s *Settings[C]) Copy(m map[string]any) {
 
 	c := s.Config()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	for k, v := range m {
 		err := pathfinder.SetValue(&c, k, v)
 		if err != nil {
diff --git a/env.go b/env.go
index 50ac0d1..7271930 100644
--- a/env.go
+++ b/env.go
@@ -14,6 +14,13 @@ func (s *Settings[C]) InitFromEnv(prefix string) *Settings[C] {
 	s.Lock()
 	defer s.Unlock()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	err := runOnTags(&s.config, []string{envTagKey}, func(k string, field reflect.Value) {
 
 		if !field.CanSet() {
diff --git a/error-handler.go b/error-handler.go
new file mode 100644
index 0000000..91f3ae4
--- /dev/null
+++ b/error-handler.go
@@ -0,0 +1,105 @@
+// Copyright 2022 schukai GmbH
+// SPDX-License-Identifier: AGPL-3.0
+
+package configuration
+
+import (
+	"fmt"
+	"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 (
+		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
new file mode 100644
index 0000000..e69de29
diff --git a/export.go b/export.go
index bbeba7f..0c57483 100644
--- a/export.go
+++ b/export.go
@@ -34,6 +34,13 @@ func (s *Settings[C]) writeProperties(writer io.Writer) error {
 
 	m, errors := getMapForProperties[C](s.config)
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if len(errors) > 0 {
 		for _, err := range errors {
 			s.errors = append(s.errors, err)
@@ -53,6 +60,13 @@ func (s *Settings[C]) WriteFile(fn string, format Format) *Settings[C] {
 
 	var file *os.File
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if fn == "" {
 		file = os.Stdout
 	} else {
@@ -73,6 +87,13 @@ func (s *Settings[C]) Write(writer io.Writer, format Format) *Settings[C] {
 
 	var err error
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	switch format {
 	case Json:
 		err = s.writeJson(writer)
diff --git a/file.go b/file.go
index f3161de..252529e 100644
--- a/file.go
+++ b/file.go
@@ -38,6 +38,13 @@ func (s *Settings[C]) AddFile(file string, format ...Format) *Settings[C] {
 
 	var f Format
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if format == nil || len(format) == 0 {
 		f = RecogniseFormat
 	} else if format != nil && len(format) == 1 {
@@ -104,6 +111,13 @@ func (s *Settings[C]) AddDirectory(d string) *Settings[C] {
 
 func (s *Settings[C]) sanitizeDirectories() {
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	wd, err := os.Getwd()
 	if err != nil {
 		s.errors = append(s.errors, err)
@@ -146,9 +160,17 @@ func (s *Settings[C]) AddWorkingDirectory() *Settings[C] {
 	s.Lock()
 	defer s.Unlock()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	current, err := os.Getwd()
 	if err != nil {
 		s.errors = append(s.errors, err)
+
 		return s
 	}
 	s.files.directories = append(s.files.directories, current)
@@ -161,6 +183,13 @@ func (s *Settings[C]) AddEtcDirectory() *Settings[C] {
 	s.Lock()
 	defer s.Unlock()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if s.mnemonic == "" {
 		s.errors = append(s.errors, MnemonicEmptyError)
 		return s
@@ -183,6 +212,16 @@ func (s *Settings[C]) AddEtcDirectory() *Settings[C] {
 // The mnemonic must be set for this function
 func (s *Settings[C]) AddUserConfigDirectory() *Settings[C] {
 
+	s.Lock()
+	defer s.Unlock()
+
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if s.mnemonic == "" {
 		s.errors = append(s.errors, MnemonicEmptyError)
 		return s
@@ -214,6 +253,13 @@ func (s *Settings[C]) SetDefaultDirectories() *Settings[C] {
 
 func (s *Settings[C]) SetFileFormat(format Format) *Settings[C] {
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if slices.Contains(availableFormats, format) {
 		s.files.format = format
 	} else {
@@ -226,6 +272,13 @@ func (s *Settings[C]) SetFileFormat(format Format) *Settings[C] {
 // Set the file name without extension
 func (s *Settings[C]) SetFileName(name string) *Settings[C] {
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if name == "" {
 		s.errors = append(s.errors, FileNameEmptyError)
 	} else {
diff --git a/flags.go b/flags.go
index 904947b..0fb13a8 100644
--- a/flags.go
+++ b/flags.go
@@ -13,6 +13,13 @@ import (
 // The file is read from the flag specified by the name
 func (s *Settings[C]) AddFileFromFlagSet(flagset *flag.FlagSet, name string, format Format) *Settings[C] {
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	flag := flagset.Lookup(name)
 	if flag == nil {
 		s.errors = append(s.errors, newFlagDoesNotExistError(name))
@@ -33,6 +40,13 @@ func (s *Settings[C]) InitFromFlagSet(flagset *flag.FlagSet) *Settings[C] {
 	s.Lock()
 	defer s.Unlock()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	err := runOnTags(&s.config, []string{flagTagKey}, func(k string, field reflect.Value) {
 
 		flag := flagset.Lookup(k)
diff --git a/import.go b/import.go
index 6ec9e29..074a413 100644
--- a/import.go
+++ b/import.go
@@ -49,6 +49,13 @@ func (s *Settings[C]) importStream(r reader) {
 	var c C
 	var err error
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	err = nil // reset error
 
 	x := r.reader
@@ -128,6 +135,13 @@ func (s *Settings[C]) Import() *Settings[C] {
 	s.Lock()
 	defer s.Unlock()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	defaults := s.config
 
 	var n C
diff --git a/settings.go b/settings.go
index ff35067..12d57bd 100644
--- a/settings.go
+++ b/settings.go
@@ -36,6 +36,13 @@ type Settings[C any] struct {
 
 func (s *Settings[C]) initDefaults() *Settings[C] {
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	err := runOnTags(&s.config, []string{"default"}, func(v string, field reflect.Value) {
 
 		if field.CanSet() {
diff --git a/watch.go b/watch.go
index a1d3d7a..9503048 100644
--- a/watch.go
+++ b/watch.go
@@ -11,6 +11,13 @@ func (s *Settings[C]) initWatch() *Settings[C] {
 
 	var err error
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if s.fileWatch.watcher != nil {
 		s.errors = append(s.errors, WatchAlreadyInitializedError)
 		return s
@@ -31,6 +38,13 @@ func (s *Settings[C]) StopWatching() *Settings[C] {
 	s.fileWatch.Lock()
 	defer s.fileWatch.Unlock()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if s.fileWatch.watcher == nil {
 		s.errors = append(s.errors, WatchNotInitializedError)
 		return s
@@ -52,6 +66,13 @@ func (s *Settings[C]) Watch() *Settings[C] {
 	s.fileWatch.Lock()
 	defer s.fileWatch.Unlock()
 
+	errorCount := len(s.errors)
+	defer func() {
+		if len(s.errors) > errorCount {
+			s.notifyErrorHooks()
+		}
+	}()
+
 	if s.fileWatch.watcher == nil {
 		s.initWatch()
 	}
-- 
GitLab