From 4aed107ad2db63a77c9d3d218ae2fcba3ff5d7ef Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Mon, 30 Mar 2020 15:41:12 +0100 Subject: [PATCH] cmd/go2avro: new command This allows the generation of Avro schemas from Go types. --- cmd/go2avro/main.go | 178 +++++++++++++++++++++++++++ cmd/go2avro/script_test.go | 25 ++++ cmd/go2avro/testdata/badtype.txt | 16 +++ cmd/go2avro/testdata/simple.txt | 31 +++++ cmd/go2avro/testdata/unknowntype.txt | 12 ++ go.mod | 1 + go.sum | 6 + 7 files changed, 269 insertions(+) create mode 100644 cmd/go2avro/main.go create mode 100644 cmd/go2avro/script_test.go create mode 100644 cmd/go2avro/testdata/badtype.txt create mode 100644 cmd/go2avro/testdata/simple.txt create mode 100644 cmd/go2avro/testdata/unknowntype.txt diff --git a/cmd/go2avro/main.go b/cmd/go2avro/main.go new file mode 100644 index 0000000..1afb2e7 --- /dev/null +++ b/cmd/go2avro/main.go @@ -0,0 +1,178 @@ +// The go2avro command generates Avro schemas for Go types. +package main + +import ( + "bytes" + "encoding/json" + stdflag "flag" + "fmt" + "go/format" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" +) + +var flag = stdflag.NewFlagSet("", stdflag.ContinueOnError) + +func main() { + os.Exit(main1()) +} + +func main1() int { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, `usage: go2avro [package.]type + +This command prints the Avro schema for a given Go type on the +standard output. + +If the package isn't specified, the current directory is used. +Current implementation restrictions mean that schemas can only +be generated for exported Go types. + +For example: + + go2avro foo.com/bar/somepkg.Foo +`[1:]) + } + if flag.Parse(os.Args[1:]) != nil { + return 2 + } + if len(flag.Args()) != 1 { + flag.Usage() + return 2 + } + if err := main2(); err != nil { + fmt.Fprintf(os.Stderr, "go2avro: %v\n", err) + return 1 + } + return 0 +} + +func main2() error { + pkgType := flag.Arg(0) + var p tmplParams + if i := strings.LastIndex(pkgType, "."); i < 0 { + var err error + p.Package, err = currentPkg() + if err != nil { + return fmt.Errorf("cannot get current package: %v", err) + } + p.Type = pkgType + } else { + p.Package = pkgType[0:i] + p.Type = pkgType[i+1:] + } + var codeBuf bytes.Buffer + if err := tmpl.Execute(&codeBuf, p); err != nil { + return fmt.Errorf("cannot execute template: %v", err) + } + code, err := format.Source(codeBuf.Bytes()) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid temporary Go code:\n-------\n%s-----\n", codeBuf.String()) + return fmt.Errorf("invalid template code: %v", err) + } + // Build the binary before executing the code so we can + // distinguish between build errors and Avro errors. + exe, err := buildGo(code) + if err != nil { + return err + } + defer os.Remove(exe) + + var outBuf bytes.Buffer + var errBuf bytes.Buffer + cmd := exec.Command(exe) + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + if err := cmd.Run(); err != nil { + if errBuf.Len() > 0 { + return fmt.Errorf("cannot get Avro type: %s", strings.TrimSpace(errBuf.String())) + } + return err + } + var indentJSON bytes.Buffer + if err := json.Indent(&indentJSON, outBuf.Bytes(), "", " "); err != nil { + return fmt.Errorf("cannot indent JSON: %v", err) + } + fmt.Printf("%s", indentJSON.String()) + return nil +} + +func buildGo(code []byte) (string, error) { + // Create the Go file in the current directory so that we + // take advantage of the current Go module. + // TODO avoid the side-effect of adding the avro import, somehow. + tmpFile, err := ioutil.TempFile(".", "go2avro_temp_*.go") + if err != nil { + return "", fmt.Errorf("cannot generate temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + _, err = tmpFile.Write(code) + tmpFile.Close() + if err != nil { + return "", fmt.Errorf("cannot write %q: %v", tmpFile.Name(), err) + } + tmpBinary, err := ioutil.TempFile(".", "go2avro_temp_bin") + if err != nil { + return "", fmt.Errorf("cannot generate temp binary file: %v", err) + } + tmpBinary.Close() + + var errBuf bytes.Buffer + cmd := exec.Command("go", "build", "-o", tmpBinary.Name(), tmpFile.Name()) + cmd.Stderr = &errBuf + if err := cmd.Run(); err != nil { + defer os.Remove(tmpBinary.Name()) + return "", fmt.Errorf("cannot build: %v", errBuf.String()) + } + // Use explicit "./" prefix because . isn't always in $PATH. + return "." + string(filepath.Separator) + tmpBinary.Name(), nil +} + +func currentPkg() (string, error) { + var buf bytes.Buffer + c := exec.Command("go", "list") + c.Stderr = os.Stderr + c.Stdout = &buf + if err := c.Run(); err != nil { + return "", err + } + return strings.TrimSpace(buf.String()), nil +} + +type tmplParams struct { + Package string + Type string +} + +var tmpl = template.Must(template.New("").Parse(` +// Code generated by avrogen. DO NOT EDIT. + +// This should be treated as a temporary file. Remove it if you find it. + +// +build ignore + +package main + +import ( + "fmt" + "os" + + "github.com/heetch/avro" + + pkg {{printf "%q" .Package}} +) + +func main() { + var x pkg.{{.Type}} + t, err := avro.TypeOf(x) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot get type: %v\n", err) + os.Exit(1) + } + fmt.Println(t) +} +`)) diff --git a/cmd/go2avro/script_test.go b/cmd/go2avro/script_test.go new file mode 100644 index 0000000..5e5a621 --- /dev/null +++ b/cmd/go2avro/script_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "os" + "testing" + + "github.com/rogpeppe/go-internal/gotooltest" + "github.com/rogpeppe/go-internal/testscript" +) + +func TestMain(m *testing.M) { + os.Exit(testscript.RunMain(m, map[string]func() int{ + "go2avro": main1, + })) +} + +func TestFoo(t *testing.T) { + p := testscript.Params{ + Dir: "testdata", + } + if err := gotooltest.Setup(&p); err != nil { + t.Fatal(err) + } + testscript.Run(t, p) +} diff --git a/cmd/go2avro/testdata/badtype.txt b/cmd/go2avro/testdata/badtype.txt new file mode 100644 index 0000000..ed00594 --- /dev/null +++ b/cmd/go2avro/testdata/badtype.txt @@ -0,0 +1,16 @@ +! go2avro T +stderr 'go2avro: cannot get Avro type: cannot get type: cannot make Avro schema for Go type chan int' + +-- bar.go -- +package bar + +type T struct { + X chan int +} + +-- go.mod -- +module example.com/foo/bar + +go 1.14 + +require github.com/heetch/avro v0.0.0-20200318154341-de261c0e4b7f // indirect diff --git a/cmd/go2avro/testdata/simple.txt b/cmd/go2avro/testdata/simple.txt new file mode 100644 index 0000000..2f51dc9 --- /dev/null +++ b/cmd/go2avro/testdata/simple.txt @@ -0,0 +1,31 @@ +go2avro T +cmp stdout expect-stdout + +go2avro example.com/foo/bar.T +cmp stdout expect-stdout + +-- expect-stdout -- +{ + "fields": [ + { + "default": 0, + "name": "X", + "type": "long" + } + ], + "name": "T", + "type": "record" +} +-- bar.go -- +package bar + +type T struct { + X int +} + +-- go.mod -- +module example.com/foo/bar + +go 1.14 + +require github.com/heetch/avro v0.0.0-20200318154341-de261c0e4b7f // indirect diff --git a/cmd/go2avro/testdata/unknowntype.txt b/cmd/go2avro/testdata/unknowntype.txt new file mode 100644 index 0000000..fb89299 --- /dev/null +++ b/cmd/go2avro/testdata/unknowntype.txt @@ -0,0 +1,12 @@ +! go2avro Foo +stderr 'undefined: bar.Foo' + +-- bar.go -- +package bar + +-- go.mod -- +module example.com/foo/bar + +go 1.14 + +require github.com/heetch/avro v0.0.0-20200318154341-de261c0e4b7f // indirect diff --git a/go.mod b/go.mod index 9025caa..b4e764a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/frankban/quicktest v1.7.2 github.com/kr/pretty v0.1.0 github.com/linkedin/goavro/v2 v2.9.7 + github.com/rogpeppe/go-internal v1.5.2 github.com/rogpeppe/gogen-avro/v7 v7.2.1 gopkg.in/retry.v1 v1.0.3 ) diff --git a/go.sum b/go.sum index c2ea90a..d6cff73 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= +github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/gogen-avro/v7 v7.2.1 h1:laf1RaIs397v8rAhLGtpznjOIuXYQDJ/7ij0dAss4Gg= github.com/rogpeppe/gogen-avro/v7 v7.2.1/go.mod h1:awhtQwpFg18PdUpdnOFr0ceVLYAn/oDCa/HE1hdbk50= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g=