├── .gitignore ├── .travis.yml ├── Godeps ├── Godeps.json └── Readme ├── LICENSE ├── README.md ├── confer.go ├── confer_test.go ├── errors └── errors.go ├── maps └── util.go ├── reader └── configreader.go ├── source ├── config.go ├── configger.go ├── env.go └── pflags.go ├── test └── fixtures │ ├── application.yaml │ ├── env_underscores.yaml │ ├── environments │ ├── development.yaml │ └── production.yaml │ ├── merging.yaml │ ├── simple.extended.yaml │ └── simple.yaml └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | Godeps/_workspace 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - tip 5 | - 1.4 6 | - 1.3 7 | 8 | install: 9 | - go get github.com/tools/godep 10 | - godep restore 11 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/jacobstr/confer", 3 | "GoVersion": "go1.4.2", 4 | "Deps": [ 5 | { 6 | "ImportPath": "github.com/BurntSushi/toml", 7 | "Comment": "v0.1.0-9-g3883ac1", 8 | "Rev": "3883ac1ce943878302255f538fce319d23226223" 9 | }, 10 | { 11 | "ImportPath": "github.com/jtolds/gls", 12 | "Rev": "f1ac7f4f24f50328e6bc838ca4437d1612a0243c" 13 | }, 14 | { 15 | "ImportPath": "github.com/kr/pretty", 16 | "Comment": "go.weekly.2011-12-22-24-gf31442d", 17 | "Rev": "f31442d60e51465c69811e2107ae978868dbea5c" 18 | }, 19 | { 20 | "ImportPath": "github.com/kr/text", 21 | "Rev": "6807e777504f54ad073ecef66747de158294b639" 22 | }, 23 | { 24 | "ImportPath": "github.com/smartystreets/assertions", 25 | "Comment": "1.5.0-391-g8121b35", 26 | "Rev": "8121b35a306f72fab16bdbc59d7809563955ca4d" 27 | }, 28 | { 29 | "ImportPath": "github.com/smartystreets/goconvey/convey", 30 | "Comment": "1.5.0-335-g9f800fa", 31 | "Rev": "9f800fa7d56a5bb44514a4b3463f3f4d21346b78" 32 | }, 33 | { 34 | "ImportPath": "github.com/spf13/cast", 35 | "Rev": "770890fb156e3654a3aaa7e696971f7e5a73df4a" 36 | }, 37 | { 38 | "ImportPath": "github.com/spf13/jwalterweatherman", 39 | "Rev": "e3682f3b5526cf86abc2d415aa312cd5531e3d0a" 40 | }, 41 | { 42 | "ImportPath": "github.com/spf13/pflag", 43 | "Rev": "463bdc838f2b35e9307e91d480878bda5fff7232" 44 | }, 45 | { 46 | "ImportPath": "gopkg.in/yaml.v2", 47 | "Rev": "7ad95dd0798a40da1ccdff6dff35fd177b5edf40" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Steve Francia 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Confer 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/jacobstr/confer.svg)](https://travis-ci.org/jacobstr/confer) 5 | 6 | A [viper](http://github.com/spf13/viper)-derived configuration management package. 7 | 8 | Significant changes include: 9 | 10 | * Materialized path access of configuration variables. 11 | * The singleton has been replaced by separate instances, largely for testability. 12 | * The ability to load and merge multiple configuration files. 13 | 14 | Features 15 | ======== 16 | 17 | 1. Merging multiple configuration sources. 18 | ```go 19 | config.ReadPaths("application.yaml", "environments/production.yaml")` 20 | ``` 21 | 22 | 2. Materialized path access of nested configuration data. 23 | ```go 24 | config.GetInt('app.database.port') 25 | ``` 26 | 3. Binding of environment variables to configuration data. 27 | 28 | `APP_DATABASE_PORT=3456 go run app.go` 29 | 30 | 4. User-defined helper methods. 31 | 32 | ## Usage 33 | 34 | ### Initialization 35 | Create your configuration instance: 36 | 37 | ```go 38 | config := confer.NewConfig() 39 | ``` 40 | 41 | Then set defaults, read paths, set overrides: 42 | ```go 43 | config.SetDefault("environment", "development") 44 | config.ReadPaths("application.yaml", "environments/production.yml") 45 | config.Set("environment", "development") 46 | ``` 47 | 48 | **No worries!** Confer will [conveniently merge](https://github.com/jacobstr/confer/confer_test.go#L155) 49 | deeply nested structures for you. My usual configuration setup looks like this: 50 | 51 | ``` 52 | config 53 | ├── application.development.yml 54 | ├── application.production.yml 55 | └── application.yml 56 | ``` 57 | 58 | For example, an application-specific config package like the one below can be used 59 | to drive a core configuration with environment specific overrides: 60 | 61 | ```go 62 | 63 | var App *confer.Config 64 | 65 | func init() { 66 | App = confer.NewConfig() 67 | appenv := os.Getenv("MYAPP_ENV"); 68 | paths := []string{"application.yml"} 69 | 70 | if (appenv != "") { 71 | paths = append(paths, fmt.Sprintf("application.%s.yml", appenv)) 72 | } 73 | 74 | if err := App.ReadPaths(paths...); err != nil { 75 | log.Warn(err) 76 | } 77 | } 78 | ``` 79 | 80 | ### Setting Defaults 81 | Sets a value if it hasn't already been set. Multiple invocations won't clobber 82 | existing values, so you'll likely want to do this before reading from files. 83 | 84 | ```go 85 | config := confer.NewConfig() 86 | config.ReadPaths("application.yaml") 87 | config.SetDefault("ContentDir", "content") 88 | config.SetDefault("LayoutDir", "layouts") 89 | config.SetDefault("Indexes", map[string]string{"tag": "tags", "category": "categories"}) 90 | ``` 91 | 92 | ### Setting Keys / Value Pairs 93 | Sets a value. Has lower precedence than environment variables or command line flags. 94 | ```go 95 | config.Set("verbose", true) 96 | config.Set("logfile", "/var/log/config.log") 97 | ``` 98 | ### Getting Values 99 | There are a variety of accessors for accessing type-coerced values: 100 | ```go 101 | Get(key string) : interface{} 102 | GetBool(key string) : bool 103 | GetFloat64(key string) : float64 104 | GetInt(key string) : int 105 | GetString(key string) : string 106 | GetStringMap(key string) : map[string]interface{} 107 | GetStringMapString(key string) : map[string]string 108 | GetStringSlice(key string) : []string 109 | GetTime(key string) : time.Time 110 | IsSet(key string) : bool 111 | ``` 112 | 113 | ### Deep Configuration Data 114 | *Materialized paths* allow easy access of deeply nested config data: 115 | ```go 116 | logger_config := config.GetStringMap("logger.stdout") 117 | ``` 118 | Because periods aren't valid environment variable characters, when using automatic environment bindings (see below), substitute with underscores: 119 | ``` 120 | LOGGER_STDOUT=/var/log/myapp go run server.go 121 | ``` 122 | 123 | ### Environment Bindings 124 | 125 | 126 | ##### Automatic Binding 127 | Confer can automatically bind all existing configuration keys to environment variables. 128 | 129 | Given some sort of `application.yaml` 130 | ```yaml 131 | --- 132 | app: 133 | log: "verbose" 134 | database: 135 | host: "localhost" 136 | ``` 137 | 138 | And this pair of calls: 139 | 140 | ```go 141 | config.ReadPaths("application.yaml") 142 | config.AutomaticEnv() 143 | ``` 144 | 145 | You'll have the following environment variables exposed for configuration: 146 | ``` 147 | APP_LOG 148 | APP_DATABASE_HOST 149 | ``` 150 | 151 | ##### Selective Binding 152 | If this automatic binding is bizarre, you can selectively bind environment variables 153 | with ``BindEnv()`. 154 | 155 | ```go 156 | config.BindEnv("APP_LOG", "app.log") 157 | ``` 158 | 159 | ### Helpers 160 | You can `Set` a `func() interface{}` at a configuration key to provide values dynamically: 161 | 162 | ```go 163 | config.Set("dbstring", func() interface {} { 164 | return fmt.Sprintf( 165 | "user=%s dbname=%s sslmode=%s", 166 | config.GetString("database.user"), 167 | config.GetString("database.name"), 168 | config.GetString("database.sslmode"), 169 | ) 170 | }) 171 | assert(config.GetString("dbstring") == "user=doug dbname=pruden sslmode=pushups") 172 | ``` 173 | -------------------------------------------------------------------------------- /confer.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2014 Steve Francia . 2 | // Copyright © 2014 Jacob Straszysnki . 3 | // 4 | // Use of this source code is governed by an MIT-style 5 | // license that can be found in the LICENSE file. 6 | 7 | // Confer is a application configuration system. 8 | // It believes that applications can be configured a variety of ways 9 | // via flags, ENVIRONMENT variables, configuration files retrieved 10 | // from the file system. 11 | // 12 | // There are 3 precedence tiers: 13 | // 14 | // 1. Command line flags. 15 | // 2. Environment variables. 16 | // 3. Attributes - (e.g. Set, SetDefault, ReadPaths) 17 | 18 | package confer 19 | 20 | import ( 21 | "fmt" 22 | "path" 23 | "path/filepath" 24 | "reflect" 25 | "strings" 26 | "time" 27 | 28 | "github.com/kr/pretty" 29 | "github.com/spf13/cast" 30 | jww "github.com/spf13/jwalterweatherman" 31 | "github.com/spf13/pflag" 32 | 33 | "github.com/jacobstr/confer/reader" 34 | . "github.com/jacobstr/confer/source" 35 | 36 | errors "github.com/jacobstr/confer/errors" 37 | "github.com/jacobstr/confer/maps" 38 | ) 39 | 40 | // Manages key/value access and aliasing across multiple configuration sources. 41 | type Config struct { 42 | pflags *PFlagSource 43 | env *EnvSource 44 | attributes *ConfigSource 45 | 46 | // The root path for configuration files. 47 | rootPath string 48 | } 49 | 50 | func NewConfig() *Config { 51 | manager := &Config{} 52 | manager.pflags = NewPFlagSource() 53 | manager.attributes = NewConfigSource() 54 | manager.env = NewEnvSource() 55 | manager.rootPath = "" 56 | 57 | return manager 58 | } 59 | 60 | // Finds a value at a provided key, returning nil if the key does not exist. 61 | // The order of precedence for configuration data is: 62 | // 1. Program arguments. 63 | // 2. Environment variables. 64 | // 3. Config file data, overrides, and defaults. 65 | func (self *Config) Find(key string) interface{} { 66 | var val interface{} 67 | var exists bool 68 | 69 | // PFlag Override first 70 | val, exists = self.pflags.Get(key) 71 | if exists { 72 | jww.TRACE.Println(key, "found in override (via pflag):", val) 73 | return val 74 | } 75 | 76 | // Periods are not supported. Allow the usage of underscores to specify nested 77 | // configuration options. 78 | val, exists = self.env.Get(key) 79 | if exists { 80 | jww.TRACE.Println(key, "Found in environment with value:", val) 81 | return val 82 | } 83 | 84 | // Attributes entail pretty much everything else. 85 | val, exists = self.attributes.Get(key) 86 | if exists { 87 | jww.TRACE.Println(key, "Found in config:", val) 88 | return val 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func (manager *Config) GetString(key string) string { 95 | return cast.ToString(manager.Get(key)) 96 | } 97 | 98 | func (manager *Config) GetBool(key string) bool { 99 | return cast.ToBool(manager.Get(key)) 100 | } 101 | 102 | func (manager *Config) GetInt(key string) int { 103 | return cast.ToInt(manager.Get(key)) 104 | } 105 | 106 | func (manager *Config) GetFloat64(key string) float64 { 107 | return cast.ToFloat64(manager.Get(key)) 108 | } 109 | 110 | func (manager *Config) GetTime(key string) time.Time { 111 | return cast.ToTime(manager.Get(key)) 112 | } 113 | 114 | func (manager *Config) GetStringSlice(key string) []string { 115 | return cast.ToStringSlice(manager.Get(key)) 116 | } 117 | 118 | func (manager *Config) GetStringMap(key string) map[string]interface{} { 119 | return cast.ToStringMap(manager.Get(key)) 120 | } 121 | 122 | func (manager *Config) GetStringMapString(key string) map[string]string { 123 | return cast.ToStringMapString(manager.Get(key)) 124 | } 125 | 126 | // Binds a configuration key to a command line flag: 127 | // pflag.Int("port", 8080, "The best alternative port") 128 | // confer.BindPFlag("port", pflag.Lookup("port")) 129 | func (manager *Config) BindPFlag(key string, flag *pflag.Flag) (err error) { 130 | if flag == nil { 131 | return fmt.Errorf("flag for %q is nil", key) 132 | } 133 | 134 | manager.pflags.Set(key, flag) 135 | 136 | switch flag.Value.Type() { 137 | case "int", "int8", "int16", "int32", "int64": 138 | manager.SetDefault(key, cast.ToInt(flag.Value.String())) 139 | case "bool": 140 | manager.SetDefault(key, cast.ToBool(flag.Value.String())) 141 | default: 142 | manager.SetDefault(key, flag.Value.String()) 143 | } 144 | return nil 145 | } 146 | 147 | // Binds a confer key to a ENV variable. ENV variables are case sensitive If only 148 | func (manager *Config) BindEnv(input ...string) (err error) { 149 | return manager.env.Bind(input...) 150 | } 151 | 152 | // Get returns an interface.. 153 | // Must be typecast or used by something that will typecast 154 | func (manager *Config) Get(key string) interface{} { 155 | jww.TRACE.Println("Looking for", key) 156 | 157 | v := manager.Find(key) 158 | 159 | if v == nil { 160 | return nil 161 | } 162 | 163 | jww.TRACE.Println("Found value", v) 164 | switch v.(type) { 165 | case bool: 166 | return cast.ToBool(v) 167 | case string: 168 | return cast.ToString(v) 169 | case int64, int32, int16, int8, int: 170 | return cast.ToInt(v) 171 | case float64, float32: 172 | return cast.ToFloat64(v) 173 | case time.Time: 174 | return cast.ToTime(v) 175 | case []string: 176 | return v 177 | } 178 | return v 179 | } 180 | 181 | // Returns true if the config key exists and is non-nil. 182 | func (manager *Config) IsSet(key string) bool { 183 | t := manager.Get(key) 184 | return t != nil 185 | } 186 | 187 | // Have confer check ENV variables for all 188 | // keys set in config, default & flags 189 | func (manager *Config) AutomaticEnv() { 190 | for _, x := range manager.AllKeys() { 191 | manager.BindEnv(x) 192 | } 193 | } 194 | 195 | // Returns true if the key provided exists in our configuration. 196 | func (manager *Config) InConfig(key string) bool { 197 | _, exists := manager.attributes.Get(key) 198 | return exists 199 | } 200 | 201 | // Set the default value for this key. 202 | // Default only used when no value is provided by the user via flag, config or ENV. 203 | func (manager *Config) SetDefault(key string, value interface{}) { 204 | if !manager.IsSet(key) { 205 | manager.attributes.Set(key, value) 206 | } 207 | } 208 | 209 | // Explicitly sets a value. Will not override command line arguments or 210 | // environment variables, as those sources have higher precedence. 211 | func (manager *Config) Set(key string, value interface{}) { 212 | manager.attributes.Set(key, value) 213 | } 214 | 215 | // Sets an optional root path. This frees you from having to specify a 216 | // redundant prefix when calling ReadPaths() later. 217 | func (manager *Config) SetRootPath(path string) { 218 | manager.rootPath = path 219 | } 220 | 221 | // Loads and sequentially + recursively merges the provided config arguments. Returns 222 | // an error if any of the files fail to load, though this may be expecte 223 | // in the case of search paths. 224 | func (manager *Config) ReadPaths(paths ...string) error { 225 | var err error 226 | var loaded interface{} 227 | 228 | merged_config := manager.attributes.ToStringMap() 229 | errs := []error{} 230 | 231 | for _, base_path := range paths { 232 | var final_path string 233 | 234 | if !filepath.IsAbs(base_path) { 235 | final_path = path.Join(manager.rootPath, base_path) 236 | } else { 237 | final_path = base_path 238 | } 239 | 240 | loaded, err = reader.ReadFile(final_path) 241 | 242 | if err != nil { 243 | errs = append(errs, err) 244 | continue 245 | } 246 | 247 | // In-place recursive coercion to stringmap. 248 | coerced := cast.ToStringMap(loaded) 249 | maps.ToStringMapRecursive(coerced) 250 | 251 | if merged_config == nil { 252 | merged_config = coerced 253 | } else { 254 | merged_config = maps.Merge( 255 | merged_config, 256 | coerced, 257 | ) 258 | } 259 | 260 | manager.attributes.FromStringMap(merged_config) 261 | } 262 | 263 | if len(errs) > 0 { 264 | return &errors.LoadError{Errors: errs} 265 | } else { 266 | return nil 267 | } 268 | } 269 | 270 | // Merges data into the our attributes configuration tier from a struct. 271 | func (manager *Config) MergeAttributes(val interface{}) error { 272 | merged_config := maps.Merge( 273 | manager.attributes.ToStringMap(), 274 | cast.ToStringMap(val), 275 | ) 276 | 277 | manager.attributes.FromStringMap(merged_config) 278 | return nil 279 | } 280 | 281 | // Returns all currently set keys, pruning ancestors and only 282 | // showing the leaves. 283 | func (manager *Config) AllKeys() []string { 284 | keys := manager.attributes.AllKeys() 285 | keys = append(keys, manager.env.AllKeys()...) 286 | keys = append(keys, manager.attributes.AllKeys()...) 287 | 288 | leaves := map[string]struct{}{} 289 | for _, key := range keys { 290 | 291 | // Filter out leaves. This is really ineffecient. 292 | val := manager.Get(key) 293 | if val == nil { 294 | leaves[key] = struct{}{} 295 | } else if reflect.TypeOf(val).Kind() != reflect.Map { 296 | leaves[key] = struct{}{} 297 | } 298 | } 299 | 300 | unique_keys := []string{} 301 | for x, _ := range leaves { 302 | // LowerCase the key for backwards-compatibility. 303 | unique_keys = append(unique_keys, strings.ToLower(x)) 304 | } 305 | 306 | return unique_keys 307 | } 308 | 309 | func (manager *Config) AllSettings() map[string]interface{} { 310 | m := map[string]interface{}{} 311 | for _, x := range manager.AllKeys() { 312 | m[x] = manager.Get(x) 313 | } 314 | 315 | return m 316 | } 317 | 318 | func (manager *Config) Debug() { 319 | fmt.Println("Flags:") 320 | pretty.Println(manager.pflags) 321 | fmt.Println("Env:") 322 | pretty.Println(manager.env) 323 | fmt.Println("Config file attributes:") 324 | pretty.Println(manager.attributes) 325 | } 326 | -------------------------------------------------------------------------------- /confer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2014 Steve Francia . 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package confer 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "sort" 12 | "testing" 13 | 14 | . "github.com/smartystreets/goconvey/convey" 15 | 16 | "github.com/jacobstr/confer/reader" 17 | "github.com/spf13/pflag" 18 | ) 19 | 20 | var yamlExample = []byte(`Hacker: true 21 | name: steve 22 | hobbies: 23 | - skateboarding 24 | - snowboarding 25 | - go 26 | clothing: 27 | jacket: leather 28 | trousers: denim 29 | age: 35 30 | eyes : brown 31 | beard: true 32 | `) 33 | 34 | var yamlOverride = []byte(`Hacker: false 35 | name: steve 36 | hobbies: 37 | - skateboarding 38 | - dancing 39 | awesomeness: supreme 40 | `) 41 | 42 | var tomlExample = []byte(` 43 | title = "TOML Example" 44 | 45 | [owner] 46 | organization = "MongoDB" 47 | Bio = "MongoDB Chief Developer Advocate & Hacker at Large" 48 | dob = 1979-05-27T07:32:00Z # First class dates? Why not?`) 49 | 50 | var jsonExample = []byte(`{ 51 | "id": "0001", 52 | "type": "donut", 53 | "name": "Cake", 54 | "ppu": 0.55, 55 | "batters": { 56 | "batter": [ 57 | { "type": "Regular" }, 58 | { "type": "Chocolate" }, 59 | { "type": "Blueberry" }, 60 | { "type": "Devil's Food" } 61 | ] 62 | } 63 | }`) 64 | 65 | var remoteExample = []byte(`{ 66 | "id":"0002", 67 | "type":"cronut", 68 | "newkey":"remote" 69 | }`) 70 | 71 | var application_yaml = map[string]interface{}{ 72 | "logging": map[string]interface{}{ 73 | "level": "info", 74 | }, 75 | "database": map[string]interface{}{ 76 | "host": "localhost", 77 | "user": "postgres", 78 | "password": "spend_an_hour_tweaking_your_pg_hba_for_this", 79 | }, 80 | "server": map[string]interface{}{ 81 | "workers": nil, 82 | }, 83 | } 84 | 85 | var app_dev_yaml = map[string]interface{}{ 86 | "root": "/home/ubuntu/killer_project", 87 | "logging": "debug", 88 | "database": map[string]interface{}{ 89 | "host": "localhost", 90 | "user": "postgres", 91 | "password": "spend_an_hour_tweaking_your_pg_hba_for_this", 92 | }, 93 | "server": map[string]interface{}{ 94 | "workers": 1, 95 | "static_assets": []interface{}{"css", "js", "img", "fonts"}, 96 | }, 97 | } 98 | 99 | //stubs for PFlag Values 100 | type stringValue string 101 | 102 | func newStringValue(val string, p *string) *stringValue { 103 | *p = val 104 | return (*stringValue)(p) 105 | } 106 | 107 | func (s *stringValue) Set(val string) error { 108 | *s = stringValue(val) 109 | return nil 110 | } 111 | 112 | func (s *stringValue) Type() string { 113 | return "string" 114 | } 115 | 116 | func (s *stringValue) String() string { 117 | return fmt.Sprintf("%s", *s) 118 | } 119 | 120 | func TestSpec(t *testing.T) { 121 | Convey("Confer", t, func() { 122 | config := NewConfig() 123 | 124 | Convey("Getting a default", func() { 125 | config.SetDefault("age", 45) 126 | So(config.Get("age"), ShouldEqual, 45) 127 | }) 128 | 129 | Convey("Marhsalling", func() { 130 | Convey("Yaml", func() { 131 | yaml, _ := reader.ReadBytes(yamlExample, "yaml") 132 | config.MergeAttributes(yaml) 133 | 134 | Convey("Existence checks", func() { 135 | So(config.InConfig("name"), ShouldEqual, true) 136 | So(config.InConfig("state"), ShouldEqual, false) 137 | }) 138 | 139 | Convey("Strings", func() { 140 | So(config.Get("name"), ShouldEqual, "steve") 141 | }) 142 | 143 | Convey("Arrays", func() { 144 | So( 145 | config.Get("hobbies"), 146 | ShouldResemble, 147 | []interface{}{"skateboarding", "snowboarding", "go"}, 148 | ) 149 | }) 150 | 151 | Convey("Integers", func() { 152 | So(config.Get("age"), ShouldEqual, 35) 153 | }) 154 | 155 | Convey("Merging", func() { 156 | yaml, _ := reader.ReadFile("test/fixtures/merging.yaml") 157 | 158 | Convey("An initial map", func() { 159 | root := map[string]interface{}{"users": yaml.(map[interface{}]interface{})["mapusers"]} 160 | config.MergeAttributes(root) 161 | So(config.GetStringMap("users"), ShouldResemble, map[string]interface{}{"bob": "/home/bob", "jim": "/home/jim"}) 162 | 163 | Convey("Should be clobbered by an integer", func() { 164 | root := map[string]interface{}{"users": yaml.(map[interface{}]interface{})["intusers"]} 165 | config.MergeAttributes(root) 166 | So(config.Get("users"), ShouldResemble, 5) 167 | 168 | Convey("Should be clobbered back to a map", func() { 169 | root := map[string]interface{}{"users": yaml.(map[interface{}]interface{})["mapusers"]} 170 | config.MergeAttributes(root) 171 | So( 172 | config.GetStringMap("users"), 173 | ShouldResemble, map[string]interface{}{"bob": "/home/bob", "jim": "/home/jim"}, 174 | ) 175 | }) 176 | }) 177 | 178 | Convey("Should be clobbered by an array", func() { 179 | root := map[string]interface{}{"users": yaml.(map[interface{}]interface{})["arrayusers"]} 180 | config.MergeAttributes(root) 181 | So(config.Get("users"), ShouldResemble, []interface{}{"bob", "jim"}) 182 | 183 | Convey("And arrays should always clobber each other", func() { 184 | root := map[string]interface{}{"users": yaml.(map[interface{}]interface{})["morearrayusers"]} 185 | config.MergeAttributes(root) 186 | So( 187 | config.Get("users"), 188 | ShouldResemble, []interface{}{"andy"}, 189 | ) 190 | }) 191 | }) 192 | 193 | Convey("Should be extended by another map", func() { 194 | root := map[string]interface{}{"users": yaml.(map[interface{}]interface{})["moreusers"]} 195 | config.MergeAttributes(root) 196 | So( 197 | config.GetStringMap("users"), 198 | ShouldResemble, 199 | map[string]interface{}{"bob": "/home/bob", "jim": "/home/jim", "andy": "/home/andy"}, 200 | ) 201 | }) 202 | }) 203 | }) 204 | }) 205 | 206 | Convey("Toml", func() { 207 | toml, _ := reader.ReadBytes(tomlExample, "toml") 208 | config.MergeAttributes(toml) 209 | So(config.Get("owner.organization"), ShouldEqual, "MongoDB") 210 | }) 211 | 212 | Convey("Json", func() { 213 | json, _ := reader.ReadBytes(jsonExample, "json") 214 | config.MergeAttributes(json) 215 | So(config.Get("ppu"), ShouldEqual, 0.55) 216 | }) 217 | }) 218 | 219 | Convey("Defaults, Overrides, Files", func() { 220 | Convey("Defaults", func() { 221 | config.SetDefault("clothing.jacket", "poncho") 222 | config.SetDefault("age", 99) 223 | 224 | So(config.Get("clothing.jacket"), ShouldEqual, "poncho") 225 | So(config.Get("age"), ShouldEqual, 99) 226 | 227 | Convey("Files should clobber defaults", func() { 228 | yaml, _ := reader.ReadBytes(yamlExample, "yaml") 229 | config.MergeAttributes(yaml) 230 | 231 | So(config.Get("clothing.jacket"), ShouldEqual, "leather") 232 | So(config.Get("age"), ShouldEqual, 35) 233 | 234 | Convey("Overrides should clobber files", func() { 235 | config.Set("clothing.jacket", "peacoat") 236 | config.Set("age", 30) 237 | So(config.Get("clothing.jacket"), ShouldEqual, "peacoat") 238 | So(config.Get("age"), ShouldEqual, 30) 239 | 240 | So(config.GetStringMap("clothing")["jacket"], ShouldEqual, "peacoat") 241 | }) 242 | 243 | Convey("All three sources should appear in AllKeys()", func() { 244 | keys := config.AllKeys() 245 | sort.Strings(keys) 246 | So( 247 | keys, 248 | ShouldResemble, 249 | []string{ 250 | "age", 251 | "beard", 252 | "clothing.jacket", 253 | "clothing.trousers", 254 | "eyes", 255 | "hacker", 256 | "hobbies", 257 | "name", 258 | }) 259 | }) 260 | }) 261 | }) 262 | }) 263 | 264 | Convey("PFlags", func() { 265 | testString := "testing" 266 | testValue := newStringValue(testString, &testString) 267 | 268 | flag := &pflag.Flag{ 269 | Name: "testflag", 270 | Value: testValue, 271 | Changed: false, 272 | } 273 | 274 | Convey("Should not appear in AllKeys() initially", func() { 275 | So(config.AllKeys(), ShouldResemble, []string{}) 276 | }) 277 | 278 | // Initial assertions after binding. 279 | config.BindPFlag("testflag", flag) 280 | So(config.Get("testflag"), ShouldEqual, "testing") 281 | 282 | Convey("Should appear in AllKeys()", func() { 283 | So(config.AllKeys(), ShouldResemble, []string{"testflag"}) 284 | }) 285 | 286 | Convey("Insensitivity before mutation", func() { 287 | So(config.Get("testFlag"), ShouldEqual, "testing") 288 | }) 289 | 290 | flag.Value.Set("testing_mutate") 291 | flag.Changed = true //hack for pflag usage 292 | So(config.Get("testflag"), ShouldEqual, "testing_mutate") 293 | 294 | Convey("Insensitivity after mutation", func() { 295 | So(config.Get("testFlag"), ShouldEqual, "testing_mutate") 296 | }) 297 | }) 298 | 299 | Convey("ReadPaths", func() { 300 | 301 | Convey("Single Path", func() { 302 | config.ReadPaths("test/fixtures/application.yaml") 303 | So(config.GetStringMap("app"), ShouldResemble, application_yaml) 304 | }) 305 | 306 | Convey("Absolute Path With Root Set", func() { 307 | config.SetRootPath("test/fixtures") 308 | currentDir, _ := os.Getwd() 309 | config.ReadPaths(currentDir + "/test/fixtures/application.yaml") 310 | So(config.GetStringMap("app"), ShouldResemble, application_yaml) 311 | }) 312 | 313 | Convey("Multiple Paths", func() { 314 | Convey("With A Missing File", func() { 315 | config.ReadPaths("test/fixtures/application.yaml", "test/fixtures/missing.yaml") 316 | So(config.GetStringMap("app"), ShouldResemble, application_yaml) 317 | }) 318 | 319 | Convey("With An Augmented Environment", func() { 320 | config.ReadPaths("test/fixtures/application.yaml", "test/fixtures/environments/development.yaml") 321 | So(config.GetStringMap("app"), ShouldResemble, app_dev_yaml) 322 | 323 | Convey("Deep access", func() { 324 | So(config.GetString("app.database.host"), ShouldEqual, "localhost") 325 | }) 326 | }) 327 | }) 328 | 329 | Convey("Rooted paths", func() { 330 | config.SetRootPath("test/fixtures") 331 | config.ReadPaths("application.yaml") 332 | So(config.GetStringMap("app"), ShouldResemble, application_yaml) 333 | }) 334 | }) 335 | 336 | Convey("Environment Variables", func() { 337 | Convey("Automatic Env", func() { 338 | config.ReadPaths("test/fixtures/application.yaml") 339 | os.Setenv("APP_LOGGING_LEVEL", "trace") 340 | config.AutomaticEnv() 341 | So(config.Get("app.logging.level"), ShouldEqual, "trace") 342 | }) 343 | 344 | Convey("Underscore translation", func() { 345 | config.ReadPaths("test/fixtures/env_underscores.yaml") 346 | os.Setenv("AWESOME_SAUCE_HEAT_LEVEL_IS_RADICAL", "yep!") 347 | config.AutomaticEnv() 348 | So(config.Get("awesome_sauce.heat_level.is_radical"), ShouldEqual, "yep!") 349 | }) 350 | }) 351 | 352 | Convey("Case Sensitivity", func() { 353 | config.ReadPaths("test/fixtures/application.yaml") 354 | funky := "aPp.DatAbase.host" 355 | regular := "app.database.host" 356 | So(config.GetString(funky), ShouldResemble, "localhost") 357 | 358 | Convey("Should manage case-insensitive key collissions", func() { 359 | config.Set(funky, "woot") 360 | So(config.GetString(funky), ShouldEqual, "woot") 361 | So(config.GetString(regular), ShouldEqual, "woot") 362 | 363 | config.Set(regular, "localhost") 364 | So(config.GetString(funky), ShouldEqual, "localhost") 365 | So(config.GetString(regular), ShouldEqual, "localhost") 366 | }) 367 | }) 368 | 369 | Convey("Helpers", func() { 370 | Convey("Returning an integer", func() { 371 | config.Set("port", func() interface{} { 372 | return 5 373 | }) 374 | So(config.GetInt("port"), ShouldEqual, 5) 375 | }) 376 | 377 | Convey("Returning a stringmap", func() { 378 | config.Set("database", func() interface{} { 379 | return map[string]string{"host": "localhost"} 380 | }) 381 | So(config.GetStringMapString("database"), ShouldResemble, map[string]string{"host": "localhost"}) 382 | }) 383 | 384 | Convey("Dbstring example", func() { 385 | config.Set("database.user", "doug") 386 | config.Set("database.dbname", "pruden") 387 | config.Set("database.sslmode", "pushups") 388 | 389 | config.Set("dbstring", func() interface{} { 390 | return fmt.Sprintf( 391 | "user=%s dbname=%s sslmode=%s", 392 | config.GetString("database.user"), 393 | config.GetString("database.dbname"), 394 | config.GetString("database.sslmode"), 395 | ) 396 | }) 397 | So(config.GetString("dbstring"), ShouldEqual, "user=doug dbname=pruden sslmode=pushups") 398 | }) 399 | }) 400 | 401 | Convey("AllSettings", func() { 402 | Convey("Should only include leaves", func() { 403 | config.ReadPaths("test/fixtures/application.yaml") 404 | So(config.AllSettings(), ShouldResemble, map[string]interface{} { 405 | "app.logging.level" : "info", 406 | "app.database.host" : "localhost", 407 | "app.database.user" : "postgres", 408 | "app.database.password" : "spend_an_hour_tweaking_your_pg_hba_for_this", 409 | "app.server.workers" : nil, 410 | }) 411 | }) 412 | 413 | Convey("Should include stubbed deep values", func() { 414 | config.Set("api.credentials.secret", "password") 415 | So(config.AllSettings(), ShouldResemble, map[string]interface{} { 416 | "api.credentials.secret" : "password", 417 | }) 418 | 419 | Convey("And retain them when we merge data", func() { 420 | config.MergeAttributes(map[string]interface{} { "api": map[string]interface {} { "user" : "stallman1337@hotmail.com"} }) 421 | So(config.AllSettings(), ShouldResemble, map[string]interface{} { 422 | "api.credentials.secret" : "password", 423 | "api.user" : "stallman1337@hotmail.com", 424 | }) 425 | }) 426 | }) 427 | }) 428 | }) 429 | } 430 | 431 | // About 9500 ns / op on my system. 432 | func BenchmarkIntAccess(b *testing.B) { 433 | configAttrs := make(map[string]interface{}) 434 | 435 | for i := 0; i < b.N; i++ { 436 | configAttrs[fmt.Sprintf("attr%s", i)] = i 437 | } 438 | 439 | config := NewConfig() 440 | config.MergeAttributes(configAttrs) 441 | 442 | b.ResetTimer() 443 | for i := 0; i < b.N; i++ { 444 | config.GetInt(fmt.Sprintf("attr%s", i)) 445 | } 446 | } 447 | 448 | // About 9500 ns / op on my system. 449 | func BenchmarkHelperAccess(b *testing.B) { 450 | configAttrs := make(map[string]interface{}) 451 | 452 | for i := 0; i < b.N; i++ { 453 | configAttrs[fmt.Sprintf("attr%s", i)] = func(c *Config) interface{} { 454 | return 5 455 | } 456 | } 457 | 458 | config := NewConfig() 459 | config.MergeAttributes(configAttrs) 460 | 461 | b.ResetTimer() 462 | for i := 0; i < b.N; i++ { 463 | config.GetInt(fmt.Sprintf("attr%s", i)) 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package err 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type UnsupportedConfigError string 9 | 10 | // Returned when we're given a file we can't handle. 11 | func (str UnsupportedConfigError) Error() string { 12 | return fmt.Sprintf("Unsupported Config Type %q", string(str)) 13 | } 14 | 15 | type LoadError struct { 16 | Msg string 17 | Errors []error 18 | } 19 | 20 | // Returned during multi-file load operations that 21 | // encounter one or more failures. 22 | func (m *LoadError) Error() string { 23 | merged := []string{} 24 | for _, err := range m.Errors { 25 | merged = append(merged, err.Error()) 26 | } 27 | return m.Msg + " " + strings.Join(merged, ", ") 28 | } 29 | -------------------------------------------------------------------------------- /maps/util.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | // A callback for Traverse. It will accept a key, a value, the current depth 10 | // and return true if we should continue deeper. 11 | type Traverser func(key string, val interface{}, depth int) bool 12 | 13 | // General purpose method for traversing a string map. 14 | func Traverse(data map[string]interface{}, cb Traverser) { 15 | traverse(data, "", 0, cb) 16 | } 17 | 18 | // Generic functional, recursive stringmap traversal. 19 | // Provides the callback with the current value, materialized path, and depth. 20 | func traverse(data map[string]interface{}, path string, depth int, cb Traverser) { 21 | for key, val := range data { 22 | var joined_key string 23 | if len(path) > 0 { 24 | joined_key = path + "." + key 25 | } else { 26 | joined_key = key 27 | } 28 | 29 | if cb(joined_key, val, depth) { 30 | if val != nil && reflect.TypeOf(val).Kind() == reflect.Map { 31 | traverse(cast.ToStringMap(val), joined_key, depth+1, cb) 32 | } 33 | } 34 | } 35 | } 36 | 37 | // Recursively collects all keys into a flattened slice of materialized paths. 38 | func CollectKeys(data map[string]interface{}, path string, max_depth int) []string { 39 | m := []string{} 40 | Traverse(data, func(key string, val interface{}, depth int) bool { 41 | m = append(m, key) 42 | return max_depth == -1 || depth <= max_depth 43 | }) 44 | return m 45 | } 46 | 47 | // Adapted from github.com/peterbourgon/mergemap 48 | var ( 49 | MaxDepth = 32 50 | ) 51 | 52 | // Merge recursively merges the src and dst maps. Key conflicts are resolved by 53 | // preferring src, or recursively descending, if both src and dst are maps. 54 | func Merge(dst, src map[string]interface{}) map[string]interface{} { 55 | return merge(dst, src, 0) 56 | } 57 | 58 | func merge(dst, src map[string]interface{}, depth int) map[string]interface{} { 59 | if depth > MaxDepth { 60 | panic("too deep!") 61 | } 62 | for key, srcVal := range src { 63 | if dstVal, ok := dst[key]; ok { 64 | srcMap, srcMapOk := mapify(srcVal) 65 | dstMap, dstMapOk := mapify(dstVal) 66 | if srcMapOk && dstMapOk { 67 | srcVal = merge(dstMap, srcMap, depth+1) 68 | } 69 | } 70 | dst[key] = srcVal 71 | } 72 | return dst 73 | } 74 | 75 | func mapify(i interface{}) (map[string]interface{}, bool) { 76 | v, err := cast.ToStringMapE(i) 77 | if err != nil { 78 | return v, false 79 | } else { 80 | return v, true 81 | } 82 | } 83 | 84 | // Recursively coerces all maps to a stringmap. Because that's how we want it. 85 | func ToStringMapRecursive(src map[string]interface{}) { 86 | for key, val := range src { 87 | coerced, err := cast.ToStringMapE(val) 88 | if err == nil { 89 | src[key] = coerced 90 | ToStringMapRecursive(coerced) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /reader/configreader.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "path/filepath" 9 | 10 | "github.com/BurntSushi/toml" 11 | "github.com/jacobstr/confer/errors" 12 | jww "github.com/spf13/jwalterweatherman" 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type ConfigFormat string 17 | 18 | const ( 19 | FormatYAML ConfigFormat = "yaml" 20 | FormatJSON ConfigFormat = "json" 21 | FormatTOML ConfigFormat = "toml" 22 | ) 23 | 24 | type ConfigReader struct { 25 | Format string 26 | reader io.Reader 27 | } 28 | 29 | // Retuns the configuration data into a generic object for for us. 30 | func (cr *ConfigReader) Export() (interface{}, error) { 31 | var config interface{} 32 | buf := new(bytes.Buffer) 33 | buf.ReadFrom(cr.reader) 34 | 35 | switch cr.Format { 36 | case "yaml": 37 | if err := yaml.Unmarshal(buf.Bytes(), &config); err != nil { 38 | jww.ERROR.Fatalf("Error parsing config: %s", err) 39 | } 40 | 41 | case "json": 42 | if err := json.Unmarshal(buf.Bytes(), &config); err != nil { 43 | jww.ERROR.Fatalf("Error parsing config: %s", err) 44 | } 45 | 46 | case "toml": 47 | if _, err := toml.Decode(buf.String(), &config); err != nil { 48 | jww.ERROR.Fatalf("Error parsing config: %s", err) 49 | } 50 | default: 51 | return nil, err.UnsupportedConfigError(cr.Format) 52 | } 53 | 54 | return config, nil 55 | } 56 | 57 | func ReadFile(path string) (interface{}, error) { 58 | file, err := ioutil.ReadFile(path) 59 | if err != nil { 60 | jww.DEBUG.Println("Error reading config file:", err) 61 | return nil, err 62 | } 63 | 64 | reader := bytes.NewReader(file) 65 | 66 | cr := &ConfigReader{Format: getConfigType(path), reader: reader} 67 | return cr.Export() 68 | } 69 | 70 | func ReadBytes(data []byte, format string) (interface{}, error) { 71 | cr := ConfigReader{ 72 | Format: format, 73 | reader: bytes.NewReader(data), 74 | } 75 | 76 | return cr.Export() 77 | } 78 | 79 | func getConfigType(path string) string { 80 | ext := filepath.Ext(path) 81 | switch ext[1:] { 82 | case "yml": 83 | return "yaml" 84 | default: 85 | return ext[1:] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /source/config.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/jacobstr/confer/maps" 8 | "github.com/spf13/cast" 9 | 10 | jww "github.com/spf13/jwalterweatherman" 11 | ) 12 | 13 | // Manages key/value access for a specific configuration source. Delegated to by 14 | // the over-arching config management functions that are aware of multiple config 15 | // sources and their precedence. 16 | type ConfigSource struct { 17 | // The raw configuration data. 18 | data map[string]interface{} 19 | 20 | // Hashmap of lower case keys to corresponding real key in data which we treat 21 | // as the canonical data store. 22 | index map[string]string 23 | } 24 | 25 | // Create a new case-insensitive, aliasable config map. 26 | func NewConfigSource() *ConfigSource { 27 | return &ConfigSource{ 28 | data: make(map[string]interface{}), 29 | index: make(map[string]string), 30 | } 31 | } 32 | 33 | // Get the value at a key. Case-insensitive, but preserving. 34 | func (self *ConfigSource) Get(key string) (val interface{}, exists bool) { 35 | index_key, index_exists := self.index[strings.ToLower(key)] 36 | 37 | // Exit if the index doesn't exist. We shouldn't have false negatives 38 | // unless our index falls out of sync. 39 | if index_exists == false { 40 | return nil, false 41 | } 42 | 43 | // Begin splitting the key apart. 44 | path := strings.Split(index_key, ".") 45 | current := self.data 46 | for _, part := range path[:len(path)-1] { 47 | if reflect.TypeOf(current).Kind() != reflect.Map { 48 | jww.TRACE.Println("Attempting deep access of a non-map.") 49 | return nil, false 50 | } else { 51 | var next interface{} 52 | next, exists := current[part] 53 | if exists == false { 54 | return nil, false 55 | } else { 56 | current = cast.ToStringMap(next) 57 | } 58 | } 59 | } 60 | 61 | val, exists = current[path[len(path)-1]] 62 | 63 | // Use a helper function if one is provided. 64 | switch v := val.(type) { 65 | case func() interface{}: 66 | return v(), exists 67 | default: 68 | return v, exists 69 | } 70 | } 71 | 72 | // Set a key in a case insensitive manner. 73 | func (self *ConfigSource) Set(key string, val interface{}) { 74 | index_key, index_exists := self.index[strings.ToLower(key)] 75 | if index_exists == false { 76 | index_key = key 77 | } 78 | 79 | path := strings.Split(index_key, ".") 80 | original_path := strings.Split(key, ".") 81 | 82 | current := self.data 83 | for depth, part := range path[:len(path)-1] { 84 | if reflect.TypeOf(current).Kind() != reflect.Map { 85 | panic("Attempting deep access of a non-map.") 86 | } else { 87 | var next interface{} 88 | next, exists := current[part] 89 | 90 | // Generate the index of our ancestors as we progress. 91 | ancestor_key := strings.Join(original_path[0:depth+1], ".") 92 | 93 | // Stub out ancestors if we're setting a deep child. 94 | if exists == false { 95 | current[part] = make(map[string]interface{}) 96 | current = current[part].(map[string]interface{}) 97 | self.index[strings.ToLower(ancestor_key)] = ancestor_key 98 | } else { 99 | current = next.(map[string]interface{}) 100 | } 101 | } 102 | } 103 | 104 | current[path[len(path)-1]] = val 105 | self.updateIndex(key, current) 106 | } 107 | 108 | // Replaces our configuration data with the provided stringmap, without merging. 109 | func (self *ConfigSource) FromStringMap(data map[string]interface{}) { 110 | self.data = data 111 | self.UpdateIndices() 112 | } 113 | 114 | // Returns data as a string map. 115 | func (self *ConfigSource) ToStringMap() map[string]interface{} { 116 | return self.data 117 | } 118 | 119 | // Updates our lookup table of insensitive materialized paths to their 120 | // corresponding 'real' keys. E.g. 121 | // 122 | // Database.Connections.Hosts <- database.connections.hosts 123 | // 124 | // By maintaining a separate index and maintaining case in the original 125 | // stringmaps (e.g. by lowercasing keys directly) we accomodate the passing 126 | // of config data to structures that ~may~ be case sensitive. I.E we avoid 127 | // destructive operations on configurationd data. 128 | func (self *ConfigSource) updateIndex(key string, data interface{}) { 129 | if data == nil { 130 | return 131 | } 132 | 133 | // Don't change the case of the original key if it already exists. 134 | _, index_exists := self.index[strings.ToLower(key)] 135 | if index_exists == false { 136 | self.index[strings.ToLower(key)] = key 137 | } 138 | 139 | if reflect.TypeOf(data).Kind() != reflect.Map { 140 | return 141 | } 142 | 143 | for child_key, val := range cast.ToStringMap(data) { 144 | var joined_key string 145 | if len(key) > 0 { 146 | joined_key = key + "." + child_key 147 | } else { 148 | joined_key = child_key 149 | } 150 | self.updateIndex(joined_key, val) 151 | } 152 | } 153 | 154 | // Index every key/value pair inside of this config sources's data. 155 | func (self *ConfigSource) UpdateIndices() { 156 | for key, val := range self.data { 157 | jww.TRACE.Println("update index", key) 158 | self.updateIndex(key, val) 159 | } 160 | } 161 | 162 | // Returns all the keys for this specific configuration source. 163 | func (self *ConfigSource) AllKeys() []string { 164 | return maps.CollectKeys(self.data, "", -1) 165 | } 166 | -------------------------------------------------------------------------------- /source/configger.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | // Abstract various configuration sources into something that can get and set 4 | // values, as well as return all of it's currently configured keys. 5 | type Configger interface { 6 | // Get a value. 7 | Get(key string) (val interface{}, exists bool) 8 | // Set a value. 9 | Set(key string, val interface{}) 10 | // Set data from a map[string]interface{}. 11 | FromStringMap(data map[string]interface{}) 12 | // Merge a map[string]interface{} into existing data. 13 | ToStringMap() map[string]interface{} 14 | } 15 | -------------------------------------------------------------------------------- /source/env.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | jww "github.com/spf13/jwalterweatherman" 9 | ) 10 | 11 | // A configuration data source that that reads environment variables. 12 | type EnvSource struct { 13 | index map[string]string 14 | } 15 | 16 | // Converts our materialized path format to a corresponding ENV_VAR friendly 17 | // format. Periods are replaced with single underscores. Note that reversing 18 | // this would generally be ambiguous as underscores are common in variable keys. 19 | func envamize(key string) string { 20 | return strings.Replace(strings.ToUpper(key), ".", "_", -1) 21 | } 22 | 23 | func NewEnvSource() *EnvSource { 24 | return &EnvSource{ 25 | index: make(map[string]string), 26 | } 27 | } 28 | 29 | // Essentially an environment variable specific alias. 30 | func (self *EnvSource) Bind(input ...string) (err error) { 31 | var key, envkey string 32 | 33 | if len(input) == 0 { 34 | return fmt.Errorf("BindEnv missing key to bind to") 35 | } 36 | 37 | if len(input) == 1 { 38 | key = input[0] 39 | } else { 40 | key = input[1] 41 | } 42 | 43 | envkey = envamize(key) 44 | 45 | jww.TRACE.Println(key, "Bound to", envkey) 46 | self.index[strings.ToLower(key)] = envkey 47 | 48 | return nil 49 | } 50 | 51 | func (self *EnvSource) AllKeys() []string { 52 | a := []string{} 53 | for x, _ := range self.index { 54 | a = append(a, strings.ToLower(x)) 55 | } 56 | return a 57 | } 58 | 59 | // Gets an environment variable. 60 | func (self *EnvSource) Get(key string) (val interface{}, exists bool) { 61 | envkey, exists := self.index[key] 62 | 63 | if exists { 64 | jww.TRACE.Println(key, "registered as env var", envkey) 65 | } 66 | 67 | if val = os.Getenv(envkey); val != "" { 68 | jww.TRACE.Println(envkey, "found in environment with val:", val) 69 | return val, true 70 | } else { 71 | jww.TRACE.Println(envkey, "env value unset:") 72 | return nil, false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /source/pflags.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/pflag" 7 | ) 8 | 9 | // A configuration source for pflags. 10 | type PFlagSource struct { 11 | data map[string]*pflag.Flag 12 | } 13 | 14 | func NewPFlagSource() *PFlagSource { 15 | return &PFlagSource{ 16 | data: make(map[string]*pflag.Flag), 17 | } 18 | } 19 | 20 | func (self *PFlagSource) Get(key string) (interface{}, bool) { 21 | val, exists := self.data[strings.ToLower(key)] 22 | if exists == false { 23 | return nil, false 24 | } 25 | 26 | if val.Changed { 27 | return val.Value.String(), exists 28 | } else { 29 | return val.Value.String(), false 30 | } 31 | } 32 | 33 | func (self *PFlagSource) Set(key string, val interface{}) { 34 | self.data[key] = val.(*pflag.Flag) 35 | } 36 | -------------------------------------------------------------------------------- /test/fixtures/application.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | app: 3 | logging: 4 | level: info 5 | database: 6 | host: localhost 7 | user: postgres 8 | password: spend_an_hour_tweaking_your_pg_hba_for_this 9 | server: 10 | workers: null 11 | -------------------------------------------------------------------------------- /test/fixtures/env_underscores.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | awesome_sauce: 3 | heat_level: 4 | is_radical: true 5 | -------------------------------------------------------------------------------- /test/fixtures/environments/development.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | app: 3 | root: /home/ubuntu/killer_project 4 | logging: debug 5 | server: 6 | workers: 1 7 | static_assets: 8 | - css 9 | - js 10 | - img 11 | - fonts 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/fixtures/environments/production.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | app: 3 | root: /opt/disruptive_enterprise_platform 4 | logging: debug 5 | database: 6 | host: .s.PGSQL.5432 7 | user: postgres 8 | password: whatever 9 | server: 10 | # Should clobber defaults.rb 11 | static_assets: null 12 | -------------------------------------------------------------------------------- /test/fixtures/merging.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Used to exercise attribute merging when we transition 3 | # from a number -> stringmap and vice versa. 4 | mapusers: 5 | bob: /home/bob 6 | jim: /home/jim 7 | 8 | moreusers: 9 | andy: /home/andy 10 | 11 | intusers: 5 12 | 13 | arrayusers: 14 | - bob 15 | - jim 16 | 17 | morearrayusers: 18 | - andy 19 | -------------------------------------------------------------------------------- /test/fixtures/simple.extended.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | app: 3 | hello: 4 | 10 5 | world: 6 | 5 7 | stuff: 8 | woot: 5 9 | -------------------------------------------------------------------------------- /test/fixtures/simple.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | app: 3 | hello: 5 4 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package confer 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "strings" 9 | 10 | jww "github.com/spf13/jwalterweatherman" 11 | ) 12 | 13 | // Check if File / Directory Exists 14 | func exists(path string) (bool, error) { 15 | _, err := os.Stat(path) 16 | if err == nil { 17 | return true, nil 18 | } 19 | if os.IsNotExist(err) { 20 | return false, nil 21 | } 22 | return false, err 23 | } 24 | 25 | // Check if a whole string exists in a slice of strings. 26 | func stringInSlice(a string, list []string) bool { 27 | for _, b := range list { 28 | if b == a { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | // Determines the users home directory by using os specific environment 36 | // variables. 37 | func userHomeDir() string { 38 | if runtime.GOOS == "windows" { 39 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 40 | if home == "" { 41 | home = os.Getenv("USERPROFILE") 42 | } 43 | return home 44 | } 45 | return os.Getenv("HOME") 46 | } 47 | 48 | // Determines the absolute path to a configuration file. 49 | func absPathify(inPath string) string { 50 | jww.INFO.Println("Trying to resolve absolute path to", inPath) 51 | 52 | if strings.HasPrefix(inPath, "$HOME") { 53 | inPath = userHomeDir() + inPath[5:] 54 | } 55 | 56 | if strings.HasPrefix(inPath, "$") { 57 | end := strings.Index(inPath, string(os.PathSeparator)) 58 | inPath = os.Getenv(inPath[1:end]) + inPath[end:] 59 | } 60 | 61 | if filepath.IsAbs(inPath) { 62 | return filepath.Clean(inPath) 63 | } 64 | 65 | p, err := filepath.Abs(inPath) 66 | if err == nil { 67 | return filepath.Clean(p) 68 | } else { 69 | jww.ERROR.Println("Couldn't discover absolute path") 70 | jww.ERROR.Println(err) 71 | } 72 | return "" 73 | } 74 | 75 | func findCWD() (string, error) { 76 | serverFile, err := filepath.Abs(os.Args[0]) 77 | 78 | if err != nil { 79 | return "", fmt.Errorf("Can't get absolute path for executable: %v", err) 80 | } 81 | 82 | path := filepath.Dir(serverFile) 83 | realFile, err := filepath.EvalSymlinks(serverFile) 84 | 85 | if err != nil { 86 | if _, err = os.Stat(serverFile + ".exe"); err == nil { 87 | realFile = filepath.Clean(serverFile + ".exe") 88 | } 89 | } 90 | 91 | if err == nil && realFile != serverFile { 92 | path = filepath.Dir(realFile) 93 | } 94 | 95 | return path, nil 96 | } 97 | --------------------------------------------------------------------------------