├── LICENSE ├── README.md ├── cached_loader.go ├── cached_loader_test.go ├── cli.go ├── cli_test.go ├── config.go ├── config_test.go ├── environment.go ├── environment_test.go ├── ini.go ├── ini_test.go ├── json.go ├── json_test.go ├── once_loader.go ├── once_loader_test.go ├── provider.go ├── resolver.go ├── resolver_test.go ├── static.go ├── test ├── config.ini ├── config.json ├── config.toml └── config.yaml ├── toml.go ├── toml_test.go ├── yaml.go └── yaml_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015, Zack Patrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Config 2 | The `go-config` package is used to simplify configuration for go applications. 3 | 4 | # Installation 5 | To install this package, run: 6 | ``` 7 | go get github.com/ridgelines/go-config 8 | ``` 9 | 10 | # The Basics 11 | The `go-config` package has three main components: **providers**, **settings**, and the **config** object. 12 | * **Providers** load settings for your application. This could be a file, environment variables, or some other means of configuration. 13 | * **Settings** represent the configuration options for your application. Settings are represented as key/value pairs. 14 | * **Config** holds all of the providers in your application. This object holds convenience functions for loading, retrieving, and converting your settings. 15 | 16 | # Built In Providers 17 | * `INIFile` - Loads settings from a `.ini` file 18 | * `JSONFile` - Loads settings from a `.json` file 19 | * `YAMLFile` - Loads settings from a `.yaml` file 20 | * `TOMLFile` - Loads settings from a `.toml` file 21 | * `CLI` - Loads settings from a [urfave/cli](https://github.com/urfave/cli) context 22 | * `Environment` - Loads settings from environment variables 23 | * `Static` - Loads settings from an in-memory map 24 | 25 | ## Single Provider Example 26 | Most application use a single file for configuration. 27 | In this example, the application uses the file `config.ini` with the following contents: 28 | ``` 29 | [global] 30 | timeout=30 31 | frequency=0.5 32 | 33 | [local] 34 | time_zone=PST 35 | enabled=true 36 | ``` 37 | First, create a **Provider** that will be responsible for loading configuration values from the `config.ini` file. 38 | Since `config.ini` is in `ini` format, use the `INIFile` provider: 39 | ``` 40 | iniFile := config.NewINIFile("config.ini") 41 | ``` 42 | 43 | Next, create a **Config** object to manage the application's providers. 44 | Since this application only has one provider, `iniFile`, pass a list with only one object into the constructor. 45 | ``` 46 | c := config.NewConfig([]config.Provider{iniFile}) 47 | ``` 48 | 49 | It is a good idea to call the `Load()` function on your config object after creating it. 50 | ``` 51 | if err := c.Load(); err != nil{ 52 | log.Fatal(err) 53 | } 54 | ``` 55 | 56 | However, calling the `Load()` function is not required. 57 | Each time a setting is requested, the `Load()` function is called first. 58 | If you are concerned about performance of calling `Load()` so frequently, see the [Advanced](#advanced) section. 59 | 60 | The `Config` object can be used to lookup settings from your providers. 61 | When performing a lookup on a setting, the key is a period-delimited string. 62 | For `ini` files, this means lookups are performed with `
.` keys. 63 | For example, the settings in `config.json` can be looked up with the following keys: 64 | ``` 65 | timeout, err := c.Int("global.timeout") 66 | ... 67 | frequency, err := c.Float("global.frequency") 68 | ... 69 | tz, err := c.String("local.time_zone") 70 | ... 71 | enabled, err := c.Bool("local.enabled") 72 | ... 73 | ``` 74 | All of the settings in your application are stored as strings. 75 | As shown above, the `Config` object has convenience functions for type conversions: `Int(), Float(), Bool()`. 76 | 77 | ## Validation 78 | The `Config` object has an optional `Validate` function that is called after the settings are loaded from the providers. 79 | Taking the `config.ini` example from above, create a `Validate` function that makes sure the `global.timeout` setting exists: 80 | ``` 81 | iniFile := config.NewINIFile("config.ini") 82 | c := config.NewConfig([]config.Provider{iniFile}) 83 | 84 | c.Validate = func(settings map[string]string) error { 85 | if val, ok := settings["global.timeout"]; !ok { 86 | return fmt.Errorf("Required setting 'global.timeout' not set!") 87 | } 88 | } 89 | ``` 90 | 91 | ## Optional Settings 92 | Looking up settings using the `String()`, `Int()`, `Float()`, or `Bool()` functions will error if the setting does not exist. 93 | The functions `StringOr()`, `IntOr()`, `FloatOr()`, and `BoolOr()` can be used to provide a default if the setting does not exist: 94 | ``` 95 | timeout, err := c.IntOr("global.timeout", 30) 96 | ... 97 | frequency, err := c.FloatOr("global.frequency", 0.5) 98 | ... 99 | tz, err := c.StringOr("local.time_zone", "PST") 100 | ... 101 | enabled, err := c.BoolOr("local.enabled", true) 102 | ... 103 | ``` 104 | 105 | ## Get All Settings 106 | All of the setting can be retrieved using the `Settings()` function: 107 | ``` 108 | settings, err := c.Settings() 109 | if err != nil{ 110 | return err 111 | } 112 | 113 | for key, val := range settings { 114 | fmt.Printf("%s = %s \n", key, val) 115 | } 116 | ``` 117 | 118 | ## Multiple Providers Example 119 | Many applications have more than one means of configuration. 120 | Building off of the [Single Provider Example](#single-provider-example) above, this example will add environment variables as a means of configuration in addition to the `config.ini` file. 121 | If they are set, the environment variables will override settings in `config.go`. 122 | 123 | As before, create a provider that will be responsible for loading configuration values from the `config.ini` file: 124 | ``` 125 | iniFile := config.NewINIFile("config.ini") 126 | ``` 127 | 128 | Next, create an `Environment` provider that will be responsible for loading configuration values from environment variables. 129 | The `Environment` provider takes a map that associates setting keys with environment variables. 130 | Since the environment variables should override the same settings keys as `config.ini`, construct the map like so: 131 | ``` 132 | mappings := map[string]string{ 133 | "APP_TIMEOUT": "global.timeout", 134 | "APP_FREQUENCY": "global.frequency", 135 | "APP_TIMEZONE": "local.time_zone", 136 | "APP_ENABLED": "local.enabled", 137 | } 138 | 139 | env := config.NewEnvironment(mappings) 140 | ``` 141 | Since there are two providers, add both of them to the `Config` object. 142 | The position of the provider in the list determines the ordering of settings lookups. 143 | Since environment variables should override the values in `config.ini`, put the `Environment` provider later in the list: 144 | ``` 145 | providers := []config.Providers{iniFile, env} 146 | c := config.NewConfig(providers) 147 | ``` 148 | 149 | ## CLI Provider Example 150 | In addition to files and environment variables, applications tend to use command line arguments for configuration. 151 | One of the most popular command line tool for golang is [urfave/cli](https://github.com/urfave/cli). 152 | This tool does an excellent job of allowing users to configure their applications using 153 | [flags](https://github.com/ 154 | 155 | angsta/cli#flags) and [environment variables](https://github.com/urfave/cli#values-from-the-environment). 156 | This works great for many applications, but can easily become messy when settings need to be loaded from other sources. 157 | The `CLI` provider aims to make configuration management from any number of providers as simple as possible. 158 | 159 | 160 | For the following example, assume the application includes a configuration file, `config.yaml`, and `main.go` with the following content: 161 | 162 | ### Config.yaml 163 | ``` 164 | message: "Hello from config.yaml" 165 | silent: false 166 | ``` 167 | 168 | ### Main.go 169 | ``` 170 | package main 171 | 172 | import ( 173 | "github.com/ 174 | "githbu.com/urfave/cli" 175 | "github.com/ridgelines 176 | 177 | /go-config" 178 | "log" 179 | "os" 180 | ) 181 | 182 | func initConfig() *config.Config { 183 | yamlFile := config.NewYAMLFile("config.yaml") 184 | return config.NewConfig([]config.Provider{yamlFile}) 185 | } 186 | 187 | func main() { 188 | conf := initConfig() 189 | 190 | app := cli.NewApp() 191 | app.Flags = []cli.Flag{ 192 | cli.StringFlag{ 193 | Name: "message", 194 | Value: "Hello from main.go", 195 | Usage: "Message to print", 196 | }, 197 | cli.BoolFlag{ 198 | Name: "silent", 199 | Usage: "Don't print the message", 200 | }, 201 | } 202 | 203 | app.Action = func(c *cli.Context) { 204 | conf.Providers = append(conf.Providers, config.NewCLI(c, false)) 205 | 206 | message, err := conf.String("message") 207 | if err != nil { 208 | log.Fatal(err) 209 | } 210 | 211 | silent, err := conf.Bool("silent") 212 | if err != nil { 213 | log.Fatal(err) 214 | } 215 | 216 | if !silent { 217 | log.Println(message) 218 | } 219 | } 220 | 221 | app.Run(os.Args) 222 | } 223 | ``` 224 | 225 | ### Creating the YAML Provider 226 | The following lines create the `YAML` provider and the `Config` object 227 | ``` 228 | yamlFile := config.NewYAMLFile("config.yaml") 229 | return config.NewConfig([]config.Provider{yamlFile}) 230 | ``` 231 | Since this application uses `config.yaml` for configuration, the `YAMLFile` is used to load settings from that file. 232 | This will load the settings `message="Hello from config.yaml"` and `silent=false`. 233 | 234 | ### Creating the CLI Provider 235 | In the `app.Action` function, a new `CLI` provider is created to load settings. The `CLI` provider takes a `*cli.Context` and boolean argument `useDefaults`. Having default values for flags is useful, but unlike other flags, boolean flags always have a default value. Since this could lead to unwanted or unexpected behavior, users must specify which setting to use: 236 | * If `useDefaults=true`, flags with default values will be loaded. 237 | In context of this example, the `CLI` provider will load the settings `message="Hello from main.go"` and `silent=false` when the user runs the application without any arguments. 238 | These settings would overwrite the `message` and `silent` settings loaded from `config.yaml`. 239 | * If `useDefaults=false`, only flags that have been set via the command line will be loaded as settings. 240 | In context of this example, the `CLI` provider will not load any settings when the user runs the application without any arguments. 241 | This allows the setting loaded by `config.yaml` to not be overwitten. 242 | 243 | The following line creates the `CLI` provider and appends it to the existing providers: 244 | ``` 245 | conf.Providers = append(conf.Providers, config.NewCLI(c, false)) 246 | ``` 247 | Note that `useDefaults=false`. This way, settings in `config.yaml` aren't overwitten by the default flag values. 248 | 249 | ### Running the Application 250 | **No Arguments**: The `message` and `silent` settings from `config.yaml` are used 251 | ``` 252 | > go run main.go 253 | Hello from config.yaml 254 | ``` 255 | **Message Flag**: The `message` setting from the `CLI` provider is used. The `silent` setting from `config.yaml` is used 256 | ``` 257 | > go run main.go --message "Hello from the command line" 258 | Hello from the command line 259 | ``` 260 | **Silent and Message Flag**: The `message` and `silent` settings from the `CLI` provider are used 261 | ``` 262 | > go run main.go --message "this shouldn't print" --silent 263 | 264 | ``` 265 | 266 | **No Arguments with `useDefaults=True`**: To further demonstrate the different behavior of `useDefault`, here is the output when `useDefault=true`. The default `message` and `silent` settings from the `CLI` are used: 267 | ``` 268 | > go run main.go 269 | Hello from main.go 270 | ``` 271 | 272 | # Advanced 273 | 274 | ## Defaults 275 | Managing default values for settings can be accomplished multiple ways: 276 | * A configuration file that contains all of the defaults 277 | * Using `Or` functions with defaults (e.g. `StringOr(...)`, `IntOr(...)`, etc.) 278 | * Using the `Static` provider 279 | 280 | The `Static` provider takes key value mappings for settings and simply returns those values when `Load()` is called. 281 | This is a nice pattern as it doesn't require additional configuration files and it places all defaults into a single place in your code. 282 | Make sure to set your defaults as the first provider in your application so they can be overridden by other providers: 283 | ``` 284 | mappings := map[string]string{ 285 | "global.timeout": "30", 286 | "global.enabled": "true", 287 | } 288 | 289 | defaults := config.NewStatic(mappings) 290 | iniFile := config.NewINIFile("config.ini") 291 | 292 | providers := []config.Provider{defaults, iniFile} 293 | c := config.NewConfig(providers) 294 | ``` 295 | 296 | ## Loading Patterns 297 | Each time a lookup is performed, the `Load()` function is called on each provider. 298 | This can lead to poor performance and be unecessary for certain providers. 299 | There are two built in objects which change how frequently loads are performed: 300 | * `OnceLoader` - Loads the provider's settings one time 301 | * `CachedLoader` - Loads the provider's settings at least one time and caches the results. 302 | The `Invalidate()` function can be called to force a new load next time a lookup is performed. 303 | 304 | Building off of the [Multiple Providers Example](#multiple-providers-example) above, 305 | use the `OnceLoader` for the `iniFile` provider and keep the default behavior for the `Environment` provider (perform a load each time a lookup is requested): 306 | ``` 307 | env := config.NewEnvironment(...) 308 | iniFile := config.NewINIFile("config.ini") 309 | iniFileLoader := config.NewOnceLoader(iniFile) 310 | 311 | providers := []config.Provider{iniFileLoader, env} 312 | c := config.NewConfig(providers) 313 | ``` 314 | The first time a lookup is performed, the provider's `Load()` function will be called. 315 | All other calls will use the same settings as the original lookup. 316 | 317 | The `CachedLoader` behaves in a similar manner except that it contains an `Invalidate()` function. 318 | After `Invalidate()` is called, the provider's `Load()` function will be executed the next time a lookup is performed. 319 | ``` 320 | env := config.NewEnvironment(...) 321 | iniFile := config.NewINIFile("config.ini") 322 | iniFileLoader := config.NewCachedLoader(iniFile) 323 | 324 | providers := []config.Provider{iniFileLoader, env} 325 | c := config.NewConfig(providers) 326 | 327 | ... 328 | // will execute iniFile.Load() next time a lookup is performed 329 | iniFileLoader.Invalidate() 330 | ... 331 | ``` 332 | 333 | ## Resolvers 334 | Sometimes, keeping setting keys consistent between different files isn't possible. 335 | For example, say the file `config.json` contained: 336 | ``` 337 | { 338 | "items": { 339 | "server": { 340 | "timeout": 30 341 | } 342 | } 343 | } 344 | ``` 345 | And another file `config.ini` contained: 346 | ``` 347 | [server] 348 | timeout=30 349 | ``` 350 | 351 | The `timeout` setting would have the key `items.server.timeout` for the `json` file and `server.timeout` for the `ini` file when they are actually intended to reference the same setting. 352 | A `Resolver` can be used to change the mappings in a provider. 353 | For example, wrap the `json` provider in a `Resolver` in order to resolve `items.server.timeout` as `server.timeout`: 354 | ``` 355 | iniFile := config.NewINIFile("config.ini") 356 | jsonFile := config.NewJSONFile("config.json") 357 | mappings := map[string]string{ 358 | "items.server.timeout": "server.timeout", 359 | } 360 | 361 | JSONFileResolver := config.NewResolver(jsonFile, mappings) 362 | 363 | providers := []config.Provider{iniFile, jsonFileResolver} 364 | c := config.NewConfig(providers) 365 | ``` 366 | 367 | The canonical key for the setting is now `server.timeout` 368 | 369 | ## Custom Providers 370 | Custom providers must fulfill the `Provider` interface: 371 | ``` 372 | type Provider interface { 373 | Load() (map[string]string, error) 374 | } 375 | ``` 376 | 377 | The `Load()` function returns settings as key/value pairs. 378 | Providers flatten namespaces using period-delimited strings. 379 | For example, the following providers and content: 380 | 381 | INI File: 382 | ``` 383 | [global] 384 | timeout = 30 385 | ``` 386 | YAML File: 387 | ``` 388 | global: 389 | timeout: 30 390 | ``` 391 | 392 | JSON File: 393 | ``` 394 | { 395 | "global": { 396 | "timeout": 30 397 | } 398 | } 399 | ``` 400 | 401 | All resolve the `global.timeout` setting to `30`. 402 | This is allows providers to override and lookup settings using a canonical key. 403 | 404 | 405 | # License 406 | This work is published under the MIT license. 407 | 408 | Please see the `LICENSE` file for details. 409 | -------------------------------------------------------------------------------- /cached_loader.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type CachedLoader struct { 4 | invalidated bool 5 | provider Provider 6 | settings map[string]string 7 | } 8 | 9 | func NewCachedLoader(provider Provider) *CachedLoader { 10 | return &CachedLoader{ 11 | invalidated: true, 12 | provider: provider, 13 | settings: map[string]string{}, 14 | } 15 | } 16 | 17 | func (this *CachedLoader) Load() (map[string]string, error) { 18 | if this.invalidated { 19 | settings, err := this.provider.Load() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | this.settings = settings 25 | this.invalidated = false 26 | } 27 | 28 | settings := map[string]string{} 29 | 30 | for key, value := range this.settings { 31 | settings[key] = value 32 | } 33 | 34 | return settings, nil 35 | } 36 | 37 | func (this *CachedLoader) Invalidate() { 38 | this.invalidated = true 39 | } 40 | -------------------------------------------------------------------------------- /cached_loader_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCachedLoader(t *testing.T) { 8 | static := NewStatic(map[string]string{"enabled": "true"}) 9 | loader := NewCachedLoader(static) 10 | 11 | settings, err := loader.Load() 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | if settings["enabled"] != "true" { 17 | t.Errorf("Enabled was '%s', expected 'true'", settings["enabled"]) 18 | } 19 | 20 | static.Set("enabled", "false") 21 | 22 | settings, err = loader.Load() 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | if settings["enabled"] != "true" { 28 | t.Errorf("Enabled was '%s', expected 'true'", settings["enabled"]) 29 | } 30 | 31 | loader.Invalidate() 32 | static.Set("enabled", "false") 33 | 34 | settings, err = loader.Load() 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if settings["enabled"] != "false" { 40 | t.Errorf("Enabled was '%s', expected 'false'", settings["enabled"]) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli" 7 | ) 8 | 9 | type CLI struct { 10 | useDefaults bool 11 | context *cli.Context 12 | } 13 | 14 | func NewCLI(context *cli.Context, useDefaults bool) *CLI { 15 | return &CLI{ 16 | useDefaults: useDefaults, 17 | context: context, 18 | } 19 | } 20 | 21 | func (this *CLI) Load() (map[string]string, error) { 22 | settings := map[string]string{} 23 | 24 | for _, flag := range this.context.FlagNames() { 25 | val := fmt.Sprintf("%v", this.context.Generic(flag)) 26 | 27 | if this.context.IsSet(flag) { 28 | settings[flag] = val 29 | } else if this.useDefaults && val != "" { 30 | settings[flag] = val 31 | } 32 | } 33 | 34 | return settings, nil 35 | } 36 | -------------------------------------------------------------------------------- /cli_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/urfave/cli" 7 | ) 8 | 9 | func TestCLILoad(t *testing.T) { 10 | var executed bool 11 | 12 | app := cli.NewApp() 13 | app.Flags = []cli.Flag{ 14 | &cli.IntFlag{ 15 | Name: "timeout", 16 | }, 17 | &cli.Float64Flag{ 18 | Name: "frequency", 19 | }, 20 | &cli.StringFlag{ 21 | Name: "time_zone", 22 | }, 23 | &cli.BoolFlag{ 24 | Name: "enabled", 25 | }, 26 | } 27 | 28 | app.Action = func(c *cli.Context) error { 29 | executed = true 30 | cliProvider := NewCLI(c, false) 31 | 32 | expectedSettings := map[string]string{ 33 | "timeout": "30", 34 | "frequency": "0.5", 35 | "time_zone": "PST", 36 | "enabled": "true", 37 | } 38 | 39 | actualSettings, err := cliProvider.Load() 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | for key, expected := range expectedSettings { 45 | actual, ok := actualSettings[key] 46 | 47 | if !ok { 48 | t.Errorf("Key '%s' not in settings", key) 49 | } 50 | 51 | if actual != expected { 52 | t.Errorf("Setting '%s' was '%s', expected '%s'", key, actual, expected) 53 | } 54 | } 55 | return err 56 | } 57 | 58 | app.Run( 59 | []string{ 60 | "main.go", 61 | "--timeout", 62 | "30", 63 | "--frequency", 64 | "0.5", 65 | "--time_zone", 66 | "PST", 67 | "--enabled", 68 | }, 69 | ) 70 | 71 | if !executed { 72 | t.Fail() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | ) 8 | 9 | type Config struct { 10 | sync.RWMutex 11 | Providers []Provider 12 | Validate func(map[string]string) error 13 | settings map[string]string 14 | } 15 | 16 | func NewConfig(providers []Provider) *Config { 17 | return &Config{ 18 | Providers: providers, 19 | settings: map[string]string{}, 20 | } 21 | } 22 | 23 | func (this *Config) Load() error { 24 | this.Lock() 25 | defer this.Unlock() 26 | this.settings = map[string]string{} 27 | for _, provider := range this.Providers { 28 | settings, err := provider.Load() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | for key, val := range settings { 34 | this.settings[key] = val 35 | } 36 | } 37 | 38 | if this.Validate != nil { 39 | if err := this.Validate(this.settings); err != nil { 40 | return err 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (this *Config) lookup(key string) (string, error) { 48 | if err := this.Load(); err != nil { 49 | return "", err 50 | } 51 | this.Lock() 52 | val, ok := this.settings[key] 53 | this.Unlock() 54 | if ok { 55 | return val, nil 56 | } 57 | 58 | return "", nil 59 | } 60 | 61 | func (this *Config) String(key string) (string, error) { 62 | val, err := this.lookup(key) 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | if val == "" { 68 | return "", fmt.Errorf("Required setting '%s' not set", key) 69 | } 70 | 71 | return val, nil 72 | } 73 | 74 | func (this *Config) StringOr(key, alt string) (string, error) { 75 | val, err := this.lookup(key) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | if val == "" { 81 | return alt, nil 82 | } 83 | 84 | return val, nil 85 | } 86 | 87 | func (this *Config) Int(key string) (int, error) { 88 | val, err := this.lookup(key) 89 | if err != nil { 90 | return 0, err 91 | } 92 | 93 | if val == "" { 94 | return 0, fmt.Errorf("Required setting '%s' not set", key) 95 | } 96 | 97 | return strconv.Atoi(val) 98 | } 99 | 100 | func (this *Config) IntOr(key string, alt int) (int, error) { 101 | val, err := this.lookup(key) 102 | if err != nil { 103 | return 0, err 104 | } 105 | 106 | if val == "" { 107 | return alt, nil 108 | } 109 | 110 | return strconv.Atoi(val) 111 | } 112 | 113 | func (this *Config) Float(key string) (float64, error) { 114 | val, err := this.lookup(key) 115 | if err != nil { 116 | return 0, err 117 | } 118 | 119 | if val == "" { 120 | return 0, fmt.Errorf("Required setting '%s' not set", key) 121 | } 122 | 123 | return strconv.ParseFloat(val, 64) 124 | } 125 | 126 | func (this *Config) FloatOr(key string, alt float64) (float64, error) { 127 | val, err := this.lookup(key) 128 | if err != nil { 129 | return 0, err 130 | } 131 | 132 | if val == "" { 133 | return alt, nil 134 | } 135 | 136 | return strconv.ParseFloat(val, 64) 137 | } 138 | 139 | func (this *Config) Bool(key string) (bool, error) { 140 | val, err := this.lookup(key) 141 | if err != nil { 142 | return false, err 143 | } 144 | 145 | if val == "" { 146 | return false, fmt.Errorf("Required setting '%s' not set", key) 147 | } 148 | 149 | return strconv.ParseBool(val) 150 | } 151 | 152 | func (this *Config) BoolOr(key string, alt bool) (bool, error) { 153 | val, err := this.lookup(key) 154 | if err != nil { 155 | return false, err 156 | } 157 | 158 | if val == "" { 159 | return alt, nil 160 | } 161 | 162 | return strconv.ParseBool(val) 163 | } 164 | 165 | func (this *Config) Settings() (map[string]string, error) { 166 | if err := this.Load(); err != nil { 167 | return nil, err 168 | } 169 | 170 | return this.settings, nil 171 | } 172 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestPrecedence(t *testing.T) { 9 | low := map[string]string{ 10 | "without_override": "false", 11 | "with_override": "false", 12 | } 13 | 14 | high := map[string]string{ 15 | "with_override": "true", 16 | } 17 | 18 | c := NewConfig( 19 | []Provider{ 20 | NewStatic(low), 21 | NewStatic(high), 22 | }, 23 | ) 24 | 25 | without, err := c.Bool("without_override") 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | 30 | if without == true { 31 | t.Errorf("Setting 'without_override' was true, expected false") 32 | } 33 | 34 | with, err := c.Bool("with_override") 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if with == false { 40 | t.Errorf("Setting 'with_override' was 'false', expected 'true'") 41 | } 42 | } 43 | 44 | func TestTypeLookups(t *testing.T) { 45 | settings := map[string]string{ 46 | "string": "some_string", 47 | "bool": "true", 48 | "int": "1", 49 | "float": "1.5", 50 | } 51 | 52 | c := NewConfig([]Provider{NewStatic(settings)}) 53 | 54 | s, err := c.String("string") 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | 59 | if s != "some_string" { 60 | t.Errorf("String setting was '%s', expected 'some_string'", s) 61 | } 62 | 63 | b, err := c.Bool("bool") 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | if b != true { 69 | t.Errorf("Bool setting was 'false', expected 'true'") 70 | } 71 | 72 | i, err := c.Int("int") 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | 77 | if i != 1 { 78 | t.Errorf("Int setting was '%d', expected '1'", i) 79 | } 80 | 81 | f, err := c.Float("float") 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | 86 | if f != 1.5 { 87 | t.Errorf("Float setting was '%f', expected '1.5'", f) 88 | } 89 | } 90 | 91 | func TestTypeOrLookups(t *testing.T) { 92 | c := NewConfig(nil) 93 | 94 | s, err := c.StringOr("string", "some_string") 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | 99 | if s != "some_string" { 100 | t.Errorf("String setting was '%s', expected 'some_string'", s) 101 | } 102 | 103 | b, err := c.BoolOr("bool", true) 104 | if err != nil { 105 | t.Error(err) 106 | } 107 | 108 | if b != true { 109 | t.Errorf("Bool setting was 'false', expected 'true'") 110 | } 111 | 112 | i, err := c.IntOr("int", 1) 113 | if err != nil { 114 | t.Error(err) 115 | } 116 | 117 | if i != 1 { 118 | t.Errorf("Int setting was '%d', expected '1'", i) 119 | } 120 | 121 | f, err := c.FloatOr("float", 1.5) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | 126 | if f != 1.5 { 127 | t.Errorf("Float setting was '%f', expected '1.5'", f) 128 | } 129 | } 130 | 131 | func TestValidate(t *testing.T) { 132 | c := NewConfig(nil) 133 | c.Validate = func(map[string]string) error { 134 | return fmt.Errorf("some error") 135 | } 136 | 137 | if err := c.Load(); err == nil { 138 | t.Errorf("Error was nil") 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type Environment struct { 8 | mappings map[string]string 9 | } 10 | 11 | func NewEnvironment(mappings map[string]string) *Environment { 12 | return &Environment{ 13 | mappings: mappings, 14 | } 15 | } 16 | 17 | func (this *Environment) Load() (map[string]string, error) { 18 | settings := map[string]string{} 19 | 20 | for env, key := range this.mappings { 21 | if val := os.Getenv(env); val != "" { 22 | settings[key] = val 23 | } 24 | } 25 | 26 | return settings, nil 27 | } 28 | -------------------------------------------------------------------------------- /environment_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestEnvironmentLoad(t *testing.T) { 9 | os.Setenv("GLOBAL_TIMEOUT", "30") 10 | os.Setenv("GLOBAL_FREQUENCY", "0.5") 11 | os.Setenv("LOCAL_TIME_ZONE", "PST") 12 | os.Setenv("LOCAL_ENABLED", "true") 13 | 14 | mappings := map[string]string{ 15 | "GLOBAL_TIMEOUT": "global.timeout", 16 | "GLOBAL_FREQUENCY": "global.frequency", 17 | "LOCAL_TIME_ZONE": "local.time_zone", 18 | "LOCAL_ENABLED": "local.enabled", 19 | } 20 | 21 | p := NewEnvironment(mappings) 22 | 23 | expectedSettings := map[string]string{ 24 | "global.timeout": "30", 25 | "global.frequency": "0.5", 26 | "local.time_zone": "PST", 27 | "local.enabled": "true", 28 | } 29 | 30 | actualSettings, err := p.Load() 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | for key, expected := range expectedSettings { 36 | actual, ok := actualSettings[key] 37 | 38 | if !ok { 39 | t.Errorf("Key '%s' not in settings", key) 40 | } 41 | 42 | if actual != expected { 43 | t.Errorf("Setting '%s' was '%s', expected '%s'", key, actual, expected) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ini.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-ini/ini" 6 | ) 7 | 8 | type INIFile struct { 9 | path string 10 | } 11 | 12 | func NewINIFile(path string) *INIFile { 13 | return &INIFile{ 14 | path: path, 15 | } 16 | } 17 | 18 | func (this *INIFile) Load() (map[string]string, error) { 19 | settings := map[string]string{} 20 | 21 | file, err := ini.Load(this.path) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | for _, section := range file.Sections() { 27 | for _, key := range section.Keys() { 28 | token := fmt.Sprintf("%s.%s", section.Name(), key.Name()) 29 | settings[token] = key.String() 30 | } 31 | } 32 | 33 | return settings, nil 34 | } 35 | -------------------------------------------------------------------------------- /ini_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestINILoad(t *testing.T) { 8 | p := NewINIFile("test/config.ini") 9 | 10 | actualSettings, err := p.Load() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | expectedSettings := map[string]string{ 16 | "global.timeout": "30", 17 | "global.frequency": "0.5", 18 | "local.time_zone": "PST", 19 | "local.enabled": "true", 20 | "ABC.def": "ghi", 21 | } 22 | 23 | for key, expected := range expectedSettings { 24 | actual, ok := actualSettings[key] 25 | 26 | if !ok { 27 | t.Errorf("Key '%s' not in settings", key) 28 | } 29 | 30 | if actual != expected { 31 | t.Errorf("Setting '%s' was '%s', expected '%s'", key, actual, expected) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | ) 8 | 9 | type JSONFile struct { 10 | path string 11 | } 12 | 13 | func NewJSONFile(path string) *JSONFile { 14 | return &JSONFile{ 15 | path: path, 16 | } 17 | } 18 | 19 | func (this *JSONFile) Load() (map[string]string, error) { 20 | encodedJSON, err := ioutil.ReadFile(this.path) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | decodedJSON := map[string]interface{}{} 26 | if err := json.Unmarshal(encodedJSON, &decodedJSON); err != nil { 27 | return nil, err 28 | } 29 | 30 | settings, err := FlattenJSON(decodedJSON, "") 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return settings, nil 36 | 37 | } 38 | 39 | func FlattenJSON(input map[string]interface{}, namespace string) (map[string]string, error) { 40 | flattened := map[string]string{} 41 | 42 | for key, value := range input { 43 | var token string 44 | if namespace == "" { 45 | token = key 46 | } else { 47 | token = fmt.Sprintf("%s.%s", namespace, key) 48 | } 49 | 50 | if child, ok := value.(map[string]interface{}); ok { 51 | settings, err := FlattenJSON(child, token) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | for k, v := range settings { 57 | flattened[k] = v 58 | } 59 | } else { 60 | flattened[token] = fmt.Sprintf("%v", value) 61 | } 62 | } 63 | 64 | return flattened, nil 65 | } 66 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestJSONLoad(t *testing.T) { 8 | p := NewJSONFile("test/config.json") 9 | 10 | actualSettings, err := p.Load() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | expectedSettings := map[string]string{ 16 | "global.timeout": "30", 17 | "global.frequency": "0.5", 18 | "local.time_zone": "PST", 19 | "local.enabled": "true", 20 | } 21 | 22 | for key, expected := range expectedSettings { 23 | actual, ok := actualSettings[key] 24 | 25 | if !ok { 26 | t.Errorf("Key '%s' not in settings", key) 27 | } 28 | 29 | if actual != expected { 30 | t.Errorf("Setting '%s' was '%s', expected '%s'", key, actual, expected) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /once_loader.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type OnceLoader struct { 8 | once sync.Once 9 | provider Provider 10 | settings map[string]string 11 | } 12 | 13 | func NewOnceLoader(provider Provider) *OnceLoader { 14 | return &OnceLoader{ 15 | once: sync.Once{}, 16 | provider: provider, 17 | settings: map[string]string{}, 18 | } 19 | } 20 | 21 | func (this *OnceLoader) Load() (map[string]string, error) { 22 | var err error 23 | 24 | this.once.Do( 25 | func() { 26 | this.settings, err = this.provider.Load() 27 | }, 28 | ) 29 | 30 | settings := map[string]string{} 31 | 32 | for key, value := range this.settings { 33 | settings[key] = value 34 | } 35 | 36 | return settings, err 37 | } 38 | -------------------------------------------------------------------------------- /once_loader_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestOnceLoader(t *testing.T) { 8 | static := NewStatic(map[string]string{"enabled": "true"}) 9 | loader := NewOnceLoader(static) 10 | 11 | settings, err := loader.Load() 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | if settings["enabled"] != "true" { 17 | t.Errorf("Enabled was '%s', expected 'true'", settings["enabled"]) 18 | } 19 | 20 | static.Set("enabled", "false") 21 | 22 | settings, err = loader.Load() 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | if settings["enabled"] != "true" { 28 | t.Errorf("Enabled was '%s', expected 'true'", settings["enabled"]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Provider interface { 4 | Load() (map[string]string, error) 5 | } 6 | -------------------------------------------------------------------------------- /resolver.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Resolver struct { 4 | provider Provider 5 | mappings map[string]string 6 | } 7 | 8 | func NewResolver(provider Provider, mappings map[string]string) *Resolver { 9 | return &Resolver{ 10 | provider: provider, 11 | mappings: mappings, 12 | } 13 | } 14 | 15 | func (this *Resolver) Load() (map[string]string, error) { 16 | settings, err := this.provider.Load() 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | resolved := map[string]string{} 22 | for key, val := range settings { 23 | if dest, ok := this.mappings[key]; ok { 24 | resolved[dest] = val 25 | } else { 26 | resolved[key] = val 27 | } 28 | } 29 | 30 | return resolved, nil 31 | } 32 | -------------------------------------------------------------------------------- /resolver_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestResolver(t *testing.T) { 8 | static := NewStatic(map[string]string{"items.server.timeout": "30"}) 9 | 10 | mappings := map[string]string{ 11 | "items.server.timeout": "server.timeout", 12 | } 13 | 14 | resolver := NewResolver(static, mappings) 15 | 16 | settings, err := resolver.Load() 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | 21 | if settings["server.timeout"] != "30" { 22 | t.Errorf("Timeout was '%s', expected '30'", settings["server.timeout"]) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /static.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Static struct { 4 | settings map[string]string 5 | } 6 | 7 | func NewStatic(settings map[string]string) *Static { 8 | return &Static{ 9 | settings: settings, 10 | } 11 | } 12 | 13 | func (this *Static) Load() (map[string]string, error) { 14 | settings := map[string]string{} 15 | 16 | for key, value := range this.settings { 17 | settings[key] = value 18 | } 19 | 20 | return settings, nil 21 | } 22 | 23 | func (this *Static) Set(key, val string) { 24 | this.settings[key] = val 25 | } 26 | -------------------------------------------------------------------------------- /test/config.ini: -------------------------------------------------------------------------------- 1 | [global] 2 | timeout=30 3 | frequency=0.5 4 | 5 | [local] 6 | time_zone=PST 7 | enabled=true 8 | 9 | [ABC] 10 | def="ghi" 11 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "timeout": 30, 4 | "frequency": 0.5 5 | }, 6 | "local": { 7 | "time_zone": "PST", 8 | "enabled": true 9 | } 10 | } -------------------------------------------------------------------------------- /test/config.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | frequency = 0.5 3 | timeout = 30 4 | 5 | [local] 6 | enabled = true 7 | time_zone = "PST" 8 | -------------------------------------------------------------------------------- /test/config.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | timeout: 30 3 | frequency: 0.5 4 | 5 | local: 6 | time_zone: "PST" 7 | enabled: true -------------------------------------------------------------------------------- /toml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | "io/ioutil" 6 | ) 7 | 8 | type TOMLFile struct { 9 | path string 10 | } 11 | 12 | func NewTOMLFile(path string) *TOMLFile { 13 | return &TOMLFile{ 14 | path: path, 15 | } 16 | } 17 | 18 | func (this *TOMLFile) Load() (map[string]string, error) { 19 | data, err := ioutil.ReadFile(this.path) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | out := make(map[string]interface{}) 25 | if _, err := toml.Decode(string(data), &out); err != nil { 26 | return nil, err 27 | } 28 | 29 | return FlattenJSON(out, "") 30 | } 31 | -------------------------------------------------------------------------------- /toml_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "testing" 4 | 5 | func TestTOMLFile(t *testing.T) { 6 | configFile := "test/config.toml" 7 | 8 | tomlProvider := NewTOMLFile(configFile) 9 | actualSettings, err := tomlProvider.Load() 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | 14 | expectedSettings := map[string]string{ 15 | "global.timeout": "30", 16 | "global.frequency": "0.5", 17 | "local.time_zone": "PST", 18 | "local.enabled": "true", 19 | } 20 | 21 | for key, expected := range expectedSettings { 22 | actual, ok := actualSettings[key] 23 | 24 | if !ok { 25 | t.Errorf("Key '%s' not in settings", key) 26 | } 27 | 28 | if actual != expected { 29 | t.Errorf("Setting '%s' was '%s', expected '%s'", key, actual, expected) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /yaml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/ghodss/yaml" 6 | "io/ioutil" 7 | ) 8 | 9 | type YAMLFile struct { 10 | path string 11 | } 12 | 13 | func NewYAMLFile(path string) *YAMLFile { 14 | return &YAMLFile{ 15 | path: path, 16 | } 17 | } 18 | 19 | func (this *YAMLFile) Load() (map[string]string, error) { 20 | encodedYAML, err := ioutil.ReadFile(this.path) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | encodedJSON, err := yaml.YAMLToJSON(encodedYAML) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | decodedJSON := map[string]interface{}{} 31 | if err := json.Unmarshal(encodedJSON, &decodedJSON); err != nil { 32 | return nil, err 33 | } 34 | 35 | return FlattenJSON(decodedJSON, "") 36 | } 37 | -------------------------------------------------------------------------------- /yaml_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestYAMLLoad(t *testing.T) { 8 | p := NewYAMLFile("test/config.yaml") 9 | 10 | actualSettings, err := p.Load() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | expectedSettings := map[string]string{ 16 | "global.timeout": "30", 17 | "global.frequency": "0.5", 18 | "local.time_zone": "PST", 19 | "local.enabled": "true", 20 | } 21 | 22 | for key, expected := range expectedSettings { 23 | actual, ok := actualSettings[key] 24 | 25 | if !ok { 26 | t.Errorf("Key '%s' not in settings", key) 27 | } 28 | 29 | if actual != expected { 30 | t.Errorf("Setting '%s' was '%s', expected '%s'", key, actual, expected) 31 | } 32 | } 33 | } 34 | --------------------------------------------------------------------------------