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

Add flag sourcer #3

Merged
merged 4 commits into from
Apr 1, 2020
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ A sourcer reads values from a particular source based on a configuration struct'
<dt>Test Environment Sourcer</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewTestEnvSourcer">test environment sourcer</a> reads the <code>env</code> tag but looks up the corresponding value from a literal map. This sourcer is meant to be used in unit tests where the full construction of a nacelle [process](https://nacelle.dev/docs/core/process) is beneficial.</dd>

<dt>Flag Sourcer</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewFlagSourcer">flag sourcer</a> reads the <code>flag</code> tag and looks up the corresponding value attached to the process's command line arguments.</dd>

<dt>File Sourcer</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewFileSourcer">file sourcer</a> reads the <code>file</code> tag and returns the value at the given path. A filename and a file parser musts be supplied on instantiation. Both <a href="https://godoc.org/github.com/go-nacelle/config#ParseYAML">ParseYAML</a> and <a href="https://godoc.org/github.com/go-nacelle/config#ParseTOML">ParseTOML</a> are supplied file parsers -- note that as JSON is a subset of YAML, <code>ParseYAML</code> will also correctly parse JSON files. If a <code>nil</code> file parser is supplied, one is chosen by the filename extension. A file sourcer will load the file tag <code>api.timeout</code> from the given file by parsing it into a map of values and recursively walking the (keys separated by dots). This can return a primitive type or a structured map, as long as the target field has a compatible type. The constructor <a href="https://godoc.org/github.com/go-nacelle/config#NewOptionalFileSourcer">NewOptionalFileSourcer</a> will return a no-op sourcer if the filename does not exist.</dd>

Expand All @@ -117,12 +120,18 @@ A tag modifier dynamically alters the tags of a configuration struct. The follow
<dt>Display Tag Setter</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewDisplayTagSetter">display tag setter</a> sets the <code>display</code> tag to the value of the <code>env</code> tag. This tag modifier can be used to provide sane defaults to the tag without doubling the length of the struct tag definition.</dd>

<dt>Flag Tag Setter</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewFlagTagSetter">flag tag setter</a> sets the <code>flag</code> tag to the value of the <code>env</code> tag. This tag modifier can be used to provide sane defaults to the tag without doubling the length of the struct tag definition.</dd>

<dt>File Tag Setter</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewFileTagSetter">file tag setter</a> sets the <code>file</code> tag to the value of the <code>env</code> tag. This tag modifier can be used to provide sane defaults to the tag without doubling the length of the struct tag definition.</dd>

<dt>Env Tag Prefixer</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewEnvTagPrefixer">environment tag prefixer</a> inserts a prefix on each <code>env</code> tags. This is useful when two distinct instances of the same configuration are required, and each one should be configured independently from the other (for example, using the same abstraction to consume from two different event busses with the same consumer code).</dd>

<dt>Flag Tag Prefixer</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewFlagTagPrefixer">flag tag prefixer</a> inserts a prefix on each <code>flag</code> tag. This effectively looks in a distinct top-level namespace in the parsed configuration. This is similar to the env tag prefixer.</dd>

<dt>File Tag Prefixer</dt>
<dd>A <a href="https://godoc.org/github.com/go-nacelle/config#NewFileTagPrefixer">file tag prefixer</a> inserts a prefix on each <code>file</code> tag. This effectively looks in a distinct top-level namespace in the parsed configuration. This is similar to the env tag prefixer.</dd>
</dl>
Expand Down
4 changes: 2 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type (
// is used by the logging package to show the content of the
// environment and config files when a value is missing or otherwise
// illegal.
Dump() map[string]string
Dump() (map[string]string, error)
}

// PostLoadConfig is a marker interface for configuration objects
Expand Down Expand Up @@ -89,7 +89,7 @@ func (c *config) Assets() []string {
return c.sourcer.Assets()
}

