diff --git a/README.md b/README.md index 4b7763b..179ea4b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Feeders provide content of the configuration. Currently, these feeders exist out * `Map`: Feeds a simple `map[string]interface{}`. * `Json`: Feeds a JSON file. * `JsonDirectory`: Feeds a directory of JSON files. +* `Yaml`: Feeds a Yaml file. Of course, you are free to implement your feeders by implementing the `Feeder` interface. @@ -126,6 +127,47 @@ v, err := c.Get("version") // 3.14 v, err := c.Get("numbers.2") // 3 +v, err := c.Get("users.0.address.city") // Delfan +``` +#### Feeding using Yaml feeder + +Yaml files are a trend these days, so why not store configurations in them ? + +`config.yaml`: + +```yaml +name: MyAppUsingGoLobbyConfig +version: 3.14 +numbers: + - 1 + - 2 + - 3 +users: + - name: Milad Rahimi + year: 1993 + address: + country: Iran + state: Lorestan + city: Delfan + - name: Amirreza Askarpour + year: 1998 + address: + country: Iran + state: Khouzestan + city: Ahvaz +``` + +Go code: + +```go +c, err := config.New(config.Options{ + Feeder: feeder.Yaml{Path: "path/to/config.yaml"}, +}) + +v, err := c.Get("version") // 3.14 + +v, err := c.Get("numbers.2") // 3 + v, err := c.Get("users.0.address.city") // Delfan ``` @@ -172,6 +214,50 @@ v, err := c.Get("app.version") // 3.14 v, err := c.Get("db.mysql.host") // localhost ``` +#### Feeding using YamlDirectory + +If you have many configuration data and it doesn't fit in a single YAML file. +In this case, you can use multiple YAML files and feed them using YamlDirectory feeder like this example: + +Sample project directory structure: + +``` +- main.go +- config +- - app.yaml +- - db.yaml +``` + +`app.yaml`: + +```yaml +name: MyApp +version: 3.14 +``` + +`db.yaml`: + +```yaml +sqlite: + path: app.db + +mysql: + host: localhost + user: root + pass: secret + +``` + +Go code: + +```go +c, err := config.New(config.Options{ + Feeder: feeder.YamlDirectory{Path: "config"}, +}) + +v, err := c.Get("app.version") // 3.14 +v, err := c.Get("db.mysql.host") // localhost +``` ### OS variables and environment files #### OS variables diff --git a/config.go b/config.go index dc02387..b1799b6 100644 --- a/config.go +++ b/config.go @@ -4,13 +4,15 @@ package config import ( "errors" - "github.com/golobby/config/env" + "fmt" "os" "os/signal" "strconv" "strings" "sync" "syscall" + + "github.com/golobby/config/env" ) // Feeder is an interface for config feeders that provide content of a config instance. @@ -23,7 +25,21 @@ type Options struct { Feeder Feeder // Feeder is the feeder that is going to feed the Config instance. Env string // Env is the file path that locates the environment file. } - +//NotFoundError happens when you try to access a key which is not defined in the configuration files. +type NotFoundError struct{ + key string +} +func (n *NotFoundError) Error() string { + return fmt.Sprintf("value not found for the key %s", n.key) +} +//TypeError happens when you try to access a key using a helper function that casts value to a type which can't be done. +type TypeError struct { + value interface{} + wanted string +} +func (t *TypeError) Error() string { + return fmt.Sprintf("value %s (%T) is not %s", t.value, t.value, t.wanted) +} // Config keeps all the Config instance data. type Config struct { env struct { @@ -64,7 +80,7 @@ func (c *Config) ReloadEnv() error { } // GetEnv returns the environment variable value for the given environment variable key. -func (c Config) GetEnv(key string) string { +func (c *Config) GetEnv(key string) string { c.env.sync.RLock() defer c.env.sync.RUnlock() @@ -78,7 +94,7 @@ func (c Config) GetEnv(key string) string { } // GetAllEnvs returns all the environment variables (key/values) -func (c Config) GetAllEnvs() map[string]string { +func (c *Config) GetAllEnvs() map[string]string { return c.env.items } @@ -154,7 +170,7 @@ func (c *Config) Set(key string, value interface{}) { // The return type is "interface{}". // It probably needs to be cast to the related data type. // It returns an error if there is no value for the given key. -func (c Config) Get(key string) (interface{}, error) { +func (c *Config) Get(key string) (interface{}, error) { c.sync.RLock() defer c.sync.RUnlock() @@ -165,7 +181,7 @@ func (c Config) Get(key string) (interface{}, error) { } if strings.Contains(key, ".") == false { - return nil, errors.New("value not found for the key " + key) + return nil, &NotFoundError{key: key} } v, err := lookup(c.items, key) @@ -174,7 +190,7 @@ func (c Config) Get(key string) (interface{}, error) { } // GetAll returns all the configuration items (key/values). -func (c Config) GetAll() map[string]interface{} { +func (c *Config) GetAll() map[string]interface{} { return c.items } @@ -182,7 +198,7 @@ func (c Config) GetAll() map[string]interface{} { // It also casts the value type to string internally. // It returns an error if the related value is not a string. // It returns an error if there is no value for the given key. -func (c Config) GetString(key string) (string, error) { +func (c *Config) GetString(key string) (string, error) { v, err := c.Get(key) if err != nil { return "", err @@ -192,14 +208,14 @@ func (c Config) GetString(key string) (string, error) { return v, nil } - return "", errors.New("value for " + key + " is not string") + return "", &TypeError{value: v, wanted: "string"} } // GetInt returns the value of the given key. // It also casts the value type to int internally. // It returns an error if the related value is not an int. // It returns an error if there is no value for the given key. -func (c Config) GetInt(key string) (int, error) { +func (c *Config) GetInt(key string) (int, error) { v, err := c.Get(key) if err != nil { return 0, err @@ -209,14 +225,14 @@ func (c Config) GetInt(key string) (int, error) { return v, nil } - return 0, errors.New("value for " + key + " is not int") + return 0, &TypeError{value: v, wanted: "int"} } // GetFloat returns the value of the given key. // It also casts the value type to float64 internally. // It returns an error if the related value is not a float64. // It returns an error if there is no value for the given key. -func (c Config) GetFloat(key string) (float64, error) { +func (c *Config) GetFloat(key string) (float64, error) { v, err := c.Get(key) if err != nil { return 0, err @@ -226,7 +242,7 @@ func (c Config) GetFloat(key string) (float64, error) { return v, nil } - return 0, errors.New("value for " + key + " is not float") + return 0, &TypeError{value: v, wanted: "float"} } // GetBool returns the value of the given key. @@ -234,7 +250,7 @@ func (c Config) GetFloat(key string) (float64, error) { // It converts the "true" and "false" string values to related boolean values. // It returns an error if the related value is not a bool. // It returns an error if there is no value for the given key. -func (c Config) GetBool(key string) (bool, error) { +func (c *Config) GetBool(key string) (bool, error) { v, err := c.Get(key) if err != nil { return false, err @@ -252,7 +268,7 @@ func (c Config) GetBool(key string) (bool, error) { } } - return false, errors.New("value for " + key + " is not bool") + return false, &TypeError{value: v, wanted: "bool"} } // GetStrictBool returns the value of the given key. @@ -260,7 +276,7 @@ func (c Config) GetBool(key string) (bool, error) { // It doesn't convert the "true" and "false" string values to related boolean values. // It returns an error if the related value is not a bool. // It returns an error if there is no value for the given key. -func (c Config) GetStrictBool(key string) (bool, error) { +func (c *Config) GetStrictBool(key string) (bool, error) { v, err := c.Get(key) if err != nil { return false, err @@ -270,11 +286,11 @@ func (c Config) GetStrictBool(key string) (bool, error) { return v, nil } - return false, errors.New("value for " + key + " is not bool") + return false, &TypeError{value: v, wanted: "bool"} } // parse replaces the placeholders with environment and OS variables. -func (c Config) parse(value interface{}) interface{} { +func (c *Config) parse(value interface{}) interface{} { if stmt, ok := value.(string); ok { if len(stmt) > 3 && stmt[0:2] == "${" && stmt[len(stmt)-1:] == "}" { pipe := strings.Index(stmt, "|") @@ -297,6 +313,10 @@ func (c Config) parse(value interface{}) interface{} { } return collection + } else if collection, ok := value.(map[interface{}]interface{}); ok { + for k, v := range collection { + collection[k] = c.parse(v) + } } return value @@ -321,6 +341,10 @@ func lookup(collection interface{}, key string) (interface{}, error) { // find returns the value of given key in the given 1D collection func find(collection interface{}, key string) (interface{}, error) { switch collection.(type) { + case map[interface{}]interface{}: + if v, ok := collection.(map[interface{}]interface{})[key]; ok { + return v, nil + } case map[string]interface{}: if v, ok := collection.(map[string]interface{})[key]; ok { return v, nil diff --git a/feeder/yaml.go b/feeder/yaml.go new file mode 100644 index 0000000..eeb292a --- /dev/null +++ b/feeder/yaml.go @@ -0,0 +1,24 @@ +package feeder + +import ( + "gopkg.in/yaml.v2" + "io/ioutil" +) + +type Yaml struct { + Path string +} + +func (y *Yaml) Feed() (map[string]interface{}, error) { + bs, err := ioutil.ReadFile(y.Path) + if err != nil { + return nil, err + } + items := make(map[string]interface{}) + + err = yaml.Unmarshal(bs, items) + if err != nil { + return nil, err + } + return items, nil +} diff --git a/feeder/yaml_directory.go b/feeder/yaml_directory.go new file mode 100644 index 0000000..3d5e952 --- /dev/null +++ b/feeder/yaml_directory.go @@ -0,0 +1,40 @@ +package feeder + +import ( + "io/ioutil" + "path/filepath" + "strings" +) + +// YamlDirectory is a feeder that feeds using a directory of yaml files. +type YamlDirectory struct { + Path string +} + +// Feed returns all the content. +func (yd YamlDirectory) Feed() (map[string]interface{}, error) { + files, err := ioutil.ReadDir(yd.Path) + if err != nil { + return nil, err + } + + all := map[string]interface{}{} + + for _, f := range files { + if f.IsDir() { + continue + } + + j := Yaml{Path: filepath.Join(yd.Path, string(filepath.Separator), f.Name())} + + items, err := j.Feed() + if err != nil { + return nil, err + } + + k := strings.Split(f.Name(), ".")[0] + all[k] = items + } + + return all, nil +} diff --git a/go.mod b/go.mod index e5f5cc2..714956d 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/golobby/config go 1.11 -require github.com/stretchr/testify v1.4.0 +require ( + github.com/stretchr/testify v1.4.0 + gopkg.in/yaml.v2 v2.3.0 +) diff --git a/go.sum b/go.sum index 8fdee58..d10bc26 100644 --- a/go.sum +++ b/go.sum @@ -9,3 +9,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=