diff --git a/.gitignore b/.gitignore index 8365624..2824a98 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ _testmain.go *.exe *.test + +*~ diff --git a/README.md b/README.md index d16a80d..806d945 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Multiconfig is able to read configuration automatically based on the given struc * Struct tags * TOML file * JSON file +* YAML file * Environment variables * Flags @@ -30,7 +31,7 @@ Lets define and struct that defines our configuration ```go type Server struct { Name string `required:"true"` - Port int `default:6060` + Port int `default:"6060"` Enabled bool Users []string } @@ -41,7 +42,7 @@ Load the configuration into multiconfig: ```go // Create a new DefaultLoader without or with an initial config file m := multiconfig.New() -m := multiconfig.NewWithPath("config.toml") // supports TOML and JSON +m := multiconfig.NewWithPath("config.toml") // supports TOML, JSON and YAML // Get an empty struct for your configuration serverConf := new(Server) @@ -88,4 +89,4 @@ Generated environment variables: ## License -The MIT License (MIT) - see LICENSE.md for more details +The MIT License (MIT) - see [LICENSE](/LICENSE) for more details diff --git a/doc.go b/doc.go index f4f3aa7..54a8149 100644 --- a/doc.go +++ b/doc.go @@ -1,5 +1,5 @@ // Package multiconfig provides a way to load and read configurations from -// multiple sources. You can read from TOML file, JSON file, Environment -// Variables and flag You can set the order of reader with MultiLoader. Package -// is extensible, you can add your custom Loader by implementing Load interface +// multiple sources. You can read from TOML file, JSON file, YAML file, Environment +// Variables and flags. You can set the order of reader with MultiLoader. Package +// is extensible, you can add your custom Loader by implementing the Load interface. package multiconfig diff --git a/env.go b/env.go index 8ce2f58..2840d8b 100644 --- a/env.go +++ b/env.go @@ -3,7 +3,7 @@ package multiconfig import ( "fmt" "os" - "reflect" + "sort" "strings" "github.com/fatih/camelcase" @@ -18,7 +18,7 @@ type EnvironmentLoader struct { // {STRUCTNAME}_FIELDNAME will be {PREFIX}_FIELDNAME Prefix string - // CamelCase adds a seperator for field names in camelcase form. A + // CamelCase adds a separator for field names in camelcase form. A // fieldname of "AccessKey" would generate a environment name of // "STRUCTNAME_ACCESSKEY". If CamelCase is enabled, the environment name // will be generated in the form of "STRUCTNAME_ACCESS_KEY" @@ -36,11 +36,13 @@ func (e *EnvironmentLoader) getPrefix(s *structs.Struct) string { // Load loads the source into the config defined by struct s func (e *EnvironmentLoader) Load(s interface{}) error { strct := structs.New(s) - + strctMap := strct.Map() prefix := e.getPrefix(strct) - for _, field := range strct.Fields() { - if err := e.processField(prefix, field); err != nil { + for key, val := range strctMap { + field := strct.Field(key) + + if err := e.processField(prefix, field, key, val); err != nil { return err } } @@ -49,14 +51,16 @@ func (e *EnvironmentLoader) Load(s interface{}) error { } // processField gets leading name for the env variable and combines the current -// field's name and generates environemnt variable names recursively -func (e *EnvironmentLoader) processField(prefix string, field *structs.Field) error { - fieldName := e.generateFieldName(prefix, field) - - switch field.Kind() { - case reflect.Struct: - for _, f := range field.Fields() { - if err := e.processField(fieldName, f); err != nil { +// field's name and generates environment variable names recursively +func (e *EnvironmentLoader) processField(prefix string, field *structs.Field, name string, strctMap interface{}) error { + fieldName := e.generateFieldName(prefix, name) + + switch strctMap.(type) { + case map[string]interface{}: + for key, val := range strctMap.(map[string]interface{}) { + field := field.Field(key) + + if err := e.processField(fieldName, field, key, val); err != nil { return err } } @@ -77,34 +81,48 @@ func (e *EnvironmentLoader) processField(prefix string, field *structs.Field) er // PrintEnvs prints the generated environment variables to the std out. func (e *EnvironmentLoader) PrintEnvs(s interface{}) { strct := structs.New(s) - + strctMap := strct.Map() prefix := e.getPrefix(strct) - for _, field := range strct.Fields() { - e.printField(prefix, field) + keys := make([]string, 0, len(strctMap)) + for key := range strctMap { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + field := strct.Field(key) + e.printField(prefix, field, key, strctMap[key]) } } // printField prints the field of the config struct for the flag.Usage -func (e *EnvironmentLoader) printField(prefix string, field *structs.Field) { - fieldName := e.generateFieldName(prefix, field) - - switch field.Kind() { - case reflect.Struct: - for _, f := range field.Fields() { - e.printField(fieldName, f) +func (e *EnvironmentLoader) printField(prefix string, field *structs.Field, name string, strctMap interface{}) { + fieldName := e.generateFieldName(prefix, name) + + switch strctMap.(type) { + case map[string]interface{}: + smap := strctMap.(map[string]interface{}) + keys := make([]string, 0, len(smap)) + for key := range smap { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + field := field.Field(key) + e.printField(fieldName, field, key, smap[key]) } default: fmt.Println(" ", fieldName) } } -// generateFieldName generates the fiels name combined with the prefix and the +// generateFieldName generates the field name combined with the prefix and the // struct's field name -func (e *EnvironmentLoader) generateFieldName(prefix string, field *structs.Field) string { - fieldName := strings.ToUpper(field.Name()) +func (e *EnvironmentLoader) generateFieldName(prefix string, name string) string { + fieldName := strings.ToUpper(name) if e.CamelCase { - fieldName = strings.ToUpper(strings.Join(camelcase.Split(field.Name()), "_")) + fieldName = strings.ToUpper(strings.Join(camelcase.Split(name), "_")) } return strings.ToUpper(prefix) + "_" + fieldName diff --git a/env_test.go b/env_test.go index ad3e794..af235a9 100644 --- a/env_test.go +++ b/env_test.go @@ -57,6 +57,23 @@ func TestENVWithPrefix(t *testing.T) { testStruct(t, s, getDefaultServer()) } +func TestENVFlattenStructPrefix(t *testing.T) { + const prefix = "Prefix" + + m := EnvironmentLoader{Prefix: prefix} + s := &TaggedServer{} + structName := structs.New(s).Name() + + // set env variables + setEnvVars(t, structName, prefix) + + if err := m.Load(s); err != nil { + t.Error(err) + } + + testPostgres(t, s.Postgres, getDefaultServer().Postgres) +} + func setEnvVars(t *testing.T, structName, prefix string) { if structName == "" { t.Fatal("struct name can not be empty") @@ -70,6 +87,9 @@ func setEnvVars(t *testing.T, structName, prefix string) { "PORT": "6060", "ENABLED": "true", "USERS": "ankara,istanbul", + "INTERVAL": "10s", + "ID": "1234567890", + "LABELS": "123,456", "POSTGRES_ENABLED": "true", "POSTGRES_PORT": "5432", "POSTGRES_HOSTS": "192.168.2.1,192.168.2.2,192.168.2.3", @@ -84,6 +104,16 @@ func setEnvVars(t *testing.T, structName, prefix string) { "DB_NAME": "configdb", "AVAILABILITY_RATIO": "8.23", } + case "TaggedServer": + env = map[string]string{ + "NAME": "koding", + "ENABLED": "true", + "PORT": "5432", + "HOSTS": "192.168.2.1,192.168.2.2,192.168.2.3", + "DBNAME": "configdb", + "AVAILABILITYRATIO": "8.23", + "FOO": "8.23,9.12,11,90", + } } if prefix == "" { diff --git a/example_test.go b/example_test.go index f4887fc..9860ce1 100644 --- a/example_test.go +++ b/example_test.go @@ -21,7 +21,7 @@ func ExampleDefaultLoader() { // It first sets the default values for each field with tag values defined // with "default", next it reads from config.toml, from environment - // variables and finally from command line flags. It panic's if loading fails. + // variables and finally from command line flags. It panics if loading fails. d.MustLoad(s) fmt.Println("Host-->", s.Name) @@ -151,3 +151,30 @@ func ExampleJSONLoader() { // Host--> koding // Users--> [ankara istanbul] } + +func ExampleYAMLLoader() { + // Our struct which is used for configuration + type ServerConfig struct { + Name string + Port int + Enabled bool + Users []string + Postgres Postgres + } + + // Instantiate loader + l := &YAMLLoader{Path: testYAML} + + s := &ServerConfig{} + err := l.Load(s) + if err != nil { + panic(err) + } + + fmt.Println("Host-->", s.Name) + fmt.Println("Users-->", s.Users) + + // Output: + // Host--> koding + // Users--> [ankara istanbul] +} diff --git a/file.go b/file.go index 3dc78ce..3416a9e 100644 --- a/file.go +++ b/file.go @@ -3,35 +3,50 @@ package multiconfig import ( "encoding/json" "errors" + "io" "io/ioutil" "os" "path/filepath" "github.com/BurntSushi/toml" + yaml "gopkg.in/yaml.v2" ) var ( - // ErrPathNotSet states that given path to file loader is empty - ErrPathNotSet = errors.New("config path is not set") + // ErrSourceNotSet states that neither the path or the reader is set on the loader + ErrSourceNotSet = errors.New("config path or reader is not set") // ErrFileNotFound states that given file is not exists ErrFileNotFound = errors.New("config file not found") ) // TOMLLoader satisifies the loader interface. It loads the configuration from -// the given toml file. +// the given toml file or Reader. type TOMLLoader struct { - Path string + Path string + Reader io.Reader } // Load loads the source into the config defined by struct s +// Defaults to using the Reader if provided, otherwise tries to read from the +// file func (t *TOMLLoader) Load(s interface{}) error { - filePath, err := getConfigPath(t.Path) - if err != nil { - return err + var r io.Reader + + if t.Reader != nil { + r = t.Reader + } else if t.Path != "" { + file, err := getConfig(t.Path) + if err != nil { + return err + } + defer file.Close() + r = file + } else { + return ErrSourceNotSet } - if _, err := toml.DecodeFile(filePath, s); err != nil { + if _, err := toml.DecodeReader(r, s); err != nil { return err } @@ -39,47 +54,86 @@ func (t *TOMLLoader) Load(s interface{}) error { } // JSONLoader satisifies the loader interface. It loads the configuration from -// the given json file. +// the given json file or Reader. type JSONLoader struct { - Path string + Path string + Reader io.Reader } -// Load loads the source into the config defined by struct s +// Load loads the source into the config defined by struct s. +// Defaults to using the Reader if provided, otherwise tries to read from the +// file func (j *JSONLoader) Load(s interface{}) error { - filePath, err := getConfigPath(j.Path) - if err != nil { - return err + var r io.Reader + if j.Reader != nil { + r = j.Reader + } else if j.Path != "" { + file, err := getConfig(j.Path) + if err != nil { + return err + } + defer file.Close() + r = file + } else { + return ErrSourceNotSet } - file, err := ioutil.ReadFile(filePath) + return json.NewDecoder(r).Decode(s) +} + +// YAMLLoader satisifies the loader interface. It loads the configuration from +// the given yaml file. +type YAMLLoader struct { + Path string + Reader io.Reader +} + +// Load loads the source into the config defined by struct s. +// Defaults to using the Reader if provided, otherwise tries to read from the +// file +func (y *YAMLLoader) Load(s interface{}) error { + var r io.Reader + + if y.Reader != nil { + r = y.Reader + } else if y.Path != "" { + file, err := getConfig(y.Path) + if err != nil { + return err + } + defer file.Close() + r = file + } else { + return ErrSourceNotSet + } + + data, err := ioutil.ReadAll(r) if err != nil { return err } - return json.Unmarshal(file, s) + return yaml.Unmarshal(data, s) } -func getConfigPath(path string) (string, error) { - if path == "" { - return "", ErrPathNotSet - } - +func getConfig(path string) (*os.File, error) { pwd, err := os.Getwd() if err != nil { - return "", err + return nil, err } - configPath := filepath.Join(pwd, path) + configPath := path + if !filepath.IsAbs(path) { + configPath = filepath.Join(pwd, path) + } // check if file with combined path is exists(relative path) if _, err := os.Stat(configPath); !os.IsNotExist(err) { - return configPath, nil + return os.Open(configPath) } - // check if file is exists it self - if _, err := os.Stat(path); !os.IsNotExist(err) { - return path, nil + f, err := os.Open(path) + if os.IsNotExist(err) { + return nil, ErrFileNotFound } - - return "", ErrFileNotFound + return f, err } diff --git a/file_test.go b/file_test.go index 8017d79..a9050cd 100644 --- a/file_test.go +++ b/file_test.go @@ -1,7 +1,36 @@ package multiconfig -import "testing" +import ( + "os" + "testing" +) +func TestYAML(t *testing.T) { + m := NewWithPath(testYAML) + + s := &Server{} + if err := m.Load(s); err != nil { + t.Error(err) + } + + testStruct(t, s, getDefaultServer()) +} + +func TestYAML_Reader(t *testing.T) { + f, err := os.Open(testYAML) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + l := MultiLoader(&TagLoader{}, &YAMLLoader{Reader: f}) + s := &Server{} + if err := l.Load(s); err != nil { + t.Error(err) + } + + testStruct(t, s, getDefaultServer()) +} func TestToml(t *testing.T) { m := NewWithPath(testTOML) @@ -13,6 +42,22 @@ func TestToml(t *testing.T) { testStruct(t, s, getDefaultServer()) } +func TestToml_Reader(t *testing.T) { + f, err := os.Open(testTOML) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + l := MultiLoader(&TagLoader{}, &TOMLLoader{Reader: f}) + s := &Server{} + if err := l.Load(s); err != nil { + t.Error(err) + } + + testStruct(t, s, getDefaultServer()) +} + func TestJSON(t *testing.T) { m := NewWithPath(testJSON) @@ -24,6 +69,22 @@ func TestJSON(t *testing.T) { testStruct(t, s, getDefaultServer()) } +func TestJSON_Reader(t *testing.T) { + f, err := os.Open(testJSON) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + l := MultiLoader(&TagLoader{}, &JSONLoader{Reader: f}) + s := &Server{} + if err := l.Load(s); err != nil { + t.Error(err) + } + + testStruct(t, s, getDefaultServer()) +} + // func TestJSON2(t *testing.T) { // ExampleEnvironmentLoader() // ExampleTOMLLoader() diff --git a/flag.go b/flag.go index 02b42d6..56cd64b 100644 --- a/flag.go +++ b/flag.go @@ -28,7 +28,7 @@ type FlagLoader struct { // struct). Use this option only if you know what you do. Flatten bool - // CamelCase adds a seperator for field names in camelcase form. A + // CamelCase adds a separator for field names in camelcase form. A // fieldname of "AccessKey" would generate a flag name "--accesskey". If // CamelCase is enabled, the flag name will be generated in the form of // "--access-key" @@ -38,8 +38,22 @@ type FlagLoader struct { // EnvLoader is used EnvPrefix string + // ErrorHandling is used to configure error handling used by + // *flag.FlagSet. + // + // By default it's flag.ContinueOnError. + ErrorHandling flag.ErrorHandling + // Args defines a custom argument list. If nil, os.Args[1:] is used. Args []string + + // FlagUsageFunc an optional function that is called to set a flag.Usage value + // The input is the raw flag name, and the output should be a string + // that will used in passed into the flag for Usage. + FlagUsageFunc func(name string) string + + // only exists for testing. This is the raw flagset that is to parse + flagSet *flag.FlagSet } // Load loads the source into the config defined by struct s @@ -47,10 +61,11 @@ func (f *FlagLoader) Load(s interface{}) error { strct := structs.New(s) structName := strct.Name() - flagSet := flag.NewFlagSet(structName, flag.ExitOnError) + flagSet := flag.NewFlagSet(structName, f.ErrorHandling) + f.flagSet = flagSet for _, field := range strct.Fields() { - f.processField(flagSet, field.Name(), field) + f.processField(field.Name(), field) } flagSet.Usage = func() { @@ -65,7 +80,7 @@ func (f *FlagLoader) Load(s interface{}) error { fmt.Println("") } - args := os.Args[1:] + args := filterArgs(os.Args[1:]) if f.Args != nil { args = f.Args } @@ -73,12 +88,28 @@ func (f *FlagLoader) Load(s interface{}) error { return flagSet.Parse(args) } +func filterArgs(args []string) []string { + r := []string{} + for i := 0; i < len(args); i++ { + if strings.Index(args[i], "test.") >= 0 { + if i + 1 < len(args) && strings.Index(args[i + 1], "-") == -1 { + i++ + } + i++ + } else { + r = append(r, args[i]) + } + } + return r +} + // processField generates a flag based on the given field and fieldName. If a // nested struct is detected, a flag for each field of that nested struct is // generated too. -func (f *FlagLoader) processField(flagSet *flag.FlagSet, fieldName string, field *structs.Field) error { +func (f *FlagLoader) processField(fieldName string, field *structs.Field) error { if f.CamelCase { fieldName = strings.Join(camelcase.Split(fieldName), "-") + fieldName = strings.Replace(fieldName, "---", "-", -1) } switch field.Kind() { @@ -90,7 +121,7 @@ func (f *FlagLoader) processField(flagSet *flag.FlagSet, fieldName string, field // first check if it's set or not, because if we have duplicate // we don't want to break the flag. Panic by giving a readable // output - flagSet.VisitAll(func(fl *flag.Flag) { + f.flagSet.VisitAll(func(fl *flag.Flag) { if strings.ToLower(ff.Name()) == fl.Name { // already defined panic(fmt.Sprintf("flag '%s' is already defined in outer struct", fl.Name)) @@ -100,7 +131,7 @@ func (f *FlagLoader) processField(flagSet *flag.FlagSet, fieldName string, field flagName = ff.Name() } - if err := f.processField(flagSet, flagName, ff); err != nil { + if err := f.processField(flagName, ff); err != nil { return err } } @@ -110,45 +141,67 @@ func (f *FlagLoader) processField(flagSet *flag.FlagSet, fieldName string, field fieldName = f.Prefix + "-" + fieldName } - flagSet.Var(newFieldValue(field), flagName(fieldName), flagUsage(fieldName)) + // we only can get the value from expored fields, unexported fields panics + if field.IsExported() { + f.flagSet.Var(newFieldValue(field), flagName(fieldName), f.flagUsage(fieldName, field)) + } } return nil } +func (f *FlagLoader) flagUsage(fieldName string, field *structs.Field) string { + if f.FlagUsageFunc != nil { + return f.FlagUsageFunc(fieldName) + } + + usage := field.Tag("flagUsage") + if usage != "" { + return usage + } + + return fmt.Sprintf("Change value of %s.", fieldName) +} + // fieldValue satisfies the flag.Value and flag.Getter interfaces -type fieldValue structs.Field +type fieldValue struct { + field *structs.Field +} func newFieldValue(f *structs.Field) *fieldValue { - fl := fieldValue(*f) - return &fl + return &fieldValue{ + field: f, + } } func (f *fieldValue) Set(val string) error { - field := (*structs.Field)(f) - return fieldSet(field, val) + return fieldSet(f.field, val) } func (f *fieldValue) String() string { - fl := (*structs.Field)(f) - return fmt.Sprintf("%v", fl.Value()) + if f.IsZero() { + return "" + } + + return fmt.Sprintf("%v", f.field.Value()) } func (f *fieldValue) Get() interface{} { - fl := (*structs.Field)(f) - return fl.Value() + if f.IsZero() { + return nil + } + + return f.field.Value() +} + +func (f *fieldValue) IsZero() bool { + return f.field == nil } // This is an unexported interface, be careful about it. // https://code.google.com/p/go/source/browse/src/pkg/flag/flag.go?name=release#101 func (f *fieldValue) IsBoolFlag() bool { - fl := (*structs.Field)(f) - if fl.Kind() == reflect.Bool { - return true - } - return false + return f.field.Kind() == reflect.Bool } -func flagUsage(name string) string { return fmt.Sprintf("Change value of %s.", name) } - func flagName(name string) string { return strings.ToLower(name) } diff --git a/flag_test.go b/flag_test.go index 7d4d22a..9a88a91 100644 --- a/flag_test.go +++ b/flag_test.go @@ -1,6 +1,8 @@ package multiconfig import ( + "flag" + "net/url" "strings" "testing" @@ -96,8 +98,90 @@ func TestFlattenAndCamelCaseFlags(t *testing.T) { if err := m.Load(s); err != nil { t.Error(err) } +} - testFlattenedStruct(t, s, getDefaultServer()) +func TestCustomUsageFunc(t *testing.T) { + const usageMsg = "foobar help" + strt := struct { + Foobar string + }{} + m := FlagLoader{ + FlagUsageFunc: (func(s string) string { return usageMsg }), + } + err := m.Load(&strt) + + if err != nil { + t.Fatalf("Unable to load struct: %s", err) + } + f := m.flagSet.Lookup("foobar") + if f == nil { + t.Fatalf("Flag foobar is not set") + } + if f.Usage != usageMsg { + t.Fatalf("usage message was %q, expected %q", f.Usage, usageMsg) + } +} + +type URL struct { + *url.URL +} + +var _ flag.Value = (*URL)(nil) + +func (u *URL) Set(s string) error { + ur, err := url.Parse(s) + if err != nil { + return err + } + u.URL = ur + return nil +} + +type Endpoint struct { + Private *URL `required:"true"` + Public *URL `required:"true"` +} + +func TestFlagValueSupport(t *testing.T) { + m := &FlagLoader{} + + m.Args = []string{ + "-private", "http://127.0.0.1/kloud/kite", + "-public", "http://127.0.0.1/kloud/kite", + } + + var e Endpoint + + if err := m.Load(&e); err != nil { + t.Fatalf("Load()=%s", err) + } + + if e.Private.String() != m.Args[1] { + t.Fatalf("got %q, want %q", e.Private, m.Args[3]) + } + + if e.Public.String() != m.Args[3] { + t.Fatalf("got %q, want %q", e.Public, m.Args[3]) + } +} +func TestCustomUsageTag(t *testing.T) { + const usageMsg = "foobar help" + strt := struct { + Foobar string `flagUsage:"foobar help"` + }{} + m := FlagLoader{} + err := m.Load(&strt) + + if err != nil { + t.Fatalf("Unable to load struct: %s", err) + } + f := m.flagSet.Lookup("foobar") + if f == nil { + t.Fatalf("Flag foobar is not set") + } + if f.Usage != usageMsg { + t.Fatalf("usage message was %q, expected %q", f.Usage, usageMsg) + } } // getFlags returns a slice of arguments that can be passed to flag.Parse() @@ -114,6 +198,9 @@ func getFlags(t *testing.T, structName, prefix string) []string { "-port": "6060", "-enabled": "", "-users": "ankara,istanbul", + "-interval": "10s", + "-id": "1234567890", + "-labels": "123,456", "-postgres-enabled": "", "-postgres-port": "5432", "-postgres-hosts": "192.168.2.1,192.168.2.2,192.168.2.3", diff --git a/multiconfig.go b/multiconfig.go index 354c57f..0070344 100644 --- a/multiconfig.go +++ b/multiconfig.go @@ -1,18 +1,19 @@ package multiconfig import ( - "errors" + "flag" "fmt" "os" "reflect" "strconv" "strings" + "time" "github.com/fatih/structs" ) // Loader loads the configuration from a source. The implementer of Loader is -// responsible of setting the default values of the struct. +// responsible for setting the default values of the struct. type Loader interface { // Load loads the source into the config defined by struct s Load(s interface{}) error @@ -46,6 +47,10 @@ func NewWithPath(path string) *DefaultLoader { loaders = append(loaders, &JSONLoader{Path: path}) } + if strings.HasSuffix(path, "yml") || strings.HasSuffix(path, "yaml") { + loaders = append(loaders, &YAMLLoader{Path: path}) + } + e := &EnvironmentLoader{} f := &FlagLoader{} @@ -114,6 +119,24 @@ func (d *DefaultLoader) MustValidate(conf interface{}) { // string value in a sane way and is usefulf or environment variables or flags // which are by nature in string types. func fieldSet(field *structs.Field, v string) error { + switch f := field.Value().(type) { + case flag.Value: + if v := reflect.ValueOf(field.Value()); v.IsNil() { + typ := v.Type() + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + if err := field.Set(reflect.New(typ).Interface()); err != nil { + return err + } + + f = field.Value().(flag.Value) + } + + return f.Set(v) + } + // TODO: add support for other types switch field.Kind() { case reflect.Bool: @@ -135,16 +158,33 @@ func fieldSet(field *structs.Field, v string) error { return err } case reflect.String: - field.Set(v) - case reflect.Slice: - // TODO add other typed slice support - if _, ok := field.Value().([]string); !ok { - return errors.New("can't set on non string slices") - } - - if err := field.Set(strings.Split(v, ",")); err != nil { + if err := field.Set(v); err != nil { return err } + case reflect.Slice: + switch t := field.Value().(type) { + case []string: + if err := field.Set(strings.Split(v, ",")); err != nil { + return err + } + case []int: + var list []int + for _, in := range strings.Split(v, ",") { + i, err := strconv.Atoi(in) + if err != nil { + return err + } + + list = append(list, i) + } + + if err := field.Set(list); err != nil { + return err + } + default: + return fmt.Errorf("multiconfig: field '%s' of type slice is unsupported: %s (%T)", + field.Name(), field.Kind(), t) + } case reflect.Float64: f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -154,8 +194,33 @@ func fieldSet(field *structs.Field, v string) error { if err := field.Set(f); err != nil { return err } + case reflect.Int64: + switch t := field.Value().(type) { + case time.Duration: + d, err := time.ParseDuration(v) + if err != nil { + return err + } + + if err := field.Set(d); err != nil { + return err + } + case int64: + p, err := strconv.ParseInt(v, 10, 0) + if err != nil { + return err + } + + if err := field.Set(p); err != nil { + return err + } + default: + return fmt.Errorf("multiconfig: field '%s' of type int64 is unsupported: %s (%T)", + field.Name(), field.Kind(), t) + } + default: - return fmt.Errorf("multiconfig: not supported type: %s", field.Kind()) + return fmt.Errorf("multiconfig: field '%s' has unsupported type: %s", field.Name(), field.Kind()) } return nil diff --git a/multiconfig_test.go b/multiconfig_test.go index 89c7f7e..3ff44ed 100644 --- a/multiconfig_test.go +++ b/multiconfig_test.go @@ -1,14 +1,21 @@ package multiconfig -import "testing" +import ( + "testing" + "time" +) type ( Server struct { - Name string `required:"true"` - Port int `default:"6060"` - Enabled bool - Users []string - Postgres Postgres + Name string `required:"true"` + Port int `default:"6060"` + ID int64 + Labels []int + Enabled bool + Users []string + Postgres Postgres + unexported string + Interval time.Duration } // Postgres holds Postgresql database related configuration @@ -18,6 +25,12 @@ type ( Hosts []string `required:"true"` DBName string `default:"configdb"` AvailabilityRatio float64 + unexported string + } + + TaggedServer struct { + Name string `required:"true"` + Postgres `structs:",flatten"` } ) @@ -35,14 +48,18 @@ type CamelCaseServer struct { var ( testTOML = "testdata/config.toml" testJSON = "testdata/config.json" + testYAML = "testdata/config.yaml" ) func getDefaultServer() *Server { return &Server{ - Name: "koding", - Port: 6060, - Enabled: true, - Users: []string{"ankara", "istanbul"}, + Name: "koding", + Port: 6060, + Enabled: true, + ID: 1234567890, + Labels: []int{123, 456}, + Users: []string{"ankara", "istanbul"}, + Interval: 10 * time.Second, Postgres: Postgres{ Enabled: true, Port: 5432, @@ -109,6 +126,24 @@ func testStruct(t *testing.T, s *Server, d *Server) { t.Errorf("Enabled value is wrong: %t, want: %t", s.Enabled, d.Enabled) } + if s.Interval != d.Interval { + t.Errorf("Interval value is wrong: %v, want: %v", s.Interval, d.Interval) + } + + if s.ID != d.ID { + t.Errorf("ID value is wrong: %v, want: %v", s.ID, d.ID) + } + + if len(s.Labels) != len(d.Labels) { + t.Errorf("Labels value is wrong: %d, want: %d", len(s.Labels), len(d.Labels)) + } else { + for i, label := range d.Labels { + if s.Labels[i] != label { + t.Errorf("Label is wrong for index: %d, label: %d, want: %d", i, s.Labels[i], label) + } + } + } + if len(s.Users) != len(d.Users) { t.Errorf("Users value is wrong: %d, want: %d", len(s.Users), len(d.Users)) } else { @@ -119,63 +154,40 @@ func testStruct(t *testing.T, s *Server, d *Server) { } } - // Explicitly state that Enabled should be true, no need to check - // `x == true` infact. - if s.Postgres.Enabled != d.Postgres.Enabled { - t.Errorf("Postgres enabled is wrong %t, want: %t", s.Postgres.Enabled, d.Postgres.Enabled) - } - - if s.Postgres.Port != d.Postgres.Port { - t.Errorf("Postgres Port value is wrong: %d, want: %d", s.Postgres.Port, d.Postgres.Port) - } - - if s.Postgres.DBName != d.Postgres.DBName { - t.Errorf("DBName is wrong: %s, want: %s", s.Postgres.DBName, d.Postgres.DBName) - } - - if s.Postgres.AvailabilityRatio != d.Postgres.AvailabilityRatio { - t.Errorf("AvailabilityRatio is wrong: %f, want: %f", s.Postgres.AvailabilityRatio, d.Postgres.AvailabilityRatio) - } - - if len(s.Postgres.Hosts) != len(d.Postgres.Hosts) { - // do not continue testing if this fails, because others is depending on this test - t.Fatalf("Hosts len is wrong: %v, want: %v", s.Postgres.Hosts, d.Postgres.Hosts) - } - - for i, host := range d.Postgres.Hosts { - if s.Postgres.Hosts[i] != host { - t.Fatalf("Hosts number %d is wrong: %v, want: %v", i, s.Postgres.Hosts[i], host) - } - } + testPostgres(t, s.Postgres, d.Postgres) } func testFlattenedStruct(t *testing.T, s *FlattenedServer, d *Server) { // Explicitly state that Enabled should be true, no need to check // `x == true` infact. - if s.Postgres.Enabled != d.Postgres.Enabled { - t.Errorf("Postgres enabled is wrong %t, want: %t", s.Postgres.Enabled, d.Postgres.Enabled) + testPostgres(t, s.Postgres, d.Postgres) +} + +func testPostgres(t *testing.T, s Postgres, d Postgres) { + if s.Enabled != d.Enabled { + t.Errorf("Postgres enabled is wrong %t, want: %t", s.Enabled, d.Enabled) } - if s.Postgres.Port != d.Postgres.Port { - t.Errorf("Postgres Port value is wrong: %d, want: %d", s.Postgres.Port, d.Postgres.Port) + if s.Port != d.Port { + t.Errorf("Postgres Port value is wrong: %d, want: %d", s.Port, d.Port) } - if s.Postgres.DBName != d.Postgres.DBName { - t.Errorf("DBName is wrong: %s, want: %s", s.Postgres.DBName, d.Postgres.DBName) + if s.DBName != d.DBName { + t.Errorf("DBName is wrong: %s, want: %s", s.DBName, d.DBName) } - if s.Postgres.AvailabilityRatio != d.Postgres.AvailabilityRatio { - t.Errorf("AvailabilityRatio is wrong: %f, want: %f", s.Postgres.AvailabilityRatio, d.Postgres.AvailabilityRatio) + if s.AvailabilityRatio != d.AvailabilityRatio { + t.Errorf("AvailabilityRatio is wrong: %f, want: %f", s.AvailabilityRatio, d.AvailabilityRatio) } - if len(s.Postgres.Hosts) != len(d.Postgres.Hosts) { + if len(s.Hosts) != len(d.Hosts) { // do not continue testing if this fails, because others is depending on this test - t.Fatalf("Hosts len is wrong: %v, want: %v", s.Postgres.Hosts, d.Postgres.Hosts) + t.Fatalf("Hosts len is wrong: %v, want: %v", s.Hosts, d.Hosts) } - for i, host := range d.Postgres.Hosts { - if s.Postgres.Hosts[i] != host { - t.Fatalf("Hosts number %d is wrong: %v, want: %v", i, s.Postgres.Hosts[i], host) + for i, host := range d.Hosts { + if s.Hosts[i] != host { + t.Fatalf("Hosts number %d is wrong: %v, want: %v", i, s.Hosts[i], host) } } } diff --git a/multivalidator.go b/multivalidator.go index ab08d65..726febf 100644 --- a/multivalidator.go +++ b/multivalidator.go @@ -7,7 +7,7 @@ func MultiValidator(validators ...Validator) Validator { return multiValidator(validators) } -// Validate tries to validate given struct with all the validators. If it doesnt +// Validate tries to validate given struct with all the validators. If it doesn't // have any Validator it will simply skip the validation step. If any of the // given validators return err, it will stop validating and return it. func (d multiValidator) Validate(s interface{}) error { diff --git a/tag.go b/tag.go index 10f9559..dddde0d 100644 --- a/tag.go +++ b/tag.go @@ -7,7 +7,7 @@ import ( ) // TagLoader satisfies the loader interface. It parses a struct's field tags -// and populated the each field with that given tag. +// and populates the each field with that given tag. type TagLoader struct { // DefaultTagName is the default tag name for struct fields to define // default values for a field. Example: diff --git a/testdata/config.json b/testdata/config.json index 0e5cd62..6c2e924 100644 --- a/testdata/config.json +++ b/testdata/config.json @@ -1,6 +1,12 @@ { "Name": "koding", "Enabled": true, + "Interval": 10000000000, + "ID": 1234567890, + "Labels": [ + 123, + 456 + ], "Users": [ "ankara", "istanbul" diff --git a/testdata/config.toml b/testdata/config.toml index ea3690c..bfc01a4 100644 --- a/testdata/config.toml +++ b/testdata/config.toml @@ -1,6 +1,9 @@ Name = "koding" -Enabled = false +Enabled = true Users = ["ankara", "istanbul"] +Interval = 10000000000 +ID = 1234567890 +Labels = [123,456] [Postgres] Enabled = true diff --git a/testdata/config.yaml b/testdata/config.yaml new file mode 100644 index 0000000..8354703 --- /dev/null +++ b/testdata/config.yaml @@ -0,0 +1,28 @@ +# server configure + +name: koding + +enabled: true + +users: + - ankara + - istanbul + +interval: 10000000000 + +id: 1234567890 + +labels: + - 123 + - 456 + +# postgres configure +postgres: + enabled: true + port: 5432 + hosts: + - 192.168.2.1 + - 192.168.2.2 + - 192.168.2.3 + availabilityratio: 8.23 + diff --git a/validator.go b/validator.go index a66a444..67d04e2 100644 --- a/validator.go +++ b/validator.go @@ -9,13 +9,13 @@ import ( // Validator validates the config against any predefined rules, those predefined // rules should be given to this package. The implementer will be responsible -// about the logic +// for the logic. type Validator interface { // Validate validates the config struct Validate(s interface{}) error } -// RequiredValidator validates the struct against zero values +// RequiredValidator validates the struct against zero values. type RequiredValidator struct { // TagName holds the validator tag name. The default is "required" TagName string