func (c *config) Dump() map[string]string {
func (c *config) Dump() (map[string]string, error) {
return c.sourcer.Dump()
}

Expand Down
41 changes: 22 additions & 19 deletions config_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const (
DefaultTag = "default"
RequiredTag = "required"
EnvTag = "env"
FlagTag = "flag"
FileTag = "file"
DisplayTag = "display"
MaskTag = "mask"
Expand Down
6 changes: 4 additions & 2 deletions env_sourcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type envSourcer struct {
prefix string
}

var _ Sourcer = &envSourcer{}

var replacePattern = regexp.MustCompile(`[^A-Za-z0-9_]+`)

// NewEnvSourcer creates a Sourcer that pulls values from the environment.
Expand Down Expand Up @@ -58,13 +60,13 @@ func (s *envSourcer) Assets() []string {
return []string{"<os environment>"}
}

func (s *envSourcer) Dump() map[string]string {
func (s *envSourcer) Dump() (map[string]string, error) {
values := map[string]string{}
for _, name := range getNames() {
values[name] = os.Getenv(name)
}

return values
return values, nil
}

func getNames() []string {
Expand Down
6 changes: 4 additions & 2 deletions env_sourcer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ func (s *EnvSourcerSuite) TestDump(t sweet.T) {
os.Setenv("X", "foo")
os.Setenv("Y", "123")

Expect(NewEnvSourcer("app").Dump()["X"]).To(Equal("foo"))
Expect(NewEnvSourcer("app").Dump()["Y"]).To(Equal("123"))
dump, err := NewEnvSourcer("app").Dump()
Expect(err).To(BeNil())
Expect(dump["X"]).To(Equal("foo"))
Expect(dump["Y"]).To(Equal("123"))
}
6 changes: 4 additions & 2 deletions error_sourcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ type errorSourcer struct {
err error
}

var _ Sourcer = &errorSourcer{}

func newErrorSourcer(err error) Sourcer {
return &errorSourcer{err}
}
Expand All @@ -20,6 +22,6 @@ func (s *errorSourcer) Assets() []string {
return nil
}

func (s *errorSourcer) Dump() map[string]string {
return nil
func (s *errorSourcer) Dump() (map[string]string, error) {
return nil, s.err
}
6 changes: 4 additions & 2 deletions file_sourcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type (
FileParser func(content []byte) (map[string]interface{}, error)
)

var _ Sourcer = &fileSourcer{}

var parserMap = map[string]FileParser{
".yaml": ParseYAML,
".yml": ParseYAML,
Expand Down Expand Up @@ -91,8 +93,8 @@ func (s *fileSourcer) Assets() []string {
return []string{s.filename}
}

func (s *fileSourcer) Dump() map[string]string {
return s.values
func (s *fileSourcer) Dump() (map[string]string, error) {
return s.values, nil
}

//
Expand Down
103 changes: 103 additions & 0 deletions flag_sourcer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package config

import (
"fmt"
"strings"
)

type flagSourcer struct {
args []string
parsed map[string]string
}

var _ Sourcer = &flagSourcer{}

// NewFlagSourcer creates a Sourcer that pulls values from the application
// flags.
func NewFlagSourcer(configs ...FlagSourcerConfigFunc) Sourcer {
return &flagSourcer{args: getFlagSourcerConfigOptions(configs).args}
}

func (s *flagSourcer) Tags() []string {
return []string{FlagTag}
}

func (s *flagSourcer) Get(values []string) (string, SourcerFlag, error) {
if values[0] == "" {
return "", FlagSkip, nil
}

if err := s.ensureParsed(); err != nil {
return "", 0, err
}

if val, ok := s.parsed[values[0]]; ok {
return val, FlagFound, nil
}

return "", FlagMissing, nil
}

func (s *flagSourcer) Assets() []string {
return []string{"<application flags>"}
}

func (s *flagSourcer) Dump() (map[string]string, error) {
if err := s.ensureParsed(); err != nil {
return nil, err
}

values := map[string]string{}
for name, value := range s.parsed {
values[name] = value
}

return values, nil
}

func (s *flagSourcer) ensureParsed() error {
if s.parsed != nil {
return nil
}
s.parsed = map[string]string{}

for len(s.args) > 0 {
name, value, err := s.parseOne()
if err != nil {
return err
}

s.parsed[name] = value
}

return nil
}

func (s *flagSourcer) parseOne() (string, string, error) {
arg := s.args[0]
s.args = s.args[1:]

numMinuses := 0
for i := 0; i < len(arg) && arg[i] == '-'; i++ {
numMinuses++
}

if len(arg) == 0 || numMinuses == 0 || numMinuses > 2 || (len(arg) > numMinuses && arg[numMinuses] == '=') {
return "", "", fmt.Errorf("illegal flag: %s", arg)
}

name := arg[numMinuses:]
value := ""

if parts := strings.SplitN(name, "=", 2); len(parts) == 2 {
name, value = parts[0], parts[1]
} else {
if len(s.args) > 0 {
value, s.args = s.args[0], s.args[1:]
} else {
return "", "", fmt.Errorf("flag needs an argument: -%s", name)
}
}

return name, value, nil
}
30 changes: 30 additions & 0 deletions flag_sourcer_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package config

import "os"

type (
flagSourcerOptions struct {
args []string
}

// FlagSourcerConfigFunc is a function used to configure instances of
// flag sourcers.
FlagSourcerConfigFunc func(*flagSourcerOptions)
)

// WithFlagSourcerArgs sets raw command line arguments.
func WithFlagSourcerArgs(args []string) FlagSourcerConfigFunc {
return func(o *flagSourcerOptions) { o.args = args }
}

func getFlagSourcerConfigOptions(configs []FlagSourcerConfigFunc) *flagSourcerOptions {
options := &flagSourcerOptions{
args: os.Args[1:],
}

for _, f := range configs {
f(options)
}

return options
}
Loading