codegen

package module
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2022 License: ISC Imports: 19 Imported by: 0

README

Go CodeGen, a simple code generation system

go-codegen is a simple template-based code generation system for go. By annotating structs with specially tagged fields, go-codegen will generate code based upon templates provided alongside your packages. The templates have access to the full, compiled structure of your program, allowing for sophisticated meta-programming when you need it.

Prior Art

This project is based off of the original go-codegen project by nullstyle. It's essentially a complete rewrite of the codebase, only drawing on some of the key concepts such as embedding structs to trigger code generation, and as such should be considered a separate project. A new name is in the works to differentiate installs of nullstyle's project vs this one.

Installation

go install github.com/CyborgMaster/go-codegen/cmd/go-codegen

Example usage

go-codegen works by scanning your file for structs with fields annotated with a codegen tag, and then will run the corresponding template with the same name as the field type in the same folder where the field type is defined.

Take for example, the following go file called commands.go:

package main

import "fmt"

//go:generate go-codegen $GOFILE

// cmdGen is a template.  Blank structs are good to use for targeting templates
// as they do not affect the compiled package.
type cmdGen struct{}

type cmd interface {
	Execute() (interface{}, error)
	MustExecute() interface{}
}

type HelloCommand struct {
	// HelloCommand needs to have the `cmd` template invoked upon it.
	// By mixing in cmd, we tell go-codegen so.
	cmdGen `codegen:""`
	Name   string
}

func (cmd *HelloCommand) Execute() (interface{}, error) {
	return "Hello, " + cmd.Name, nil
}

type GoodbyeCommand struct {
	cmdGen `codegen:""`
	Name   string
}

func (cmd *GoodbyeCommand) Execute() (interface{}, error) {
	return "Goodbye, " + cmd.Name, nil
}

func main() {
	var c cmd
	c = &HelloCommand{Name: "You"}
	fmt.Println(c.MustExecute())
	c = &GoodbyeCommand{Name: "You"}
	fmt.Println(c.MustExecute())
}

Notice that HelloCommand doesn't have a MustExecute method. This code will be generated by go-codegen. Now we need to write the cmdGen template.

Create a new file named cmdGen.tmpl in the same package where the cmdGen struct is defined:

// MustExecute behaves like Execute, but panics if an error occurs.
func (cmd *{{ .StructName }}) MustExecute() interface{} {
	result, err := cmd.Execute()
	if err != nil {
		panic(err)
	}
	return result
}

Notice the {{ .StructName }} expression: It's just normal go template code.

Now, given both files, lets generate the code. Run go generate . in the package directory, and you'll see a file called commands_generated.go whose content looks like:

// Code generated by go-codegen; DO NOT EDIT.

package main

// MustExecute behaves like Execute, but panics if an error occurs.
func (cmd *GoodbyeCommand) MustExecute() interface{} {
	result, err := cmd.Execute()

	if err != nil {
		panic(err)
	}

	return result
}

// MustExecute behaves like Execute, but panics if an error occurs.
func (cmd *HelloCommand) MustExecute() interface{} {
	result, err := cmd.Execute()

	if err != nil {
		panic(err)
	}

	return result
}

When to Use Code Generation

Note that these are my opinions on when code generation is a good solution. Your millage may vary.

The primary reason that code generation is a good fit for Go is that Go doesn't have generics. In fact, it is likely that when generics are officially released for Go, the majority of code generated by go-codegen would be better if rewritten as generic Go code. Until that time, our only generic options in Go are interfaces and reflection. Code that can use those tools should, as generic code is "better" than generated code.

In many cases, interfaces can be used to provide generic functionality. One can define a utility function with the generic behavior that accepts objects of an interface type that houses the object-specific behavior. The go sort package is a good example of that. It uses an interface to allow a generic sort function to be able to sort arbitrary collections of different types.

However, this interface paradigm tends to break down in two cases:

  1. Code that must return a specific type.

    While accepting an interface tends to work well, returning an interface tends to be more problematic. If you have code that follows the same pattern that must return a type related to your input type, often times your only option is codegen.

  2. Code that must accept a generic slice.

    Because []ConcreteType cannot be used as an argument of []InterfaceType even though ConcreteType implements InterfaceType, your only recourse is to create a new slice of InterfaceType and copy the entries over before calling the function, or have your generic function accept an empty interface{} and lose all type safety. Another option is to codegen a wrapper that does the slice conversion and copy for you, or if the generic function is simple enough, codegen the type specific version of it.

Template Invocation

Templates are invoked by annotating a field with a codegen struct tag.

Template Data

The "data" available in a template invocation is represented with a TemplateContext struct. You can see it here.

Arguments

String arguments may be passed to the template by including them in the struct tag as key=value pairs, comma separated. e.g.

type HelloArgs struct {
	// The argsGen template will have available to it a "hello" arg with the
	// value "there", and a "cool" arg with the value "stuff".
	argsGen `codegen:"hello=there,cool=stuff"`

	// other fields ...
}

