Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Inception. Go programs that generate Go code

Inception. Go programs that generate Go code

A review of tools helping reduce boilerplate code in Go using code generation and tips on how they were implemented.

Ernesto Jiménez

August 19, 2016
Tweet

More Decks by Ernesto Jiménez

Other Decks in Programming

Transcript

  1. Go programs that: 1. Parse data to generate Go code

    2. Modify Go code 3. Parse Go code to generate Go code
  2. package main import ( "testing" "github.com/stretchr/testify/mock" ) type downcaser interface

    { Downcase(string) (string, error) } func TestMock(t *testing.T) { m := &mockDowncaser{} m.On("Downcase", "FOO").Return("foo", nil) m.Downcase("FOO") m.AssertNumberOfCalls(t, "Downcase", 1) }
  3. type mockDowncaser struct { mock.Mock } func (m *mockDowncaser) Downcase(a0

    string) (string, error) { ret := m.Called(a0) return ret.Get(0).(string), ret.Error(1) }
  4. type downcaser interface { Downcase(string) (string, error) } func (m

    *mockDowncaser) Downcase(a0 string) (string, error) { ret := m.Called(a0) return ret.Get(0).(string), ret.Error(1) }
  5. mock_downcaser_test.go package main import "github.com/stretchr/testify/mock" type mockDowncaser struct { mock.Mock

    } func (_m *mockDowncaser) Downcase(_a0 string) (string, error) { ret := _m.Called(_a0) var r0 string if rf, ok := ret.Get(0).(func(string) string); ok { r0 = rf(_a0) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(_a0) } else { r1 = ret.Error(1) } return r0, r1 }
  6. package main import ( "testing" ) type downcaser interface {

    Downcase(string) (string, error) } //go:generate mockery -inpkg -testonly -name=downcaser func TestMock(t *testing.T) { m := &mockDowncaser{} m.On("Downcase", "FOO").Return("foo", nil) m.Downcase("FOO") m.AssertNumberOfCalls(t, "Downcase", 1) }
  7. $ go test # github.com/ernesto-jimenez/test ./main_test.go:14: undefined: mockDowncaser FAIL github.com/ernesto-jimenez/test

    [build failed] $ go generate Generating mock for: downcaser $ go test PASS ok github.com/ernesto-jimenez/test 0.011s
  8. Yeah, this is a known issue. I'm not sure how

    deal with it because it requires being able to parse entire packages to find the other interfaces.
  9. $ goautomock -o=- io.Reader /* * CODE GENERATED AUTOMATICALLY WITH

    github.com/ernesto-jimenez/gogen/automock * THIS FILE SHOULD NOT BE EDITED BY HAND */ package app import ( "fmt" mock "github.com/stretchr/testify/mock" ) // ReaderMock mock type ReaderMock struct { mock.Mock } // Read mocked method func (m *ReaderMock) Read(p0 []byte) (int, error) { // […]
  10. two functions per assertion func Panics(t TestingT, fn func(), msgAndArgs

    ...interface{}) bool { if funcDidPanic, _ := didPanic(f); !funcDidPanic { return Fail(t, fmt.Sprintf("func %#v should panic", f), msgAndArgs...) } return true } func (a *Assertions) Panics(fn func(), msgAndArgs ...interface{}) bool { return Panics(a.t, expected, actual, msgAndArgs…) }
  11. two extra functions per assertion func Panics(t TestingT, fn func(),

    msgAndArgs ...interface{}) bool { if !assert.Panics(t, fn, msgAndArgs...) { t.FailNow() } } func (a *Assertions) Panics(fn func(), msgAndArgs ...interface{}) bool { Panics(a.t, fn, msgAndArgs...) }
  12. //go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_forward.go.tmpl //go:generate go run ../_codegen/main.go

    -output-package=require -template=require_forward.go.tmpl //go:generate go run ../_codegen/main.go -output-package=require -template=require.go.tmpl
  13. var DefaultClient = New() func New() *Client { return &Client{}

    } func (c *Client) Do(interface{}) error { // […] } // Do calls `DefaultClient.Do` func Do(v interface{}) error { return DefaultClient.Do(v) }
  14. var DefaultClient = New() //go:generate goexportdefault DefaultClient func New() *Client

    { return &Client{} } func (c *Client) Do(interface{}) error { // […] }
  15. /* * CODE GENERATED AUTOMATICALLY WITH goexportdefault * THIS FILE

    MUST NOT BE EDITED BY HAND * * Install goexportdefault with: * go get github.com/ernesto-jimenez/gogen/cmd/goexportdefault */ package example import () // Do is a wrapper around DefaultClient.Do func Do(p0 interface{}) error { return DefaultClient.Do(p0) }
  16. if v, ok := m["name"].(string); ok { s.Name = v

    } else if v, exists := m["name"]; exists && v != nil { return fmt.Errorf("expected field name to be string but got %T", m["name"]) }
  17. $ go test ./unmarshalmap/testpkg --- FAIL: TestNestedUnmarshalMap (0.00s) test.go:35: UnmarshalMap()

    method from *testpkg.Nested out of date. regenerate the code FAIL FAIL github.com/ernesto-jimenez/gogen/unmarshalmap/testpkg 0.010s $ go generate ./unmarshalmap/testpkg Generating func (*Nested) UnmarshalMap(map[string]interface{}) error $ go test ./unmarshalmap/testpkg ok github.com/ernesto-jimenez/gogen/unmarshalmap/testpkg 0.009s
  18. package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var

    _ = API("adder", func() { Title("The adder API") Description("A teaser for goa") Host("localhost:8080") Scheme("http") }) var _ = Resource("operands", func() { Action("add", func() { Routing(GET("add/:left/:right")) Description("add returns the sum of the left and right parameters in the response body") Params(func() { Param("left", Integer, "Left operand") Param("right", Integer, "Right operand") }) Response(OK, "plain/text") }) })
  19. $ tree goa-adder . ├── app │ ├── contexts.go │

    ├── controllers.go │ ├── hrefs.go │ ├── media_types.go │ ├── test │ │ └── operands_testing.go │ └── user_types.go ├── client │ ├── client.go │ ├── media_types.go │ ├── operands.go │ └── user_types.go ├── design │ └── design.go ├── main.go ├── operands.go ├── swagger │ ├── swagger.json │ └── swagger.yaml └── tool ├── adder-cli │ └── main.go └── cli └── commands.go 8 directories, 17 files
  20. func main() { // Create service service := goa.New("adder") //

    Mount middleware service.Use(middleware.RequestID()) service.Use(middleware.LogRequest(true)) service.Use(middleware.ErrorHandler(service, true)) service.Use(middleware.Recover()) // Mount "operands" controller c := NewOperandsController(service) app.MountOperandsController(service, c) // Start service if err := service.ListenAndServe(":8080"); err != nil { service.LogError("startup", "err", err) } }
  21. // OperandsController implements the operands resource. type OperandsController struct {

    *goa.Controller } // Add runs the add action. func (c *OperandsController) Add(ctx *app.AddOperandsContext) error { // OperandsController_Add: start_implement // Put your logic here // OperandsController_Add: end_implement return nil }
  22. // Add runs the add action. func (c *OperandsController) Add(ctx

    *app.AddOperandsContext) error { sum := ctx.Left + ctx.Right return ctx.OK([]byte(strconv.Itoa(sum))) }
  23. $ goa-adder [INFO] mount ctrl=Operands action=Add route=GET /add/:left/:right [INFO] listen

    transport=http addr=:8080 [INFO] started req_id=iaRusv3Joj-1 GET=/add/10/23 from=::1 ctrl=OperandsController action=Add [INFO] params req_id=iaRusv3Joj-1 right=23 left=10 [INFO] completed req_id=iaRusv3Joj-1 status=200 bytes=2 time=316.398µs ------------------------------------------------ $ adder-cli add operands --left 10 --right 23 [INFO] started id=ACowX1Kj GET=http://localhost:8080/add/10/23 [INFO] completed id=ACowX1Kj status=200 time=5.981623ms 33
  24. package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var

    _ = API("adder", func() { Title("The adder API") Description("A teaser for goa") Host("localhost:8080") Scheme("http") }) var _ = Resource("operands", func() { Action("add", func() { Routing(GET("add/:left/:right")) Description("add returns the sum of the left and right parameters in the response body") Params(func() { Param("left", Integer, "Left operand") Param("right", Integer, "Right operand") }) Response(OK, "plain/text") }) })
  25. package main import ( "fmt" "strings" "github.com/goadesign/goa/dslengine" "github.com/goadesign/goa/goagen/gen_app" _ "github.com/ernesto-jimenez/golang-uk/goa-adder/design"

    ) func main() { // Check if there were errors while running the first DSL pass dslengine.FailOnError(dslengine.Errors) // Now run the secondary DSLs dslengine.FailOnError(dslengine.Run()) files, err := genapp.Generate() dslengine.FailOnError(err) // We're done fmt.Println(strings.Join(files, "\n")) }