diff --git a/api.go b/api.go index 809b9e7760edb422167e13e7a6de5c8a76ed261a..9dcbb033bd706c383c6074fdb5d12bfdadc9b4a6 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/change.go b/change.go index f5136700208561ac5a8272f2ebe0a8230f100d12..cb083d7804b799c6dfd85ade5439a7b893d69643 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 1fde765b46b0ec3eaf509b893430e050afe0c1c3..df98baf6ada81073d75fee01ac6884d669f90c23 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 50ac0d1f63fec8725dbdaf88a2f09fe53cf6aa48..72719309da177bbef851ba3a6bb24bf4ae949db4 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 0000000000000000000000000000000000000000..91f3ae462a4f001e9815cf4378e1a53adadfafc0 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/export.go b/export.go index bbeba7f0482c2beba1355d4a81865037957cfdbb..0c57483150b4a4d704d844a4006abf4f183900bc 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 f3161deb01ad3fabd0de8f619195b79f8758c053..252529e71ad892f1e6e9bfa70a3cda00334d075e 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 904947ba41fcd5c2df2506582b94e068eceece0e..0fb13a8a3d9b50dfcf0a7836bf488c61918ddb75 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 6ec9e29dc641513eeaeeb0d77de0f029f48566da..074a4132819db11f2f0c52f17e859034f3407574 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 ff3506767fcf76e6bf879c14dcfc2278417f3756..12d57bdbd1edf116ac65b3bc06e572aab26bacfa 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 a1d3d7ac18739f14091b5ab9372852abaed45914..950304861360e4c443f049c7392ede751308906b 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() }