Skip to content

kevinconway/wrapgen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wrapgen - A code generator for Go interfaces

This project can inject the details of any Go interface into a custom template. The goal is to enable teams who either make frequent use of interface wrappers to layer on behavior, such as using a decorator pattern, or teams who often need to generate standard implementations of some interfaces, such as those generating stubs for a test.

This project is heavily inspired by https://github.com/golang/mock which contains a tool called mockgen that can generate an advanced mock implementation of any Go interface.

Usage

Quick Example

go install github.com/kevinconway/wrapgen/v2
go install golang.org/x/tools/cmd/goimports

${GOPATH}/bin/wrapgen \
  --source=io \
  --interface=Reader \
  --interface=Writer \
  --package=wrappers \
  --template="https://raw.githubusercontent.com/kevinconway/wrapgen/master/templates/logtime.txt" \
  | ${GOPATH}/bin/goimports

The output will look like:

package wrappers
// Code generated by wrapgen DO NOT EDIT

import (
        "io"
        "log"
        "time"
)

type WrapsReader struct {
        wrapped io.Reader
}

func (w *WrapsReader) Read(p []byte) (int, error) {
        start := time.Now()
        defer func() {
                log.Println("Read latency:", time.Now().Since(start))
        }()
        var n, err = w.wrapped.Read(p)
        return n, err
}

type WrapsWriter struct {
        wrapped io.Writer
}

func (w *WrapsWriter) Write(p []byte) (int, error) {
        start := time.Now()
        defer func() {
                log.Println("Write latency:", time.Now().Since(start))
        }()
        var n, err = w.wrapped.Write(p)
        return n, err
}

All output is written to stdout and stderr by default. An optional destination file may be provided as an alternative to stdout. Template paths may either be URLs or file system paths. It is highly suggested to use goimports or another formatter as a post-processor.

Overrider Example

${GOPATH}/bin/wrapgen \
  --source=io \
  --interface=Reader \
  --interface=Writer \
  --package=wrappers \
  --template="https://raw.githubusercontent.com/kevinconway/wrapgen/master/templates/overrider.txt" \
  | ${GOPATH}/bin/goimports

The output will look like:

package wrappers

// Code generated by wrapgen DO NOT EDIT

import (
	srcPkgAlias "io"
)

type (
	ReadFunc func(p []byte) (int, error)
)

type TestReader struct {
	Reader srcPkgAlias.Reader

	ReadFunc ReadFunc
}

func (t *TestReader) Read(p []byte) (int, error) {
	if t.ReadFunc != nil {
		return t.ReadFunc(p)
	}
	return t.Reader.Read(p)
}

var _ srcPkgAlias.Reader = (*TestReader)(nil)

type (
	WriteFunc func(p []byte) (int, error)
)

type TestWriter struct {
	Writer srcPkgAlias.Writer

	WriteFunc WriteFunc
}

func (t *TestWriter) Write(p []byte) (int, error) {
	if t.WriteFunc != nil {
		return t.WriteFunc(p)
	}
	return t.Writer.Write(p)
}

var _ srcPkgAlias.Writer = (*TestWriter)(nil)

CLI Options

wrapgen --help

Usage of wrapgen:
      --destination string   Filename for the rendered template. Defaults to STDOUT. (default "-")
      --interface strings   The name of the interface to render.
      --leftdelim string    Left-hand side delimiter for the template. (default "#!")
      --package string      The package name that the resulting file will be in. Defaults to the source package.
      --rightdelim string   Right-hand side delimiter for the template. (default "!#")
      --source string       The import path of the package to render.
      --template string     The template to render.
      --timeout duration    Maximum runtime allowed for rendering. (default 1m0s)

Any number of interfaces may be given by providing more --interface flags.

Writing Templates

The templates/basic.txt template from this project is the best way to get started writing your own. It demonstrates the how to manage import statements, iterate over the collected interfaces, and already covers the complexity of rendering method arguments and outputs correctly.

Whether using template/basics.txt as a starter or generating a new template from scratch, the template content must be valid text/template markup. By default, the character sets #! and !# are used as the left and right delimiters, respectively. Those characters are the default because they rarely, if ever, conflict with common character sets in Go code. You can adjust these with CLI flags.

The root context injected into the template is Package from the following:

// Package is a container for all exported interfaces of a Go package.
type Package struct {
	Name       string
	Source     *Import
	Interfaces []*Interface
	Imports    []*Import
	// ImportsWithSource will contain Source if the destination package is set and
	// Source is not included in Imports.
	ImportsWithSource []*Import
}

// Import is a package name and path that is imported by another package.
type Import struct {
	Package string
	Path    string
}

// Interface is an exported interface defined in a package.
type Interface struct {
	SrcType Type
	Name    string
	Methods []*Method
}

// Method is a named function attached to an interface.
type Method struct {
	Name string
	In   []*Parameter
	Out  []*Parameter
}

// Parameter is a named parameter used by a Method.
type Parameter struct {
	Name string
	Type Type
}

// Type is a Go type definition that can be rendered into a valid
// Go code snippet.
type Type interface {
	String() string
}

Those are the structures available within any template. Each Type is specialized and will render correctly when calling String(). For example, a Parameter with Type of read-only channel of integers will render as <-chan int when calling String() on the type. The templates/basic.txt template contains examples of inspecting the type string in order to support variadics. This practice can be extended to, for example, determine if the first parameter is a context and optionall fetch a value from it.

License

This project is available under the Apache2.0 license. See the LICENSE file in this repository for the complete license text.