├── .travis.yml ├── LICENSE.md ├── README.md ├── gofigure.go ├── gofigure_test.go └── sources ├── commandline.go ├── commandline_test.go ├── environment.go ├── environment_test.go └── sources.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 4 | - 1.3 5 | - 1.4 6 | - 1.5 7 | - tip 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ian Kent 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gofigure [![GoDoc](https://godoc.org/github.com/ian-kent/gofigure?status.svg)](https://godoc.org/github.com/ian-kent/gofigure) [![Build Status](https://travis-ci.org/ian-kent/gofigure.svg?branch=master)](https://travis-ci.org/ian-kent/gofigure) [![Coverage Status](https://coveralls.io/repos/ian-kent/gofigure/badge.png?branch=master)](https://coveralls.io/r/ian-kent/gofigure?branch=master) 2 | ======== 3 | 4 | Go configuration made easy! 5 | 6 | - Just define a struct and call Gofigure 7 | - Supports strings, ints/uints/floats, slices and nested structs 8 | - Supports environment variables and command line flags 9 | 10 | Requires Go 1.2+ because of differences in Go's flag package. 11 | 12 | ### Example 13 | 14 | `go get github.com/ian-kent/gofigure` 15 | 16 | ```go 17 | package main 18 | 19 | import "github.com/ian-kent/gofigure" 20 | 21 | type config struct { 22 | gofigure interface{} `envPrefix:"BAR" order:"flag,env"` 23 | RemoteAddr string `env:"REMOTE_ADDR" flag:"remote-addr" flagDesc:"Remote address"` 24 | LocalAddr string `env:"LOCAL_ADDR" flag:"local-addr" flagDesc:"Local address"` 25 | NumCPU int `env:"NUM_CPU" flag:"num-cpu" flagDesc:"Number of CPUs"` 26 | Sources []string `env:"SOURCES" flag:"source" flagDesc:"Source URL (can be provided multiple times)"` 27 | Numbers []int `env:"NUMBERS" flag:"number" flagDesc:"Number (can be provided multiple times)"` 28 | Advanced struct{ 29 | MaxBytes int64 `env:"MAX_BYTES" flag:"max-bytes" flagDesc:"Max bytes"` 30 | MaxErrors int64 `env:"MAX_ERRORS" flag:"max-errors" flagDesc:"Max errors"` 31 | } 32 | } 33 | 34 | func main() { 35 | var cfg config 36 | err := gofigure.Gofigure(&cfg) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | // use cfg 41 | } 42 | ``` 43 | 44 | ### gofigure field 45 | 46 | The gofigure field is used to configure Gofigure. 47 | 48 | The `order` tag is used to set configuration source order, e.g. 49 | environment variables first then command line options second. 50 | 51 | Any field matching `camelCase` format will be parsed into `camel` 52 | and `case`, and passed to the source matching `camel`. 53 | 54 | For example, the `envPrefix` field is split into `env` and `prefix`, 55 | and the tag value is passed to the environment variable source as 56 | the `prefix` parameter. 57 | 58 | ### Arrays and environment variables 59 | 60 | Array support for environment variables is currently experimental. 61 | 62 | To enable it, set `GOFIGURE_ENV_ARRAY=1`. 63 | 64 | When enabled, the environment variable is split on commas, e.g. 65 | 66 | ``` 67 | struct { 68 | EnvArray []string `env:"MY_ENV_VAR"` 69 | } 70 | 71 | MY_ENV_VAR=a,b,c 72 | 73 | EnvArray = []string{"a", "b", "c"} 74 | ``` 75 | 76 | ### Licence 77 | 78 | Copyright ©‎ 2014, Ian Kent (http://www.iankent.eu). 79 | 80 | Released under MIT license, see [LICENSE](LICENSE.md) for details. 81 | -------------------------------------------------------------------------------- /gofigure.go: -------------------------------------------------------------------------------- 1 | // Package gofigure simplifies configuration of Go applications. 2 | // 3 | // Define a struct and call Gofigure() 4 | package gofigure 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "log" 10 | "os" 11 | "reflect" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/ian-kent/gofigure/sources" 17 | ) 18 | 19 | // Debug controls log output 20 | var Debug = false 21 | 22 | func init() { 23 | env := os.Getenv("GOFIGURE_DEBUG") 24 | if len(env) > 0 { 25 | Debug, _ = strconv.ParseBool(env) 26 | } 27 | 28 | sources.Logger = printf 29 | sources.Debug = Debug 30 | return 31 | } 32 | 33 | func printf(message string, args ...interface{}) { 34 | if !Debug { 35 | return 36 | } 37 | log.Printf(message, args...) 38 | } 39 | 40 | /* TODO 41 | * - Add file/http sources 42 | * - Add "decoders", e.g. json/env/xml 43 | * - Default value (if gofigure is func()*StructType) 44 | * - Ignore lowercased "unexported" fields? 45 | */ 46 | 47 | // gofiguration represents a parsed struct 48 | type gofiguration struct { 49 | order []string 50 | params map[string]map[string]string 51 | fields map[string]*gofiguritem 52 | flagged bool 53 | parent *gofiguration 54 | children []*gofiguration 55 | s interface{} 56 | } 57 | 58 | func (gfg *gofiguration) printf(message string, args ...interface{}) { 59 | printf(message, args...) 60 | } 61 | 62 | // gofiguritem represents a single struct field 63 | type gofiguritem struct { 64 | keys map[string]string 65 | field string 66 | goField reflect.StructField 67 | goValue reflect.Value 68 | inner *gofiguration 69 | } 70 | 71 | // Sources contains a map of struct field tag names to source implementation 72 | var Sources = map[string]sources.Source{ 73 | "env": &sources.Environment{}, 74 | "flag": &sources.CommandLine{}, 75 | } 76 | 77 | // DefaultOrder sets the default order used 78 | var DefaultOrder = []string{"env", "flag"} 79 | 80 | // ErrUnsupportedType is returned if the interface isn't a 81 | // pointer to a struct 82 | var ErrUnsupportedType = errors.New("Unsupported interface type") 83 | 84 | // ErrInvalidOrder is returned if the "order" struct tag is invalid 85 | var ErrInvalidOrder = errors.New("Invalid order") 86 | 87 | // ErrUnsupportedFieldType is returned for unsupported field types, 88 | // e.g. chan or func 89 | var ErrUnsupportedFieldType = errors.New("Unsupported field type") 90 | 91 | // ParseStruct creates a gofiguration from a struct. 92 | // 93 | // It returns ErrUnsupportedType if s is not a struct or a 94 | // pointer to a struct. 95 | func parseStruct(s interface{}) (*gofiguration, error) { 96 | var v reflect.Value 97 | if reflect.TypeOf(s) != reflect.TypeOf(v) { 98 | v = reflect.ValueOf(s) 99 | 100 | if v.Kind() == reflect.PtrTo(reflect.TypeOf(s)).Kind() { 101 | v = reflect.Indirect(v) 102 | } 103 | } else { 104 | v = s.(reflect.Value) 105 | } 106 | 107 | if v.Kind() != reflect.Struct { 108 | return nil, ErrUnsupportedType 109 | } 110 | 111 | t := v.Type() 112 | 113 | gfg := &gofiguration{ 114 | params: make(map[string]map[string]string), 115 | order: DefaultOrder, 116 | fields: make(map[string]*gofiguritem), 117 | s: s, 118 | } 119 | 120 | err := gfg.parseGofigureField(t) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | gfg.parseFields(v, t) 126 | 127 | return gfg, nil 128 | } 129 | 130 | func getStructTags(tag string) map[string]string { 131 | // http://golang.org/src/pkg/reflect/type.go?s=20885:20906#L747 132 | m := make(map[string]string) 133 | for tag != "" { 134 | // skip leading space 135 | i := 0 136 | for i < len(tag) && tag[i] == ' ' { 137 | i++ 138 | } 139 | tag = tag[i:] 140 | if tag == "" { 141 | break 142 | } 143 | 144 | // scan to colon. 145 | // a space or a quote is a syntax error 146 | i = 0 147 | for i < len(tag) && tag[i] != ' ' && tag[i] != ':' && tag[i] != '"' { 148 | i++ 149 | } 150 | if i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' { 151 | break 152 | } 153 | name := string(tag[:i]) 154 | tag = tag[i+1:] 155 | 156 | // scan quoted string to find value 157 | i = 1 158 | for i < len(tag) && tag[i] != '"' { 159 | if tag[i] == '\\' { 160 | i++ 161 | } 162 | i++ 163 | } 164 | if i >= len(tag) { 165 | break 166 | } 167 | qvalue := string(tag[:i+1]) 168 | tag = tag[i+1:] 169 | 170 | value, _ := strconv.Unquote(qvalue) 171 | m[name] = value 172 | } 173 | return m 174 | } 175 | 176 | var argRe = regexp.MustCompile("([a-z]+)([A-Z][a-z]+)") 177 | 178 | func (gfg *gofiguration) parseGofigureField(t reflect.Type) error { 179 | gf, ok := t.FieldByName("gofigure") 180 | if ok { 181 | tags := getStructTags(string(gf.Tag)) 182 | for name, value := range tags { 183 | if name == "order" { 184 | oParts := strings.Split(value, ",") 185 | for _, p := range oParts { 186 | if _, ok := Sources[p]; !ok { 187 | return ErrInvalidOrder 188 | } 189 | } 190 | gfg.order = oParts 191 | continue 192 | } 193 | // Parse orderKey:"value" tags, e.g. 194 | // envPrefix, which gets split into 195 | // gfg.params["env"]["prefix"] = "value" 196 | // gfg.params["env"] is then passed to 197 | // source registered with that key 198 | match := argRe.FindStringSubmatch(name) 199 | if len(match) > 1 { 200 | if _, ok := gfg.params[match[1]]; !ok { 201 | gfg.params[match[1]] = make(map[string]string) 202 | } 203 | gfg.params[match[1]][strings.ToLower(match[2])] = value 204 | } 205 | } 206 | } 207 | return nil 208 | } 209 | 210 | func (gfg *gofiguration) parseFields(v reflect.Value, t reflect.Type) { 211 | gfg.printf("Found %d fields", t.NumField()) 212 | for i := 0; i < t.NumField(); i++ { 213 | f := t.Field(i).Name 214 | if f == "gofigure" { 215 | gfg.printf("Skipped field '%s'", f) 216 | continue 217 | } 218 | 219 | gfg.printf("Parsed field '%s'", f) 220 | 221 | gfi := &gofiguritem{ 222 | field: f, 223 | goField: t.Field(i), 224 | goValue: v.Field(i), 225 | keys: make(map[string]string), 226 | } 227 | tag := t.Field(i).Tag 228 | if len(tag) > 0 { 229 | gfi.keys = getStructTags(string(tag)) 230 | } 231 | gfg.fields[f] = gfi 232 | } 233 | } 234 | 235 | func (gfg *gofiguration) cleanupSources() { 236 | for _, o := range gfg.order { 237 | Sources[o].Cleanup() 238 | } 239 | } 240 | 241 | func (gfg *gofiguration) initSources() error { 242 | for _, o := range gfg.order { 243 | err := Sources[o].Init(gfg.params[o]) 244 | if err != nil { 245 | return err 246 | } 247 | } 248 | return nil 249 | } 250 | 251 | func (gfg *gofiguration) registerFields() error { 252 | for _, gfi := range gfg.fields { 253 | kn := gfi.field 254 | 255 | var err error 256 | switch gfi.goField.Type.Kind() { 257 | case reflect.Struct: 258 | gfg.printf("Registering as struct type") 259 | // TODO do shit 260 | sGfg, err := parseStruct(gfi.goValue) 261 | if err != nil { 262 | return err 263 | } 264 | sGfg.apply(gfg) 265 | gfi.inner = sGfg 266 | default: 267 | gfg.printf("Registering as default type") 268 | for _, o := range gfg.order { 269 | if k, ok := gfi.keys[o]; ok { 270 | kn = k 271 | } 272 | gfg.printf("Registering '%s' for source '%s' with key '%s'", gfi.field, o, kn) 273 | err = Sources[o].Register(kn, "", gfi.keys, gfi.goField.Type) 274 | } 275 | } 276 | 277 | if err != nil { 278 | return err 279 | } 280 | } 281 | return nil 282 | } 283 | 284 | func numVal(i string) string { 285 | if len(i) == 0 { 286 | return "0" 287 | } 288 | return i 289 | } 290 | 291 | func (gfi *gofiguritem) populateDefaultType(order []string) error { 292 | // FIXME could just preserve types 293 | var v string 294 | switch gfi.goField.Type.Kind() { 295 | case reflect.Bool: 296 | v = fmt.Sprintf("%t", gfi.goValue.Bool()) 297 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 298 | v = fmt.Sprintf("%d", gfi.goValue.Int()) 299 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 300 | v = fmt.Sprintf("%d", gfi.goValue.Uint()) 301 | case reflect.Float32, reflect.Float64: 302 | v = fmt.Sprintf("%f", gfi.goValue.Float()) 303 | case reflect.String: 304 | v = gfi.goValue.String() 305 | } 306 | 307 | var prevVal = &v 308 | 309 | for _, source := range order { 310 | kn := gfi.field 311 | if k, ok := gfi.keys[source]; ok { 312 | kn = k 313 | } 314 | 315 | val, err := Sources[source].Get(kn, prevVal) 316 | if err != nil { 317 | return err 318 | } 319 | 320 | prevVal = &val 321 | 322 | printf("Got value '%s' from source '%s' for key '%s'", val, source, gfi.field) 323 | 324 | switch gfi.goField.Type.Kind() { 325 | case reflect.Bool: 326 | if len(val) == 0 { 327 | printf("Setting bool value to false") 328 | val = "false" 329 | } 330 | b, err := strconv.ParseBool(val) 331 | if err != nil { 332 | return err 333 | } 334 | gfi.goValue.SetBool(b) 335 | case reflect.Int: 336 | i, err := strconv.ParseInt(numVal(val), 10, 64) 337 | if err != nil { 338 | return err 339 | } 340 | gfi.goValue.SetInt(i) 341 | case reflect.Int8: 342 | i, err := strconv.ParseInt(numVal(val), 10, 8) 343 | if err != nil { 344 | return err 345 | } 346 | gfi.goValue.SetInt(i) 347 | case reflect.Int16: 348 | i, err := strconv.ParseInt(numVal(val), 10, 16) 349 | if err != nil { 350 | return err 351 | } 352 | gfi.goValue.SetInt(i) 353 | case reflect.Int32: 354 | i, err := strconv.ParseInt(numVal(val), 10, 32) 355 | if err != nil { 356 | return err 357 | } 358 | gfi.goValue.SetInt(i) 359 | case reflect.Int64: 360 | i, err := strconv.ParseInt(numVal(val), 10, 64) 361 | if err != nil { 362 | return err 363 | } 364 | gfi.goValue.SetInt(i) 365 | case reflect.Uint: 366 | i, err := strconv.ParseUint(numVal(val), 10, 64) 367 | if err != nil { 368 | return err 369 | } 370 | gfi.goValue.SetUint(i) 371 | case reflect.Uint8: 372 | i, err := strconv.ParseUint(numVal(val), 10, 8) 373 | if err != nil { 374 | return err 375 | } 376 | gfi.goValue.SetUint(i) 377 | case reflect.Uint16: 378 | i, err := strconv.ParseUint(numVal(val), 10, 16) 379 | if err != nil { 380 | return err 381 | } 382 | gfi.goValue.SetUint(i) 383 | case reflect.Uint32: 384 | i, err := strconv.ParseUint(numVal(val), 10, 32) 385 | if err != nil { 386 | return err 387 | } 388 | gfi.goValue.SetUint(i) 389 | case reflect.Uint64: 390 | i, err := strconv.ParseUint(numVal(val), 10, 64) 391 | if err != nil { 392 | return err 393 | } 394 | gfi.goValue.SetUint(i) 395 | case reflect.Float32: 396 | f, err := strconv.ParseFloat(numVal(val), 32) 397 | if err != nil { 398 | return err 399 | } 400 | gfi.goValue.SetFloat(f) 401 | case reflect.Float64: 402 | f, err := strconv.ParseFloat(numVal(val), 64) 403 | if err != nil { 404 | return err 405 | } 406 | gfi.goValue.SetFloat(f) 407 | case reflect.String: 408 | gfi.goValue.SetString(val) 409 | default: 410 | return ErrUnsupportedFieldType 411 | } 412 | } 413 | 414 | return nil 415 | } 416 | 417 | func (gfi *gofiguritem) populateSliceType(order []string) error { 418 | var prevVal *[]string 419 | 420 | for _, source := range order { 421 | kn := gfi.field 422 | if k, ok := gfi.keys[source]; ok { 423 | kn = k 424 | } 425 | 426 | printf("Looking for field '%s' with key '%s' in source '%s'", gfi.field, kn, source) 427 | val, err := Sources[source].GetArray(kn, prevVal) 428 | if err != nil { 429 | return err 430 | } 431 | 432 | // This causes duplication between array sources depending on order 433 | //prevVal = &val 434 | 435 | printf("Got value '%+v' from array source '%s' for key '%s'", val, source, gfi.field) 436 | 437 | switch gfi.goField.Type.Kind() { 438 | case reflect.Slice: 439 | switch gfi.goField.Type.Elem().Kind() { 440 | case reflect.String: 441 | for _, s := range val { 442 | printf("Appending string value '%s' to slice", s) 443 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(s))) 444 | } 445 | case reflect.Int: 446 | for _, s := range val { 447 | printf("Appending int value '%s' to slice", s) 448 | i, err := strconv.ParseInt(numVal(s), 10, 64) 449 | if err != nil { 450 | return err 451 | } 452 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(int(i)))) 453 | } 454 | case reflect.Int8: 455 | for _, s := range val { 456 | printf("Appending int8 value '%s' to slice", s) 457 | i, err := strconv.ParseInt(numVal(s), 10, 8) 458 | if err != nil { 459 | return err 460 | } 461 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(int8(i)))) 462 | } 463 | case reflect.Int16: 464 | for _, s := range val { 465 | printf("Appending int16 value '%s' to slice", s) 466 | i, err := strconv.ParseInt(numVal(s), 10, 16) 467 | if err != nil { 468 | return err 469 | } 470 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(int16(i)))) 471 | } 472 | case reflect.Int32: 473 | for _, s := range val { 474 | printf("Appending int32 value '%s' to slice", s) 475 | i, err := strconv.ParseInt(numVal(s), 10, 32) 476 | if err != nil { 477 | return err 478 | } 479 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(int32(i)))) 480 | } 481 | case reflect.Int64: 482 | for _, s := range val { 483 | printf("Appending int64 value '%s' to slice", s) 484 | i, err := strconv.ParseInt(numVal(s), 10, 64) 485 | if err != nil { 486 | return err 487 | } 488 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(int64(i)))) 489 | } 490 | case reflect.Uint: 491 | for _, s := range val { 492 | printf("Appending uint value '%s' to slice", s) 493 | i, err := strconv.ParseUint(numVal(s), 10, 64) 494 | if err != nil { 495 | return err 496 | } 497 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(uint(i)))) 498 | } 499 | case reflect.Uint8: 500 | for _, s := range val { 501 | printf("Appending uint8 value '%s' to slice", s) 502 | i, err := strconv.ParseUint(numVal(s), 10, 8) 503 | if err != nil { 504 | return err 505 | } 506 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(uint8(i)))) 507 | } 508 | case reflect.Uint16: 509 | for _, s := range val { 510 | printf("Appending uint16 value '%s' to slice", s) 511 | i, err := strconv.ParseUint(numVal(s), 10, 16) 512 | if err != nil { 513 | return err 514 | } 515 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(uint16(i)))) 516 | } 517 | case reflect.Uint32: 518 | for _, s := range val { 519 | printf("Appending uint32 value '%s' to slice", s) 520 | i, err := strconv.ParseUint(numVal(s), 10, 32) 521 | if err != nil { 522 | return err 523 | } 524 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(uint32(i)))) 525 | } 526 | case reflect.Uint64: 527 | for _, s := range val { 528 | printf("Appending uint64 value '%s' to slice", s) 529 | i, err := strconv.ParseUint(numVal(s), 10, 64) 530 | if err != nil { 531 | return err 532 | } 533 | gfi.goValue.Set(reflect.Append(gfi.goValue, reflect.ValueOf(uint64(i)))) 534 | } 535 | // TODO floats 536 | default: 537 | //return ErrUnsupportedFieldType 538 | } 539 | } 540 | } 541 | 542 | return nil 543 | } 544 | 545 | func (gfi *gofiguritem) populateStructType(order []string) error { 546 | return gfi.inner.populateStruct() 547 | } 548 | 549 | func (gfg *gofiguration) populateStruct() error { 550 | if gfg == nil { 551 | return nil 552 | } 553 | 554 | for _, gfi := range gfg.fields { 555 | printf("Populating field %s", gfi.field) 556 | switch gfi.goField.Type.Kind() { 557 | case reflect.Invalid, reflect.Uintptr, reflect.Complex64, 558 | reflect.Complex128, reflect.Chan, reflect.Func, 559 | reflect.Ptr, reflect.UnsafePointer: 560 | return ErrUnsupportedFieldType 561 | case reflect.Interface: 562 | // TODO 563 | return ErrUnsupportedFieldType 564 | case reflect.Map: 565 | // TODO 566 | return ErrUnsupportedFieldType 567 | case reflect.Slice: 568 | printf("Calling populateSliceType") 569 | err := gfi.populateSliceType(gfg.order) 570 | if err != nil { 571 | return err 572 | } 573 | case reflect.Struct: 574 | printf("Calling populateStructType") 575 | err := gfi.populateStructType(gfg.order) 576 | if err != nil { 577 | return err 578 | } 579 | case reflect.Array: 580 | // TODO 581 | return ErrUnsupportedFieldType 582 | default: 583 | printf("Calling populateDefaultType") 584 | err := gfi.populateDefaultType(gfg.order) 585 | if err != nil { 586 | return err 587 | } 588 | } 589 | } 590 | 591 | for _, c := range gfg.children { 592 | err := c.populateStruct() 593 | if err != nil { 594 | return err 595 | } 596 | } 597 | 598 | return nil 599 | } 600 | 601 | // Apply applies the gofiguration to the struct 602 | func (gfg *gofiguration) apply(parent *gofiguration) error { 603 | gfg.parent = parent 604 | 605 | if parent == nil { 606 | defer gfg.cleanupSources() 607 | 608 | err := gfg.initSources() 609 | if err != nil { 610 | return err 611 | } 612 | } else { 613 | parent.children = append(parent.children, gfg) 614 | } 615 | 616 | err := gfg.registerFields() 617 | if err != nil { 618 | return err 619 | } 620 | 621 | if parent == nil { 622 | return gfg.populateStruct() 623 | } 624 | 625 | return nil 626 | } 627 | 628 | // Gofigure parses and applies the configuration defined by the struct. 629 | // 630 | // It returns ErrUnsupportedType if s is not a pointer to a struct. 631 | func Gofigure(s interface{}) error { 632 | gfg, err := parseStruct(s) 633 | if err != nil { 634 | return err 635 | } 636 | return gfg.apply(nil) 637 | } 638 | -------------------------------------------------------------------------------- /gofigure_test.go: -------------------------------------------------------------------------------- 1 | package gofigure 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "reflect" 9 | "testing" 10 | 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | // TODO 15 | // - flagDesc 16 | // - flag and env default keys 17 | 18 | func ExampleGofigure() { 19 | os.Args = []string{"gofigure", "-remote-addr", "localhost:8080"} 20 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 21 | 22 | type example struct { 23 | gofigure interface{} `envPrefix:"BAR" order:"flag,env"` 24 | RemoteAddr string `env:"REMOTE_ADDR" flag:"remote-addr" flagDesc:"Remote address"` 25 | LocalAddr string `env:"LOCAL_ADDR" flag:"local-addr" flagDesc:"Local address"` 26 | NumCPU int `env:"NUM_CPU" flag:"num-cpu" flagDesc:"Number of CPUs"` 27 | Sources []string `env:"SOURCES" flag:"source" flagDesc:"Source URL (can be provided multiple times)"` 28 | Numbers []int `env:"NUMBERS" flag:"number" flagDesc:"Number (can be provided multiple times)"` 29 | } 30 | 31 | var cfg example 32 | 33 | // Pass a reference to Gofigure 34 | err := Gofigure(&cfg) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | // Fields on cfg should be set! 40 | fmt.Printf("%+v", cfg) 41 | } 42 | 43 | func ExampleGofigure_withDefault() { 44 | os.Args = []string{"gofigure", "-remote-addr", "localhost:8080"} 45 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 46 | 47 | type example struct { 48 | gofigure interface{} `envPrefix:"BAR" order:"flag,env"` 49 | RemoteAddr string `env:"REMOTE_ADDR" flag:"remote-addr" flagDesc:"Remote address"` 50 | LocalAddr string `env:"LOCAL_ADDR" flag:"local-addr" flagDesc:"Local address"` 51 | NumCPU int `env:"NUM_CPU" flag:"num-cpu" flagDesc:"Number of CPUs"` 52 | Sources []string `env:"SOURCES" flag:"source" flagDesc:"Source URL (can be provided multiple times)"` 53 | Numbers []int `env:"NUMBERS" flag:"number" flagDesc:"Number (can be provided multiple times)"` 54 | } 55 | 56 | var cfg = example{ 57 | RemoteAddr: "localhost:6060", 58 | LocalAddr: "localhost:49808", 59 | NumCPU: 10, 60 | Sources: []string{"test1.local", "test2.local"}, 61 | Numbers: []int{1, 2, 3}, 62 | } 63 | 64 | // Pass a reference to Gofigure 65 | err := Gofigure(&cfg) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | // Fields on cfg should be set! 71 | fmt.Printf("%+v", cfg) 72 | } 73 | 74 | func ExampleGofigure_withNestedStruct() { 75 | os.Args = []string{"gofigure", "-remote-addr", "localhost:8080", "-local-addr", "localhost:49808"} 76 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 77 | 78 | type example struct { 79 | gofigure interface{} `envPrefix:"BAR" order:"flag,env"` 80 | RemoteAddr string `env:"REMOTE_ADDR" flag:"remote-addr" flagDesc:"Remote address"` 81 | Advanced struct { 82 | LocalAddr string `env:"LOCAL_ADDR" flag:"local-addr" flagDesc:"Local address"` 83 | } 84 | } 85 | 86 | var cfg example 87 | 88 | // Pass a reference to Gofigure 89 | err := Gofigure(&cfg) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | 94 | // Fields on cfg should be set! 95 | fmt.Printf("%+v", cfg) 96 | } 97 | 98 | func clear() { 99 | os.Clearenv() 100 | os.Args = []string{"gofigure"} 101 | } 102 | 103 | // MyConfigFoo is a basic test struct 104 | type MyConfigFoo struct { 105 | gofigure interface{} `envPrefix:"FOO" order:"env,flag"` 106 | BindAddr string `env:"BIND_ADDR" flag:"bind-addr"` 107 | } 108 | 109 | // MyConfigBar is a basic test struct with multiple fields 110 | type MyConfigBar struct { 111 | gofigure interface{} `envPrefix:"BAR" order:"flag,env"` 112 | RemoteAddr string `env:"REMOTE_ADDR" flag:"remote-addr"` 113 | LocalAddr string `env:"LOCAL_ADDR" flag:"local-addr"` 114 | } 115 | 116 | // MyConfigBaz is used to test invalid order values 117 | type MyConfigBaz struct { 118 | gofigure interface{} `order:"FOO,BAR"` 119 | } 120 | 121 | // MyConfigBay is used to test invalid envPrefix values 122 | type MyConfigBay struct { 123 | gofigure interface{} `envPrefix:"!"` 124 | } 125 | 126 | // MyConfigFull is used to test Go type support 127 | type MyConfigFull struct { 128 | gofigure interface{} 129 | BoolField bool 130 | IntField int 131 | Int8Field int8 132 | Int16Field int16 133 | Int32Field int32 134 | Int64Field int64 135 | UintField uint 136 | Uint8Field uint8 137 | Uint16Field uint16 138 | Uint32Field uint32 139 | Uint64Field uint64 140 | Float32Field float32 141 | Float64Field float64 142 | ArrayIntField []int 143 | ArrayStringField []string 144 | } 145 | 146 | func TestParseStruct(t *testing.T) { 147 | Convey("parseStruct should return an error unless given a pointer to a struct", t, func() { 148 | info, e := parseStruct(1) 149 | So(e, ShouldNotBeNil) 150 | So(e, ShouldEqual, ErrUnsupportedType) 151 | So(info, ShouldBeNil) 152 | }) 153 | 154 | Convey("parseStruct should keep a reference to the struct", t, func() { 155 | ref := &MyConfigFoo{} 156 | info, e := parseStruct(ref) 157 | So(e, ShouldBeNil) 158 | So(info, ShouldNotBeNil) 159 | So(info.s, ShouldEqual, ref) 160 | }) 161 | 162 | Convey("parseStruct should read gofigure envPrefix tag correctly", t, func() { 163 | info, e := parseStruct(&MyConfigFoo{}) 164 | So(e, ShouldBeNil) 165 | So(info, ShouldNotBeNil) 166 | So(info.params["env"]["prefix"], ShouldEqual, "FOO") 167 | 168 | info, e = parseStruct(&MyConfigBar{}) 169 | So(e, ShouldBeNil) 170 | So(info, ShouldNotBeNil) 171 | So(info.params["env"]["prefix"], ShouldEqual, "BAR") 172 | }) 173 | 174 | Convey("parseStruct should read gofigure order tag correctly", t, func() { 175 | info, e := parseStruct(&MyConfigFoo{}) 176 | So(e, ShouldBeNil) 177 | So(info, ShouldNotBeNil) 178 | So(info.order, ShouldResemble, []string{"env", "flag"}) 179 | 180 | info, e = parseStruct(&MyConfigBar{}) 181 | So(e, ShouldBeNil) 182 | So(info, ShouldNotBeNil) 183 | So(info.order, ShouldResemble, []string{"flag", "env"}) 184 | }) 185 | 186 | Convey("Invalid order should return error", t, func() { 187 | info, e := parseStruct(&MyConfigBaz{}) 188 | So(e, ShouldNotBeNil) 189 | So(e, ShouldEqual, ErrInvalidOrder) 190 | So(info, ShouldBeNil) 191 | }) 192 | 193 | Convey("parseStruct should read fields correctly", t, func() { 194 | info, e := parseStruct(&MyConfigFoo{}) 195 | So(e, ShouldBeNil) 196 | So(info, ShouldNotBeNil) 197 | So(len(info.fields), ShouldEqual, 1) 198 | _, ok := info.fields["BindAddr"] 199 | So(ok, ShouldEqual, true) 200 | So(info.fields["BindAddr"].field, ShouldEqual, "BindAddr") 201 | So(info.fields["BindAddr"].keys["env"], ShouldEqual, "BIND_ADDR") 202 | So(info.fields["BindAddr"].keys["flag"], ShouldEqual, "bind-addr") 203 | So(info.fields["BindAddr"].goField, ShouldNotBeNil) 204 | So(info.fields["BindAddr"].goField.Type.Kind(), ShouldEqual, reflect.String) 205 | So(info.fields["BindAddr"].goValue, ShouldNotBeNil) 206 | 207 | info, e = parseStruct(&MyConfigBar{}) 208 | So(e, ShouldBeNil) 209 | So(info, ShouldNotBeNil) 210 | So(len(info.fields), ShouldEqual, 2) 211 | _, ok = info.fields["RemoteAddr"] 212 | So(ok, ShouldEqual, true) 213 | So(info.fields["RemoteAddr"].field, ShouldEqual, "RemoteAddr") 214 | So(info.fields["RemoteAddr"].keys["env"], ShouldEqual, "REMOTE_ADDR") 215 | So(info.fields["RemoteAddr"].keys["flag"], ShouldEqual, "remote-addr") 216 | So(info.fields["RemoteAddr"].goField, ShouldNotBeNil) 217 | So(info.fields["RemoteAddr"].goField.Type.Kind(), ShouldEqual, reflect.String) 218 | So(info.fields["RemoteAddr"].goValue, ShouldNotBeNil) 219 | _, ok = info.fields["LocalAddr"] 220 | So(ok, ShouldEqual, true) 221 | So(info.fields["LocalAddr"].field, ShouldEqual, "LocalAddr") 222 | So(info.fields["LocalAddr"].keys["env"], ShouldEqual, "LOCAL_ADDR") 223 | So(info.fields["LocalAddr"].keys["flag"], ShouldEqual, "local-addr") 224 | So(info.fields["LocalAddr"].goField, ShouldNotBeNil) 225 | So(info.fields["LocalAddr"].goField.Type.Kind(), ShouldEqual, reflect.String) 226 | So(info.fields["LocalAddr"].goValue, ShouldNotBeNil) 227 | }) 228 | 229 | clear() 230 | } 231 | 232 | func TestGofigure(t *testing.T) { 233 | Convey("Gofigure should set field values", t, func() { 234 | os.Clearenv() 235 | os.Args = []string{"gofigure", "-bind-addr", "abcdef"} 236 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 237 | var cfg MyConfigFoo 238 | err := Gofigure(&cfg) 239 | So(err, ShouldBeNil) 240 | So(cfg, ShouldNotBeNil) 241 | So(cfg.BindAddr, ShouldEqual, "abcdef") 242 | }) 243 | 244 | Convey("Gofigure should set multiple field values", t, func() { 245 | os.Clearenv() 246 | os.Args = []string{"gofigure", "-remote-addr", "foo", "-local-addr", "bar"} 247 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 248 | var cfg2 MyConfigBar 249 | err := Gofigure(&cfg2) 250 | So(err, ShouldBeNil) 251 | So(cfg2, ShouldNotBeNil) 252 | So(cfg2.RemoteAddr, ShouldEqual, "foo") 253 | So(cfg2.LocalAddr, ShouldEqual, "bar") 254 | }) 255 | 256 | Convey("Gofigure should support environment variables", t, func() { 257 | os.Clearenv() 258 | os.Args = []string{"gofigure"} 259 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 260 | os.Setenv("FOO_BIND_ADDR", "bindaddr") 261 | var cfg MyConfigFoo 262 | err := Gofigure(&cfg) 263 | So(err, ShouldBeNil) 264 | So(cfg, ShouldNotBeNil) 265 | So(cfg.BindAddr, ShouldEqual, "bindaddr") 266 | }) 267 | 268 | Convey("Gofigure should preserve order", t, func() { 269 | os.Clearenv() 270 | os.Args = []string{"gofigure", "-bind-addr", "abc"} 271 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 272 | os.Setenv("FOO_BIND_ADDR", "def") 273 | var cfg MyConfigFoo 274 | err := Gofigure(&cfg) 275 | So(err, ShouldBeNil) 276 | So(cfg, ShouldNotBeNil) 277 | So(cfg.BindAddr, ShouldEqual, "abc") 278 | 279 | os.Clearenv() 280 | os.Args = []string{"gofigure", "-remote-addr", "abc"} 281 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 282 | os.Setenv("BAR_REMOTE_ADDR", "def") 283 | var cfg2 MyConfigBar 284 | err = Gofigure(&cfg2) 285 | So(err, ShouldBeNil) 286 | So(cfg2, ShouldNotBeNil) 287 | So(cfg2.RemoteAddr, ShouldEqual, "def") 288 | }) 289 | 290 | clear() 291 | } 292 | 293 | func TestBoolField(t *testing.T) { 294 | Convey("Can set a bool field to true (flag)", t, func() { 295 | os.Clearenv() 296 | os.Args = []string{ 297 | "gofigure", 298 | "-bool-field", "true", 299 | } 300 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 301 | var cfg MyConfigFull 302 | err := Gofigure(&cfg) 303 | So(err, ShouldBeNil) 304 | So(cfg, ShouldNotBeNil) 305 | So(cfg.BoolField, ShouldEqual, true) 306 | }) 307 | 308 | Convey("Can set a bool field to false (flag)", t, func() { 309 | os.Clearenv() 310 | os.Args = []string{ 311 | "gofigure", 312 | "-bool-field", "false", 313 | } 314 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 315 | var cfg MyConfigFull 316 | err := Gofigure(&cfg) 317 | So(err, ShouldBeNil) 318 | So(cfg, ShouldNotBeNil) 319 | So(cfg.BoolField, ShouldEqual, false) 320 | }) 321 | 322 | Convey("Can set a bool field to true (env)", t, func() { 323 | os.Clearenv() 324 | os.Args = []string{ 325 | "gofigure", 326 | } 327 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 328 | os.Setenv("BOOL_FIELD", "true") 329 | var cfg MyConfigFull 330 | err := Gofigure(&cfg) 331 | So(err, ShouldBeNil) 332 | So(cfg, ShouldNotBeNil) 333 | So(cfg.BoolField, ShouldEqual, true) 334 | }) 335 | 336 | Convey("Can set a bool field to false (env)", t, func() { 337 | os.Clearenv() 338 | os.Args = []string{ 339 | "gofigure", 340 | } 341 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 342 | os.Setenv("BOOL_FIELD", "false") 343 | var cfg MyConfigFull 344 | err := Gofigure(&cfg) 345 | So(err, ShouldBeNil) 346 | So(cfg, ShouldNotBeNil) 347 | So(cfg.BoolField, ShouldEqual, false) 348 | }) 349 | 350 | Convey("Not setting a bool field gives false", t, func() { 351 | os.Clearenv() 352 | os.Args = []string{ 353 | "gofigure", 354 | } 355 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 356 | var cfg MyConfigFull 357 | err := Gofigure(&cfg) 358 | So(err, ShouldBeNil) 359 | So(cfg, ShouldNotBeNil) 360 | So(cfg.BoolField, ShouldEqual, false) 361 | }) 362 | 363 | clear() 364 | } 365 | 366 | func TestIntField(t *testing.T) { 367 | Convey("Can set int fields (flag)", t, func() { 368 | os.Clearenv() 369 | os.Args = []string{ 370 | "gofigure", 371 | "-int-field", "123", 372 | "-int8-field", "2", 373 | "-int16-field", "10", 374 | "-int32-field", "33", 375 | "-int64-field", "81", 376 | } 377 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 378 | var cfg MyConfigFull 379 | err := Gofigure(&cfg) 380 | So(err, ShouldBeNil) 381 | So(cfg, ShouldNotBeNil) 382 | So(cfg.IntField, ShouldEqual, 123) 383 | So(cfg.Int8Field, ShouldEqual, 2) 384 | So(cfg.Int16Field, ShouldEqual, 10) 385 | So(cfg.Int32Field, ShouldEqual, 33) 386 | So(cfg.Int64Field, ShouldEqual, 81) 387 | }) 388 | 389 | Convey("Can set int fields to negative values (flag)", t, func() { 390 | os.Args = []string{ 391 | "gofigure", 392 | "-int-field", "-123", 393 | "-int8-field", "-2", 394 | "-int16-field", "-10", 395 | "-int32-field", "-33", 396 | "-int64-field", "-81", 397 | } 398 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 399 | var cfg MyConfigFull 400 | err := Gofigure(&cfg) 401 | So(err, ShouldBeNil) 402 | So(cfg, ShouldNotBeNil) 403 | So(cfg.IntField, ShouldEqual, -123) 404 | So(cfg.Int8Field, ShouldEqual, -2) 405 | So(cfg.Int16Field, ShouldEqual, -10) 406 | So(cfg.Int32Field, ShouldEqual, -33) 407 | So(cfg.Int64Field, ShouldEqual, -81) 408 | }) 409 | 410 | Convey("Can set int fields (env)", t, func() { 411 | os.Clearenv() 412 | os.Setenv("INT_FIELD", "123") 413 | os.Setenv("INT8_FIELD", "2") 414 | os.Setenv("INT16_FIELD", "10") 415 | os.Setenv("INT32_FIELD", "33") 416 | os.Setenv("INT64_FIELD", "81") 417 | os.Args = []string{ 418 | "gofigure", 419 | } 420 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 421 | var cfg MyConfigFull 422 | err := Gofigure(&cfg) 423 | So(err, ShouldBeNil) 424 | So(cfg, ShouldNotBeNil) 425 | So(cfg.IntField, ShouldEqual, 123) 426 | So(cfg.Int8Field, ShouldEqual, 2) 427 | So(cfg.Int16Field, ShouldEqual, 10) 428 | So(cfg.Int32Field, ShouldEqual, 33) 429 | So(cfg.Int64Field, ShouldEqual, 81) 430 | }) 431 | 432 | Convey("Can set int fields to negative values (env)", t, func() { 433 | os.Clearenv() 434 | os.Args = []string{ 435 | "gofigure", 436 | } 437 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 438 | os.Setenv("INT_FIELD", "-123") 439 | os.Setenv("INT8_FIELD", "-2") 440 | os.Setenv("INT16_FIELD", "-10") 441 | os.Setenv("INT32_FIELD", "-33") 442 | os.Setenv("INT64_FIELD", "-81") 443 | var cfg MyConfigFull 444 | err := Gofigure(&cfg) 445 | So(err, ShouldBeNil) 446 | So(cfg, ShouldNotBeNil) 447 | So(cfg.IntField, ShouldEqual, -123) 448 | So(cfg.Int8Field, ShouldEqual, -2) 449 | So(cfg.Int16Field, ShouldEqual, -10) 450 | So(cfg.Int32Field, ShouldEqual, -33) 451 | So(cfg.Int64Field, ShouldEqual, -81) 452 | }) 453 | 454 | Convey("Unset int fields are 0", t, func() { 455 | os.Clearenv() 456 | os.Args = []string{ 457 | "gofigure", 458 | } 459 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 460 | var cfg MyConfigFull 461 | err := Gofigure(&cfg) 462 | So(err, ShouldBeNil) 463 | So(cfg, ShouldNotBeNil) 464 | So(cfg.IntField, ShouldEqual, 0) 465 | So(cfg.Int8Field, ShouldEqual, 0) 466 | So(cfg.Int16Field, ShouldEqual, 0) 467 | So(cfg.Int32Field, ShouldEqual, 0) 468 | So(cfg.Int64Field, ShouldEqual, 0) 469 | }) 470 | 471 | clear() 472 | } 473 | 474 | func TestUintField(t *testing.T) { 475 | Convey("Can set uint fields (flag)", t, func() { 476 | os.Clearenv() 477 | os.Args = []string{ 478 | "gofigure", 479 | "-uint-field", "123", 480 | "-uint8-field", "2", 481 | "-uint16-field", "10", 482 | "-uint32-field", "33", 483 | "-uint64-field", "81", 484 | } 485 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 486 | var cfg MyConfigFull 487 | err := Gofigure(&cfg) 488 | So(err, ShouldBeNil) 489 | So(cfg, ShouldNotBeNil) 490 | So(cfg.UintField, ShouldEqual, 123) 491 | So(cfg.Uint8Field, ShouldEqual, 2) 492 | So(cfg.Uint16Field, ShouldEqual, 10) 493 | So(cfg.Uint32Field, ShouldEqual, 33) 494 | So(cfg.Uint64Field, ShouldEqual, 81) 495 | }) 496 | 497 | Convey("Can set int fields (env)", t, func() { 498 | os.Clearenv() 499 | os.Setenv("UINT_FIELD", "123") 500 | os.Setenv("UINT8_FIELD", "2") 501 | os.Setenv("UINT16_FIELD", "10") 502 | os.Setenv("UINT32_FIELD", "33") 503 | os.Setenv("UINT64_FIELD", "81") 504 | os.Args = []string{ 505 | "gofigure", 506 | } 507 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 508 | var cfg MyConfigFull 509 | err := Gofigure(&cfg) 510 | So(err, ShouldBeNil) 511 | So(cfg, ShouldNotBeNil) 512 | So(cfg.UintField, ShouldEqual, 123) 513 | So(cfg.Uint8Field, ShouldEqual, 2) 514 | So(cfg.Uint16Field, ShouldEqual, 10) 515 | So(cfg.Uint32Field, ShouldEqual, 33) 516 | So(cfg.Uint64Field, ShouldEqual, 81) 517 | }) 518 | 519 | Convey("Unset uint fields are 0", t, func() { 520 | os.Clearenv() 521 | os.Args = []string{ 522 | "gofigure", 523 | } 524 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 525 | var cfg MyConfigFull 526 | err := Gofigure(&cfg) 527 | So(err, ShouldBeNil) 528 | So(cfg, ShouldNotBeNil) 529 | So(cfg.UintField, ShouldEqual, 0) 530 | So(cfg.Uint8Field, ShouldEqual, 0) 531 | So(cfg.Uint16Field, ShouldEqual, 0) 532 | So(cfg.Uint32Field, ShouldEqual, 0) 533 | So(cfg.Uint64Field, ShouldEqual, 0) 534 | }) 535 | 536 | clear() 537 | } 538 | 539 | func TestArrayField(t *testing.T) { 540 | Convey("String array should work", t, func() { 541 | os.Clearenv() 542 | os.Args = []string{ 543 | "gofigure", 544 | "-array-string-field", "foo", 545 | "-array-string-field", "bar", 546 | } 547 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 548 | var cfg MyConfigFull 549 | err := Gofigure(&cfg) 550 | So(err, ShouldBeNil) 551 | So(cfg, ShouldNotBeNil) 552 | So(cfg.ArrayStringField, ShouldNotBeNil) 553 | So(len(cfg.ArrayStringField), ShouldEqual, 2) 554 | So(cfg.ArrayStringField, ShouldResemble, []string{"foo", "bar"}) 555 | }) 556 | 557 | Convey("String array should work (env)", t, func() { 558 | os.Clearenv() 559 | os.Setenv("GOFIGURE_ENV_ARRAY", "1") 560 | os.Setenv("ARRAY_STRING_FIELD", "foo,bar,baz") 561 | os.Args = []string{ 562 | "gofigure", 563 | } 564 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 565 | var cfg MyConfigFull 566 | err := Gofigure(&cfg) 567 | So(err, ShouldBeNil) 568 | So(cfg, ShouldNotBeNil) 569 | So(cfg.ArrayStringField, ShouldResemble, []string{"foo", "bar", "baz"}) 570 | }) 571 | 572 | Convey("Int array should work", t, func() { 573 | os.Args = []string{ 574 | "gofigure", 575 | "-array-int-field", "1", 576 | "-array-int-field", "2", 577 | } 578 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 579 | var cfg MyConfigFull 580 | err := Gofigure(&cfg) 581 | So(err, ShouldBeNil) 582 | So(cfg, ShouldNotBeNil) 583 | So(cfg.ArrayIntField, ShouldNotBeNil) 584 | So(len(cfg.ArrayIntField), ShouldEqual, 2) 585 | So(cfg.ArrayIntField, ShouldResemble, []int{1, 2}) 586 | }) 587 | 588 | clear() 589 | } 590 | -------------------------------------------------------------------------------- /sources/commandline.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "flag" 5 | "reflect" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var flagRe1 = regexp.MustCompile("(.)([A-Z][a-z]+)") 11 | var flagRe2 = regexp.MustCompile("([a-z0-9])([A-Z])") 12 | 13 | func camelToFlag(camel string) (flag string) { 14 | flag = flagRe1.ReplaceAllString(camel, "${1}-${2}") 15 | flag = flagRe2.ReplaceAllString(flag, "${1}-${2}") 16 | return strings.ToLower(flag) 17 | } 18 | 19 | // CommandLine implements command line configuration using the flag package 20 | type CommandLine struct { 21 | flags map[string]*string 22 | arrayFlags map[string]*arrayValue 23 | oldCl *flag.FlagSet 24 | } 25 | 26 | type arrayValue []string 27 | 28 | func (aV *arrayValue) Set(value string) error { 29 | printf("Set called for arrayValue: %s", value) 30 | if *aV == nil { 31 | *aV = make([]string, 0, 1) 32 | } 33 | *aV = append(*aV, value) 34 | return nil 35 | } 36 | 37 | func (aV *arrayValue) String() string { 38 | return strings.Join(*aV, ", ") 39 | } 40 | 41 | // Init is called at the start of a new struct 42 | func (cl *CommandLine) Init(args map[string]string) error { 43 | cl.flags = make(map[string]*string) 44 | cl.arrayFlags = make(map[string]*arrayValue) 45 | cl.oldCl = flag.CommandLine 46 | // flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 47 | return nil 48 | } 49 | 50 | // Cleanup is called at the end of parsing 51 | func (cl *CommandLine) Cleanup() { 52 | flag.CommandLine = cl.oldCl 53 | } 54 | 55 | // Register is called to register each struct field 56 | func (cl *CommandLine) Register(key, defaultValue string, params map[string]string, t reflect.Type) error { 57 | if _, ok := cl.flags[key]; ok { 58 | return ErrKeyExists 59 | } 60 | key = camelToFlag(key) 61 | 62 | // TODO validate key? 63 | // TODO use typed calls instead of StringVar 64 | printf("Got type %s", t.Kind()) 65 | switch t.Kind() { 66 | case reflect.Slice: 67 | printf("Registering slice type for %s", key) 68 | var val arrayValue 69 | if len(defaultValue) > 0 { 70 | val = append(val, defaultValue) 71 | } 72 | cl.arrayFlags[key] = &val 73 | 74 | // TODO validate description in some way? 75 | desc := params["flagDesc"] 76 | 77 | flag.Var(&val, key, desc) 78 | default: 79 | printf("Registering default type for %s", key) 80 | val := defaultValue 81 | cl.flags[key] = &val 82 | 83 | // TODO validate description in some way? 84 | desc := params["flagDesc"] 85 | 86 | flag.StringVar(&val, key, defaultValue, desc) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // Get is called to retrieve a key value 93 | func (cl *CommandLine) Get(key string, overrideDefault *string) (string, error) { 94 | key = camelToFlag(key) 95 | printf("Looking up key '%s'", key) 96 | 97 | if !flag.CommandLine.Parsed() { 98 | flag.Parse() 99 | } 100 | // TODO check if flag exists/overrideDefault 101 | val := "" 102 | if v, ok := cl.flags[key]; ok { 103 | printf("Found flag value '%s'", *v) 104 | val = *v 105 | } 106 | if len(val) > 0 { 107 | printf("Returning val '%s'", val) 108 | return val, nil 109 | } 110 | if overrideDefault != nil { 111 | printf("Returning overrideDefault '%s'", *overrideDefault) 112 | return *overrideDefault, nil 113 | } 114 | return "", nil 115 | } 116 | 117 | func (cl *CommandLine) GetArray(key string, overrideDefault *[]string) ([]string, error) { 118 | key = camelToFlag(key) 119 | printf("Looking up array key '%s'", key) 120 | 121 | if !flag.CommandLine.Parsed() { 122 | flag.Parse() 123 | } 124 | // TODO check if flag exists/overrideDefault 125 | val := []string{} 126 | if v, ok := cl.arrayFlags[key]; ok { 127 | printf("Found flag value '%s'", *v) 128 | val = *v 129 | } 130 | if len(val) > 0 { 131 | printf("Returning val '%s'", val) 132 | return val, nil 133 | } 134 | if overrideDefault != nil { 135 | printf("Returning overrideDefault '%s'", *overrideDefault) 136 | return *overrideDefault, nil 137 | } 138 | return val, nil 139 | } 140 | -------------------------------------------------------------------------------- /sources/commandline_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestCamelToFlag(t *testing.T) { 10 | Convey("camelToFlag converts CamelCase to flag-case", t, func() { 11 | So(camelToFlag("CamelCase"), ShouldEqual, "camel-case") 12 | So(camelToFlag("camelCase"), ShouldEqual, "camel-case") 13 | So(camelToFlag("CaMeLCase"), ShouldEqual, "ca-me-l-case") 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /sources/environment.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/ian-kent/envconf" 10 | ) 11 | 12 | // Environment implements environment variable configuration using envconf 13 | type Environment struct { 14 | prefix string 15 | infix string 16 | fields map[string]string 17 | supportArrays bool 18 | } 19 | 20 | var camelRe1 = regexp.MustCompile("(.)([A-Z][a-z]+)") 21 | var camelRe2 = regexp.MustCompile("([a-z0-9])([A-Z])") 22 | 23 | func camelToSnake(camel string) (snake string) { 24 | snake = camelRe1.ReplaceAllString(camel, "${1}_${2}") 25 | snake = camelRe2.ReplaceAllString(snake, "${1}_${2}") 26 | return strings.ToUpper(snake) 27 | } 28 | 29 | // Init is called at the start of a new struct 30 | func (env *Environment) Init(args map[string]string) error { 31 | env.infix = "_" 32 | env.prefix = "" 33 | env.fields = make(map[string]string) 34 | 35 | if envPrefix, ok := args["prefix"]; ok { 36 | env.prefix = envPrefix 37 | } 38 | if envInfix, ok := args["infix"]; ok { 39 | env.infix = envInfix 40 | } 41 | 42 | if v := os.Getenv("GOFIGURE_ENV_ARRAY"); v == "1" || strings.ToLower(v) == "true" || strings.ToLower(v) == "y" { 43 | env.supportArrays = true 44 | } 45 | 46 | return nil 47 | } 48 | 49 | // Register is called to register each struct field 50 | func (env *Environment) Register(key, defaultValue string, params map[string]string, t reflect.Type) error { 51 | env.fields[camelToSnake(key)] = defaultValue 52 | return nil 53 | } 54 | 55 | // Get is called to retrieve a key value 56 | func (env *Environment) Get(key string, overrideDefault *string) (string, error) { 57 | key = camelToSnake(key) 58 | def := env.fields[key] 59 | if overrideDefault != nil { 60 | def = *overrideDefault 61 | } 62 | eK := key 63 | if len(env.prefix) > 0 { 64 | eK = env.prefix + env.infix + key 65 | } 66 | val, err := envconf.FromEnv(eK, def) 67 | return val.(string), err 68 | } 69 | 70 | // GetArray is called to retrieve an array value 71 | func (env *Environment) GetArray(key string, overrideDefault *[]string) ([]string, error) { 72 | var oD *string 73 | if overrideDefault != nil { 74 | if len(*overrideDefault) > 0 { 75 | ovr := (*overrideDefault)[0] 76 | oD = &ovr 77 | } 78 | } 79 | v, e := env.Get(key, oD) 80 | arr := []string{v} 81 | 82 | if env.supportArrays { 83 | if strings.Contains(v, ",") { 84 | arr = strings.Split(v, ",") 85 | } 86 | } 87 | 88 | if len(v) > 0 { 89 | return arr, e 90 | } 91 | return []string{}, e 92 | } 93 | 94 | // Cleanup is called at the end of parsing 95 | func (env *Environment) Cleanup() { 96 | 97 | } 98 | -------------------------------------------------------------------------------- /sources/environment_test.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | ) 8 | 9 | func TestCamelToSnake(t *testing.T) { 10 | Convey("camelToSnake converts CamelCase to snake_case", t, func() { 11 | So(camelToSnake("CamelCase"), ShouldEqual, "CAMEL_CASE") 12 | So(camelToSnake("camelCase"), ShouldEqual, "CAMEL_CASE") 13 | So(camelToSnake("CaMeLCase"), ShouldEqual, "CA_ME_L_CASE") 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /sources/sources.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "reflect" 7 | ) 8 | 9 | // Logger is called for each log message. If nil, 10 | // log.Printf will be called instead. 11 | var Logger func(message string, args ...interface{}) 12 | 13 | // Debug controls sources debug output 14 | var Debug = false 15 | 16 | func printf(message string, args ...interface{}) { 17 | if !Debug { 18 | return 19 | } 20 | if Logger != nil { 21 | Logger(message, args...) 22 | } else { 23 | log.Printf(message, args...) 24 | } 25 | } 26 | 27 | var ( 28 | // ErrKeyExists should be returned when the key has already 29 | // been registered with the source and it can't be re-registered 30 | // e.g. when using CommandLine, which uses the flag package 31 | ErrKeyExists = errors.New("Key already exists") 32 | ) 33 | 34 | // Source should be implemented by Gofigure sources, e.g. 35 | // environment, command line, file, http etc 36 | type Source interface { 37 | // Init is called at the start of a new struct 38 | Init(args map[string]string) error 39 | // Cleanup is called at the end of parsing 40 | Cleanup() 41 | // Register is called to register each struct field 42 | Register(key, defaultValue string, params map[string]string, t reflect.Type) error 43 | // Get is called to retrieve a key value 44 | // - FIXME could use interface{} and maintain types, e.g. json? 45 | Get(key string, overrideDefault *string) (string, error) 46 | // GetArray is called to retrieve an array value 47 | GetArray(key string, overrideDefault *[]string) ([]string, error) 48 | } 49 | --------------------------------------------------------------------------------