├── .travis.yml ├── doc.go ├── .gitignore ├── testdata ├── config.toml ├── config.json ├── config.yaml └── demo.go ├── tag_test.go ├── multiloader.go ├── multivalidator.go ├── LICENSE ├── validator_test.go ├── tag.go ├── file_test.go ├── validator.go ├── README.md ├── file.go ├── env.go ├── env_test.go ├── example_test.go ├── multiconfig_test.go ├── flag.go ├── flag_test.go └── multiconfig.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.3 3 | script: go test ./... 4 | 5 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package multiconfig provides a way to load and read configurations from 2 | // multiple sources. You can read from TOML file, JSON file, YAML file, Environment 3 | // Variables and flags. You can set the order of reader with MultiLoader. Package 4 | // is extensible, you can add your custom Loader by implementing the Load interface. 5 | package multiconfig 6 | -------------------------------------------------------------------------------- /.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 | 25 | *~ 26 | -------------------------------------------------------------------------------- /testdata/config.toml: -------------------------------------------------------------------------------- 1 | Name = "koding" 2 | Enabled = true 3 | Users = ["ankara", "istanbul"] 4 | Interval = 10000000000 5 | ID = 1234567890 6 | Labels = [123,456] 7 | 8 | [Postgres] 9 | Enabled = true 10 | Port = 5432 11 | Hosts = ["192.168.2.1", "192.168.2.2", "192.168.2.3"] 12 | AvailabilityRatio = 8.23 13 | -------------------------------------------------------------------------------- /testdata/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "koding", 3 | "Enabled": true, 4 | "Interval": 10000000000, 5 | "ID": 1234567890, 6 | "Labels": [ 7 | 123, 8 | 456 9 | ], 10 | "Users": [ 11 | "ankara", 12 | "istanbul" 13 | ], 14 | "Postgres": { 15 | "Enabled": true, 16 | "Port": 5432, 17 | "Hosts": [ 18 | "192.168.2.1", 19 | "192.168.2.2", 20 | "192.168.2.3" 21 | ], 22 | "AvailabilityRatio": 8.23 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testdata/config.yaml: -------------------------------------------------------------------------------- 1 | # server configure 2 | 3 | name: koding 4 | 5 | enabled: true 6 | 7 | users: 8 | - ankara 9 | - istanbul 10 | 11 | interval: 10000000000 12 | 13 | id: 1234567890 14 | 15 | labels: 16 | - 123 17 | - 456 18 | 19 | # postgres configure 20 | postgres: 21 | enabled: true 22 | port: 5432 23 | hosts: 24 | - 192.168.2.1 25 | - 192.168.2.2 26 | - 192.168.2.3 27 | availabilityratio: 8.23 28 | 29 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import "testing" 4 | 5 | func TestDefaultValues(t *testing.T) { 6 | m := &TagLoader{} 7 | s := new(Server) 8 | if err := m.Load(s); err != nil { 9 | t.Error(err) 10 | } 11 | 12 | if s.Port != getDefaultServer().Port { 13 | t.Errorf("Port value is wrong: %d, want: %d", s.Port, getDefaultServer().Port) 14 | } 15 | 16 | if s.Postgres.DBName != getDefaultServer().Postgres.DBName { 17 | t.Errorf("Postgres DBName value is wrong: %s, want: %s", s.Postgres.DBName, getDefaultServer().Postgres.DBName) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /multiloader.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | type multiLoader []Loader 4 | 5 | // MultiLoader creates a loader that executes the loaders one by one in order 6 | // and returns on the first error. 7 | func MultiLoader(loader ...Loader) Loader { 8 | return multiLoader(loader) 9 | } 10 | 11 | // Load loads the source into the config defined by struct s 12 | func (m multiLoader) Load(s interface{}) error { 13 | for _, loader := range m { 14 | if err := loader.Load(s); err != nil { 15 | return err 16 | } 17 | } 18 | 19 | return nil 20 | } 21 | 22 | // MustLoad loads the source into the struct, it panics if gets any error 23 | func (m multiLoader) MustLoad(s interface{}) { 24 | if err := m.Load(s); err != nil { 25 | panic(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /multivalidator.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | type multiValidator []Validator 4 | 5 | // MultiValidator accepts variadic validators and satisfies Validator interface. 6 | func MultiValidator(validators ...Validator) Validator { 7 | return multiValidator(validators) 8 | } 9 | 10 | // Validate tries to validate given struct with all the validators. If it doesn't 11 | // have any Validator it will simply skip the validation step. If any of the 12 | // given validators return err, it will stop validating and return it. 13 | func (d multiValidator) Validate(s interface{}) error { 14 | for _, validator := range d { 15 | if err := validator.Validate(s); err != nil { 16 | return err 17 | } 18 | } 19 | 20 | return nil 21 | } 22 | 23 | // MustValidate validates the struct, it panics if gets any error 24 | func (d multiValidator) MustValidate(s interface{}) { 25 | if err := d.Validate(s); err != nil { 26 | panic(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Koding, Inc. 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 | -------------------------------------------------------------------------------- /testdata/demo.go: -------------------------------------------------------------------------------- 1 | // main package demonstrates the usage of the multiconfig package 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/koding/multiconfig" 8 | ) 9 | 10 | type ( 11 | // Server holds supported types by the multiconfig package 12 | Server struct { 13 | Name string 14 | Port int `default:"6060"` 15 | Enabled bool 16 | Users []string 17 | Postgres Postgres 18 | } 19 | 20 | // Postgres is here for embedded struct feature 21 | Postgres struct { 22 | Enabled bool 23 | Port int 24 | Hosts []string 25 | DBName string 26 | AvailabilityRatio float64 27 | } 28 | ) 29 | 30 | func main() { 31 | m := multiconfig.NewWithPath("config.toml") // supports TOML and JSON 32 | 33 | // Get an empty struct for your configuration 34 | serverConf := new(Server) 35 | 36 | // Populated the serverConf struct 37 | m.MustLoad(serverConf) // Check for error 38 | 39 | fmt.Println("After Loading: ") 40 | fmt.Printf("%+v\n", serverConf) 41 | 42 | if serverConf.Enabled { 43 | fmt.Println("Enabled field is set to true") 44 | } else { 45 | fmt.Println("Enabled field is set to false") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /validator_test.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import "testing" 4 | 5 | func TestValidators(t *testing.T) { 6 | s := getDefaultServer() 7 | s.Name = "" 8 | 9 | err := (&RequiredValidator{}).Validate(s) 10 | if err == nil { 11 | t.Fatal("Name should be required") 12 | } 13 | } 14 | 15 | func TestValidatorsEmbededStruct(t *testing.T) { 16 | s := getDefaultServer() 17 | s.Postgres.Port = 0 18 | 19 | err := (&RequiredValidator{}).Validate(s) 20 | if err == nil { 21 | t.Fatal("Port should be required") 22 | } 23 | } 24 | 25 | func TestValidatorsCustomTag(t *testing.T) { 26 | s := getDefaultServer() 27 | 28 | validator := (&RequiredValidator{ 29 | TagName: "customRequired", 30 | TagValue: "yes", 31 | }) 32 | 33 | // test happy path 34 | err := validator.Validate(s) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | // validate sad case 40 | s.Postgres.Port = 0 41 | err = validator.Validate(s) 42 | if err == nil { 43 | t.Fatal("Port should be required") 44 | } 45 | 46 | errStr := "multiconfig: field 'Postgres.Port' is required" 47 | if err.Error() != errStr { 48 | t.Fatalf("Err string is wrong: expected %s, got: %s", errStr, err.Error()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/fatih/structs" 7 | ) 8 | 9 | // TagLoader satisfies the loader interface. It parses a struct's field tags 10 | // and populates the each field with that given tag. 11 | type TagLoader struct { 12 | // DefaultTagName is the default tag name for struct fields to define 13 | // default values for a field. Example: 14 | // 15 | // // Field's default value is "koding". 16 | // Name string `default:"koding"` 17 | // 18 | // The default value is "default" if it's not set explicitly. 19 | DefaultTagName string 20 | } 21 | 22 | func (t *TagLoader) Load(s interface{}) error { 23 | if t.DefaultTagName == "" { 24 | t.DefaultTagName = "default" 25 | } 26 | 27 | for _, field := range structs.Fields(s) { 28 | 29 | if err := t.processField(t.DefaultTagName, field); err != nil { 30 | return err 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // processField gets tagName and the field, recursively checks if the field has the given 38 | // tag, if yes, sets it otherwise ignores 39 | func (t *TagLoader) processField(tagName string, field *structs.Field) error { 40 | switch field.Kind() { 41 | case reflect.Struct: 42 | for _, f := range field.Fields() { 43 | if err := t.processField(tagName, f); err != nil { 44 | return err 45 | } 46 | } 47 | default: 48 | defaultVal := field.Tag(t.DefaultTagName) 49 | if defaultVal == "" { 50 | return nil 51 | } 52 | 53 | err := fieldSet(field, defaultVal) 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestYAML(t *testing.T) { 9 | m := NewWithPath(testYAML) 10 | 11 | s := &Server{} 12 | if err := m.Load(s); err != nil { 13 | t.Error(err) 14 | } 15 | 16 | testStruct(t, s, getDefaultServer()) 17 | } 18 | 19 | func TestYAML_Reader(t *testing.T) { 20 | f, err := os.Open(testYAML) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | defer f.Close() 25 | 26 | l := MultiLoader(&TagLoader{}, &YAMLLoader{Reader: f}) 27 | s := &Server{} 28 | if err := l.Load(s); err != nil { 29 | t.Error(err) 30 | } 31 | 32 | testStruct(t, s, getDefaultServer()) 33 | } 34 | func TestToml(t *testing.T) { 35 | m := NewWithPath(testTOML) 36 | 37 | s := &Server{} 38 | if err := m.Load(s); err != nil { 39 | t.Error(err) 40 | } 41 | 42 | testStruct(t, s, getDefaultServer()) 43 | } 44 | 45 | func TestToml_Reader(t *testing.T) { 46 | f, err := os.Open(testTOML) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | defer f.Close() 51 | 52 | l := MultiLoader(&TagLoader{}, &TOMLLoader{Reader: f}) 53 | s := &Server{} 54 | if err := l.Load(s); err != nil { 55 | t.Error(err) 56 | } 57 | 58 | testStruct(t, s, getDefaultServer()) 59 | } 60 | 61 | func TestJSON(t *testing.T) { 62 | m := NewWithPath(testJSON) 63 | 64 | s := &Server{} 65 | if err := m.Load(s); err != nil { 66 | t.Error(err) 67 | } 68 | 69 | testStruct(t, s, getDefaultServer()) 70 | } 71 | 72 | func TestJSON_Reader(t *testing.T) { 73 | f, err := os.Open(testJSON) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | defer f.Close() 78 | 79 | l := MultiLoader(&TagLoader{}, &JSONLoader{Reader: f}) 80 | s := &Server{} 81 | if err := l.Load(s); err != nil { 82 | t.Error(err) 83 | } 84 | 85 | testStruct(t, s, getDefaultServer()) 86 | } 87 | 88 | // func TestJSON2(t *testing.T) { 89 | // ExampleEnvironmentLoader() 90 | // ExampleTOMLLoader() 91 | // } 92 | -------------------------------------------------------------------------------- /validator.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/fatih/structs" 8 | ) 9 | 10 | // Validator validates the config against any predefined rules, those predefined 11 | // rules should be given to this package. The implementer will be responsible 12 | // for the logic. 13 | type Validator interface { 14 | // Validate validates the config struct 15 | Validate(s interface{}) error 16 | } 17 | 18 | // RequiredValidator validates the struct against zero values. 19 | type RequiredValidator struct { 20 | // TagName holds the validator tag name. The default is "required" 21 | TagName string 22 | 23 | // TagValue holds the expected value of the validator. The default is "true" 24 | TagValue string 25 | } 26 | 27 | // Validate validates the given struct agaist field's zero values. If 28 | // intentionaly, the value of a field is `zero-valued`(e.g false, 0, "") 29 | // required tag should not be set for that field. 30 | func (e *RequiredValidator) Validate(s interface{}) error { 31 | if e.TagName == "" { 32 | e.TagName = "required" 33 | } 34 | 35 | if e.TagValue == "" { 36 | e.TagValue = "true" 37 | } 38 | 39 | for _, field := range structs.Fields(s) { 40 | if err := e.processField("", field); err != nil { 41 | return err 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (e *RequiredValidator) processField(fieldName string, field *structs.Field) error { 49 | fieldName += field.Name() 50 | switch field.Kind() { 51 | case reflect.Struct: 52 | // this is used for error messages below, when we have an error at the 53 | // child properties add parent properties into the error message as well 54 | fieldName += "." 55 | 56 | for _, f := range field.Fields() { 57 | if err := e.processField(fieldName, f); err != nil { 58 | return err 59 | } 60 | } 61 | default: 62 | val := field.Tag(e.TagName) 63 | if val != e.TagValue { 64 | return nil 65 | } 66 | 67 | if field.IsZero() { 68 | return fmt.Errorf("multiconfig: field '%s' is required", fieldName) 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiconfig [![GoDoc](https://godoc.org/github.com/koding/multiconfig?status.svg)](http://godoc.org/github.com/koding/multiconfig) [![Build Status](https://travis-ci.org/koding/multiconfig.svg?branch=master)](https://travis-ci.org/koding/multiconfig) 2 | 3 | Load configuration from multiple sources. Multiconfig makes loading/parsing 4 | from different configuration sources an easy task. The problem with any app is 5 | that with time there are many options how to populate a set of configs. 6 | Multiconfig makes it easy by dynamically creating all necessary options. 7 | Checkout the example below to see it in action. 8 | 9 | ## Features 10 | 11 | Multiconfig is able to read configuration automatically based on the given struct's field names from the following sources: 12 | 13 | * Struct tags 14 | * TOML file 15 | * JSON file 16 | * YAML file 17 | * Environment variables 18 | * Flags 19 | 20 | 21 | ## Install 22 | 23 | ```bash 24 | go get github.com/koding/multiconfig 25 | ``` 26 | 27 | ## Usage and Examples 28 | 29 | Lets define and struct that defines our configuration 30 | 31 | ```go 32 | type Server struct { 33 | Name string `required:"true"` 34 | Port int `default:"6060"` 35 | Enabled bool 36 | Users []string 37 | } 38 | ``` 39 | 40 | Load the configuration into multiconfig: 41 | 42 | ```go 43 | // Create a new DefaultLoader without or with an initial config file 44 | m := multiconfig.New() 45 | m := multiconfig.NewWithPath("config.toml") // supports TOML, JSON and YAML 46 | 47 | // Get an empty struct for your configuration 48 | serverConf := new(Server) 49 | 50 | // Populated the serverConf struct 51 | err := m.Load(serverConf) // Check for error 52 | m.MustLoad(serverConf) // Panic's if there is any error 53 | 54 | // Access now populated fields 55 | serverConf.Port // by default 6060 56 | serverConf.Name // "koding" 57 | ``` 58 | 59 | Run your app: 60 | 61 | ```sh 62 | # Sets default values first which are defined in each field tag value. 63 | # Starts to load from config.toml 64 | $ app 65 | 66 | # Override any config easily with environment variables, environment variables 67 | # are automatically generated in the form of STRUCTNAME_FIELDNAME 68 | $ SERVER_PORT=4000 SERVER_NAME="koding" app 69 | 70 | # Or pass via flag. Flags are also automatically generated based on the field 71 | # name 72 | $ app -port 4000 -users "gopher,koding" 73 | 74 | # Print dynamically generated flags and environment variables: 75 | $ app -help 76 | Usage of app: 77 | -enabled=true: Change value of Enabled. 78 | -name=Koding: Change value of Name. 79 | -port=6060: Change value of Port. 80 | -users=[ankara istanbul]: Change value of Users. 81 | 82 | Generated environment variables: 83 | SERVER_NAME 84 | SERVER_PORT 85 | SERVER_ENABLED 86 | SERVER_USERS 87 | ``` 88 | 89 | 90 | ## License 91 | 92 | The MIT License (MIT) - see [LICENSE](/LICENSE) for more details 93 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/BurntSushi/toml" 12 | yaml "gopkg.in/yaml.v2" 13 | ) 14 | 15 | var ( 16 | // ErrSourceNotSet states that neither the path or the reader is set on the loader 17 | ErrSourceNotSet = errors.New("config path or reader is not set") 18 | 19 | // ErrFileNotFound states that given file is not exists 20 | ErrFileNotFound = errors.New("config file not found") 21 | ) 22 | 23 | // TOMLLoader satisifies the loader interface. It loads the configuration from 24 | // the given toml file or Reader. 25 | type TOMLLoader struct { 26 | Path string 27 | Reader io.Reader 28 | } 29 | 30 | // Load loads the source into the config defined by struct s 31 | // Defaults to using the Reader if provided, otherwise tries to read from the 32 | // file 33 | func (t *TOMLLoader) Load(s interface{}) error { 34 | var r io.Reader 35 | 36 | if t.Reader != nil { 37 | r = t.Reader 38 | } else if t.Path != "" { 39 | file, err := getConfig(t.Path) 40 | if err != nil { 41 | return err 42 | } 43 | defer file.Close() 44 | r = file 45 | } else { 46 | return ErrSourceNotSet 47 | } 48 | 49 | if _, err := toml.DecodeReader(r, s); err != nil { 50 | return err 51 | } 52 | 53 | return nil 54 | } 55 | 56 | // JSONLoader satisifies the loader interface. It loads the configuration from 57 | // the given json file or Reader. 58 | type JSONLoader struct { 59 | Path string 60 | Reader io.Reader 61 | } 62 | 63 | // Load loads the source into the config defined by struct s. 64 | // Defaults to using the Reader if provided, otherwise tries to read from the 65 | // file 66 | func (j *JSONLoader) Load(s interface{}) error { 67 | var r io.Reader 68 | if j.Reader != nil { 69 | r = j.Reader 70 | } else if j.Path != "" { 71 | file, err := getConfig(j.Path) 72 | if err != nil { 73 | return err 74 | } 75 | defer file.Close() 76 | r = file 77 | } else { 78 | return ErrSourceNotSet 79 | } 80 | 81 | return json.NewDecoder(r).Decode(s) 82 | } 83 | 84 | // YAMLLoader satisifies the loader interface. It loads the configuration from 85 | // the given yaml file. 86 | type YAMLLoader struct { 87 | Path string 88 | Reader io.Reader 89 | } 90 | 91 | // Load loads the source into the config defined by struct s. 92 | // Defaults to using the Reader if provided, otherwise tries to read from the 93 | // file 94 | func (y *YAMLLoader) Load(s interface{}) error { 95 | var r io.Reader 96 | 97 | if y.Reader != nil { 98 | r = y.Reader 99 | } else if y.Path != "" { 100 | file, err := getConfig(y.Path) 101 | if err != nil { 102 | return err 103 | } 104 | defer file.Close() 105 | r = file 106 | } else { 107 | return ErrSourceNotSet 108 | } 109 | 110 | data, err := ioutil.ReadAll(r) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return yaml.Unmarshal(data, s) 116 | } 117 | 118 | func getConfig(path string) (*os.File, error) { 119 | pwd, err := os.Getwd() 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | configPath := path 125 | if !filepath.IsAbs(path) { 126 | configPath = filepath.Join(pwd, path) 127 | } 128 | 129 | // check if file with combined path is exists(relative path) 130 | if _, err := os.Stat(configPath); !os.IsNotExist(err) { 131 | return os.Open(configPath) 132 | } 133 | 134 | f, err := os.Open(path) 135 | if os.IsNotExist(err) { 136 | return nil, ErrFileNotFound 137 | } 138 | return f, err 139 | } 140 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/fatih/camelcase" 10 | "github.com/fatih/structs" 11 | ) 12 | 13 | // EnvironmentLoader satisifies the loader interface. It loads the 14 | // configuration from the environment variables in the form of 15 | // STRUCTNAME_FIELDNAME. 16 | type EnvironmentLoader struct { 17 | // Prefix prepends given string to every environment variable 18 | // {STRUCTNAME}_FIELDNAME will be {PREFIX}_FIELDNAME 19 | Prefix string 20 | 21 | // CamelCase adds a separator for field names in camelcase form. A 22 | // fieldname of "AccessKey" would generate a environment name of 23 | // "STRUCTNAME_ACCESSKEY". If CamelCase is enabled, the environment name 24 | // will be generated in the form of "STRUCTNAME_ACCESS_KEY" 25 | CamelCase bool 26 | } 27 | 28 | func (e *EnvironmentLoader) getPrefix(s *structs.Struct) string { 29 | if e.Prefix != "" { 30 | return e.Prefix 31 | } 32 | 33 | return s.Name() 34 | } 35 | 36 | // Load loads the source into the config defined by struct s 37 | func (e *EnvironmentLoader) Load(s interface{}) error { 38 | strct := structs.New(s) 39 | strctMap := strct.Map() 40 | prefix := e.getPrefix(strct) 41 | 42 | for key, val := range strctMap { 43 | field := strct.Field(key) 44 | 45 | if err := e.processField(prefix, field, key, val); err != nil { 46 | return err 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | 53 | // processField gets leading name for the env variable and combines the current 54 | // field's name and generates environment variable names recursively 55 | func (e *EnvironmentLoader) processField(prefix string, field *structs.Field, name string, strctMap interface{}) error { 56 | fieldName := e.generateFieldName(prefix, name) 57 | 58 | switch strctMap.(type) { 59 | case map[string]interface{}: 60 | for key, val := range strctMap.(map[string]interface{}) { 61 | field := field.Field(key) 62 | 63 | if err := e.processField(fieldName, field, key, val); err != nil { 64 | return err 65 | } 66 | } 67 | default: 68 | v := os.Getenv(fieldName) 69 | if v == "" { 70 | return nil 71 | } 72 | 73 | if err := fieldSet(field, v); err != nil { 74 | return err 75 | } 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // PrintEnvs prints the generated environment variables to the std out. 82 | func (e *EnvironmentLoader) PrintEnvs(s interface{}) { 83 | strct := structs.New(s) 84 | strctMap := strct.Map() 85 | prefix := e.getPrefix(strct) 86 | 87 | keys := make([]string, 0, len(strctMap)) 88 | for key := range strctMap { 89 | keys = append(keys, key) 90 | } 91 | sort.Strings(keys) 92 | 93 | for _, key := range keys { 94 | field := strct.Field(key) 95 | e.printField(prefix, field, key, strctMap[key]) 96 | } 97 | } 98 | 99 | // printField prints the field of the config struct for the flag.Usage 100 | func (e *EnvironmentLoader) printField(prefix string, field *structs.Field, name string, strctMap interface{}) { 101 | fieldName := e.generateFieldName(prefix, name) 102 | 103 | switch strctMap.(type) { 104 | case map[string]interface{}: 105 | smap := strctMap.(map[string]interface{}) 106 | keys := make([]string, 0, len(smap)) 107 | for key := range smap { 108 | keys = append(keys, key) 109 | } 110 | sort.Strings(keys) 111 | for _, key := range keys { 112 | field := field.Field(key) 113 | e.printField(fieldName, field, key, smap[key]) 114 | } 115 | default: 116 | fmt.Println(" ", fieldName) 117 | } 118 | } 119 | 120 | // generateFieldName generates the field name combined with the prefix and the 121 | // struct's field name 122 | func (e *EnvironmentLoader) generateFieldName(prefix string, name string) string { 123 | fieldName := strings.ToUpper(name) 124 | if e.CamelCase { 125 | fieldName = strings.ToUpper(strings.Join(camelcase.Split(name), "_")) 126 | } 127 | 128 | return strings.ToUpper(prefix) + "_" + fieldName 129 | } 130 | -------------------------------------------------------------------------------- /env_test.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/fatih/structs" 9 | ) 10 | 11 | func TestENV(t *testing.T) { 12 | m := EnvironmentLoader{} 13 | s := &Server{} 14 | structName := structs.Name(s) 15 | 16 | // set env variables 17 | setEnvVars(t, structName, "") 18 | 19 | if err := m.Load(s); err != nil { 20 | t.Error(err) 21 | } 22 | 23 | testStruct(t, s, getDefaultServer()) 24 | } 25 | 26 | func TestCamelCaseEnv(t *testing.T) { 27 | m := EnvironmentLoader{ 28 | CamelCase: true, 29 | } 30 | s := &CamelCaseServer{} 31 | structName := structs.Name(s) 32 | 33 | // set env variables 34 | setEnvVars(t, structName, "") 35 | 36 | if err := m.Load(s); err != nil { 37 | t.Error(err) 38 | } 39 | 40 | testCamelcaseStruct(t, s, getDefaultCamelCaseServer()) 41 | } 42 | 43 | func TestENVWithPrefix(t *testing.T) { 44 | const prefix = "Prefix" 45 | 46 | m := EnvironmentLoader{Prefix: prefix} 47 | s := &Server{} 48 | structName := structs.New(s).Name() 49 | 50 | // set env variables 51 | setEnvVars(t, structName, prefix) 52 | 53 | if err := m.Load(s); err != nil { 54 | t.Error(err) 55 | } 56 | 57 | testStruct(t, s, getDefaultServer()) 58 | } 59 | 60 | func TestENVFlattenStructPrefix(t *testing.T) { 61 | const prefix = "Prefix" 62 | 63 | m := EnvironmentLoader{Prefix: prefix} 64 | s := &TaggedServer{} 65 | structName := structs.New(s).Name() 66 | 67 | // set env variables 68 | setEnvVars(t, structName, prefix) 69 | 70 | if err := m.Load(s); err != nil { 71 | t.Error(err) 72 | } 73 | 74 | testPostgres(t, s.Postgres, getDefaultServer().Postgres) 75 | } 76 | 77 | func setEnvVars(t *testing.T, structName, prefix string) { 78 | if structName == "" { 79 | t.Fatal("struct name can not be empty") 80 | } 81 | 82 | var env map[string]string 83 | switch structName { 84 | case "Server": 85 | env = map[string]string{ 86 | "NAME": "koding", 87 | "PORT": "6060", 88 | "ENABLED": "true", 89 | "USERS": "ankara,istanbul", 90 | "INTERVAL": "10s", 91 | "ID": "1234567890", 92 | "LABELS": "123,456", 93 | "POSTGRES_ENABLED": "true", 94 | "POSTGRES_PORT": "5432", 95 | "POSTGRES_HOSTS": "192.168.2.1,192.168.2.2,192.168.2.3", 96 | "POSTGRES_DBNAME": "configdb", 97 | "POSTGRES_AVAILABILITYRATIO": "8.23", 98 | "POSTGRES_FOO": "8.23,9.12,11,90", 99 | } 100 | case "CamelCaseServer": 101 | env = map[string]string{ 102 | "ACCESS_KEY": "123456", 103 | "NORMAL": "normal", 104 | "DB_NAME": "configdb", 105 | "AVAILABILITY_RATIO": "8.23", 106 | } 107 | case "TaggedServer": 108 | env = map[string]string{ 109 | "NAME": "koding", 110 | "ENABLED": "true", 111 | "PORT": "5432", 112 | "HOSTS": "192.168.2.1,192.168.2.2,192.168.2.3", 113 | "DBNAME": "configdb", 114 | "AVAILABILITYRATIO": "8.23", 115 | "FOO": "8.23,9.12,11,90", 116 | } 117 | } 118 | 119 | if prefix == "" { 120 | prefix = structName 121 | } 122 | 123 | prefix = strings.ToUpper(prefix) 124 | 125 | for key, val := range env { 126 | env := prefix + "_" + key 127 | if err := os.Setenv(env, val); err != nil { 128 | t.Fatal(err) 129 | } 130 | } 131 | } 132 | 133 | func TestENVgetPrefix(t *testing.T) { 134 | e := &EnvironmentLoader{} 135 | s := &Server{} 136 | 137 | st := structs.New(s) 138 | 139 | prefix := st.Name() 140 | 141 | if p := e.getPrefix(st); p != prefix { 142 | t.Errorf("Prefix is wrong: %s, want: %s", p, prefix) 143 | } 144 | 145 | prefix = "Test" 146 | e = &EnvironmentLoader{Prefix: prefix} 147 | if p := e.getPrefix(st); p != prefix { 148 | t.Errorf("Prefix is wrong: %s, want: %s", p, prefix) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func ExampleDefaultLoader() { 9 | // Our struct which is used for configuration 10 | type ServerConfig struct { 11 | Name string `default:"gopher"` 12 | Port int `default:"6060"` 13 | Enabled bool 14 | Users []string 15 | } 16 | 17 | // Instantiate a default loader. 18 | d := NewWithPath("testdata/config.toml") 19 | 20 | s := &ServerConfig{} 21 | 22 | // It first sets the default values for each field with tag values defined 23 | // with "default", next it reads from config.toml, from environment 24 | // variables and finally from command line flags. It panics if loading fails. 25 | d.MustLoad(s) 26 | 27 | fmt.Println("Host-->", s.Name) 28 | fmt.Println("Port-->", s.Port) 29 | 30 | // Output: 31 | // Host--> koding 32 | // Port--> 6060 33 | 34 | } 35 | 36 | func ExampleMultiLoader() { 37 | // Our struct which is used for configuration 38 | type ServerConfig struct { 39 | Name string 40 | Port int 41 | Enabled bool 42 | Users []string 43 | Postgres Postgres 44 | } 45 | 46 | os.Setenv("SERVERCONFIG_NAME", "koding") 47 | os.Setenv("SERVERCONFIG_PORT", "6060") 48 | 49 | // Create a custom multi loader intance based on your needs. 50 | f := &FlagLoader{} 51 | e := &EnvironmentLoader{} 52 | 53 | l := MultiLoader(f, e) 54 | 55 | // Load configs into our s variable from the sources above 56 | s := &ServerConfig{} 57 | err := l.Load(s) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | fmt.Println("Host-->", s.Name) 63 | fmt.Println("Port-->", s.Port) 64 | 65 | // Output: 66 | // Host--> koding 67 | // Port--> 6060 68 | } 69 | 70 | func ExampleEnvironmentLoader() { 71 | // Our struct which is used for configuration 72 | type ServerConfig struct { 73 | Name string 74 | Port int 75 | Enabled bool 76 | Users []string 77 | Postgres Postgres 78 | } 79 | 80 | // Assume those values defined before running the Loader 81 | os.Setenv("SERVERCONFIG_NAME", "koding") 82 | os.Setenv("SERVERCONFIG_PORT", "6060") 83 | 84 | // Instantiate loader 85 | l := &EnvironmentLoader{} 86 | 87 | s := &ServerConfig{} 88 | err := l.Load(s) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | fmt.Println("Host-->", s.Name) 94 | fmt.Println("Port-->", s.Port) 95 | 96 | // Output: 97 | // Host--> koding 98 | // Port--> 6060 99 | } 100 | 101 | func ExampleTOMLLoader() { 102 | // Our struct which is used for configuration 103 | type ServerConfig struct { 104 | Name string 105 | Port int 106 | Enabled bool 107 | Users []string 108 | Postgres Postgres 109 | } 110 | 111 | // Instantiate loader 112 | l := &TOMLLoader{Path: testTOML} 113 | 114 | s := &ServerConfig{} 115 | err := l.Load(s) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | fmt.Println("Host-->", s.Name) 121 | fmt.Println("Users-->", s.Users) 122 | 123 | // Output: 124 | // Host--> koding 125 | // Users--> [ankara istanbul] 126 | } 127 | 128 | func ExampleJSONLoader() { 129 | // Our struct which is used for configuration 130 | type ServerConfig struct { 131 | Name string 132 | Port int 133 | Enabled bool 134 | Users []string 135 | Postgres Postgres 136 | } 137 | 138 | // Instantiate loader 139 | l := &JSONLoader{Path: testJSON} 140 | 141 | s := &ServerConfig{} 142 | err := l.Load(s) 143 | if err != nil { 144 | panic(err) 145 | } 146 | 147 | fmt.Println("Host-->", s.Name) 148 | fmt.Println("Users-->", s.Users) 149 | 150 | // Output: 151 | // Host--> koding 152 | // Users--> [ankara istanbul] 153 | } 154 | 155 | func ExampleYAMLLoader() { 156 | // Our struct which is used for configuration 157 | type ServerConfig struct { 158 | Name string 159 | Port int 160 | Enabled bool 161 | Users []string 162 | Postgres Postgres 163 | } 164 | 165 | // Instantiate loader 166 | l := &YAMLLoader{Path: testYAML} 167 | 168 | s := &ServerConfig{} 169 | err := l.Load(s) 170 | if err != nil { 171 | panic(err) 172 | } 173 | 174 | fmt.Println("Host-->", s.Name) 175 | fmt.Println("Users-->", s.Users) 176 | 177 | // Output: 178 | // Host--> koding 179 | // Users--> [ankara istanbul] 180 | } 181 | -------------------------------------------------------------------------------- /multiconfig_test.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | type ( 9 | Server struct { 10 | Name string `required:"true"` 11 | Port int `default:"6060"` 12 | ID int64 13 | Labels []int 14 | Enabled bool 15 | Users []string 16 | Postgres Postgres 17 | unexported string 18 | Interval time.Duration 19 | } 20 | 21 | // Postgres holds Postgresql database related configuration 22 | Postgres struct { 23 | Enabled bool 24 | Port int `required:"true" customRequired:"yes"` 25 | Hosts []string `required:"true"` 26 | DBName string `default:"configdb"` 27 | AvailabilityRatio float64 28 | unexported string 29 | } 30 | 31 | TaggedServer struct { 32 | Name string `required:"true"` 33 | Postgres `structs:",flatten"` 34 | } 35 | ) 36 | 37 | type FlattenedServer struct { 38 | Postgres Postgres 39 | } 40 | 41 | type CamelCaseServer struct { 42 | AccessKey string 43 | Normal string 44 | DBName string `default:"configdb"` 45 | AvailabilityRatio float64 46 | } 47 | 48 | var ( 49 | testTOML = "testdata/config.toml" 50 | testJSON = "testdata/config.json" 51 | testYAML = "testdata/config.yaml" 52 | ) 53 | 54 | func getDefaultServer() *Server { 55 | return &Server{ 56 | Name: "koding", 57 | Port: 6060, 58 | Enabled: true, 59 | ID: 1234567890, 60 | Labels: []int{123, 456}, 61 | Users: []string{"ankara", "istanbul"}, 62 | Interval: 10 * time.Second, 63 | Postgres: Postgres{ 64 | Enabled: true, 65 | Port: 5432, 66 | Hosts: []string{"192.168.2.1", "192.168.2.2", "192.168.2.3"}, 67 | DBName: "configdb", 68 | AvailabilityRatio: 8.23, 69 | }, 70 | } 71 | } 72 | 73 | func getDefaultCamelCaseServer() *CamelCaseServer { 74 | return &CamelCaseServer{ 75 | AccessKey: "123456", 76 | Normal: "normal", 77 | DBName: "configdb", 78 | AvailabilityRatio: 8.23, 79 | } 80 | } 81 | 82 | func TestNewWithPath(t *testing.T) { 83 | var _ Loader = NewWithPath(testTOML) 84 | } 85 | 86 | func TestLoad(t *testing.T) { 87 | m := NewWithPath(testTOML) 88 | 89 | s := new(Server) 90 | if err := m.Load(s); err != nil { 91 | t.Error(err) 92 | } 93 | 94 | testStruct(t, s, getDefaultServer()) 95 | } 96 | 97 | func TestDefaultLoader(t *testing.T) { 98 | m := New() 99 | 100 | s := new(Server) 101 | if err := m.Load(s); err != nil { 102 | t.Error(err) 103 | } 104 | 105 | if err := m.Validate(s); err != nil { 106 | t.Error(err) 107 | } 108 | testStruct(t, s, getDefaultServer()) 109 | 110 | s.Name = "" 111 | if err := m.Validate(s); err == nil { 112 | t.Error("Name should be required") 113 | } 114 | } 115 | 116 | func testStruct(t *testing.T, s *Server, d *Server) { 117 | if s.Name != d.Name { 118 | t.Errorf("Name value is wrong: %s, want: %s", s.Name, d.Name) 119 | } 120 | 121 | if s.Port != d.Port { 122 | t.Errorf("Port value is wrong: %d, want: %d", s.Port, d.Port) 123 | } 124 | 125 | if s.Enabled != d.Enabled { 126 | t.Errorf("Enabled value is wrong: %t, want: %t", s.Enabled, d.Enabled) 127 | } 128 | 129 | if s.Interval != d.Interval { 130 | t.Errorf("Interval value is wrong: %v, want: %v", s.Interval, d.Interval) 131 | } 132 | 133 | if s.ID != d.ID { 134 | t.Errorf("ID value is wrong: %v, want: %v", s.ID, d.ID) 135 | } 136 | 137 | if len(s.Labels) != len(d.Labels) { 138 | t.Errorf("Labels value is wrong: %d, want: %d", len(s.Labels), len(d.Labels)) 139 | } else { 140 | for i, label := range d.Labels { 141 | if s.Labels[i] != label { 142 | t.Errorf("Label is wrong for index: %d, label: %d, want: %d", i, s.Labels[i], label) 143 | } 144 | } 145 | } 146 | 147 | if len(s.Users) != len(d.Users) { 148 | t.Errorf("Users value is wrong: %d, want: %d", len(s.Users), len(d.Users)) 149 | } else { 150 | for i, user := range d.Users { 151 | if s.Users[i] != user { 152 | t.Errorf("User is wrong for index: %d, user: %s, want: %s", i, s.Users[i], user) 153 | } 154 | } 155 | } 156 | 157 | testPostgres(t, s.Postgres, d.Postgres) 158 | } 159 | 160 | func testFlattenedStruct(t *testing.T, s *FlattenedServer, d *Server) { 161 | // Explicitly state that Enabled should be true, no need to check 162 | // `x == true` infact. 163 | testPostgres(t, s.Postgres, d.Postgres) 164 | } 165 | 166 | func testPostgres(t *testing.T, s Postgres, d Postgres) { 167 | if s.Enabled != d.Enabled { 168 | t.Errorf("Postgres enabled is wrong %t, want: %t", s.Enabled, d.Enabled) 169 | } 170 | 171 | if s.Port != d.Port { 172 | t.Errorf("Postgres Port value is wrong: %d, want: %d", s.Port, d.Port) 173 | } 174 | 175 | if s.DBName != d.DBName { 176 | t.Errorf("DBName is wrong: %s, want: %s", s.DBName, d.DBName) 177 | } 178 | 179 | if s.AvailabilityRatio != d.AvailabilityRatio { 180 | t.Errorf("AvailabilityRatio is wrong: %f, want: %f", s.AvailabilityRatio, d.AvailabilityRatio) 181 | } 182 | 183 | if len(s.Hosts) != len(d.Hosts) { 184 | // do not continue testing if this fails, because others is depending on this test 185 | t.Fatalf("Hosts len is wrong: %v, want: %v", s.Hosts, d.Hosts) 186 | } 187 | 188 | for i, host := range d.Hosts { 189 | if s.Hosts[i] != host { 190 | t.Fatalf("Hosts number %d is wrong: %v, want: %v", i, s.Hosts[i], host) 191 | } 192 | } 193 | } 194 | 195 | func testCamelcaseStruct(t *testing.T, s *CamelCaseServer, d *CamelCaseServer) { 196 | if s.AccessKey != d.AccessKey { 197 | t.Errorf("AccessKey is wrong: %s, want: %s", s.AccessKey, d.AccessKey) 198 | } 199 | 200 | if s.Normal != d.Normal { 201 | t.Errorf("Normal is wrong: %s, want: %s", s.Normal, d.Normal) 202 | } 203 | 204 | if s.DBName != d.DBName { 205 | t.Errorf("DBName is wrong: %s, want: %s", s.DBName, d.DBName) 206 | } 207 | 208 | if s.AvailabilityRatio != d.AvailabilityRatio { 209 | t.Errorf("AvailabilityRatio is wrong: %f, want: %f", s.AvailabilityRatio, d.AvailabilityRatio) 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/fatih/camelcase" 11 | "github.com/fatih/structs" 12 | ) 13 | 14 | // FlagLoader satisfies the loader interface. It creates on the fly flags based 15 | // on the field names and parses them to load into the given pointer of struct 16 | // s. 17 | type FlagLoader struct { 18 | // Prefix prepends the prefix to each flag name i.e: 19 | // --foo is converted to --prefix-foo. 20 | // --foo-bar is converted to --prefix-foo-bar. 21 | Prefix string 22 | 23 | // Flatten doesn't add prefixes for nested structs. So previously if we had 24 | // a nested struct `type T struct{Name struct{ ...}}`, this would generate 25 | // --name-foo, --name-bar, etc. When Flatten is enabled, the flags will be 26 | // flattend to the form: --foo, --bar, etc.. Panics if the nested structs 27 | // has a duplicate field name in the root level of the struct (outer 28 | // struct). Use this option only if you know what you do. 29 | Flatten bool 30 | 31 | // CamelCase adds a separator for field names in camelcase form. A 32 | // fieldname of "AccessKey" would generate a flag name "--accesskey". If 33 | // CamelCase is enabled, the flag name will be generated in the form of 34 | // "--access-key" 35 | CamelCase bool 36 | 37 | // EnvPrefix is just a placeholder to print the correct usages when an 38 | // EnvLoader is used 39 | EnvPrefix string 40 | 41 | // ErrorHandling is used to configure error handling used by 42 | // *flag.FlagSet. 43 | // 44 | // By default it's flag.ContinueOnError. 45 | ErrorHandling flag.ErrorHandling 46 | 47 | // Args defines a custom argument list. If nil, os.Args[1:] is used. 48 | Args []string 49 | 50 | // FlagUsageFunc an optional function that is called to set a flag.Usage value 51 | // The input is the raw flag name, and the output should be a string 52 | // that will used in passed into the flag for Usage. 53 | FlagUsageFunc func(name string) string 54 | 55 | // only exists for testing. This is the raw flagset that is to parse 56 | flagSet *flag.FlagSet 57 | } 58 | 59 | // Load loads the source into the config defined by struct s 60 | func (f *FlagLoader) Load(s interface{}) error { 61 | strct := structs.New(s) 62 | structName := strct.Name() 63 | 64 | flagSet := flag.NewFlagSet(structName, f.ErrorHandling) 65 | f.flagSet = flagSet 66 | 67 | for _, field := range strct.Fields() { 68 | f.processField(field.Name(), field) 69 | } 70 | 71 | flagSet.Usage = func() { 72 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 73 | flagSet.PrintDefaults() 74 | fmt.Fprintf(os.Stderr, "\nGenerated environment variables:\n") 75 | e := &EnvironmentLoader{ 76 | Prefix: f.EnvPrefix, 77 | CamelCase: f.CamelCase, 78 | } 79 | e.PrintEnvs(s) 80 | fmt.Println("") 81 | } 82 | 83 | args := filterArgs(os.Args[1:]) 84 | if f.Args != nil { 85 | args = f.Args 86 | } 87 | 88 | return flagSet.Parse(args) 89 | } 90 | 91 | func filterArgs(args []string) []string { 92 | r := []string{} 93 | for i := 0; i < len(args); i++ { 94 | if strings.Index(args[i], "test.") >= 0 { 95 | if i + 1 < len(args) && strings.Index(args[i + 1], "-") == -1 { 96 | i++ 97 | } 98 | i++ 99 | } else { 100 | r = append(r, args[i]) 101 | } 102 | } 103 | return r 104 | } 105 | 106 | // processField generates a flag based on the given field and fieldName. If a 107 | // nested struct is detected, a flag for each field of that nested struct is 108 | // generated too. 109 | func (f *FlagLoader) processField(fieldName string, field *structs.Field) error { 110 | if f.CamelCase { 111 | fieldName = strings.Join(camelcase.Split(fieldName), "-") 112 | fieldName = strings.Replace(fieldName, "---", "-", -1) 113 | } 114 | 115 | switch field.Kind() { 116 | case reflect.Struct: 117 | for _, ff := range field.Fields() { 118 | flagName := field.Name() + "-" + ff.Name() 119 | 120 | if f.Flatten { 121 | // first check if it's set or not, because if we have duplicate 122 | // we don't want to break the flag. Panic by giving a readable 123 | // output 124 | f.flagSet.VisitAll(func(fl *flag.Flag) { 125 | if strings.ToLower(ff.Name()) == fl.Name { 126 | // already defined 127 | panic(fmt.Sprintf("flag '%s' is already defined in outer struct", fl.Name)) 128 | } 129 | }) 130 | 131 | flagName = ff.Name() 132 | } 133 | 134 | if err := f.processField(flagName, ff); err != nil { 135 | return err 136 | } 137 | } 138 | default: 139 | // Add custom prefix to the flag if it's set 140 | if f.Prefix != "" { 141 | fieldName = f.Prefix + "-" + fieldName 142 | } 143 | 144 | // we only can get the value from expored fields, unexported fields panics 145 | if field.IsExported() { 146 | f.flagSet.Var(newFieldValue(field), flagName(fieldName), f.flagUsage(fieldName, field)) 147 | } 148 | } 149 | 150 | return nil 151 | } 152 | 153 | func (f *FlagLoader) flagUsage(fieldName string, field *structs.Field) string { 154 | if f.FlagUsageFunc != nil { 155 | return f.FlagUsageFunc(fieldName) 156 | } 157 | 158 | usage := field.Tag("flagUsage") 159 | if usage != "" { 160 | return usage 161 | } 162 | 163 | return fmt.Sprintf("Change value of %s.", fieldName) 164 | } 165 | 166 | // fieldValue satisfies the flag.Value and flag.Getter interfaces 167 | type fieldValue struct { 168 | field *structs.Field 169 | } 170 | 171 | func newFieldValue(f *structs.Field) *fieldValue { 172 | return &fieldValue{ 173 | field: f, 174 | } 175 | } 176 | 177 | func (f *fieldValue) Set(val string) error { 178 | return fieldSet(f.field, val) 179 | } 180 | 181 | func (f *fieldValue) String() string { 182 | if f.IsZero() { 183 | return "" 184 | } 185 | 186 | return fmt.Sprintf("%v", f.field.Value()) 187 | } 188 | 189 | func (f *fieldValue) Get() interface{} { 190 | if f.IsZero() { 191 | return nil 192 | } 193 | 194 | return f.field.Value() 195 | } 196 | 197 | func (f *fieldValue) IsZero() bool { 198 | return f.field == nil 199 | } 200 | 201 | // This is an unexported interface, be careful about it. 202 | // https://code.google.com/p/go/source/browse/src/pkg/flag/flag.go?name=release#101 203 | func (f *fieldValue) IsBoolFlag() bool { 204 | return f.field.Kind() == reflect.Bool 205 | } 206 | 207 | func flagName(name string) string { return strings.ToLower(name) } 208 | -------------------------------------------------------------------------------- /flag_test.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "flag" 5 | "net/url" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/fatih/structs" 10 | ) 11 | 12 | func TestFlag(t *testing.T) { 13 | m := &FlagLoader{} 14 | s := &Server{} 15 | structName := structs.Name(s) 16 | 17 | // get flags 18 | args := getFlags(t, structName, "") 19 | 20 | m.Args = args[1:] 21 | 22 | if err := m.Load(s); err != nil { 23 | t.Error(err) 24 | } 25 | 26 | testStruct(t, s, getDefaultServer()) 27 | } 28 | 29 | func TestFlagWithPrefix(t *testing.T) { 30 | const prefix = "Prefix" 31 | 32 | m := FlagLoader{Prefix: prefix} 33 | s := &Server{} 34 | structName := structs.Name(s) 35 | 36 | // get flags 37 | args := getFlags(t, structName, prefix) 38 | 39 | m.Args = args[1:] 40 | 41 | if err := m.Load(s); err != nil { 42 | t.Error(err) 43 | } 44 | 45 | testStruct(t, s, getDefaultServer()) 46 | } 47 | 48 | func TestFlattenFlags(t *testing.T) { 49 | m := FlagLoader{ 50 | Flatten: true, 51 | } 52 | s := &FlattenedServer{} 53 | structName := structs.Name(s) 54 | 55 | // get flags 56 | args := getFlags(t, structName, "") 57 | 58 | m.Args = args[1:] 59 | 60 | if err := m.Load(s); err != nil { 61 | t.Error(err) 62 | } 63 | 64 | testFlattenedStruct(t, s, getDefaultServer()) 65 | } 66 | 67 | func TestCamelcaseFlags(t *testing.T) { 68 | m := FlagLoader{ 69 | CamelCase: true, 70 | } 71 | s := &CamelCaseServer{} 72 | structName := structs.Name(s) 73 | 74 | // get flags 75 | args := getFlags(t, structName, "") 76 | 77 | m.Args = args[1:] 78 | 79 | if err := m.Load(s); err != nil { 80 | t.Error(err) 81 | } 82 | 83 | testCamelcaseStruct(t, s, getDefaultCamelCaseServer()) 84 | } 85 | 86 | func TestFlattenAndCamelCaseFlags(t *testing.T) { 87 | m := FlagLoader{ 88 | Flatten: true, 89 | CamelCase: true, 90 | } 91 | s := &FlattenedServer{} 92 | 93 | // get flags 94 | args := getFlags(t, "FlattenedCamelCaseServer", "") 95 | 96 | m.Args = args[1:] 97 | 98 | if err := m.Load(s); err != nil { 99 | t.Error(err) 100 | } 101 | } 102 | 103 | func TestCustomUsageFunc(t *testing.T) { 104 | const usageMsg = "foobar help" 105 | strt := struct { 106 | Foobar string 107 | }{} 108 | m := FlagLoader{ 109 | FlagUsageFunc: (func(s string) string { return usageMsg }), 110 | } 111 | err := m.Load(&strt) 112 | 113 | if err != nil { 114 | t.Fatalf("Unable to load struct: %s", err) 115 | } 116 | f := m.flagSet.Lookup("foobar") 117 | if f == nil { 118 | t.Fatalf("Flag foobar is not set") 119 | } 120 | if f.Usage != usageMsg { 121 | t.Fatalf("usage message was %q, expected %q", f.Usage, usageMsg) 122 | } 123 | } 124 | 125 | type URL struct { 126 | *url.URL 127 | } 128 | 129 | var _ flag.Value = (*URL)(nil) 130 | 131 | func (u *URL) Set(s string) error { 132 | ur, err := url.Parse(s) 133 | if err != nil { 134 | return err 135 | } 136 | u.URL = ur 137 | return nil 138 | } 139 | 140 | type Endpoint struct { 141 | Private *URL `required:"true"` 142 | Public *URL `required:"true"` 143 | } 144 | 145 | func TestFlagValueSupport(t *testing.T) { 146 | m := &FlagLoader{} 147 | 148 | m.Args = []string{ 149 | "-private", "http://127.0.0.1/kloud/kite", 150 | "-public", "http://127.0.0.1/kloud/kite", 151 | } 152 | 153 | var e Endpoint 154 | 155 | if err := m.Load(&e); err != nil { 156 | t.Fatalf("Load()=%s", err) 157 | } 158 | 159 | if e.Private.String() != m.Args[1] { 160 | t.Fatalf("got %q, want %q", e.Private, m.Args[3]) 161 | } 162 | 163 | if e.Public.String() != m.Args[3] { 164 | t.Fatalf("got %q, want %q", e.Public, m.Args[3]) 165 | } 166 | } 167 | func TestCustomUsageTag(t *testing.T) { 168 | const usageMsg = "foobar help" 169 | strt := struct { 170 | Foobar string `flagUsage:"foobar help"` 171 | }{} 172 | m := FlagLoader{} 173 | err := m.Load(&strt) 174 | 175 | if err != nil { 176 | t.Fatalf("Unable to load struct: %s", err) 177 | } 178 | f := m.flagSet.Lookup("foobar") 179 | if f == nil { 180 | t.Fatalf("Flag foobar is not set") 181 | } 182 | if f.Usage != usageMsg { 183 | t.Fatalf("usage message was %q, expected %q", f.Usage, usageMsg) 184 | } 185 | } 186 | 187 | // getFlags returns a slice of arguments that can be passed to flag.Parse() 188 | func getFlags(t *testing.T, structName, prefix string) []string { 189 | if structName == "" { 190 | t.Fatal("struct name can not be empty") 191 | } 192 | 193 | var flags map[string]string 194 | switch structName { 195 | case "Server": 196 | flags = map[string]string{ 197 | "-name": "koding", 198 | "-port": "6060", 199 | "-enabled": "", 200 | "-users": "ankara,istanbul", 201 | "-interval": "10s", 202 | "-id": "1234567890", 203 | "-labels": "123,456", 204 | "-postgres-enabled": "", 205 | "-postgres-port": "5432", 206 | "-postgres-hosts": "192.168.2.1,192.168.2.2,192.168.2.3", 207 | "-postgres-dbname": "configdb", 208 | "-postgres-availabilityratio": "8.23", 209 | } 210 | case "FlattenedServer": 211 | flags = map[string]string{ 212 | "--enabled": "", 213 | "--port": "5432", 214 | "--hosts": "192.168.2.1,192.168.2.2,192.168.2.3", 215 | "--dbname": "configdb", 216 | "--availabilityratio": "8.23", 217 | } 218 | case "FlattenedCamelCaseServer": 219 | flags = map[string]string{ 220 | "--enabled": "", 221 | "--port": "5432", 222 | "--hosts": "192.168.2.1,192.168.2.2,192.168.2.3", 223 | "--db-name": "configdb", 224 | "--availability-ratio": "8.23", 225 | } 226 | case "CamelCaseServer": 227 | flags = map[string]string{ 228 | "--access-key": "123456", 229 | "--normal": "normal", 230 | "--db-name": "configdb", 231 | "--availability-ratio": "8.23", 232 | } 233 | } 234 | 235 | prefix = strings.ToLower(prefix) 236 | 237 | args := []string{"multiconfig-test"} 238 | for key, val := range flags { 239 | flag := key 240 | if prefix != "" { 241 | flag = "-" + prefix + key 242 | } 243 | 244 | if val == "" { 245 | args = append(args, flag) 246 | } else { 247 | args = append(args, flag, val) 248 | } 249 | } 250 | 251 | return args 252 | } 253 | -------------------------------------------------------------------------------- /multiconfig.go: -------------------------------------------------------------------------------- 1 | package multiconfig 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fatih/structs" 13 | ) 14 | 15 | // Loader loads the configuration from a source. The implementer of Loader is 16 | // responsible for setting the default values of the struct. 17 | type Loader interface { 18 | // Load loads the source into the config defined by struct s 19 | Load(s interface{}) error 20 | } 21 | 22 | // DefaultLoader implements the Loader interface. It initializes the given 23 | // pointer of struct s with configuration from the default sources. The order 24 | // of load is TagLoader, FileLoader, EnvLoader and lastly FlagLoader. An error 25 | // in any step stops the loading process. Each step overrides the previous 26 | // step's config (i.e: defining a flag will override previous environment or 27 | // file config). To customize the order use the individual load functions. 28 | type DefaultLoader struct { 29 | Loader 30 | Validator 31 | } 32 | 33 | // NewWithPath returns a new instance of Loader to read from the given 34 | // configuration file. 35 | func NewWithPath(path string) *DefaultLoader { 36 | loaders := []Loader{} 37 | 38 | // Read default values defined via tag fields "default" 39 | loaders = append(loaders, &TagLoader{}) 40 | 41 | // Choose what while is passed 42 | if strings.HasSuffix(path, "toml") { 43 | loaders = append(loaders, &TOMLLoader{Path: path}) 44 | } 45 | 46 | if strings.HasSuffix(path, "json") { 47 | loaders = append(loaders, &JSONLoader{Path: path}) 48 | } 49 | 50 | if strings.HasSuffix(path, "yml") || strings.HasSuffix(path, "yaml") { 51 | loaders = append(loaders, &YAMLLoader{Path: path}) 52 | } 53 | 54 | e := &EnvironmentLoader{} 55 | f := &FlagLoader{} 56 | 57 | loaders = append(loaders, e, f) 58 | loader := MultiLoader(loaders...) 59 | 60 | d := &DefaultLoader{} 61 | d.Loader = loader 62 | d.Validator = MultiValidator(&RequiredValidator{}) 63 | return d 64 | } 65 | 66 | // New returns a new instance of DefaultLoader without any file loaders. 67 | func New() *DefaultLoader { 68 | loader := MultiLoader( 69 | &TagLoader{}, 70 | &EnvironmentLoader{}, 71 | &FlagLoader{}, 72 | ) 73 | 74 | d := &DefaultLoader{} 75 | d.Loader = loader 76 | d.Validator = MultiValidator(&RequiredValidator{}) 77 | return d 78 | } 79 | 80 | // MustLoadWithPath loads with the DefaultLoader settings and from the given 81 | // Path. It exits if the config cannot be parsed. 82 | func MustLoadWithPath(path string, conf interface{}) { 83 | d := NewWithPath(path) 84 | d.MustLoad(conf) 85 | } 86 | 87 | // MustLoad loads with the DefaultLoader settings. It exits if the config 88 | // cannot be parsed. 89 | func MustLoad(conf interface{}) { 90 | d := New() 91 | d.MustLoad(conf) 92 | } 93 | 94 | // MustLoad is like Load but panics if the config cannot be parsed. 95 | func (d *DefaultLoader) MustLoad(conf interface{}) { 96 | if err := d.Load(conf); err != nil { 97 | fmt.Fprintln(os.Stderr, err) 98 | os.Exit(2) 99 | } 100 | 101 | // we at koding, believe having sane defaults in our system, this is the 102 | // reason why we have default validators in DefaultLoader. But do not cause 103 | // nil pointer panics if one uses DefaultLoader directly. 104 | if d.Validator != nil { 105 | d.MustValidate(conf) 106 | } 107 | } 108 | 109 | // MustValidate validates the struct. It exits with status 1 if it can't 110 | // validate. 111 | func (d *DefaultLoader) MustValidate(conf interface{}) { 112 | if err := d.Validate(conf); err != nil { 113 | fmt.Fprintln(os.Stderr, err) 114 | os.Exit(2) 115 | } 116 | } 117 | 118 | // fieldSet sets field value from the given string value. It converts the 119 | // string value in a sane way and is usefulf or environment variables or flags 120 | // which are by nature in string types. 121 | func fieldSet(field *structs.Field, v string) error { 122 | switch f := field.Value().(type) { 123 | case flag.Value: 124 | if v := reflect.ValueOf(field.Value()); v.IsNil() { 125 | typ := v.Type() 126 | if typ.Kind() == reflect.Ptr { 127 | typ = typ.Elem() 128 | } 129 | 130 | if err := field.Set(reflect.New(typ).Interface()); err != nil { 131 | return err 132 | } 133 | 134 | f = field.Value().(flag.Value) 135 | } 136 | 137 | return f.Set(v) 138 | } 139 | 140 | // TODO: add support for other types 141 | switch field.Kind() { 142 | case reflect.Bool: 143 | val, err := strconv.ParseBool(v) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | if err := field.Set(val); err != nil { 149 | return err 150 | } 151 | case reflect.Int: 152 | i, err := strconv.Atoi(v) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | if err := field.Set(i); err != nil { 158 | return err 159 | } 160 | case reflect.String: 161 | if err := field.Set(v); err != nil { 162 | return err 163 | } 164 | case reflect.Slice: 165 | switch t := field.Value().(type) { 166 | case []string: 167 | if err := field.Set(strings.Split(v, ",")); err != nil { 168 | return err 169 | } 170 | case []int: 171 | var list []int 172 | for _, in := range strings.Split(v, ",") { 173 | i, err := strconv.Atoi(in) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | list = append(list, i) 179 | } 180 | 181 | if err := field.Set(list); err != nil { 182 | return err 183 | } 184 | default: 185 | return fmt.Errorf("multiconfig: field '%s' of type slice is unsupported: %s (%T)", 186 | field.Name(), field.Kind(), t) 187 | } 188 | case reflect.Float64: 189 | f, err := strconv.ParseFloat(v, 64) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | if err := field.Set(f); err != nil { 195 | return err 196 | } 197 | case reflect.Int64: 198 | switch t := field.Value().(type) { 199 | case time.Duration: 200 | d, err := time.ParseDuration(v) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | if err := field.Set(d); err != nil { 206 | return err 207 | } 208 | case int64: 209 | p, err := strconv.ParseInt(v, 10, 0) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | if err := field.Set(p); err != nil { 215 | return err 216 | } 217 | default: 218 | return fmt.Errorf("multiconfig: field '%s' of type int64 is unsupported: %s (%T)", 219 | field.Name(), field.Kind(), t) 220 | } 221 | 222 | default: 223 | return fmt.Errorf("multiconfig: field '%s' has unsupported type: %s", field.Name(), field.Kind()) 224 | } 225 | 226 | return nil 227 | } 228 | --------------------------------------------------------------------------------