Skip to content

Commit

Permalink
refactor: allow the loader to pass the delimiter to the parsers
Browse files Browse the repository at this point in the history
  • Loading branch information
krak3n committed May 25, 2020
1 parent 1af7a0a commit 6735fb9
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 104 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ func main() {
var cfg Config

// Initialise gofig with the destination struct
gfg, err := gofig.New(&cfg)
gfg, err := gofig.New(&cfg, gofig.WithDebug())
gofig.Must(err)

// Setsup a yaml parser with file notification support
yml := gofig.FromFileWithNotify(yaml.New(), fsnotify.New("./config.yaml"))
yml := gofig.FromFileAndNotify(yaml.New(), fsnotify.New("./config.yaml"))

// Parse the yaml file and then environment variables
gofig.Must(gfg.Parse(yml, env.New(env.HasAndTrimPrefix("GOFIG"))))
gofig.Must(gfg.Parse(yml, env.New(env.WithPrefix("GOFIG"))))

// Setup gofig notification channel to send notification of configuration updates
notifyCh := make(chan error, 1)
Expand Down Expand Up @@ -91,13 +91,14 @@ GoFig implements it's parsers as sub modules. Currently it supports:

# Roadmap

* [x] (PoC) Support notification of config changes via `Notifier` interface
* [x] (PoC) Implement File notifier on changes to files via `fsnotify`
* [ ] Parser Order Priority on Notify events, e.g file changes should not override env var config
* [ ] Test Suite / Code Coverage reporting
* [ ] Helpful errors
* [ ] Support pointer values
* [ ] Default Values via a struct tag, e.g: `gofig:"foo,default=bar"`
* [ ] Support `omitempty` for pointer values which should not be initialised to their zero value.
* [x] (PoC) Support notification of config changes via `Notifier` interface
* [x] (PoC) Implement File notifier on changes to files via `fsnotify`
* [ ] Add support for:
* [ ] ETCD Parser / Notifier
* [ ] Consul Parser / Notifier
Expand Down
19 changes: 14 additions & 5 deletions loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Loader struct {
debug bool
keyFormatter Formatter
structTag string
delimiter string
}

// New constructs a new Loader
Expand All @@ -50,6 +51,7 @@ func New(dst interface{}, opts ...Option) (*Loader, error) {
return key
})),
structTag: DefaultStructTag,
delimiter: ".",
}

