diff --git a/encoding/gjson/gjson.go b/encoding/gjson/gjson.go index 675a0765679..f103be4a618 100644 --- a/encoding/gjson/gjson.go +++ b/encoding/gjson/gjson.go @@ -22,13 +22,14 @@ import ( ) const ( - ContentTypeJson = `json` - ContentTypeJs = `js` - ContentTypeXml = `xml` - ContentTypeIni = `ini` - ContentTypeYaml = `yaml` - ContentTypeYml = `yml` - ContentTypeToml = `toml` + ContentTypeJson = `json` + ContentTypeJs = `js` + ContentTypeXml = `xml` + ContentTypeIni = `ini` + ContentTypeYaml = `yaml` + ContentTypeYml = `yml` + ContentTypeToml = `toml` + ContentTypeProperties = `properties` ) const ( diff --git a/encoding/gjson/gjson_api_encoding.go b/encoding/gjson/gjson_api_encoding.go index 8d95cb94648..7f08f0cccbd 100644 --- a/encoding/gjson/gjson_api_encoding.go +++ b/encoding/gjson/gjson_api_encoding.go @@ -8,6 +8,7 @@ package gjson import ( "github.com/gogf/gf/v2/encoding/gini" + "github.com/gogf/gf/v2/encoding/gproperties" "github.com/gogf/gf/v2/encoding/gtoml" "github.com/gogf/gf/v2/encoding/gxml" "github.com/gogf/gf/v2/encoding/gyaml" @@ -197,3 +198,30 @@ func (j *Json) MustToIni() []byte { func (j *Json) MustToIniString() string { return string(j.MustToIni()) } + +// ======================================================================== +// properties +// ======================================================================== +// Toproperties json to properties +func (j *Json) ToProperties() ([]byte, error) { + return gproperties.Encode(j.Map()) +} + +// TopropertiesString properties to string +func (j *Json) ToPropertiesString() (string, error) { + b, e := j.ToProperties() + return string(b), e +} + +func (j *Json) MustToProperties() []byte { + result, err := j.ToProperties() + if err != nil { + panic(err) + } + return result +} + +// MustTopropertiesString +func (j *Json) MustToPropertiesString() string { + return string(j.MustToProperties()) +} diff --git a/encoding/gjson/gjson_api_new_load.go b/encoding/gjson/gjson_api_new_load.go index b0a2194f1b3..23d761fefbc 100644 --- a/encoding/gjson/gjson_api_new_load.go +++ b/encoding/gjson/gjson_api_new_load.go @@ -11,6 +11,7 @@ import ( "reflect" "github.com/gogf/gf/v2/encoding/gini" + "github.com/gogf/gf/v2/encoding/gproperties" "github.com/gogf/gf/v2/encoding/gtoml" "github.com/gogf/gf/v2/encoding/gxml" "github.com/gogf/gf/v2/encoding/gyaml" @@ -174,6 +175,17 @@ func LoadToml(data interface{}, safe ...bool) (*Json, error) { return doLoadContentWithOptions(gconv.Bytes(data), option) } +// LoadProperties creates a Json object from given TOML format content. +func LoadProperties(data interface{}, safe ...bool) (*Json, error) { + option := Options{ + Type: ContentTypeProperties, + } + if len(safe) > 0 && safe[0] { + option.Safe = true + } + return doLoadContentWithOptions(gconv.Bytes(data), option) +} + // LoadContent creates a Json object from given content, it checks the data type of `content` // automatically, supporting data content type as follows: // JSON, XML, INI, YAML and TOML. @@ -222,7 +234,8 @@ func IsValidDataType(dataType string) bool { ContentTypeYaml, ContentTypeYml, ContentTypeToml, - ContentTypeIni: + ContentTypeIni, + ContentTypeProperties: return true } return false @@ -288,6 +301,10 @@ func doLoadContentWithOptions(data []byte, options Options) (*Json, error) { if data, err = gini.ToJson(data); err != nil { return nil, err } + case ContentTypeProperties: + if data, err = gproperties.ToJson(data); err != nil { + return nil, err + } default: err = gerror.NewCodef( @@ -335,6 +352,8 @@ func checkDataType(content []byte) string { (gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*".+"`, content) || gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, content)) { // Must contain "[xxx]" section. return ContentTypeIni + } else if gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*\w+`, content) { + return ContentTypeProperties } else { return "" } diff --git a/encoding/gjson/gjson_z_example_load_test.go b/encoding/gjson/gjson_z_example_load_test.go index 41fe9189d3d..102029f6f42 100644 --- a/encoding/gjson/gjson_z_example_load_test.go +++ b/encoding/gjson/gjson_z_example_load_test.go @@ -182,6 +182,7 @@ func ExampleIsValidDataType() { fmt.Println(gjson.IsValidDataType("txt")) fmt.Println(gjson.IsValidDataType("")) fmt.Println(gjson.IsValidDataType(".json")) + fmt.Println(gjson.IsValidDataType(".properties")) // Output: // true @@ -192,6 +193,7 @@ func ExampleIsValidDataType() { // false // false // true + // true } func ExampleLoad_Xml() { @@ -200,3 +202,16 @@ func ExampleLoad_Xml() { fmt.Println(j.Get("doc.name")) fmt.Println(j.Get("doc.score")) } + +func ExampleLoad_Properties() { + jsonFilePath := gtest.DataPath("properties", "data1.properties") + j, _ := gjson.Load(jsonFilePath) + fmt.Println(j.Get("pr.name")) + fmt.Println(j.Get("pr.score")) + fmt.Println(j.Get("pr.sex")) + + //Output: + // john + // 100 + // 0 +} diff --git a/encoding/gjson/gjson_z_example_test.go b/encoding/gjson/gjson_z_example_test.go index fa765be83fc..2bda1b8c18b 100644 --- a/encoding/gjson/gjson_z_example_test.go +++ b/encoding/gjson/gjson_z_example_test.go @@ -641,6 +641,80 @@ func ExampleJson_MustToIniString() { //Name=John } +// ======================================================================== +// Properties +// ======================================================================== +func ExampleJson_ToProperties() { + type BaseInfo struct { + Name string + Age int + } + + info := BaseInfo{ + Name: "John", + Age: 18, + } + + j := gjson.New(info) + pr, _ := j.ToProperties() + fmt.Println(string(pr)) + + // May Output: + // name = John + // age = 18 +} + +func ExampleJson_ToPropertiesString() { + type BaseInfo struct { + Name string + } + + info := BaseInfo{ + Name: "John", + } + + j := gjson.New(info) + pr, _ := j.ToPropertiesString() + fmt.Println(pr) + + // Output: + // name = John +} + +func ExampleJson_MustToProperties() { + type BaseInfo struct { + Name string + } + + info := BaseInfo{ + Name: "John", + } + + j := gjson.New(info) + pr := j.MustToProperties() + fmt.Println(string(pr)) + + // Output: + // name = John +} + +func ExampleJson_MustToPropertiesString() { + type BaseInfo struct { + Name string + } + + info := BaseInfo{ + Name: "John", + } + + j := gjson.New(info) + pr := j.MustToPropertiesString() + fmt.Println(pr) + + // Output: + // name = John +} + func ExampleJson_MarshalJSON() { type BaseInfo struct { Name string diff --git a/encoding/gjson/gjson_z_unit_feature_load_test.go b/encoding/gjson/gjson_z_unit_feature_load_test.go index 4517989a40f..51f9a2d8b9b 100644 --- a/encoding/gjson/gjson_z_unit_feature_load_test.go +++ b/encoding/gjson/gjson_z_unit_feature_load_test.go @@ -361,3 +361,58 @@ gfcli: t.AssertNil(err) }) } + +func Test_Load_Properties(t *testing.T) { + var data = ` + +#注释 + + +addr.ip = 127.0.0.1 +addr.port=9001 +addr.enable=true +DBINFO.type=mysql +DBINFO.user=root +DBINFO.password=password + +` + + gtest.C(t, func(t *gtest.T) { + j, err := gjson.LoadContent(data) + if err != nil { + gtest.Fatal(err) + } + + t.Assert(j.Get("addr.ip").String(), "127.0.0.1") + t.Assert(j.Get("addr.port").String(), "9001") + t.Assert(j.Get("addr.enable").String(), "true") + t.Assert(j.Get("DBINFO.type").String(), "mysql") + t.Assert(j.Get("DBINFO.user").String(), "root") + t.Assert(j.Get("DBINFO.password").String(), "password") + + _, err = j.ToProperties() + if err != nil { + gtest.Fatal(err) + } + }) + + gtest.C(t, func(t *gtest.T) { + j, err := gjson.LoadProperties(data, true) + if err != nil { + gtest.Fatal(err) + } + + t.Assert(j.Get("addr.ip").String(), "127.0.0.1") + t.Assert(j.Get("addr.port").String(), "9001") + t.Assert(j.Get("addr.enable").String(), "true") + t.Assert(j.Get("DBINFO.type").String(), "mysql") + t.Assert(j.Get("DBINFO.user").String(), "root") + t.Assert(j.Get("DBINFO.password").String(), "password") + }) + + gtest.C(t, func(t *gtest.T) { + errData := []byte("i\\u1 : 123456789") + _, err := gjson.LoadContentType("properties", errData, true) + t.AssertNE(err, nil) + }) +} diff --git a/encoding/gjson/testdata/properties/data1.properties b/encoding/gjson/testdata/properties/data1.properties new file mode 100644 index 00000000000..8f9dad5d9b7 --- /dev/null +++ b/encoding/gjson/testdata/properties/data1.properties @@ -0,0 +1,3 @@ +pr.name=john +pr.score=100 +pr.sex=0 \ No newline at end of file diff --git a/encoding/gproperties/gproperties.go b/encoding/gproperties/gproperties.go new file mode 100644 index 00000000000..b2612932200 --- /dev/null +++ b/encoding/gproperties/gproperties.go @@ -0,0 +1,137 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +// Package gproperties provides accessing and converting for .properties content. +package gproperties + +import ( + "bytes" + "sort" + "strings" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/util/gconv" + "github.com/magiconair/properties" +) + +// Decode converts properties format to map. +func Decode(data []byte) (res map[string]interface{}, err error) { + res = make(map[string]interface{}) + pr, err := properties.Load(data, properties.UTF8) + if err != nil || pr == nil { + err = gerror.Wrapf(err, `Lib magiconair load Properties data failed.`) + return nil, err + } + for _, key := range pr.Keys() { + // ignore existence check: we know it's there + value, _ := pr.Get(key) + // recursively build nested maps + path := strings.Split(key, ".") + lastKey := strings.ToLower(path[len(path)-1]) + deepestMap := deepSearch(res, path[0:len(path)-1]) + + // set innermost value + deepestMap[lastKey] = value + } + return res, nil +} + +// Encode converts map to properties format. +func Encode(data map[string]interface{}) (res []byte, err error) { + pr := properties.NewProperties() + + flattened := map[string]interface{}{} + + flattened = flattenAndMergeMap(flattened, data, "", ".") + + keys := make([]string, 0, len(flattened)) + + for key := range flattened { + keys = append(keys, key) + } + + sort.Strings(keys) + + for _, key := range keys { + _, _, err := pr.Set(key, gconv.String(flattened[key])) + if err != nil { + err = gerror.Wrapf(err, `Sets the property key to the corresponding value failed.`) + return nil, err + } + } + + var buf bytes.Buffer + + _, err = pr.Write(&buf, properties.UTF8) + if err != nil { + err = gerror.Wrapf(err, `Properties Write buf failed.`) + return nil, err + } + + return buf.Bytes(), nil +} + +// ToJson convert .properties format to JSON. +func ToJson(data []byte) (res []byte, err error) { + prMap, err := Decode(data) + if err != nil { + return nil, err + } + return json.Marshal(prMap) +} + +// deepSearch scans deep maps, following the key indexes listed in the sequence "path". +// The last value is expected to be another map, and is returned. +func deepSearch(m map[string]interface{}, path []string) map[string]interface{} { + for _, k := range path { + m2, ok := m[k] + if !ok { + // intermediate key does not exist + // => create it and continue from there + m3 := make(map[string]interface{}) + m[k] = m3 + m = m3 + continue + } + m3, ok := m2.(map[string]interface{}) + if !ok { + m3 = make(map[string]interface{}) + m[k] = m3 + } + // continue search from here + m = m3 + } + return m +} + +// flattenAndMergeMap recursively flattens the given map into a new map +func flattenAndMergeMap(shadow map[string]interface{}, m map[string]interface{}, prefix string, delimiter string) map[string]interface{} { + if shadow != nil && prefix != "" && shadow[prefix] != nil { + return shadow + } + + var m2 map[string]interface{} + if prefix != "" { + prefix += delimiter + } + for k, val := range m { + fullKey := prefix + k + switch val.(type) { + case map[string]interface{}: + m2 = val.(map[string]interface{}) + case map[interface{}]interface{}: + m2 = gconv.Map(val) + default: + // immediate value + shadow[strings.ToLower(fullKey)] = val + continue + } + // recursively merge to shadow map + shadow = flattenAndMergeMap(shadow, m2, fullKey, delimiter) + } + return shadow +} diff --git a/encoding/gproperties/gproperties_z_unit_test.go b/encoding/gproperties/gproperties_z_unit_test.go new file mode 100644 index 00000000000..f3ff793108b --- /dev/null +++ b/encoding/gproperties/gproperties_z_unit_test.go @@ -0,0 +1,132 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gproperties_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/encoding/gproperties" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/test/gtest" +) + +var pStr string = ` +# 模板引擎目录 +viewpath = "/home/www/templates/" +# redis数据库配置 +redis.disk = "127.0.0.1:6379,0" +redis.cache = "127.0.0.1:6379,1" +#SQL配置 +sql.mysql.0.type = mysql +sql.mysql.0.ip = 127.0.0.1 +sql.mysql.0.user = root +` +var errorTests = []struct { + input, msg string +}{ + // unicode literals + {"key\\u1 = value", "invalid unicode literal"}, + {"key\\u12 = value", "invalid unicode literal"}, + {"key\\u123 = value", "invalid unicode literal"}, + {"key\\u123g = value", "invalid unicode literal"}, + {"key\\u123", "invalid unicode literal"}, + + // circular references + {"key=${key}", `circular reference in:\nkey=\$\{key\}`}, + {"key1=${key2}\nkey2=${key1}", `circular reference in:\n(key1=\$\{key2\}\nkey2=\$\{key1\}|key2=\$\{key1\}\nkey1=\$\{key2\})`}, + + // malformed expressions + {"key=${ke", "malformed expression"}, + {"key=valu${ke", "malformed expression"}, +} + +func TestDecode(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := make(map[string]interface{}) + m["properties"] = pStr + res, err := gproperties.Encode(m) + if err != nil { + t.Errorf("encode failed. %v", err) + return + } + decodeMap, err := gproperties.Decode(res) + if err != nil { + t.Errorf("decode failed. %v", err) + return + } + t.Assert(decodeMap["properties"], pStr) + }) + + gtest.C(t, func(t *gtest.T) { + for _, v := range errorTests { + _, err := gproperties.Decode(([]byte)(v.input)) + if err == nil { + t.Errorf("encode should be failed. %v", err) + return + } + t.AssertIN(`Lib magiconair load Properties data failed.`, strings.Split(err.Error(), ":")) + } + }) +} +func TestEncode(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := make(map[string]interface{}) + m["properties"] = pStr + res, err := gproperties.Encode(m) + if err != nil { + t.Errorf("encode failed. %v", err) + return + } + decodeMap, err := gproperties.Decode(res) + if err != nil { + t.Errorf("decode failed. %v", err) + return + } + t.Assert(decodeMap["properties"], pStr) + }) +} + +func TestToJson(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + res, err := gproperties.Encode(map[string]interface{}{ + "sql": g.Map{ + "userName": "admin", + "password": "123456", + }, + "user": "admin", + "no": 123, + }) + fmt.Print(string(res)) + jsonPr, err := gproperties.ToJson(res) + if err != nil { + t.Errorf("ToJson failed. %v", err) + return + } + fmt.Print(string(jsonPr)) + + p := gjson.New(res) + expectJson, err := p.ToJson() + if err != nil { + t.Errorf("parser ToJson failed. %v", err) + return + } + t.Assert(jsonPr, expectJson) + }) + gtest.C(t, func(t *gtest.T) { + for _, v := range errorTests { + _, err := gproperties.ToJson(([]byte)(v.input)) + if err == nil { + t.Errorf("encode should be failed. %v", err) + return + } + t.AssertIN(`Lib magiconair load Properties data failed.`, strings.Split(err.Error(), ":")) + } + }) +} diff --git a/go.mod b/go.mod index d601a55ddb1..2a8f5f58c03 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( github.com/go-redis/redis/v8 v8.11.4 github.com/gorilla/websocket v1.5.0 github.com/grokify/html-strip-tags-go v0.0.1 + github.com/kr/pretty v0.3.0 // indirect + github.com/magiconair/properties v1.8.6 github.com/olekukonko/tablewriter v0.0.5 go.opentelemetry.io/otel v1.7.0 go.opentelemetry.io/otel/sdk v1.7.0 diff --git a/go.sum b/go.sum index 1012c9550ae..73700b678c3 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E= github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -45,6 +46,15 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -67,6 +77,8 @@ github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= @@ -139,8 +151,10 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=