diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66fd13c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/.travis b/.travis new file mode 100644 index 0000000..28febbd --- /dev/null +++ b/.travis @@ -0,0 +1,17 @@ +dist: bionic + +language: go + +go: + - 1.13.x + +install: + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + - go get golang.org/x/lint/golint + +script: + - test -z "`gofmt -l -d .`" + - test -z "`golint ./...`" + - go test -v -covermode=count -coverprofile=coverage.out + - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ed4b1a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Qiang Xue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1967ab6 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# go-env + +[![GoDoc](https://godoc.org/github.com/qiangxue/go-env?status.png)](http://godoc.org/github.com/qiangxue/go-env) +[![Build Status](https://travis-ci.org/qiangxue/go-env.svg?branch=master)](https://travis-ci.org/qiangxue/go-env) +[![Coverage Status](https://coveralls.io/repos/github/qiangxue/go-env/badge.svg?branch=master)](https://coveralls.io/github/qiangxue/go-env?branch=master) +[![Go Report](https://goreportcard.com/badge/github.com/qiangxue/go-env)](https://goreportcard.com/report/github.com/qiangxue/go-env) + +## Description + +go-env is a Go library that can populate a struct with environment variable values. A common use of go-env is +to load a configuration struct with values set in the environment variables. + +## Requirements + +Go 1.13 or above. + + +## Getting Started + +### Installation + +Run the following command to install the package: + +``` +go get github.com/qiangxue/go-env +``` + +### Loading From Environment Variables + +The easiest way of using go-env is to call `env.Load()`, like the following: + +```go +package main + +import ( + "fmt" + "github.com/qiangxue/go-env" + "os" +) + +type Config struct { + Host string + Port int +} + +func main() { + _ = os.Setenv("APP_HOST", "127.0.0.1") + _ = os.Setenv("APP_PORT", "8080") + + var cfg Config + if err := env.Load(&cfg); err != nil { + panic(err) + } + fmt.Println(cfg.Host) + fmt.Println(cfg.Port) + // Output: + // 127.0.0.1 + // 8080 +} +``` + +### Environment Variable Names + +When go-env populates a struct from environment variables, it uses the following rules to match +a struct field with an environment variable: +- Only public struct fields will be populated +- If the field has an `env` tag, use the tag value as the name, unless the tag value is `-` in which case it means + the field should NOT be populated. +- If the field has no `env` tag, turn the field name into snake format and use that as the name. For example, + a field name `HostName` will be turned into `Host_Name`, and `MyURL` becomes `My_URL`. +- Names are turned into upper case and prefixed with the specified prefix when they are used to look up + in the environment variables. + +By default, prefix `APP_` will be used. You can customize the prefix by using `env.New()` to create +a customized loader. For example, + +```go +package main + +import ( + "fmt" + "github.com/qiangxue/go-env" + "log" + "os" +) + +type Config struct { + Host string `env:"ES_HOST"` + Port int `env:"ES_PORT"` + Password string `env:"ES_PASSWORD,secret"` +} + +func main() { + _ = os.Setenv("API_HOST", "127.0.0.1") + _ = os.Setenv("API_PORT", "8080") + _ = os.Setenv("API_PASSWORD", "test") + + var cfg Config + loader := env.New("API_", log.Printf) + if err := loader.Load(&cfg); err != nil { + panic(err) + } + fmt.Println(cfg.Host) + fmt.Println(cfg.Port) + fmt.Println(cfg.Password) + // Output: + // 127.0.0.1 + // 8080 + // test +} +``` + +In the above code, the `Password` field is tagged as `secret`. The log function respects this flag by masking +the field value when logging it in order not to reveal sensitive information. + +By setting the prefix to an empty string, you can disable the name prefix completely. + + +### Data Parsing Rules + +Because the values of environment variables are strings, if the corresponding struct fields are of different types, +go-env will convert the string values into appropriate types before assigning them to the struct fields. + +- If a struct contains embedded structs, the fields of the embedded structs will be populated like they are directly +under the containing struct. + +- If a struct field type implements `env.Setter`, `env.TextMarshaler`, or `env.BinaryMarshaler` interface, +the corresponding interface method will be used to load a string value into the field. + +- If a struct field is of a primary type, such as `int`, `string`, `bool`, etc., a string value will be parsed +accordingly and assigned to the field. For example, the string value `TRUE` can be parsed correctly into a +boolean `true` value, while `TrUE` will cause a parsing error. + +- If a struct field is of a complex type, such as map, slice, struct, the string value will be treated as a JSON +string, and `json.Unmarshal()` will be called to populate the struct field from the JSON string. diff --git a/env.go b/env.go new file mode 100644 index 0000000..25b8fc3 --- /dev/null +++ b/env.go @@ -0,0 +1,247 @@ +// Copyright 2019 Qiang Xue. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package env + +import ( + "encoding" + "encoding/json" + "errors" + "fmt" + "log" + "os" + "reflect" + "regexp" + "strconv" + "strings" +) + +type ( + // Loader loads a struct with values returned by a lookup function. + Loader struct { + log LogFunc + prefix string + lookup LookupFunc + } + + // LogFunc logs a message. + LogFunc func(format string, args ...interface{}) + + // LookupFunc looks up a name and returns the corresponding value and a flag indicating if the name is found. + LookupFunc func(name string) (string, bool) + + // Setter sets the object with a string value. + Setter interface { + // Set sets the object with a string value. + Set(value string) error + } +) + +var ( + // ErrStructPointer represents the error that a pointer to a struct is expected. + ErrStructPointer = errors.New("must be a pointer to a struct") + // ErrNilPointer represents the error that a nil pointer is received + ErrNilPointer = errors.New("the pointer should not be nil") + // TagName specifies the tag name for customizing struct field names when loading environment variables + TagName = "env" + + // nameRegex is used to convert a string from camelCase into snake format + nameRegex = regexp.MustCompile(`([^A-Z_])([A-Z])`) + // loader is the default loader used by the "Load" function at the package level. + loader = New("APP_", log.Printf) +) + +// New creates a new environment variable loader. +// The prefix will be used to prefix the struct field names when they are used to read from environment variables. +func New(prefix string, log LogFunc) *Loader { + return &Loader{prefix: prefix, lookup: os.LookupEnv, log: log} +} + +// NewWithLookup creates a new loader using the given lookup function. +// The prefix will be used to prefix the struct field names when they are used to read from environment variables. +func NewWithLookup(prefix string, lookup LookupFunc, log LogFunc) *Loader { + return &Loader{prefix: prefix, lookup: lookup, log: log} +} + +// Load populates a struct with the values read from the corresponding environment variables. +// Load uses "APP_" as the prefix for environment variable names. It uses log.Printf() to log the data population +// of each struct field. +// For more details on how Load() works, please refer to Loader.Load(). +func Load(structPtr interface{}) error { + return loader.Load(structPtr) +} + +// Load populates a struct with the values read returned by the specified lookup function. +// The struct must be specified as a pointer. +// +// Load calls a lookup function for each public struct field. If the function returns a value, it is parsed according +// to the field type and assigned to the field. +// +// Load uses the following rules to determine what name should be used to look up the value for a struct field: +// - If the field has an "env" tag, use the tag value as the name, unless the tag is "-" in which case it means +// the field should be skipped. +// - If the field has no "env" tag, turn the field name into snake format and use that as the name. +// - Names are turned into upper case and prefixed with the specified prefix. +// +// The following types of struct fields are supported: +// - types implementing Setter, TextUnmarshaler, BinaryUnmarshaler: the corresponding interface method will be used +// to populate the field with a string +// - primary types (e.g. int, string): appropriate parsing functions will be called to parse a string value +// - other types (e.g. array, struct): the string value is assumed to be in JSON format and is decoded/assigned to the field. +// +// Load will log every field that is populated. In case when a field is tagged with `env:",secret"`, the value being +// logged will be masked for security purpose. +func (l *Loader) Load(structPtr interface{}) error { + rval := reflect.ValueOf(structPtr) + if rval.Kind() != reflect.Ptr || !rval.IsNil() && rval.Elem().Kind() != reflect.Struct { + return ErrStructPointer + } + if rval.IsNil() { + return ErrNilPointer + } + + rval = rval.Elem() + rtype := rval.Type() + + for i := 0; i < rval.NumField(); i++ { + f := rval.Field(i) + if !f.CanSet() { + continue + } + + ft := rtype.Field(i) + + if ft.Anonymous { + f = indirect(f) + if f.Kind() == reflect.Struct { + // populate embedded struct + if err := l.Load(f.Addr().Interface()); err != nil { + return err + } + } + continue + } + + name, secret := getName(ft.Tag.Get(TagName), ft.Name) + if name == "-" { + continue + } + + name = l.prefix + strings.ToUpper(name) + + if value, ok := l.lookup(name); ok { + logValue := value + if l.log != nil { + if secret { + l.log("set %v with $%v=\"***\"", ft.Name, name) + } else { + l.log("set %v with $%v=\"%v\"", ft.Name, name, logValue) + } + } + if err := setValue(f, value); err != nil { + return fmt.Errorf("error reading \"%v\": %v", ft.Name, err) + } + } + } + return nil +} + +// indirect dereferences pointers and returns the actual value it points to. +// If a pointer is nil, it will be initialized with a new value. +func indirect(v reflect.Value) reflect.Value { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + return v +} + +// getName generates the environment variable name from a struct field tag and the field name. +func getName(tag string, field string) (string, bool) { + secret := false + if idx := strings.Index(tag, ","); idx != -1 { + tag, secret = tag[:idx], tag[idx+1:] == "secret" + } + if tag == "" { + return camelCaseToSnake(field), secret + } + return tag, secret +} + +// camelCaseToSnake converts a name from camelCase format into snake format. +func camelCaseToSnake(name string) string { + return nameRegex.ReplaceAllString(name, "${1}_$2") +} + +// setValue assigns a string value to a reflection value using appropriate string parsing and conversion logic. +func setValue(rval reflect.Value, value string) error { + rval = indirect(rval) + rtype := rval.Type() + + if !rval.CanAddr() { + return errors.New("the value is unaddressable") + } + + // if the reflection value implements supported interface, use the interface to set the value + pval := rval.Addr().Interface() + if p, ok := pval.(Setter); ok { + return p.Set(value) + } + if p, ok := pval.(encoding.TextUnmarshaler); ok { + return p.UnmarshalText([]byte(value)) + } + if p, ok := pval.(encoding.BinaryUnmarshaler); ok { + return p.UnmarshalBinary([]byte(value)) + } + + // parse the string according to the type of the reflection value and assign it + switch rtype.Kind() { + case reflect.String: + rval.SetString(value) + break + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + val, err := strconv.ParseInt(value, 0, rtype.Bits()) + if err != nil { + return err + } + + rval.SetInt(val) + break + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val, err := strconv.ParseUint(value, 0, rtype.Bits()) + if err != nil { + return err + } + rval.SetUint(val) + break + case reflect.Bool: + val, err := strconv.ParseBool(value) + if err != nil { + return err + } + rval.SetBool(val) + break + case reflect.Float32, reflect.Float64: + val, err := strconv.ParseFloat(value, rtype.Bits()) + if err != nil { + return err + } + rval.SetFloat(val) + break + case reflect.Slice: + if rtype.Elem().Kind() == reflect.Uint8 { + sl := reflect.ValueOf([]byte(value)) + rval.Set(sl) + return nil + } + fallthrough + default: + // assume the string is in JSON format for non-basic types + return json.Unmarshal([]byte(value), rval.Addr().Interface()) + } + + return nil +} diff --git a/env_test.go b/env_test.go new file mode 100644 index 0000000..82bda8f --- /dev/null +++ b/env_test.go @@ -0,0 +1,300 @@ +// Copyright 2016 Qiang Xue. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package env + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "reflect" + "strconv" + "strings" + "testing" +) + +func Test_indirect(t *testing.T) { + var a int + assert.Equal(t, reflect.ValueOf(a).Kind(), indirect(reflect.ValueOf(a)).Kind()) + var b *int + bi := indirect(reflect.ValueOf(&b)) + assert.Equal(t, reflect.ValueOf(a).Kind(), bi.Kind()) + if assert.NotNil(t, b) { + assert.Equal(t, 0, *b) + } +} + +type mySet bool + +func (v *mySet) Set(value string) error { + r, err := strconv.ParseBool(strings.ToUpper(value)) + if err != nil { + return err + } + *v = mySet(r) + return nil +} + +type myInt int64 + +func (v *myInt) UnmarshalText(data []byte) error { + var x int64 + x, err := strconv.ParseInt(string(data), 10, 0) + if err != nil { + return err + } + *v = myInt(x) + return err +} + +type myString string + +func (v *myString) UnmarshalBinary(data []byte) error { + *v = myString(string(data) + "ok") + return nil +} + +func Test_setValue(t *testing.T) { + cfg := struct { + str1 string + str2 *string + int1 int + uint1 uint64 + bool1 bool + float1 float32 + slice1 []byte + slice2 []int + slice3 []string + map1 map[string]int + myint1 myInt + myint2 *myInt + mystr1 myString + mystr2 *myString + myset mySet + }{} + + tests := []struct { + tag string + rval reflect.Value + value string + expected interface{} + equal bool + err bool + }{ + {"t0.1", reflect.ValueOf(&cfg.str1), "abc", "abc", true, false}, + {"t0.2", reflect.ValueOf(&cfg.str2), "abc", "abc", true, false}, + {"t1.1", reflect.ValueOf(&cfg.int1), "1", int(1), true, false}, + {"t1.2", reflect.ValueOf(&cfg.int1), "1", int64(1), false, false}, + {"t1.3", reflect.ValueOf(&cfg.int1), "a1", int(1), true, true}, + {"t2.1", reflect.ValueOf(&cfg.uint1), "1", uint64(1), true, false}, + {"t2.2", reflect.ValueOf(&cfg.uint1), "1", uint32(1), false, false}, + {"t2.3", reflect.ValueOf(&cfg.uint1), "a1", uint64(1), true, true}, + {"t3.1", reflect.ValueOf(&cfg.bool1), "1", true, true, false}, + {"t3.2", reflect.ValueOf(&cfg.bool1), "TRuE", true, true, true}, + {"t3.3", reflect.ValueOf(&cfg.bool1), "TRUE", true, true, false}, + {"t4.1", reflect.ValueOf(&cfg.float1), "12.1", float32(12.1), true, false}, + {"t4.2", reflect.ValueOf(&cfg.float1), "12.1", float64(12.1), false, false}, + {"t4.3", reflect.ValueOf(&cfg.float1), "a12.1", float32(12.1), true, true}, + {"t5.1", reflect.ValueOf(&cfg.slice1), "abc", []byte("abc"), true, false}, + {"t5.2", reflect.ValueOf(&cfg.slice2), "[1,2]", []int{1, 2}, true, false}, + {"t5.3", reflect.ValueOf(&cfg.slice3), "[\"1\",\"2\"]", []string{"1", "2"}, true, false}, + {"t5.4", reflect.ValueOf(&cfg.map1), "{\"a\":1,\"b\":2}", map[string]int{"a": 1, "b": 2}, true, false}, + {"t5.5", reflect.ValueOf(&cfg.map1), "a:1,b:2", "", true, true}, + {"t6.1", reflect.ValueOf(&cfg.myint1), "1", myInt(1), true, false}, + {"t6.2", reflect.ValueOf(&cfg.myint2), "1", myInt(1), true, false}, + {"t6.3", reflect.ValueOf(&cfg.mystr1), "1", myString("1ok"), true, false}, + {"t6.4", reflect.ValueOf(&cfg.mystr2), "1", myString("1ok"), true, false}, + {"t7.1", reflect.ValueOf(&cfg.myset), "1", mySet(true), true, false}, + {"t7.2", reflect.ValueOf(&cfg.myset), "TRuE", mySet(true), true, false}, + {"t7.3", reflect.ValueOf(&cfg.myset), "TRUE", mySet(true), true, false}, + {"t8.1", reflect.ValueOf("test"), "test", "test", true, true}, + } + + for _, test := range tests { + err := setValue(test.rval, test.value) + if test.err { + assert.NotNil(t, err, test.tag) + } else if assert.Nil(t, err, test.tag) { + actual := indirect(test.rval) + if test.equal { + assert.True(t, reflect.DeepEqual(test.expected, actual.Interface()), test.tag) + } else { + assert.False(t, reflect.DeepEqual(test.expected, actual.Interface()), test.tag) + } + } + } +} + +func Test_camelCaseToSnake(t *testing.T) { + tests := []struct { + tag string + input string + expected string + }{ + {"t1", "test", "test"}, + {"t2", "MyName", "My_Name"}, + {"t3", "My2Name", "My2_Name"}, + {"t4", "MyID", "My_ID"}, + {"t5", "My_Name", "My_Name"}, + {"t6", "MyFullName", "My_Full_Name"}, + {"t7", "URLName", "URLName"}, + {"t8", "MyURLName", "My_URLName"}, + } + + for _, test := range tests { + output := camelCaseToSnake(test.input) + assert.Equal(t, test.expected, output, test.tag) + } +} + +func Test_getName(t *testing.T) { + tests := []struct { + tag string + tg string + field string + name string + secret bool + }{ + {"t1", "", "Name", "Name", false}, + {"t2", "", "MyName", "My_Name", false}, + {"t3", "NaME", "Name", "NaME", false}, + {"t4", "NaME,secret", "Name", "NaME", true}, + {"t5", ",secret", "Name", "Name", true}, + {"t6", "NaME,", "Name", "NaME", false}, + } + + for _, test := range tests { + name, secret := getName(test.tg, test.field) + assert.Equal(t, test.name, name, test.tag) + assert.Equal(t, test.secret, secret, test.tag) + } +} + +type myLogger struct { + logs []string +} + +func (l *myLogger) Log(format string, args ...interface{}) { + l.logs = append(l.logs, fmt.Sprintf(format, args...)) +} + +func mockLog(format string, args ...interface{}) { +} + +func mockLookup(name string) (string, bool) { + data := map[string]string{ + "HOST": "localhost", + "PORT": "8080", + "URL": "http://example.com", + "PASSWORD": "xyz", + } + value, ok := data[name] + return value, ok +} + +func mockLookup2(name string) (string, bool) { + data := map[string]string{ + "APP_HOST": "localhost", + "APP_PORT": "8080", + "APP_URL": "http://example.com", + "APP_PASSWORD": "xyz", + } + value, ok := data[name] + return value, ok +} + +func mockLookup3(name string) (string, bool) { + data := map[string]string{ + "PORT": "a8080", + } + value, ok := data[name] + return value, ok +} + +type Embedded struct { + URL string + Port int +} + +type Config1 struct { + Host string + Port int + Embedded +} + +type Config2 struct { + host string + Prt int `env:"PORT"` + URL string `env:"-"` + Password string `env:",secret"` +} + +type Config3 struct { + Embedded +} + +func TestLoader_Load(t *testing.T) { + l := NewWithLookup("", mockLookup, nil) + + var cfg Config1 + err := l.Load(&cfg) + if assert.Nil(t, err) { + assert.Equal(t, "localhost", cfg.Host) + assert.Equal(t, 8080, cfg.Port) + assert.Equal(t, "http://example.com", cfg.URL) + } + + err = l.Load(cfg) + assert.Equal(t, ErrStructPointer, err) + var cfg1 *Config1 + err = l.Load(cfg1) + assert.Equal(t, ErrNilPointer, err) + + logger := &myLogger{} + l = NewWithLookup("", mockLookup, logger.Log) + var cfg2 Config2 + err = l.Load(&cfg2) + if assert.Nil(t, err) { + assert.Equal(t, "", cfg2.host) + assert.Equal(t, 8080, cfg2.Prt) + assert.Equal(t, "", cfg2.URL) + assert.Equal(t, "xyz", cfg2.Password) + assert.Equal(t, []string{`set Prt with $PORT="8080"`, `set Password with $PASSWORD="***"`}, logger.logs) + } + + var cfg3 Config1 + l = NewWithLookup("", mockLookup3, nil) + err = l.Load(&cfg3) + assert.NotNil(t, err) + + var cfg4 Config3 + l = NewWithLookup("", mockLookup3, nil) + err = l.Load(&cfg4) + assert.NotNil(t, err) +} + +func TestNew(t *testing.T) { + l := New("T_", mockLog) + assert.Equal(t, "T_", l.prefix) +} + +func TestNewWithLookup(t *testing.T) { + l := NewWithLookup("T_", mockLookup, mockLog) + assert.Equal(t, "T_", l.prefix) +} + +func TestLoad(t *testing.T) { + var cfg Config1 + oldLookup := loader.lookup + loader.lookup = mockLookup2 + oldLog := loader.log + loader.log = nil + err := Load(&cfg) + if assert.Nil(t, err) { + assert.Equal(t, "localhost", cfg.Host) + assert.Equal(t, 8080, cfg.Port) + assert.Equal(t, "http://example.com", cfg.URL) + } + loader.lookup = oldLookup + loader.log = oldLog +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..de26502 --- /dev/null +++ b/example_test.go @@ -0,0 +1,27 @@ +package env_test + +import ( + "fmt" + "github.com/qiangxue/go-env" + "os" +) + +type Config struct { + Host string + Port int +} + +func Example_one() { + _ = os.Setenv("APP_HOST", "127.0.0.1") + _ = os.Setenv("APP_PORT", "8080") + + var cfg Config + if err := env.Load(&cfg); err != nil { + panic(err) + } + fmt.Println(cfg.Host) + fmt.Println(cfg.Port) + // Output: + // 127.0.0.1 + // 8080 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..980b2b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/qiangxue/go-env + +go 1.13 + +require github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8fdee58 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +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/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.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=