Skip to content

Commit

Permalink
Merge pull request #66 from heetch/origin/041-go2avro
Browse files Browse the repository at this point in the history
cmd/go2avro: new command
  • Loading branch information
rogpeppe authored Mar 31, 2020
2 parents bc796dc + 4aed107 commit fdc8949
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 0 deletions.
178 changes: 178 additions & 0 deletions cmd/go2avro/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
`))
25 changes: 25 additions & 0 deletions cmd/go2avro/script_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 16 additions & 0 deletions cmd/go2avro/testdata/badtype.txt
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions cmd/go2avro/testdata/simple.txt
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions cmd/go2avro/testdata/unknowntype.txt
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=

0 comments on commit fdc8949

Please sign in to comment.