From 3713cb4e91c64c1b88d436b85dd03967d9b48f77 Mon Sep 17 00:00:00 2001 From: Julia Ogris Date: Mon, 21 Feb 2022 09:23:30 +1100 Subject: [PATCH] pb: Initialise pb cli Create basic `pb` command converting proto message from one encoding format to another, currently supporting JSON, prototext, binary proto. Add basic docs, started by @camh- and goreleaser config. Signed-off-by: Julia Ogris --- .gitattributes | 2 + .goreleaser.yml | 11 ++ Makefile | 11 +- README.md | 32 +++++ cmd/pb/main.go | 248 +++++++++++++++++++++++++++++++++++ cmd/pb/main_test.go | 121 +++++++++++++++++ cmd/pb/testdata/pbtest.pb | 12 ++ cmd/pb/testdata/pbtest.proto | 33 +++++ go.mod | 2 + go.sum | 12 +- 10 files changed, 476 insertions(+), 8 deletions(-) create mode 100644 .gitattributes create mode 100644 .goreleaser.yml create mode 100644 README.md create mode 100644 cmd/pb/main.go create mode 100644 cmd/pb/main_test.go create mode 100644 cmd/pb/testdata/pbtest.pb create mode 100644 cmd/pb/testdata/pbtest.proto diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5193124 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.pb linguist-generated=true +go.sum linguist-generated=true diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..fc3a41a --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,11 @@ +dist: out/dist +builds: + - main: ./cmd/pb + id: pb + binary: pb +archives: + - builds: ['pb'] + id: all + - builds: ['pb'] + id: pb + name_template: 'pb_{{.Version}}_{{.Os}}_{{.Arch}}' diff --git a/Makefile b/Makefile index 06640bd..a79c61a 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # --- Global ------------------------------------------------------------------- O = out -COVERAGE = 90 +COVERAGE = 85 VERSION ?= $(shell git describe --tags --dirty --always) REPO_ROOT = $(shell git rev-parse --show-toplevel) @@ -18,13 +18,13 @@ clean:: ## Remove generated files .PHONY: all ci clean # --- Build -------------------------------------------------------------------- -GO_LDFLAGS = -X main.version=$(VERSION) +CMDS = ./cmd/pb build: | $(O) ## Build reflect binaries - go build -o $(O)/protog -ldflags='$(GO_LDFLAGS)' . + go build -o $(O) $(CMDS) install: ## Build and install binaries in $GOBIN - go install -ldflags='$(GO_LDFLAGS)' . + go install $(CMDS) .PHONY: build install @@ -50,16 +50,15 @@ FAIL_COVERAGE = { echo '$(COLOUR_RED)FAIL - Coverage below $(COVERAGE)%$(COLOUR_ .PHONY: check-coverage check-uptodate cover test # --- Lint --------------------------------------------------------------------- - lint: ## Lint go source code golangci-lint run .PHONY: lint # --- Protos --------------------------------------------------------------------- - proto: protosync --dest proto google/api/annotations.proto + protoc -I cmd/pb/testdata --include_imports -o cmd/pb/testdata/pbtest.pb cmd/pb/testdata/pbtest.proto protoc -I proto -I registry/testdata --include_imports -o registry/testdata/regtest.pb registry/testdata/regtest.proto protoc -I proto -I httprule/internal --go_out=. --go_opt=module=foxygo.at/protog --go-grpc_out=. --go-grpc_opt=module=foxygo.at/protog test.proto echo.proto gosimports -w . diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb2ef31 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# A toolkit for working with protobuf and gRPC in Go + +[![ci](https://github.com/foxygoat/protog/actions/workflows/cicd.yaml/badge.svg?branch=master)](https://github.com/foxygoat/protog/actions/workflows/cicd.yaml?branch=master) +[![Godoc](https://img.shields.io/badge/godoc-ref-blue)](https://pkg.go.dev/foxygo.at/protog) +[![Slack chat](https://img.shields.io/badge/slack-gophers-795679?logo=slack)](https://gophers.slack.com/messages/foxygoat) + +protog is a toolkit for Google's protobuf and gRPC projects. It contains a +couple of Go packages and command line tools. + +It is developed against the protobuf-go v2 API (google.golang.org/protobuf) +which simplifies a lot of reflection-based protobuf code compared to v1 +(github.com/golang/protobuf). + +Build, test and install with `make`. See further options with `make help`. + +## pb + +`pb` is a CLI tool for converting proto messages between different formats. + +Sample usage: + + # create base-massage.pb binary encoded proto message file from input JSON + pb -P cmd/pb/testdata/pbtest.pb -o base-message.pb BaseMessage '{"f" : "some_field"}' + + # convert binary encoded proto message from pb file back to JSON + pb -P cmd/pb/testdata/pbtest.pb -O json BaseMessage @base-message.pb + +Output: + + { + "f": "some_field" + } diff --git a/cmd/pb/main.go b/cmd/pb/main.go new file mode 100644 index 0000000..bc8b741 --- /dev/null +++ b/cmd/pb/main.go @@ -0,0 +1,248 @@ +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strings" + + "foxygo.at/protog/registry" + "github.com/alecthomas/kong" + "golang.org/x/sys/unix" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" +) + +var ( + version = "tip" + commit = "HEAD" + date = "now" + description = ` +pb translates encoded Protobuf message from one format to another +` + cli struct { + Version kong.VersionFlag `help:"Show version."` + PBConfig PBConfig `embed:""` + } +) + +type PBConfig struct { + Protoset *registry.Files `short:"P" help:"Protoset of Message being translated" required:""` + Out string `short:"o" help:"Output file name"` + InFormat string `short:"I" help:"Input format (json, pb)" enum:"json,pb,j,p," default:""` + OutFormat string `short:"O" help:"Output format (j[son], p[b],t[xt])" enum:"json,pb,txt,j,p,t," default:""` + MessageType string `arg:"" help:"Message type to be translated" required:""` + In string `arg:"" help:"Message value JSON encoded" optional:""` +} + +func main() { + kctx := kong.Parse(&cli, + kong.Description(description), + kong.Vars{"version": fmt.Sprintf("%s (%s on %s)", version, commit, date)}, + kong.TypeMapper(reflect.TypeOf(cli.PBConfig.Protoset), kong.MapperFunc(registryMapper)), + ) + err := run(cli.PBConfig) + kctx.FatalIfErrorf(err) +} + +type unmarshaler func([]byte, proto.Message) error +type marshaler func(proto.Message) ([]byte, error) + +func run(cfg PBConfig) error { + md, err := lookupMessage(cfg.Protoset, cfg.MessageType) + if err != nil { + return err + } + in, err := cfg.readInput() + if err != nil { + return err + } + unmarshal, err := cfg.unmarshaler() + if err != nil { + return err + } + message := dynamicpb.NewMessage(md) + if err := unmarshal(in, message); err != nil { + return err + } + marshal, err := cfg.marshaler() + if err != nil { + return err + } + b, err := marshal(message) + if err != nil { + return err + } + return cfg.writeOutput(b) +} + +func (c *PBConfig) readInput() ([]byte, error) { + if c.In == "" { + return io.ReadAll(os.Stdin) + } + if strings.HasPrefix(c.In, "@") { + return os.ReadFile(c.In[1:]) + } + return []byte(c.In), nil +} + +func (c *PBConfig) writeOutput(b []byte) error { + if c.Out == "" { + if getFormat("", c.OutFormat) == "pb" && isTTY() { + return fmt.Errorf("not writing binary to terminal. Use -O json or -O txt to output a textual format") + } + _, err := os.Stdout.Write(b) + return err + } + return os.WriteFile(c.Out, b, 0666) +} + +func (c *PBConfig) unmarshaler() (unmarshaler, error) { + format := getFormat(c.In, c.InFormat) + switch format { + case "json": + o := protojson.UnmarshalOptions{Resolver: c.Protoset} + return o.Unmarshal, nil + case "pb": + o := proto.UnmarshalOptions{Resolver: c.Protoset} + return o.Unmarshal, nil + case "txt": + o := prototext.UnmarshalOptions{Resolver: c.Protoset} + return o.Unmarshal, nil + } + return nil, fmt.Errorf("unknown input format %s", format) +} + +func (c *PBConfig) marshaler() (marshaler, error) { + format := getFormat("@"+c.Out, c.OutFormat) + switch format { + case "json": + o := protojson.MarshalOptions{Resolver: c.Protoset, Multiline: true} + return func(m proto.Message) ([]byte, error) { + b, err := o.Marshal(m) + if err != nil { + return nil, err + } + return append(b, byte('\n')), nil + }, nil + case "pb": + o := proto.MarshalOptions{} + return o.Marshal, nil + case "txt": + o := prototext.MarshalOptions{Resolver: c.Protoset, Multiline: true} + return o.Marshal, nil + } + return nil, fmt.Errorf("unknown output format %s", format) +} + +func getFormat(contentOrFile string, format string) string { + if format != "" { + return canonicalFormat(format) + } + ext := filepath.Ext(contentOrFile) + // default to JSON for stdout, string input and files without extension + if contentOrFile == "@" || !strings.HasPrefix(contentOrFile, "@") || ext == "" { + return "json" + } + return canonicalFormat(ext[1:]) +} + +func canonicalFormat(format string) string { + switch format { + case "json", "j": + return "json" + case "pb", "p": + return "pb" + case "txt", "t", "prototext", "prototxt": + return "txt" + } + return format +} + +func lookupMessage(reg *registry.Files, name string) (protoreflect.MessageDescriptor, error) { + var result []protoreflect.MessageDescriptor + reg.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + for i := 0; i < fd.Messages().Len(); i++ { + md := fd.Messages().Get(i) + mds, exactMatch := lookupMessageInMD(md, name) + if exactMatch { + result = mds + return false + } + result = append(result, mds...) + } + return true + }) + + if len(result) == 0 { + return nil, fmt.Errorf("message not found: %s", name) + } + if len(result) > 1 { + return nil, fmt.Errorf("ambiguous message name: %s", name) + } + return result[0], nil +} + +func lookupMessageInMD(md protoreflect.MessageDescriptor, name string) (mds []protoreflect.MessageDescriptor, exactMatch bool) { + mdName := string(md.FullName()) + if name == mdName || name == "."+mdName { + // If we have a full name match, we're done and will also + // ignore any other partial name matches. + return []protoreflect.MessageDescriptor{md}, true + } + mdLowerName := "." + strings.ToLower(mdName) + lowerName := strings.ToLower(name) + if lowerName == mdLowerName || strings.HasSuffix(mdLowerName, "."+lowerName) { + mds = append(mds, md) + } + subMessages := md.Messages() + for i := 0; i < subMessages.Len(); i++ { + md = subMessages.Get(i) + subMDs, exactMatch := lookupMessageInMD(md, name) + if exactMatch { + return subMDs, true + } + mds = append(mds, subMDs...) + } + return mds, false +} + +func registryMapper(kctx *kong.DecodeContext, target reflect.Value) error { + reg, ok := target.Interface().(*registry.Files) + if !ok { + panic("target is not a *registry.Files") + } + var filename string + if err := kctx.Scan.PopValueInto("file", &filename); err != nil { + return err + } + files, err := registryFiles(filename) + if err != nil { + return err + } + *reg = *files + return nil +} + +func registryFiles(filename string) (*registry.Files, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + fds := descriptorpb.FileDescriptorSet{} + if err := proto.Unmarshal(b, &fds); err != nil { + return nil, err + } + return registry.NewFiles(&fds) +} + +func isTTY() bool { + _, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) + return err == nil +} diff --git a/cmd/pb/main_test.go b/cmd/pb/main_test.go new file mode 100644 index 0000000..320a46b --- /dev/null +++ b/cmd/pb/main_test.go @@ -0,0 +1,121 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRunJSON(t *testing.T) { + tmpDir := t.TempDir() + files, err := registryFiles("testdata/pbtest.pb") + require.NoError(t, err) + + cfg := PBConfig{ + Protoset: files, + Out: filepath.Join(tmpDir, "out.json"), + MessageType: "BaseMessage", + In: `{"f": "F" }`, + } + + formats := []string{"json", "j", ""} + for _, format := range formats { + t.Run("format-"+format, func(t *testing.T) { + cfg.OutFormat = format + require.NoError(t, run(cfg)) + want := `{"f": "F" }` + requireJSONFileContent(t, want, cfg.Out) + }) + } +} + +func TestRunPrototext(t *testing.T) { + tmpDir := t.TempDir() + files, err := registryFiles("testdata/pbtest.pb") + require.NoError(t, err) + + cfg := PBConfig{ + Protoset: files, + Out: filepath.Join(tmpDir, "out.txt"), + MessageType: "BaseMessage", + In: `{"f": "F" }`, + } + formats := []string{"txt", "t", "prototxt"} + for _, format := range formats { + t.Run("format-"+format, func(t *testing.T) { + cfg.OutFormat = format + require.NoError(t, run(cfg)) + want := `f:"F"` + "\n" + out := filepath.Join(tmpDir, "out.txt") + b, err := os.ReadFile(out) + require.NoError(t, err) + // prototext is made unstable with random whitespace. stabilize for this basic test. + got := strings.ReplaceAll(string(b), " ", "") + require.Equal(t, want, got) + }) + } +} + +func TestRunMessages(t *testing.T) { + tmpDir := t.TempDir() + files, err := registryFiles("testdata/pbtest.pb") + require.NoError(t, err) + + cfg := PBConfig{ + Protoset: files, + Out: filepath.Join(tmpDir, "out.json"), + In: `{"f": "F" }`, + } + messageTypes := []string{"BaseMessage", "pbtest.BaseMessage", ".pbtest.BaseMessage", "basemessage"} + for _, messageType := range messageTypes { + t.Run("message-"+messageType, func(t *testing.T) { + cfg.MessageType = messageType + require.NoError(t, run(cfg)) + want := `{"f": "F" }` + requireJSONFileContent(t, want, cfg.Out) + }) + } +} + +func TestRunMessageErr(t *testing.T) { + tmpDir := t.TempDir() + files, err := registryFiles("testdata/pbtest.pb") + require.NoError(t, err) + + cfg := PBConfig{ + Protoset: files, + Out: filepath.Join(tmpDir, "out.json"), + In: `{"f": "F" }`, + } + messageTypes := []string{"Message", "..pbtest.BaseMessage"} + for _, messageType := range messageTypes { + t.Run("message-"+messageType, func(t *testing.T) { + cfg.MessageType = messageType + require.Error(t, run(cfg)) + }) + } +} + +func TestRunInErr(t *testing.T) { + tmpDir := t.TempDir() + files, err := registryFiles("testdata/pbtest.pb") + require.NoError(t, err) + + cfg := PBConfig{ + Protoset: files, + Out: filepath.Join(tmpDir, "out.json"), + MessageType: "BaseMessage", + In: `{"MISSING": "F" }`, + } + require.Error(t, run(cfg)) +} + +func requireJSONFileContent(t *testing.T, wantStr string, gotFile string) { + t.Helper() + b, err := os.ReadFile(gotFile) + require.NoError(t, err) + require.JSONEq(t, wantStr, string(b)) +} diff --git a/cmd/pb/testdata/pbtest.pb b/cmd/pb/testdata/pbtest.pb new file mode 100644 index 0000000..5053682 --- /dev/null +++ b/cmd/pb/testdata/pbtest.pb @@ -0,0 +1,12 @@ + +– + pbtest.protopbtest"& + BaseMessage +f ( Rf* 耀€€"­ +ExtensionMessage +f ( RfI +NestedExtension +nf ( Rnf2& +ef3.pbtest.BaseMessageë ( Ref32@ +ef2.pbtest.BaseMessageê ( 2.pbtest.ExtensionMessageRef2:& +ef1.pbtest.BaseMessageé ( Ref1 \ No newline at end of file diff --git a/cmd/pb/testdata/pbtest.proto b/cmd/pb/testdata/pbtest.proto new file mode 100644 index 0000000..772ae7c --- /dev/null +++ b/cmd/pb/testdata/pbtest.proto @@ -0,0 +1,33 @@ +syntax = "proto2"; + +package pbtest; + +// A base message to be extended +message BaseMessage { + optional string f = 1; + extensions 1000 to max; +} + +// A simple top-level extension +extend BaseMessage { + optional string ef1 = 1001; +} + +// A message scope for more extensions +message ExtensionMessage { + optional string f = 1; + + // An extension scoped within a message + extend BaseMessage { + optional ExtensionMessage ef2 = 1002; + }; + + message NestedExtension { + optional string nf = 1; + + // An extension scoped within a nested message + extend BaseMessage { + optional string ef3 = 1003; + }; + }; +} diff --git a/go.mod b/go.mod index 1a5149a..999e7b2 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module foxygo.at/protog go 1.16 require ( + github.com/alecthomas/kong v0.4.1 github.com/stretchr/testify v1.7.0 + golang.org/x/sys v0.0.0-20210510120138-977fb7262007 google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67 google.golang.org/grpc v1.39.1 google.golang.org/protobuf v1.27.1 diff --git a/go.sum b/go.sum index c89e68e..0b1b920 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,19 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alecthomas/kong v0.4.1 h1:0sFnMts+ijOiFuSHsMB9MlDi3NGINBkx9KIw1/gcuDw= +github.com/alecthomas/kong v0.4.1/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -41,6 +46,8 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -138,7 +145,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=