goa Plugin Generators


goa Plugins make it possible to generate new kinds of outputs from any DSL. The possibilities are really endless, clients in different languages, domain specific type conversions, database bindings etc.

Generators

Generators consume the data structures produced by DSLs to generate artifacts. Generators can be written for existing and new DSLs (see goa Plugin DSLs for information on how to create new DSLs). The generated artifacts can be anything, the DSL engine provides the generation orchestration and is oblivious of the actual output.

Implementing a Generator

A generator consists of a Go package that implements the Generate function:

func Generate() ([]string, error)

The function returns the list of generated filenames in case of success or a descriptive error otherwise. The generator accesses the DSL output data structures directly from the corresponding DSL packages. For example the goa API DSL exposes a Design package variable of type APIDefinition that contains the built-up API definition.

Writing Generators for the goa API DSL

On top of the Design package variable the goa DSL also exposes GeneratedMediaTypes which contains the set of media types that was generated dynamically by the engine rather than defined by the user (this happens when a Media Type is use inline with CollectionOf.

A generator wanting to act on a goa API DSL output would thus look like this:

import "github.com/goadesign/goa/design"

// Generate generates artifacts from goa API DSL definitions.
func Generate() ([]string, error) {
	api := design.Design
	// ... use api to generate stuff
	genMedia := design.GeneratedMediaTypes
	// ... use genMedia to generate stuff
}

The Generate method can take advantage of the APIDefinition IterateXXX methods to iterate through the API resources, media types and types to guarantee that the order doesn’t change between two invocations of the function (thereby generating different outputs even if the design hasn’t changed).

Metadata

A simple way to tack on information to existing definitions for the benefit of generators is to use metadata. The goa design language allows defining metadata on a number of definitions: API, Resource, Action, Response and Attribute (which means Type and MediaType as well since these definitions are attributes). Here is an example defining a “pseudo” metadata value on a resource:

var _ = Resource("Bottle", func() {
	Description("A bottle of wine")
	Metadata("pseudo:port")
	// ...
}

The DSL engine package defines the metadata definition data structure - MetadataDefinition.

Writing the Artifacts

The codegen package comes with a number of helper functions that help deal with generating Go code. For example it contains functions that can produce the code for defining a data structure given an instance of the design package DataStructure interface.

Integrating With goagen

goagen is the tool used to generate the artifacts from DSLs in goa. The gen subcommand allows specifying a Go package path to a generator package - that is a package that implements the Generate function. This command accepts one flag:

--pkg-path=PKG-PATH specifies the Go package import path to the plugin package.

Example

Let’s implement a generator that traverses the definitions created by the goa API DSL and creates a single file names.txt containing the names of the API resources sorted in alphabetical order. If a resource has a metadata pair with the key “pseudo” then the plugin uses the metadata value instead.

package genresnames

import (
	"flag"
	"github.com/goadesign/goa/design"
	"github.com/goadesign/goa/goagen/codegen"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
)

func Generate() ([]string, error) {
	var (
		ver    string
		outDir string
	)
	set := flag.NewFlagSet("app", flag.PanicOnError)
	set.String("design", "", "") // Consume design argument so Parse doesn't complain
	set.StringVar(&ver, "version", "", "")
	set.StringVar(&outDir, "out", "", "")
	set.Parse(os.Args[2:])

	// First check compatibility
	if err := codegen.CheckVersion(ver); err != nil {
		return nil, err
	}

	return WriteNames(design.Design, outDir)
}

// WriteNames creates the names.txt file.
func WriteNames(api *design.APIDefinition, outDir string) ([]string, error) {
	// Now iterate through the resources to gather their names
	names := make([]string, len(api.Resources))
	i := 0
	api.IterateResources(func(res *design.ResourceDefinition) error {
		if n, ok := res.Metadata["pseudo"]; ok {
			names[i] = n[0]
		} else {
			names[i] = res.Name
		}
		i++
		return nil
	})

	content := strings.Join(names, "\n")
	// Write the output file and return its name
	outputFile := filepath.Join(outDir, "names.txt")
	if err := ioutil.WriteFile(outputFile, []byte(content), 0755); err != nil {
		return nil, err
	}
	return []string{outputFile}, nil
}

Invoke the genresnames generator with:

goagen gen -d /path/to/your/design --pkg-path=/go/path/to/genresnames