From 4373bad335aab3b47e1a91cc437d79fd4cc629d7 Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Tue, 24 Nov 2020 19:07:41 -0500 Subject: [PATCH 1/5] feat: support strings option over HTTP API --- http/client.go | 9 +++++++++ http/parse.go | 23 ++++++++++++++--------- request.go | 33 ++++++++++++++++++++------------- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/http/client.go b/http/client.go index ca4fa93a..a4500258 100644 --- a/http/client.go +++ b/http/client.go @@ -230,6 +230,15 @@ func getQuery(req *cmds.Request) (string, error) { if OptionSkipMap[k] { continue } + + optArr, ok := v.([]string) + if ok { + for _, o := range optArr { + query.Add(k, o) + } + continue + } + str := fmt.Sprintf("%v", v) query.Set(k, str) } diff --git a/http/parse.go b/http/parse.go index a7bfa67b..0915ddf0 100644 --- a/http/parse.go +++ b/http/parse.go @@ -61,22 +61,28 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) { } opts, stringArgs2 := parseOptions(r) + iopts := make(map[string]interface{}, len(opts)) optDefs, err := root.GetOptions(pth) if err != nil { return nil, err } for k, v := range opts { + iopts[k] = v if optDef, ok := optDefs[k]; ok { name := optDef.Names()[0] if k != name { - opts[name] = v - delete(opts, k) + iopts[name] = v + delete(iopts, k) + } + + if optDef.Type() != cmds.Strings && len(v) > 0 { + iopts[name] = v[0] } } } // default to setting encoding to JSON - if _, ok := opts[cmds.EncLong]; !ok { - opts[cmds.EncLong] = cmds.JSON + if _, ok := iopts[cmds.EncLong]; !ok { + iopts[cmds.EncLong] = cmds.JSON } stringArgs = append(stringArgs, stringArgs2...) @@ -148,7 +154,7 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) { } ctx := logging.ContextWithLoggable(r.Context(), uuidLoggable()) - req, err := cmds.NewRequest(ctx, pth, opts, args, f, root) + req, err := cmds.NewRequest(ctx, pth, iopts, args, f, root) if err != nil { return nil, err } @@ -162,8 +168,8 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) { return req, err } -func parseOptions(r *http.Request) (map[string]interface{}, []string) { - opts := make(map[string]interface{}) +func parseOptions(r *http.Request) (map[string][]string, []string) { + opts := make(map[string][]string) var args []string query := r.URL.Query() @@ -171,8 +177,7 @@ func parseOptions(r *http.Request) (map[string]interface{}, []string) { if k == "arg" { args = v } else { - - opts[k] = v[0] + opts[k] = v } } diff --git a/request.go b/request.go index 4eca412a..ea747254 100644 --- a/request.go +++ b/request.go @@ -118,21 +118,28 @@ func checkAndConvertOptions(root *Command, opts OptMap, path []string) (OptMap, kind := reflect.TypeOf(v).Kind() if kind != opt.Type() { - if str, ok := v.(string); ok { - val, err := opt.Parse(str) - if err != nil { - value := fmt.Sprintf("value %q", v) - if len(str) == 0 { - value = "empty value" - } - return options, fmt.Errorf("could not convert %s to type %q (for option %q)", - value, opt.Type().String(), "-"+k) + if opt.Type() == Strings { + if _, ok := v.([]string); !ok { + return options, fmt.Errorf("option %q should be type %q, but got type %q", + k, opt.Type().String(), kind.String()) } - options[k] = val - } else { - return options, fmt.Errorf("option %q should be type %q, but got type %q", - k, opt.Type().String(), kind.String()) + if str, ok := v.(string); ok { + val, err := opt.Parse(str) + if err != nil { + value := fmt.Sprintf("value %q", v) + if len(str) == 0 { + value = "empty value" + } + return options, fmt.Errorf("could not convert %s to type %q (for option %q)", + value, opt.Type().String(), "-"+k) + } + options[k] = val + + } else { + return options, fmt.Errorf("option %q should be type %q, but got type %q", + k, opt.Type().String(), kind.String()) + } } } From 6d7b776da50e9c1a4d4ba5b6c0ff4e4a3798b8da Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Tue, 1 Dec 2020 15:28:26 -0500 Subject: [PATCH 2/5] refactor: type checking options in http client --- http/client.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/http/client.go b/http/client.go index a4500258..d19c9fb9 100644 --- a/http/client.go +++ b/http/client.go @@ -231,16 +231,17 @@ func getQuery(req *cmds.Request) (string, error) { continue } - optArr, ok := v.([]string) - if ok { - for _, o := range optArr { + switch val := v.(type) { + case []string: + for _, o := range val { query.Add(k, o) } - continue + case bool, int, int64, uint, uint64, float64, string: + str := fmt.Sprintf("%v", v) + query.Set(k, str) + default: + return "", fmt.Errorf("unsupported query parameter type. key: %s, value: %v", k, v) } - - str := fmt.Sprintf("%v", v) - query.Set(k, str) } args := req.Arguments From 11d59a2d85fd970d9fd841d6e57522ebd128e44e Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Tue, 1 Dec 2020 16:53:58 -0500 Subject: [PATCH 3/5] refactor: cleanup code --- request.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/request.go b/request.go index ea747254..5443b66b 100644 --- a/request.go +++ b/request.go @@ -124,22 +124,22 @@ func checkAndConvertOptions(root *Command, opts OptMap, path []string) (OptMap, k, opt.Type().String(), kind.String()) } } else { - if str, ok := v.(string); ok { - val, err := opt.Parse(str) - if err != nil { - value := fmt.Sprintf("value %q", v) - if len(str) == 0 { - value = "empty value" - } - return options, fmt.Errorf("could not convert %s to type %q (for option %q)", - value, opt.Type().String(), "-"+k) - } - options[k] = val - - } else { + str, ok := v.(string) + if !ok { return options, fmt.Errorf("option %q should be type %q, but got type %q", k, opt.Type().String(), kind.String()) } + + val, err := opt.Parse(str) + if err != nil { + value := fmt.Sprintf("value %q", v) + if len(str) == 0 { + value = "empty value" + } + return options, fmt.Errorf("could not convert %s to type %q (for option %q)", + value, opt.Type().String(), "-"+k) + } + options[k] = val } } From 91330cf0436457f101012d1b16ae2ce249ffb0b9 Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Tue, 1 Dec 2020 17:48:32 -0500 Subject: [PATCH 4/5] refactor: stricter type checking in http query parser --- http/parse.go | 60 ++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/http/parse.go b/http/parse.go index 0915ddf0..2bd3ac21 100644 --- a/http/parse.go +++ b/http/parse.go @@ -60,33 +60,45 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) { return nil, ErrNotFound } - opts, stringArgs2 := parseOptions(r) - iopts := make(map[string]interface{}, len(opts)) + opts := make(map[string]interface{}) optDefs, err := root.GetOptions(pth) if err != nil { return nil, err } - for k, v := range opts { - iopts[k] = v - if optDef, ok := optDefs[k]; ok { - name := optDef.Names()[0] - if k != name { - iopts[name] = v - delete(iopts, k) + + query := r.URL.Query() + // Note: len(v) is guaranteed by the above function to always be greater than 0 + for k, v := range query { + if k == "arg" { + stringArgs = append(stringArgs, v...) + } else { + optDef, ok := optDefs[k] + if !ok { + opts[k] = v[0] + continue } - if optDef.Type() != cmds.Strings && len(v) > 0 { - iopts[name] = v[0] + name := optDef.Names()[0] + opts[name] = v + + switch optType := optDef.Type(); optType { + case cmds.Strings: + opts[name] = v + case cmds.Bool, cmds.Int, cmds.Int64, cmds.Uint, cmds.Uint64, cmds.Float, cmds.String: + if len(v) > 1 { + return nil, fmt.Errorf("expected key %s to have only a single value, received %v", name, v) + } + opts[name] = v[0] + default: + return nil, fmt.Errorf("unsupported option type. key: %s, type: %v", k, optType) } } } // default to setting encoding to JSON - if _, ok := iopts[cmds.EncLong]; !ok { - iopts[cmds.EncLong] = cmds.JSON + if _, ok := opts[cmds.EncLong]; !ok { + opts[cmds.EncLong] = cmds.JSON } - stringArgs = append(stringArgs, stringArgs2...) - // count required argument definitions numRequired := 0 for _, argDef := range cmd.Arguments { @@ -154,7 +166,7 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) { } ctx := logging.ContextWithLoggable(r.Context(), uuidLoggable()) - req, err := cmds.NewRequest(ctx, pth, iopts, args, f, root) + req, err := cmds.NewRequest(ctx, pth, opts, args, f, root) if err != nil { return nil, err } @@ -168,22 +180,6 @@ func parseRequest(r *http.Request, root *cmds.Command) (*cmds.Request, error) { return req, err } -func parseOptions(r *http.Request) (map[string][]string, []string) { - opts := make(map[string][]string) - var args []string - - query := r.URL.Query() - for k, v := range query { - if k == "arg" { - args = v - } else { - opts[k] = v - } - } - - return opts, args -} - // parseResponse decodes a http.Response to create a cmds.Response func parseResponse(httpRes *http.Response, req *cmds.Request) (cmds.Response, error) { res := &Response{ From d57bbdf4c698e1e3cdc5fbc06c1048913b0dde8e Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Tue, 1 Dec 2020 23:29:44 -0500 Subject: [PATCH 5/5] chore: go mod tidy --- go.sum | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/go.sum b/go.sum index 45f10e65..7da8d5a7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Kubuxu/go-os-helper v0.0.1 h1:EJiD2VUQyh5A9hWJLmc6iWg6yIcJ7jpBcwC8GMGXfDk= github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y= @@ -5,27 +6,23 @@ github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuv github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= 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/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= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/ipfs/go-ipfs-files v0.0.8 h1:8o0oFJkJ8UkO/ABl8T6ac6tKF3+NIpj67aAB6ZpusRg= github.com/ipfs/go-ipfs-files v0.0.8/go.mod h1:wiN/jSG8FKyk7N0WyctKSvq3ljIa2NNTiZB55kpTdOs= -github.com/ipfs/go-log v1.0.3 h1:Gg7SUYSZ7BrqaKMwM+hRgcAkKv4QLfzP4XPQt5Sx/OI= -github.com/ipfs/go-log v1.0.3/go.mod h1:OsLySYkwIbiSUR/yBTdv1qPtcE4FW3WPWk/ewz9Ru+A= github.com/ipfs/go-log v1.0.4 h1:6nLQdX4W8P9yZZFH7mO+X/PzjN8Laozm/lMJ6esdgzY= github.com/ipfs/go-log v1.0.4/go.mod h1:oDCg2FkjogeFOhqqb+N39l2RpTNPL6F/StPkB3kPgcs= -github.com/ipfs/go-log/v2 v2.0.3 h1:Q2gXcBoCALyLN/pUQlz1qgu0x3uFV6FzP9oXhpfyJpc= -github.com/ipfs/go-log/v2 v2.0.3/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0= github.com/ipfs/go-log/v2 v2.0.5 h1:fL4YI+1g5V/b1Yxr1qAiXTMg1H8z9vx/VmJxBuQMHvU= github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -42,23 +39,19 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e h1:T5PdfK/M1xyrHwynxMIVMWLS7f/qHwfslZphxtGnw7s= github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e/go.mod h1:XDKHRm5ThF8YJjx001LtgelzsoaEcvnA7lVWz9EeX3g= -go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d h1:2+ZP7EfsZV7Vvmx3TIqSlSzATMkTAKqM14YGFPoSKjI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -72,17 +65,19 @@ golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=