These are available inside the template by calling $.Arg, and their presence can be detected by calling $.HasArg

{{ if $.HasArg "hello" }}
  var hello := {{ $.Arg "hello" }}
{{ end }}
Nested Templates

Templates may be recursive. If the type which defines a template is itself a struct with a field with a codegen tag, then both the outer and the nested template will be invoked on the root calling struct.

type FooGen struct {
	BarGen `codegen:""`
}

type BarGen struct{}

// Baz will have both the `FooGen` and the `BarGen` template invoked on it.
type Baz struct {
	FooGen `codgen:""`
}

Variables passed to the outer template invocation will be forwarded to the inner invocation as well.

Finding templates

A template is expected to be found within the same directory where the type referenced by the field is defined, using a name of the form TypeName.tmpl.

Adding Imports

A template may add additional imports into the generated go file by calling the AddImport method on the TemplateContext like so:

{{ $.AddImport "net/http" }}
Advanced Template Features

The Sprig template library is available as global functions within a template. In addition, the singular and plural methods from jinzhu/inflection, are available.

The entire type system of your program is parsed using the go compiler and is inspectable from within the template. The struct that is the target of the invocation is available at .Struct and is a types.Struct. This can be used to check nearly all properties of your struct and its fields. A couple of helper functions are available to make it easier:

  • typeName converts a types.Type to a string in the format "package.Name"
  • structField gets a field from a struct by name.
  • pointerType wraps an existing types.Type in a new one representing a pointer to that type.
  • $.Implements returns true if a types.Type implements an interface, given by fully qualified name.

This lets you do all sorts of things like find the field in your struct that embeds a type from a specific package:

{{ range $fieldNum := until .Struct.NumFields }}
  {{ $f := $.Struct.Field $fieldNum }}
  {{ $type := $f.Type | typeName }}
  {{ if $f.Embedded | and ($type | hasPrefix "mypkg.") }}
     // codgen here ...
  {{ end }}
{{ end }}

or if a pointer to a given type implements an interface defined elsewhere:

{{ if $.Implements
  (pointerType $foundType)
  "github.com/user/project/package/DefinedInterface" }}

  // code here...

{{ end }}

or only assign a field if it exists:

func (s *{{ .StructName }}) Apply(value *some.Type) {
  {{ if structField .Struct "Name" }}
    s.Name = value.Name
  {{ end }}

  // more code...
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Version = "v0.7.1"

Functions

func Output

func Output(ctx *GenContext, filePath string) error

func ParseTemplate

func ParseTemplate(path string) (*template.Template, error)

func ProcessFile

func ProcessFile(filePath string) error

func RunTemplate

func RunTemplate(
	template *template.Template,
	aStruct *types.Named,
	args map[string]string,
	info TypeInfo,
) (string, error)

Types

type GenContext

type GenContext struct {
	PackageName string
	// contains filtered or unexported fields
}

Context represents the context in which a code generation operation is run.

func NewGenContext

func NewGenContext(fset *token.FileSet, rootPackage *types.Package) *GenContext

func (*GenContext) AddImport

func (ctx *GenContext) AddImport(pkg string)

func (*GenContext) Generated

func (ctx *GenContext) Generated() []string

func (*GenContext) GetType

func (ctx *GenContext) GetType(fullName string) (types.Type, error)

func (*GenContext) Imports

func (ctx *GenContext) Imports() []string

func (*GenContext) RunTemplate

func (ctx *GenContext) RunTemplate(invocation Invocation, aStruct *types.Named) error

type Invocation

type Invocation struct {
	GenType *types.Named
	Args    map[string]string
}

func InvocationsForStruct

func InvocationsForStruct(aStruct *types.Struct) ([]Invocation, error)

type TemplateContext

type TemplateContext struct {
	Args         map[string]string
	StructName   string
	TemplateName string
	PackageName  string
	PackagePath  string
	Struct       *types.Named
	// contains filtered or unexported fields
}

func (*TemplateContext) AddImport

func (c *TemplateContext) AddImport(name string) string

For a function to be callable from a template, it must return something.

func (*TemplateContext) AddImportType added in v0.5.0

func (c *TemplateContext) AddImportType(t types.Type) (string, error)

func (*TemplateContext) Arg

func (c *TemplateContext) Arg(name string) string

func (*TemplateContext) DefaultArg added in v0.3.0

func (c *TemplateContext) DefaultArg(name, defaultVal string) string

func (*TemplateContext) HasArg

func (c *TemplateContext) HasArg(name string) bool

func (*TemplateContext) Implements

func (c *TemplateContext) Implements(aType types.Type, interfaceName string) (bool, error)

func (*TemplateContext) RequireArg added in v0.3.0

func (c *TemplateContext) RequireArg(name string) (string, error)

func (*TemplateContext) TypeString added in v0.6.0

func (c *TemplateContext) TypeString(t types.Type) string

type TypeInfo

type TypeInfo interface {
	AddImport(pkg string)
	GetType(fullName string) (types.Type, error)
}

Directories

Path Synopsis
cmd
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL