Skip to content

Commit

Permalink
README.md, Makefile, example
Browse files Browse the repository at this point in the history
  • Loading branch information
sergolius committed Dec 9, 2019
1 parent 765720b commit e66cf7c
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 44 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ env:
- GO111MODULE=on

script:
- go test -v
- make test
- make bench
13 changes: 8 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
test: ## Run unit tests
go test -count=1 -short ./...
test:
go test -race -v ./...

cover: dep
bench:
go test -run="^$$" -bench=.

cover:
go test $(shell go list ./... | grep -v /vendor/;) -cover -v

dep: ## Get the dependencies
GO111MODULE=on go mod vendor
dep:
GO111MODULE=on go mod vendor
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,77 @@
# go-config [![Build Status](https://travis-ci.org/Yalantis/go-config.svg?branch=master)](https://travis-ci.org/Yalantis/go-config)

go-config allows to initialize configuration in flexible way using from default, file, environment variables value.
### Initialization
Done in three steps:
1. init with value from `default` tag
2. merge with config file if `filepath` is provided
3. override with environment variables which stored under `envconfig` tag

### Supported file extensions
- json

### Supported types
- Standard types: `bool`, `float`, `int`(`uint`), `slice`, `string`
- `time.Duration`, `time.Time`: full support with aliases `config.Duration`, `config.Time`
- Custom types, slice of custom types

### Usage

#### Default value
```go
type Server struct {
Addr string `default:"localhost:8080"`
}
```
#### Environment value
```go
type Server struct {
Addr string `envconfig:"SERVER_ADDR"`
}
```
#### Combined default, json, env
```go
type Server struct {
Addr string `json:"addr" envconfig:"SERVER_ADDR" default:"localhost:8080"`
}
```
#### Slice
Default strings separator is comma.
```
REDIS_ADDR=127.0.0.1:6377,127.0.0.1:6378,127.0.0.1:6379
```
```go
type Redis struct {
Addrs []string `json:"addrs" envconfig:"REDIS_ADDR" default:"localhost:6378,localhost:6379"`
}
```
Slice of structs could be parsed from environment by defining `envprefix`.
Every ENV group override element stored at `index` of slice or append new one.
Sparse slices are not allowed.
```go
var cfg struct {
...
Replicas []Postgres `json:"replicas" envprefix:"REPLICAS"`
...
}
```
Environment key should has next pattern:
`${envprefix}_${index}_${envconfig}` or `${envprefix}_${index}_${StructFieldName}`
```
REPLICAS_0_POSTGRES_USER=replica REPLICAS_2_USER=replica
```
#### `time.Duration`, `time.Time`
In case using json file you have to use aliases `config.Duration`, `config.Time`, that properly unmarshal it self
```go
type NATS struct {
...
ReconnectInterval config.Duration `json:"reconnect_interval" envconfig:"NATS_RECONNECT_INTERVAL" default:"2s"`
}
```
Otherwise `time.Duration`, `time.Time` might be used directly:
```go
var cfg struct {
ReadTimeout time.Duration `envconfig:"READ_TIMEOUT" default:"1s"`
WriteTimeout time.Duration `envconfig:"WRITE_TIMEOUT" default:"10s"`
}
```
18 changes: 7 additions & 11 deletions apply_env_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
)

var (
ErrDstNotSlice = errors.New("dst must be a slice")
ErrNotSlice = errors.New("should be a slice")
ErrNotSettable = errors.New("should be settable")
ErrPrefixRequired = errors.New("prefix is required")
ErrDstUnsettable = errors.New("unsetable dst value")
)

var camelCaseRegex = regexp.MustCompile("(^[A-Za-z])|_([A-Za-z])")
Expand All @@ -35,19 +35,19 @@ func applyEnvOverridesToSlice(prefix string, dst interface{}) error {
// fallback
rv = reflect.ValueOf(dst)
if rv.Kind() != reflect.Ptr {
return ErrDstNotPointer
return ErrNotPointer
}
}

riv := indirectWalk(rv)
rit := indirectType(riv.Type())

if rit.Kind() != reflect.Slice {
return ErrDstNotSlice
return ErrNotSlice
}

if !rv.CanSet() && !riv.CanSet() {
return fmt.Errorf("unsettable type: %s %s at prefix: %s", rv.Type(), riv.Type(), prefix)
return fmt.Errorf("not settable type: %s %s at prefix: %s: %v", rv.Type(), riv.Type(), prefix, ErrNotSettable)
}

parseKeyVal, err := regexp.Compile(prefix + "_(\\d+)_(\\w+)=(.+)")
Expand Down Expand Up @@ -132,12 +132,8 @@ func applyEnvOverridesToSlice(prefix string, dst interface{}) error {
return nil
}

if riv.CanSet() {
setPtrValue(riv, ptr, tmp)
return nil
}

return ErrDstUnsettable
setPtrValue(riv, ptr, tmp)
return nil
}

// setPtrValue set as Ptr or Value
Expand Down
4 changes: 2 additions & 2 deletions apply_env_overrides_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ func TestApplyEnvOverridesToSlice(t *testing.T) {
name: "not a pointer",
prefix: "PREFIX_S",
value: []Payload{},
err: ErrDstNotPointer,
err: ErrNotPointer,
},
{
name: "not a slice",
prefix: "PREFIX_S",
value: new(int),
err: ErrDstNotSlice,
err: ErrNotSlice,
},
{
name: "fail on time.ParseDuration",
Expand Down
16 changes: 10 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
)

var (
ErrDstNotPointer = errors.New("dst should be a pointer")
ErrNotStruct = errors.New("should be a structure")
ErrNotPointer = errors.New("should be a pointer")
ErrNotStruct = errors.New("should be a structure")
)

var (
Expand All @@ -28,14 +28,15 @@ var (
const (
envConfigTag = "envconfig"
envPrefixTag = "envprefix"
defaultTag = "default"
)

// Init reads and init configuration to `config` variable, which must be a reference of struct
func Init(config interface{}, filename string) error {
v := reflect.ValueOf(config)

if v.Kind() != reflect.Ptr {
return ErrDstNotPointer
return ErrNotPointer
}

v = reflect.Indirect(v)
Expand Down Expand Up @@ -106,7 +107,7 @@ func applyDefault(t reflect.StructField, v reflect.Value) error {
return nil
}

value, ok := t.Tag.Lookup("default")
value, ok := t.Tag.Lookup(defaultTag)
if !ok {
return nil
}
Expand Down Expand Up @@ -167,8 +168,11 @@ func applyEnv(v reflect.Value) error {
}

func applyEnvValue(t reflect.StructField, v reflect.Value) error {
if value, ok := t.Tag.Lookup(envPrefixTag); ok && indirectType(v.Type()).Kind() == reflect.Slice {
return applyEnvOverridesToSlice(value, v)
switch indirectType(v.Type()).Kind() {
case reflect.Slice:
if value, ok := t.Tag.Lookup(envPrefixTag); ok {
return applyEnvOverridesToSlice(value, v)
}
}

if v.Kind() == reflect.Struct && !isTime(v.Type()) {
Expand Down
2 changes: 1 addition & 1 deletion config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestInit(t *testing.T) {
name: "not a pointer",
filePath: "",
cfg: cfg,
error: ErrDstNotPointer.Error(),
error: ErrNotPointer.Error(),
},
{
name: "not a struct",
Expand Down
2 changes: 1 addition & 1 deletion configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestInitOnConfiguration(t *testing.T) {
name: "not pointer",
value: Configuration{},
expect: Configuration{},
err: ErrDstNotPointer,
err: ErrNotPointer,
},
{
name: "fail on time.ParseDuration",
Expand Down
12 changes: 7 additions & 5 deletions env.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package config

import "syscall"
import "os"

var lookupEnv = syscall.Getenv
var environ = syscall.Environ
var Setenv = syscall.Setenv
var Unsetenv = syscall.Unsetenv
var lookupEnv = os.LookupEnv
var environ = os.Environ
var Setenv = os.Setenv
var Unsetenv = os.Unsetenv

//var ExpandEnv = os.ExpandEnv
77 changes: 77 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package config_test

import (
"fmt"
"github.com/Yalantis/go-config"
"log"
"time"
)

func init() {
// setup ENV
_ = config.Setenv("VERSION", "0.0.1")
_ = config.Setenv("REPLICAS_0_POSTGRES_USER", "replica0")
_ = config.Setenv("REPLICAS_0_POSTGRES_PORT", "5433")
_ = config.Setenv("REPLICAS_1_USER", "replica1")
_ = config.Setenv("REPLICAS_1_PORT", "5433")
_ = config.Setenv("REDIS_ADDR", "127.0.0.1:6377,127.0.0.1:6378,127.0.0.1:6379")
_ = config.Setenv("READ_TIMEOUT", "30s")
}

func ExampleInit() {
type Server struct {
Addr string `json:"addr" envconfig:"SERVER_ADDR" default:"localhost:8080"`
}

type Postgres struct {
Host string `json:"host" envconfig:"POSTGRES_HOST" default:"localhost"`
Port string `json:"port" envconfig:"POSTGRES_PORT" default:"5432"`
User string `json:"user" envconfig:"POSTGRES_USER" default:"postgres"`
Password string `json:"password" envconfig:"POSTGRES_PASSWORD" default:"12345"`
}

type Redis struct {
Addrs []string `json:"addrs" envconfig:"REDIS_ADDR" default:"localhost:6379"`
}

type NATS struct {
ServerURL string `json:"server_url" envconfig:"NATS_SERVER_URL" default:"nats://localhost:4222"`
MaxReconnectionAttempts int `json:"max_reconnection_attempts" envconfig:"NATS_MAX_RECONNECT_ATTEMPTS" default:"5"`
ReconnectInterval config.Duration `json:"reconnect_interval" envconfig:"NATS_RECONNECT_INTERVAL" default:"2s"`
}

type Websocket struct {
Port string `json:"port" envconfig:"WEBSOCKET_PORT" default:"9876"`
}

var cfg struct {
Version string `envconfig:"VERSION" default:"0"`
Server Server `json:"server"`
Postgres Postgres `json:"postgres"`
Replicas []Postgres `json:"replicas" envprefix:"REPLICAS"`
Redis Redis `json:"redis"`
NATS NATS `json:"nats"`
Websocket Websocket `json:"websocket"`
}

if err := config.Init(&cfg, "testdata/config.json"); err != nil {
log.Fatalln(err)
}

fmt.Println(cfg)
// Output: {0.0.1 {localhost:8080} {localhost 5432 postgres 12345} [{localhost 5433 replica0 12345} {localhost 5433 replica1 12345}] {[127.0.0.1:6377 127.0.0.1:6378 127.0.0.1:6379]} {nats://localhost:4222 5 2000000000} {9876}}
}

func ExampleInitTimeout() {
var cfg struct {
ReadTimeout time.Duration `envconfig:"READ_TIMEOUT" default:"1s"`
WriteTimeout time.Duration `envconfig:"WRITE_TIMEOUT" default:"10s"`
}

if err := config.Init(&cfg, ""); err != nil {
log.Fatalln(err)
}

fmt.Println(cfg)
// Output: {30s 10s}
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module gitlab.yalantis.com/gophers/config
module github.com/Yalantis/go-config

require github.com/stretchr/testify v1.3.0
go 1.13

go 1.11+
require github.com/stretchr/testify v1.4.0
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=
17 changes: 10 additions & 7 deletions testdata/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@
"password": "12345"
},
"redis": {
"addrs": "127.0.0.1:6374,127.0.0.1:6375,127.0.0.1:6376,127.0.0.1:6377,127.0.0.1:6378,127.0.0.1:6379",
"addrs": [
"127.0.0.1:6374",
"127.0.0.1:6375",
"127.0.0.1:6376",
"127.0.0.1:6377",
"127.0.0.1:6378",
"127.0.0.1:6379"
],
"password": "",
"db": 1,
"pool_size": 10
},
"nats": {
"server_urls": "nats://localhost:4222",
"server_url": "nats://localhost:4222",
"max_reconnection_attempts": 5,
"reconnect_interval_sec": 2
},
"websocket": {
"port": "9876",
"in_messages_chan_size": 5000
"reconnect_interval": "2s"
}
}

0 comments on commit e66cf7c

Please sign in to comment.