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 support for configuration subsets #11

Open
wants to merge 1 commit into
base: master
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
7 changes: 6 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ go:
- 1.4
- 1.3

install:
before_install:
- go get github.com/tools/godep

install:
- godep restore

script:
- go test -v ./...
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ GetStringMapString(key string) : map[string]string
GetStringSlice(key string) : []string
GetTime(key string) : time.Time
IsSet(key string) : bool
NewConfigSubset(prefix string) : *confer.Config
```

### Deep Configuration Data
Expand All @@ -120,6 +121,47 @@ Because periods aren't valid environment variable characters, when using automat
LOGGER_STDOUT=/var/log/myapp go run server.go
```

### Configuration Subsets
An alternative to the `GetStringMap` / *Materialized paths* is a configuration subset. This allows you to create a new *confer.Config object which is a subset of the full configuration, specified by a key `prefix`. This makes passing around a subset of the configuration easy, allowing you to access the values using the getter methods listed above as you would normally. The setter methods are also supported on a configuration subset, as well as nested configuration subsets.

##### Example Config
```yml
---
my-app:
logging:
enabled: true
level: debug
server:
workers:
- 127.0.0.1
- 127.0.0.2
- 127.0.0.3
database:
bird:
host: feathers
user: postgres
password: superl33+
wind:
host: sea-spray
user: postgres
password: easyPZ
```

##### Config Subset

```go
windDbConfig := config.NewConfigSubset("my-app.database.wind")
host := windDbConfig.GetString("host")
```

##### Nested Config Subset

```go
dbConfig := config.NewConfigSubset("my-app.database")
windDbConfig := dbConfig.NewConfigSubset("wind")
host := windDbConfig.GetString("host")
```

### Environment Bindings


Expand Down
37 changes: 37 additions & 0 deletions confer.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type Config struct {

// The root path for configuration files.
rootPath string

// Key prefix for config subsets. The prefixes defines the root of the subset.
keyPrefix string
}

func NewConfig() *Config {
Expand All @@ -53,6 +56,7 @@ func NewConfig() *Config {
manager.attributes = NewConfigSource()
manager.env = NewEnvSource()
manager.rootPath = ""
manager.keyPrefix = ""

return manager
}
Expand Down Expand Up @@ -152,6 +156,8 @@ func (manager *Config) BindEnv(input ...string) (err error) {
// Get returns an interface..
// Must be typecast or used by something that will typecast
func (manager *Config) Get(key string) interface{} {
key = manager.GetFullKey(key)

jww.TRACE.Println("Looking for", key)

v := manager.Find(key)
Expand All @@ -178,6 +184,19 @@ func (manager *Config) Get(key string) interface{} {
return v
}

// NewConfigSubset returns a new config based on
func (manager *Config) NewConfigSubset(prefix string) *Config {
configSubset := &Config{}
*configSubset = *manager
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. You're choosing to copy the data here rather than maintaining a reference. Would it surprise you to find that your subset was unchanged after modifying a value in the parent config?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No surprise to me :-).This was intentional, as I only want to modify the subset of config passed. That's why I chose to prefix the function name with "new". I can change it to a reference, but the intent was to make a copy of the subset.


// Use GetFullKey here, as the config may already be a subset. This allows
// support of nested config subsets.
prefix = manager.GetFullKey(prefix)
configSubset.keyPrefix = prefix

return configSubset
}

// Returns true if the config key exists and is non-nil.
func (manager *Config) IsSet(key string) bool {
t := manager.Get(key)
Expand All @@ -194,13 +213,15 @@ func (manager *Config) AutomaticEnv() {

// Returns true if the key provided exists in our configuration.
func (manager *Config) InConfig(key string) bool {
key = manager.GetFullKey(key)
_, exists := manager.attributes.Get(key)
return exists
}

// Set the default value for this key.
// Default only used when no value is provided by the user via flag, config or ENV.
func (manager *Config) SetDefault(key string, value interface{}) {
key = manager.GetFullKey(key)
if !manager.IsSet(key) {
manager.attributes.Set(key, value)
}
Expand All @@ -209,6 +230,7 @@ func (manager *Config) SetDefault(key string, value interface{}) {
// Explicitly sets a value. Will not override command line arguments or
// environment variables, as those sources have higher precedence.
func (manager *Config) Set(key string, value interface{}) {
key = manager.GetFullKey(key)
manager.attributes.Set(key, value)
}

Expand Down Expand Up @@ -288,6 +310,10 @@ func (manager *Config) AllKeys() []string {
leaves := map[string]struct{}{}
for _, key := range keys {

if manager.keyPrefix != "" && !strings.HasPrefix(key, manager.keyPrefix) {
continue
}

// Filter out leaves. This is really ineffecient.
val := manager.Get(key)
if val == nil {
Expand Down Expand Up @@ -323,3 +349,14 @@ func (manager *Config) Debug() {
fmt.Println("Config file attributes:")
pretty.Println(manager.attributes)
}

// GetFullKey accounts for a key prefix (config subset) and returns the full key.
func (manager *Config) GetFullKey(key string) string {
if manager.keyPrefix != "" {
jww.TRACE.Println(key, "Using key prefix in operation: ", manager.keyPrefix)

key = manager.keyPrefix + "." + key
}

return key
}
49 changes: 49 additions & 0 deletions confer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,55 @@ func TestSpec(t *testing.T) {
})
})

Convey("Config Subset", func() {
config.ReadPaths("test/fixtures/application.yaml")
keyPrefix := "app.database"
configSubset := config.NewConfigSubset(keyPrefix)

Convey("Returning a config", func() {
So(configSubset, ShouldHaveSameTypeAs, NewConfig())
})

Convey("Subset gets", func() {
So(configSubset.GetString("host"), ShouldEqual, "localhost")
So(configSubset.GetString("user"), ShouldEqual, "postgres")
So(configSubset.InConfig("password"), ShouldEqual, true)
})

Convey("Subset sets", func() {
configSubset.SetDefault("port", 5432)
configSubset.Set("ssl.mode", "disable")
configSubset.Set("max_open_conns", 30)

So(configSubset.Get("ssl.mode"), ShouldEqual, "disable")
So(configSubset.Get("max_open_conns"), ShouldEqual, 30)
So(configSubset.IsSet("port"), ShouldEqual, true)
So(configSubset.Get("port"), ShouldEqual, 5432)

// Keys for config subset should be (8 in total): [
// app.database app.database.host app.database.user app.database.password
// app.database.port app.database.ssl app.database.ssl.mode
// app.database.max_open_conns
// ]
So(len(configSubset.AllKeys()), ShouldEqual, 8)
})

Convey("Nested Subsets", func() {
configSubset.Set("ssl.mode", "verify-full")
configSubsetNested := configSubset.NewConfigSubset("ssl")
configSubsetNested.SetDefault("connect_timeout", 0)

So(configSubsetNested.IsSet("mode"), ShouldEqual, true)
So(configSubsetNested.GetString("mode"), ShouldEqual, "verify-full")
So(configSubsetNested.InConfig("connect_timeout"), ShouldEqual, true)

// Keys for nested config subset should be (3 in total): [
// app.database.ssl app.database.ssl.mode app.database.ssl.connect_timeout
// ]
So(len(configSubsetNested.AllKeys()), ShouldEqual, 3)
})
})

Convey("Helpers", func() {
Convey("Returning an integer", func() {
config.Set("port", func() interface{} {
Expand Down