diff --git a/README.md b/README.md index 07ff6ad..c751991 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) # Config -GoLobby Config is lightweight yet powerful configuration management for Go projects. +GoLobby Config is a lightweight yet powerful configuration manager for Go projects. It takes advantage of dot env files and OS variables alongside config files to be your ultimate requirement. ## Documentation @@ -21,10 +21,10 @@ go get github.com/golobby/config/v3 ``` ### Quick Start -The following example demonstrates how to use the package and a JSON configuration file. +The following example demonstrates how to use a JSON configuration file. ```go -// My configuration struct +// The configuration struct type MyConfig struct { App struct { Name string @@ -35,16 +35,22 @@ type MyConfig struct { Pi float64 } -// An instance of my configuration struct +// Create an instance of the configuration struct myConfig := MyConfig{} // Create a feeder that provides the configuration data from a JSON file jsonFeeder := feeder.Json{Path: "config.json"} -// Create a Config instance, pass the feeder and feed the configuration struct -err := config.New(jsonFeeder).Feed(&myConfig) +// Create a Config instance and feed `myConfig` using `jsonFeeder` +c := config.New() +c.AddFeeder(jsonFeeder) +c.AddStruct(&myConfig) +err := c.Feed() -// Use myConfig... +// Or use method chaining: +// err := config.New().AddFeeder(jsonFeeder).AddStruct(&myConfig).Feed() + +// Use `myConfig`... ``` ### Feeders @@ -61,85 +67,34 @@ You can also create your custom feeders by implementing the `Feeder` interface o #### Json Feeder The `Json` feeder uses Go built-in `json` package to load JSON files. -The example below shows how to use the `Json` feeder. - -The JSON file: https://github.com/golobby/config/blob/v3/assets/sample1.json +The snippet below shows how to use the `Json` feeder. ```go -type MyConfig struct { - App struct { - Name string - Port int - } - Debug bool - Production bool - Pi float64 -} - -myConfig := MyConfig{} - jsonFeeder := feeder.Json{Path: "sample1.json"} - -err := config.New(jsonFeeder).Feed(&myConfig) - -// Use myConfig... +c := config.New().AddFeeder(jsonFeeder) ``` #### Yaml Feeder The `Yaml` feeder uses the [YAML package](https://github.com/go-yaml/yaml) (v3) to load YAML files. -The example below shows how to use the `Yaml` feeder. - -The YAML file: https://github.com/golobby/config/blob/v3/assets/sample1.yaml +The snippet below shows how to use the `Yaml` feeder. ```go -type MyConfig struct { - App struct { - Name string - Port int - } - Debug bool - Production bool - Pi float64 -} - -myConfig := MyConfig{} - yamlFeeder := feeder.Yaml{Path: "sample1.yaml"} - -err := config.New(yamlFeeder).Feed(&myConfig) - -// Use myConfig... +c := config.New().AddFeeder(yamlFeeder) ``` #### Toml Feeder The `Toml` feeder uses the [BurntSushi TOML package](https://github.com/BurntSushi/toml) to load TOML files. - -The TOML file: https://github.com/golobby/config/blob/v3/assets/sample1.toml +The snippet below shows how to use the `Toml` feeder. ```go -type MyConfig struct { - App struct { - Name string - Port int - } - Debug bool - Production bool - Pi float64 -} - -myConfig := MyConfig{} - tomlFeeder := feeder.Toml{Path: "sample1.toml"} - -err := config.New(tomlFeeder).Feed(&myConfig) - -// Use myConfig... +c := config.New().AddFeeder(tomlFeeder) ``` #### DotEnv Feeder -Dot env (.env) files are popular configuration files. -They are usually declared per environment (production, local, test, etc.) differently. The `DotEnv` feeder uses the [GoLobby DotEnv](https://github.com/golobby/dotenv) package to load `.env` files. +The example below shows how to use the `DotEnv` feeder. The `.env` file: https://github.com/golobby/config/blob/v3/assets/.env.sample1 @@ -152,28 +107,21 @@ type MyConfig struct { Debug bool `env:"DEBUG"` Production bool `env:"PRODUCTION"` Pi float64 `env:"PI"` + IDs []int `env:"IDS"` } myConfig := MyConfig{} - dotEnvFeeder := feeder.DotEnv{Path: ".env"} - -err := config.New(dotEnvFeeder).Feed(&myConfig) - -// Use myConfig... +err := config.New().AddFeeder(dotEnvFeeder).AddStruct(&myConfig).Feed() ``` -You must add a `env` tag for each field that determines the related dot env key. +You must add a `env` tag for each field that determines the related dot env variable. If there isn't any value for a field in the related file, it ignores the struct field. - You can read more about this feeder in the [GoLobby DotEnv](https://github.com/golobby/dotenv) package repository. #### Env Feeder -You may keep it simple stupid with no configuration files at all! - -The `Env` feeder works fine in simple cases and cloud environments. -It feeds your structs by OS environment variables. -This feeder is built on top of the [GoLobby Env](https://github.com/golobby/env) package. +The `Env` feeder is built on top of the [GoLobby Env](https://github.com/golobby/env) package. +The example below shows how to use the `Env` feeder. ```go _ = os.Setenv("APP_NAME", "Shop") @@ -181,34 +129,36 @@ _ = os.Setenv("APP_PORT", "8585") _ = os.Setenv("DEBUG", "true") _ = os.Setenv("PRODUCTION", "false") _ = os.Setenv("PI", "3.14") +_ = os.Setenv("IPS", "192.168.0.1", "192.168.0.2") +_ = os.Setenv("IDS", "10, 11, 12, 13") type MyConfig struct { App struct { Name string `env:"APP_NAME"` Port int `env:"APP_PORT"` } - Debug bool `env:"DEBUG"` - Production bool `env:"PRODUCTION"` - Pi float64 `env:"PI"` + Debug bool `env:"DEBUG"` + Production bool `env:"PRODUCTION"` + Pi float64 `env:"PI"` + IPs []string `env:"IPS"` + IDs []int16 `env:"IDS"` } myConfig := MyConfig{} envFeeder := feeder.DotEnv{} - -err := config.New(envFeeder).Feed(&myConfig) - -// Use myConfig... +err := config.New().AddFeeder(envFeeder).AddStruct(&myConfig).Feed() ``` You must add a `env` tag for each field that determines the related OS environment variable name. If there isn't any value for a field in OS environment variables, it ignores the struct field. - You can read more about this feeder in the [GoLobby Env](https://github.com/golobby/env) package repository. ### Multiple Feeders One of the key features in the GoLobby Config package is feeding using multiple feeders. Lately added feeders overrides early added ones. +The example below demonstrates how to use a JSON file as the main configuration feeder and override the configurations with dot env and os variables. + * JSON file: https://github.com/golobby/config/blob/v3/assets/sample1.json * DotEnv file: https://github.com/golobby/config/blob/v3/assets/.env.sample2 * Env (OS) variables: Defined in the Go code! @@ -216,6 +166,7 @@ Lately added feeders overrides early added ones. ```go _ = os.Setenv("PRODUCTION", "true") _ = os.Setenv("APP_PORT", "6969") +_ = os.Setenv("IDs", "6, 9") type MyConfig struct { App struct { @@ -225,6 +176,7 @@ type MyConfig struct { Debug bool `env:"DEBUG"` Production bool `env:"PRODUCTION"` Pi float64 `env:"PI"` + IDs []int32 `env:"IDS"` } myConfig := MyConfig{} @@ -233,13 +185,19 @@ feeder1 := feeder.Json{Path: "sample1.json"} feeder2 := feeder.DotEnv{Path: ".env.sample2"} feeder3 := feeder.Env{} -err := config.New(feeder1, feeder2, feeder3).Feed(&myConfig) +err := config.New() + .AddFeeder(feeder1) + .AddFeeder(feeder2) + .AddFeeder(feeder3) + .AddStruct(&myConfig) + .Feed() fmt.Println(c.App.Name) // Blog [from DotEnv] fmt.Println(c.App.Port) // 6969 [from Env] fmt.Println(c.Debug) // false [from DotEnv] fmt.Println(c.Production) // true [from Env] fmt.Println(c.Pi) // 3.14 [from Json] +fmt.Println(c.IDs) // 6, 9 [from Env] ``` What happened? @@ -247,36 +205,38 @@ What happened? * The `Json` feeder as the first feeder sets all the struct fields from the JSON file. * The `DotEnv` feeder as the second feeder overrides existing fields. The `APP_NAME` and `DEBUG` fields exist in the `.env.sample2` file. -* The `Env` feeder as the last feeder overrides existing variables in the OS environment. - The `APP_PORT` and `PRODUCTION` fields are defined. +* The `Env` feeder as the last feeder overrides existing fields, as well. + The `APP_PORT` and `PRODUCTION` fields are defined in the OS environment. -### Refresh -The `Refresh()` method re-feeds the structs using the provided feeders. -It makes each feeder reload configuration data and feed the given structs again. +### Re-feed +You can re-feed the structs every time you need to. +Just call the `Feed()` method again. ```go -c := config.New(feeder1, feeder2, feeder3) -err := c.Feed(&myConfig) +c := config.New().AddFeeder(feeder).AddStruct(&myConfig) +err := c.Feed() -err = c.Refresh() +// Is it time to re-feed? +err = c.Feed() -// myConfig fields are updated! +// Use `myConfig` with updated data! ``` ### Listener One of the GoLobby Config features is the ability to update the configuration structs without redeployment. It takes advantage of OS signals to handle this requirement. -Config instances listen to the "SIGHUP" operating system signal and refresh structs (call the `Refresh()` method). +Config instances listen to the "SIGHUP" operating system signal and refresh structs (call the `Feed()` method). -To enable the listener for a Config instance, you should call the `WithListener()` method. -It gets a fallback function and calls it when the `Refresh()` method fails and returns an error. +To enable the listener for a Config instance, you should call the `SetupListener()` method. +It gets a fallback function and calls it when the `Feed()` method fails and returns an error. ```go -c := config.New(feeder).WithListener(func(err error) { +c := config.New().AddFeeder(feeder).AddStruct(&myConfig) +c.SetupListener(func(err error) { fmt.Println(err) }) -err := c.Feed(&myConfig) +err := c.Feed() ``` You can send the `SIGHUP` signal to your running application with the following shell command. @@ -294,5 +254,4 @@ To get your application process ID, you can use the `ps` shell command. A lightweight package for loading OS environment variables into structs for Go projects ## License - GoLobby Config is released under the [MIT License](http://opensource.org/licenses/mit-license.php). diff --git a/assets/.env.sample1 b/assets/.env.sample1 index 5a86e05..55ea9ea 100644 --- a/assets/.env.sample1 +++ b/assets/.env.sample1 @@ -4,3 +4,4 @@ APP_PORT=8585 DEBUG=true PRODUCTION=false PI=3.14 +IDS=10,11,12,13 diff --git a/assets/.env.sample2 b/assets/.env.sample2 index 4f57073..08902c2 100644 --- a/assets/.env.sample2 +++ b/assets/.env.sample2 @@ -1,3 +1,5 @@ # Sample2 DotEnv APP_NAME=Blog DEBUG=false +IPS="192.168.0.1, 192.168.0.2" +IDS="10,11 , 12 , 13" \ No newline at end of file diff --git a/assets/sample1.json b/assets/sample1.json index 6a31f56..effaa1a 100644 --- a/assets/sample1.json +++ b/assets/sample1.json @@ -5,5 +5,14 @@ }, "Debug": true, "Production": false, - "Pi": 3.14 + "Pi": 3.14, + "IPs": [ + "192.168.0.1", + "192.168.0.2" + ], + "IDs": [ + 10, + 11, + 13 + ] } \ No newline at end of file diff --git a/config.go b/config.go index 4c154f8..130f684 100644 --- a/config.go +++ b/config.go @@ -17,39 +17,46 @@ type Feeder interface { // Config is the configuration manager. // To use the package facilities, there should be at least one instance of it. -// It holds the configuration feeders and structures that it is going to feed them. +// It holds the configuration feeders and structs. type Config struct { - Feeders []Feeder // Feeders is the list of configuration feeders that provides configuration data. - Structures []interface{} // Structures is the list of structures that are going to be fed. + Feeders []Feeder // Feeders is the list of feeders that provides configuration data. + Structs []interface{} // Structs is the list of structs that holds the configuration data. } // New creates a brand new instance of Config to use the package facilities. -// It gets feeders that are going to feed the configuration structures. -func New(feeders ...Feeder) *Config { - return &Config{Feeders: feeders} +func New() *Config { + return &Config{} } -// Feed gets a structure and feeds it using the provided feeders. -func (c *Config) Feed(structure interface{}) error { - c.Structures = append(c.Structures, structure) - return c.feedStructure(structure) +// AddFeeder adds a feeder that provides configuration data. +func (c *Config) AddFeeder(f Feeder) *Config { + c.Feeders = append(c.Feeders, f) + return c +} + +// AddStruct adds a struct that holds the configuration data. +func (c *Config) AddStruct(s interface{}) *Config { + c.Structs = append(c.Structs, s) + return c } -// Refresh refreshes registered structures using the provided feeders. -func (c *Config) Refresh() error { - for _, s := range c.Structures { - if err := c.feedStructure(s); err != nil { - return err +// Feed binds configuration data from added feeders to the added structs. +func (c *Config) Feed() error { + for _, s := range c.Structs { + for _, f := range c.Feeders { + if err := c.feedStruct(f, s); err != nil { + return err + } } } return nil } -// WithListener adds an OS signal listener to the Config instance. -// The listener listens to the SIGHUP signal and refreshes the Config instance. +// SetupListener adds an OS signal listener to the Config instance. +// The listener listens to the `SIGHUP` signal and refreshes the Config instance. // It would call the provided fallback if the refresh process failed. -func (c *Config) WithListener(fallback func(err error)) *Config { +func (c *Config) SetupListener(fallback func(err error)) *Config { s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGHUP) @@ -57,7 +64,7 @@ func (c *Config) WithListener(fallback func(err error)) *Config { go func() { for { <-s - if err := c.Refresh(); err != nil { + if err := c.Feed(); err != nil { fallback(err) } } @@ -66,12 +73,10 @@ func (c *Config) WithListener(fallback func(err error)) *Config { return c } -// feedStructure gets a structure and feeds it using all the provided feeders. -func (c *Config) feedStructure(structure interface{}) error { - for _, f := range c.Feeders { - if err := f.Feed(structure); err != nil { - return fmt.Errorf("config: faild to feed struct; err %v", err) - } +// feedStruct feeds a struct using given feeder. +func (c *Config) feedStruct(f Feeder, s interface{}) error { + if err := f.Feed(s); err != nil { + return fmt.Errorf("config: faild to feed struct; err %v", err) } return nil diff --git a/config_test.go b/config_test.go index 29c1ba1..d8e30e4 100644 --- a/config_test.go +++ b/config_test.go @@ -12,13 +12,16 @@ import ( func TestFeed(t *testing.T) { c := &struct{}{} - err := config.New(feeder.Env{}).Feed(c) + err := config.New().AddFeeder(feeder.Env{}).AddStruct(c).Feed() assert.NoError(t, err) } func TestFeed_With_Invalid_File_It_Should_Fail(t *testing.T) { - c := &struct{}{} - err := config.New(feeder.Json{}).Feed(c) + s := struct{}{} + c := config.New() + c.AddFeeder(feeder.Json{}) + c.AddStruct(&s) + err := c.Feed() assert.Error(t, err) } @@ -31,16 +34,18 @@ func TestFeed_WithMultiple_Feeders(t *testing.T) { Name string `env:"APP_NAME"` Port int `env:"APP_PORT"` } - Debug bool `env:"DEBUG"` - Production bool `env:"PRODUCTION"` - Pi float64 `env:"PI"` + Debug bool `env:"DEBUG"` + Production bool `env:"PRODUCTION"` + Pi float64 `env:"PI"` + IPs []string `env:"IPS"` + IDs []int16 `env:"IDS"` }{} f1 := feeder.Json{Path: "assets/sample1.json"} f2 := feeder.DotEnv{Path: "assets/.env.sample2"} f3 := feeder.Env{} - err := config.New(f1, f2, f3).Feed(c) + err := config.New().AddFeeder(f1).AddFeeder(f2).AddFeeder(f3).AddStruct(c).Feed() assert.NoError(t, err) assert.Equal(t, "Blog", c.App.Name) @@ -48,30 +53,32 @@ func TestFeed_WithMultiple_Feeders(t *testing.T) { assert.Equal(t, false, c.Debug) assert.Equal(t, true, c.Production) assert.Equal(t, 3.14, c.Pi) + assert.Equal(t, []string{"192.168.0.1", "192.168.0.2"}, c.IPs) + assert.Equal(t, []int16{10, 11, 12, 13}, c.IDs) } -func TestConfig_Refresh(t *testing.T) { +func TestConfig_Feed_For_Refreshing(t *testing.T) { _ = os.Setenv("NAME", "One") s := &struct { Name string `env:"NAME"` }{} - c := config.New(feeder.Env{}) - err := c.Feed(s) + c := config.New().AddFeeder(feeder.Env{}).AddStruct(s) + err := c.Feed() assert.NoError(t, err) assert.Equal(t, "One", s.Name) _ = os.Setenv("NAME", "Two") - err = c.Refresh() + err = c.Feed() assert.NoError(t, err) assert.Equal(t, "Two", s.Name) } -func TestConfig_WithListener(t *testing.T) { +func TestConfig_SetupListener(t *testing.T) { _ = os.Setenv("PI", "3.14") s := &struct { @@ -79,11 +86,11 @@ func TestConfig_WithListener(t *testing.T) { }{} fallbackTested := false - c := config.New(feeder.Env{}).WithListener(func(err error) { + c := config.New().AddFeeder(feeder.Env{}).AddStruct(s).SetupListener(func(err error) { fallbackTested = true }) - err := c.Feed(s) + err := c.Feed() assert.NoError(t, err) assert.Equal(t, 3.14, s.Pi) diff --git a/go.mod b/go.mod index dc6d37b..fea7008 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.11 require ( github.com/BurntSushi/toml v0.4.1 - github.com/golobby/dotenv v1.2.0 - github.com/golobby/env/v2 v2.1.0 + github.com/golobby/dotenv v1.3.0 + github.com/golobby/env/v2 v2.2.0 github.com/stretchr/testify v1.7.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index a4b5cfc..893d405 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golobby/cast v1.1.4 h1:AJ5mNMUOnbha2XWpQIPwnAcahBRB3/ENugWsDM69VgU= -github.com/golobby/cast v1.1.4/go.mod h1:WCusT3z1fzp4XVBUGbWy61insoQS8CPJHNTQwlW8qnM= -github.com/golobby/dotenv v1.2.0 h1:kDA2zKOf9byk040lju6hcm7ma5KRPvPCiSZiSgeVcys= -github.com/golobby/dotenv v1.2.0/go.mod h1:iEIqJeTeQQ8BwujHnTCGf4mO21OKObYFe6MsHs9fIPM= -github.com/golobby/env/v2 v2.1.0 h1:szUrj5lC+WsnAEbcBzWXyRE0O3fOAI2lgI0SRwtQI9Q= -github.com/golobby/env/v2 v2.1.0/go.mod h1:UM4kv1vShjJQcC54Au2GZsQ8fauFygf+BOJpCcgy1W8= +github.com/golobby/cast v1.3.0 h1:8nM9nYU5Pzi1LWXwISx0xhW/7oWXPt9r0hdTC1nnPSI= +github.com/golobby/cast v1.3.0/go.mod h1:WCusT3z1fzp4XVBUGbWy61insoQS8CPJHNTQwlW8qnM= +github.com/golobby/dotenv v1.3.0 h1:vp1ABeCFxESxHRReWklnhsmH23h8nzgKKhS2Ld+QUXE= +github.com/golobby/dotenv v1.3.0/go.mod h1:EWUdOzuDlA1g4hdjo++WD37DhNZw33Oce8ryH3liZTQ= +github.com/golobby/env/v2 v2.2.0 h1:OzWNfKmXocjvVQ86lLSgfWGMMbL2HwUZWKW1pKIMJw0= +github.com/golobby/env/v2 v2.2.0/go.mod h1:gIDZcMfoaeTsYTLViD2crQ5XsXQV69t+MvIU/M8KMK0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=