for _, opt := range opts {
Expand Down Expand Up @@ -83,6 +85,9 @@ func (l *Loader) log() Logger {

// parse parses an single parser.
func (l *Loader) parse(p Parser) error {
// Set the delimiter
p.SetDelimeter(l.delimiter)

// Send the keys
errCh := make(chan error, 1)
keyCh := make(chan string, len(l.fields))
Expand Down Expand Up @@ -125,8 +130,8 @@ func (l *Loader) parse(p Parser) error {
field, ok := l.find(key)
if ok {
if field.Value.Kind() == reflect.Map {
key = strings.Trim(strings.Replace(key, field.Key, "", -1), ".")
if err := setMap(field, key, val); err != nil {
key = strings.Trim(strings.Replace(key, field.Key, "", -1), l.delimiter)
if err := setMap(field, key, val, l.delimiter); err != nil {
return err
}

Expand All @@ -152,7 +157,11 @@ func (l *Loader) flatten(rv reflect.Value, rt reflect.Type, key string) {
if fv.CanSet() {
tag := TagFromStructField(ft, l.structTag)

k := l.keyFormatter.Format(strings.Trim(strings.Join(append(strings.Split(key, "."), tag.Name), "."), "."))
k := l.keyFormatter.Format(
strings.Trim(
strings.Join(
append(strings.Split(key, l.delimiter), tag.Name), l.delimiter),
l.delimiter))

l.log().Printf("<Field %s kind:%s key:%s tag:%s>", ft.Name, fv.Kind(), k, tag)

Expand All @@ -172,9 +181,9 @@ func (l *Loader) find(key string) (Field, bool) {
return f, ok
}

elms := strings.Split(key, ".")
elms := strings.Split(key, l.delimiter)

key = strings.Join(elms[:len(elms)-1], ".")
key = strings.Join(elms[:len(elms)-1], l.delimiter)
if key == "" {
return f, false
}
Expand Down
46 changes: 18 additions & 28 deletions notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,43 +73,33 @@ func (l *Loader) Close() error {
return err.NilOrError()
}

// ParseNotifierFunc implements the Notifier and Parser interface.
type ParseNotifierFunc func() (Parser, Notifier)

// Keys consumes the keys but does nothing with them.
func (fn ParseNotifierFunc) Keys(c <-chan string) error {
for {
_, ok := <-c
if !ok {
return nil
}
}
// FileNotifyParser parses and watches for notifications from a notifier.
type FileNotifyParser struct {
*FileParser

notifier FileNotifier
}

// Values calls the wrapped function returning the values from the returned Parser Values method.
func (fn ParseNotifierFunc) Values() (<-chan func() (string, interface{}), error) {
p, _ := fn()
// NewFileNotifyParser constructs a new FileNotifyParser.
func NewFileNotifyParser(parser ParseReadCloser, notifier FileNotifier) *FileNotifyParser {
return &FileNotifyParser{
FileParser: NewFileParser(parser, notifier.Path()),

return p.Values()
notifier: notifier,
}
}

// Notify calls the wrapped function returning the values from the returned Notifier Notify method.
func (fn ParseNotifierFunc) Notify() <-chan error {
_, n := fn()

return n.Notify()
func (p *FileNotifyParser) Notify() <-chan error {
return p.notifier.Notify()
}

// Close calls the wrapped function returning the values from the returned Notifier Close method.
func (fn ParseNotifierFunc) Close() error {
_, n := fn()

return n.Close()
func (p *FileNotifyParser) Close() error {
return p.notifier.Close()
}

// FromFileWithNotify reads a file anf notifies of changes.
func FromFileWithNotify(parser ReaderParser, notifier FileNotifier) NotifyParser {
return ParseNotifierFunc(func() (Parser, Notifier) {
return FromFile(parser, notifier.Path()), notifier
})
// FromFileAndNotify reads a file anf notifies of changes.
func FromFileAndNotify(parser ParseReadCloser, notifier FileNotifier) NotifyParser {
return NewFileNotifyParser(parser, notifier)
}
136 changes: 96 additions & 40 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ import (
"strings"
)

// A DelimeterSetter sets the key delimeter used for flattened keys, default is .
type DelimeterSetter interface {
SetDelimeter(string)
}

// A Parser parses configuration.
type Parser interface {
DelimeterSetter

// Keys sends flattened keys (e.g foo.bar.fizz_buzz) to the parser. The Parser then can then decide, if
// it wishes to format the key and store internal mapping or not.
// This is useful for parsers like environment variables where keys such as foo.bar.fizz_buzz would need to be
// converted too FOO_BAR_FIZZ_BUZZ with a mapping to the original key.
// This allows us to maintain case sensitity in key lookups within the laoder.
// This allows us to maintain case sensitivity in key lookups within the loader.
// Most parsers such as YAML, TOML and JSON will not process these keys.
Keys(keys <-chan string) error

Expand All @@ -39,26 +46,10 @@ type Parser interface {
Values() (<-chan func() (key string, value interface{}), error)
}

// A ParserFunc is an adapter allowing regular methods to act as Parser's.
type ParserFunc func() (<-chan func() (key string, value interface{}), error)
// A ParseReadCloser parses configuration from an io.ReadCloser.
type ParseReadCloser interface {
DelimeterSetter

// Keys consumes the keys but does nothing with them.
func (fn ParserFunc) Keys(c <-chan string) error {
for {
_, ok := <-c
if !ok {
return nil
}
}
}

// Values calls the wrapped fn returning it's values.
func (fn ParserFunc) Values() (<-chan func() (string, interface{}), error) {
return fn()
}

// A ReaderParser parses configuration from an io.Reader.
type ReaderParser interface {
Values(src io.ReadCloser) (<-chan func() (key string, value interface{}), error)
}

Expand All @@ -67,6 +58,13 @@ type InMemoryParser struct {
values map[string]interface{}
}

// NewInMemoryParser constructs a new InMemoryParser.
func NewInMemoryParser() *InMemoryParser {
return &InMemoryParser{
values: make(map[string]interface{}),
}
}

// Add adds a value to the in memory values.
func (p *InMemoryParser) Add(k string, v interface{}) {
p.values[k] = v
Expand All @@ -77,6 +75,9 @@ func (p *InMemoryParser) Delete(k string) {
delete(p.values, k)
}

// SetDelimeter is a no-op.
func (p *InMemoryParser) SetDelimeter(string) {}

// Keys consumes the keys but does nothing with them.
func (p *InMemoryParser) Keys(c <-chan string) error {
for {
Expand Down Expand Up @@ -106,35 +107,90 @@ func (p *InMemoryParser) Values() (<-chan func() (string, interface{}), error) {
return ch, nil
}

// NewInMemoryParser constructs a new InMemoryParser.
func NewInMemoryParser() *InMemoryParser {
return &InMemoryParser{
values: make(map[string]interface{}),
// ReadCloseParser parses config from io.ReadCloser's.
type ReadCloseParser struct {
parser ParseReadCloser
src io.ReadCloser
}

// NewReadCloseParser constructs a new ReadCloseParser.
func NewReadCloseParser(parser ParseReadCloser, src io.ReadCloser) *ReadCloseParser {
return &ReadCloseParser{
parser: parser,
src: src,
}
}

// SetDelimeter sets the parsers delimeter.
func (p *ReadCloseParser) SetDelimeter(d string) {
p.parser.SetDelimeter(d)
}

// Keys is a no-op key consumer.
func (p *ReadCloseParser) Keys(c <-chan string) error {
for {
_, ok := <-c
if !ok {
return nil
}
}
}

// Values returns values from the parser back to gofig.
func (p *ReadCloseParser) Values() (<-chan func() (string, interface{}), error) {
return p.parser.Values(p.src)
}

// FromString parsers configuration from a string.
func FromString(parser ReaderParser, v string) Parser {
return ParserFunc(func() (<-chan func() (string, interface{}), error) {
return parser.Values(ioutil.NopCloser(strings.NewReader(v)))
})
func FromString(parser ParseReadCloser, v string) Parser {
return NewReadCloseParser(parser, ioutil.NopCloser(strings.NewReader(v)))
}

// FromBytes parsers configuration from a byte slice.
func FromBytes(parser ReaderParser, b []byte) Parser {
return ParserFunc(func() (<-chan func() (string, interface{}), error) {
return parser.Values(ioutil.NopCloser(bytes.NewReader(b)))
})
func FromBytes(parser ParseReadCloser, b []byte) Parser {
return NewReadCloseParser(parser, ioutil.NopCloser(bytes.NewReader(b)))
}

// FromFile reads a file.
func FromFile(parser ReaderParser, path string) Parser {
return ParserFunc(func() (<-chan func() (string, interface{}), error) {
f, err := os.Open(path)
if err != nil {
return nil, err
// FileParser parsers configuration from a file.
type FileParser struct {
parser ParseReadCloser
path string
}

// NewFileParser constructs a new FileParser.
func NewFileParser(parser ParseReadCloser, path string) *FileParser {
return &FileParser{
parser: parser,
path: path,
}
}

// SetDelimeter sets the parsers delimeter.
func (p *FileParser) SetDelimeter(d string) {
p.parser.SetDelimeter(d)
}

// Keys is a no-op key consumer.
func (p *FileParser) Keys(c <-chan string) error {
for {
_, ok := <-c
if !ok {
return nil
}
}
}

// Values opens the file for reading and passed it to the parser to return values back to gofig.
func (p *FileParser) Values() (<-chan func() (string, interface{}), error) {
f, err := os.Open(p.path)
if err != nil {
return nil, err
}

return parser.Values(f)
})
return p.parser.Values(f)
}

// FromFile reads a file.
func FromFile(parser ParseReadCloser, path string) Parser {
return NewFileParser(parser, path)
}
3 changes: 2 additions & 1 deletion parsers/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ package env
// Use Option methods to configure the parsers behaviour.
func New(opts ...Option) *Parser {
p := &Parser{
keys: map[string]string{},
keys: map[string]string{},
delimiter: ".",
}

for _, opt := range opts {
Expand Down
12 changes: 9 additions & 3 deletions parsers/env/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@ type Parser struct {
prefix string
suffix string

keys map[string]string
delimiter string
keys map[string]string
}

// SetDelimeter sets the key delimiter.
func (p *Parser) SetDelimeter(v string) {
p.delimiter = v
}

// Keys consumes the keys from the channel.
func (p *Parser) Keys(c <-chan string) error {
// Range over the keys we need to look for and convert to env vars formats.
// Range over the keys we need to look for and convert to env variables formats.
for key := range c {
// Break the key at the . delimiter
elms := strings.Split(key, ".")
elms := strings.Split(key, p.delimiter)

// Add prefix / suffix
elms = append([]string{p.prefix}, elms...)
Expand Down
Loading

0 comments on commit 6735fb9

Please sign in to comment.