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

feat[v4]: add starting watching files manually #23

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
## Installation

```shell
go get github.com/Clarilab/envi/v3
go get github.com/Clarilab/envi/v4
```

## Importing

```go
import "github.com/Clarilab/envi/v3"
import "github.com/Clarilab/envi/v4"
```

## Usage
Expand Down Expand Up @@ -91,7 +91,7 @@ Load loads all config files and environment variables into the input struct.
Supported types are JSON, YAML and text files, as well as strings on the struct root level.

If you want to watch a file for changes, the "watch" tag has to be set to true and the underlying struct
has to implement the envi.FileWatcher interface.
has to implement the envi.FileWatcher interface. To actually start watching the files, the StartWatching() method must be called.

While using the "default" tag, the "env" tag can be omitted. If not omitted, the value from the
environment variable will be used.
Expand Down
58 changes: 42 additions & 16 deletions envi.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,24 @@ type FileWatcher interface {
}

type fileWatcherInstance struct {
watcher *fsnotify.Watcher
cancel context.CancelFunc
field reflect.Value
unmarshal unmarshalFunc
cancel context.CancelFunc
close func() error
}

func (f *fileWatcherInstance) setCancel(cancel context.CancelFunc) {
f.cancel = cancel
}

func (f *fileWatcherInstance) setClose(close func() error) {
f.close = close
}

// Envi holds references to all active file watchers.
type Envi struct {
errorChan chan error
fileWatchers map[string]fileWatcherInstance
fileWatchers map[string]*fileWatcherInstance
}

// Errors returns an error channel where filewatcher errors are sent to.
Expand All @@ -58,7 +68,7 @@ func (e *Envi) Close() error {
for filePath, instance := range e.fileWatchers {
instance.cancel()

if err := instance.watcher.Close(); err != nil {
if err := instance.close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close watcher for file %s with error: %w", filePath, err))
}
}
Expand All @@ -74,7 +84,7 @@ func (e *Envi) Close() error {
func New() *Envi {
return &Envi{
errorChan: make(chan error, 100),
fileWatchers: make(map[string]fileWatcherInstance, 0),
fileWatchers: make(map[string]*fileWatcherInstance, 0),
}
}

Expand Down Expand Up @@ -213,10 +223,7 @@ func (e *Envi) loadConfig(config any) error {
}

if watchTag == "true" {
err = e.watchFile(field, path, unmarshalFunc)
if err != nil {
return fmt.Errorf(errMsg, err)
}
e.addFileWatcher(field, path, unmarshalFunc)
}
case reflect.String:
tagVal := getStructTag(t.Field(i), tagEnv)
Expand All @@ -238,6 +245,19 @@ func (e *Envi) loadConfig(config any) error {
return nil
}

// StartWatching starts watching all files with enabled file watching.
func (e *Envi) StartWatching() error {
const errMsg = "error while starting file watching: %w"

for path, watcher := range e.fileWatchers {
if err := e.watchFile(watcher, path); err != nil {
return fmt.Errorf(errMsg, err)
}
}

return nil
}

func unmarshalText(data []byte, v any) error {
val := strings.Trim(string(data), "\n")

Expand Down Expand Up @@ -330,8 +350,15 @@ func handleDefaults(field reflect.Value) error {
return nil
}

func (e *Envi) watchFile(field reflect.Value, path string, unmarshal unmarshalFunc) error {
const errMsg = "error while watching file: %w"
func (e *Envi) addFileWatcher(field reflect.Value, path string, unmarshal unmarshalFunc) {
e.fileWatchers[path] = &fileWatcherInstance{
field: field,
unmarshal: unmarshal,
}
}

func (e *Envi) watchFile(instance *fileWatcherInstance, path string) error {
const errMsg = "error while starting file watcher: %w"

watcher, err := fsnotify.NewWatcher()
if err != nil {
Expand All @@ -340,12 +367,10 @@ func (e *Envi) watchFile(field reflect.Value, path string, unmarshal unmarshalFu

ctx, cancel := context.WithCancel(context.Background())

e.fileWatchers[path] = fileWatcherInstance{
watcher: watcher,
cancel: cancel,
}
instance.setCancel(cancel)
instance.setClose(watcher.Close)

go e.fileWatcher(ctx, watcher, field, path, unmarshal)
go e.fileWatcher(ctx, watcher, instance.field, path, instance.unmarshal)

err = watcher.Add(filepath.Dir(path)) // needs to be the directory of the file to ensure working on linux systems
if err != nil {
Expand All @@ -356,6 +381,7 @@ func (e *Envi) watchFile(field reflect.Value, path string, unmarshal unmarshalFu

return nil
}

func validate(e any) []error {
v := reflect.ValueOf(e)
t := reflect.TypeOf(e)
Expand Down
7 changes: 6 additions & 1 deletion envi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"sync/atomic"
"testing"

"github.com/Clarilab/envi/v3"
"github.com/Clarilab/envi/v4"
)

// !!! Attention: The tests in this file are not meant to be run in parallel because of the t.Setenv usage !!!
Expand Down Expand Up @@ -205,6 +205,11 @@ func Test_Filewatcher(t *testing.T) {
t.Fatal(err)
}

err = enviClient.StartWatching()
if err != nil {
t.Fatal(err)
}

if err := os.WriteFile(
"test.yaml",
[]byte(fmt.Sprintf("%s: %s", "PETER", "PANUS")),
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/Clarilab/envi/v3
module github.com/Clarilab/envi/v4

go 1.22

Expand Down