Automating boilerplate/scaffolding code with custom code generation in Go, with jen

Featured image for sharing metadata for article

As written about in Automating boilerplate/scaffolding code with custom code generation in Go, being able to take advantage of generating Go code can be super handy.

However, you may get to the point where you're working with the codegen that you find working with text-based templates are getting a little unwieldy and you want to find an alternative.

I recently discovered github.com/dave/jennifer which provides a handy way to write Go code to generate Go code.

Similar to the other post, we'll aim to generate the following code:

type ErrBadRequest struct {
	message string
	cause   error
}

func (e *ErrBadRequest) Error() string {
	return e.message
}

func (e *ErrBadRequest) Unwrap() error {
	return e.cause
}

func NewErrBadRequest() error {
	return &ErrBadRequest{
		message: "There was a problem processing the request",
	}
}

func NewErrBadRequestWithMessage(message string) error {
	return &ErrBadRequest{
		message: message,
	}
}

func NewErrBadRequestWithCause(cause error) error {
	return &ErrBadRequest{
		message: "The was a problem processing the request",
		cause:   cause,
	}
}

func NewErrBadRequestWithMessageAndCause(message string, cause error) error {
	return &ErrBadRequest{
		message: message,
		cause:   cause,
	}
}

We'll do this by creating the following Go program:

package main

import (
	_ "embed"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"

	"github.com/dave/jennifer/jen"
	"gopkg.in/yaml.v3"
)

func must(err error) {
	if err != nil {
		log.Printf("There was an unexpected error: %s", err)
		os.Exit(1)
	}
}

type config struct {
	Package string `yaml:"package"`
	Output  string `yaml:"output"`
	Errors  []struct {
		Name    string `yaml:"name"`
		Message string `yaml:"message"`
	} `yaml:"errors"`
}

func buildFile(c config) *jen.File {
	f := jen.NewFile(c.Package)
	f.HeaderComment("Code generated by error-codegen DO NOT EDIT")

	errors := buildErrors(c)
	f.Add(errors...)

	return f
}

func buildErrors(c config) (statements []jen.Code) {
	for _, e := range c.Errors {
		errName := fmt.Sprintf("Err%s", e.Name)

		errStruct := jen.Type().Id(errName).Struct(
			jen.Id("message").String(),
			jen.Id("cause").Error(),
		)
		statements = append(statements, errStruct.Line().Line())

		errorMethod := jen.Func().Params(
			jen.Id("e").Op("*").Id(errName),
		).
			Id("Error").Params().
			String().
			Block(
				jen.Return(jen.Id("e").Dot("message")),
			)
		statements = append(statements, errorMethod.Line().Line())

		unwrapMethod := jen.Func().Params(
			jen.Id("e").Op("*").Id(errName),
		).
			Id("Unwrap").Params().
			Error().
			Block(
				jen.Return(jen.Id("e").Dot("cause")),
			)
		statements = append(statements, unwrapMethod.Line().Line())

		newMethod := jen.Func().Id(
			fmt.Sprintf("New%s", errName),
		).Params().Error().Block(
			jen.Return(
				jen.Op("&").Id(errName).Block(
					jen.Id("message").Op(":").Lit(e.Message).Op(","),
				),
			),
		)
		statements = append(statements, newMethod.Line().Line())

		newWithMessageMethod := jen.Func().Id(
			fmt.Sprintf("New%sWithMessage", errName),
		).Params(
			jen.Id("message").String(),
		).Error().Block(
			jen.Return(
				jen.Op("&").Id(errName).Block(
					jen.Id("message").Op(":").Id("message").Op(","),
				),
			),
		)
		statements = append(statements, newWithMessageMethod.Line().Line())

		newWithCauseMethod := jen.Func().Id(
			fmt.Sprintf("New%sWithCause", errName),
		).Params(
			jen.Id("message").String(),
			jen.Id("cause").Error(),
		).Error().Block(
			jen.Return(
				jen.Op("&").Id(errName).Block(
					jen.Id("message").Op(":").Id("message").Op(","),
					jen.Id("cause").Op(":").Id("cause").Op(","),
				),
			),
		)
		statements = append(statements, newWithCauseMethod.Line().Line())

		newWithMessageAndCauseMethod := jen.Func().Id(
			fmt.Sprintf("New%sWithMessageAndCause", errName),
		).Params(
			jen.Id("message").String(),
			jen.Id("cause").Error(),
		).Error().Block(
			jen.Return(
				jen.Op("&").Id(errName).Block(
					jen.Id("message").Op(":").Id("message").Op(","),
					jen.Id("cause").Op(":").Id("cause").Op(","),
				),
			),
		)
		statements = append(statements, newWithMessageAndCauseMethod.Line().Line())

	}

	return
}

func main() {
	configPathPtr := flag.String("config", "", "configuration file")
	flag.Parse()

	if configPathPtr == nil {
		log.Printf("Expected a configuration file, but received `nil`")
		os.Exit(1)
	}

	configPath := *configPathPtr
	b, err := ioutil.ReadFile(configPath)
	must(err)

	var config config

	err = yaml.Unmarshal(b, &config)
	must(err)

	f := buildFile(config)

	err = f.Save(config.Output)
	must(err)
}

Notice how in a small example like this, that this can be rather verbose and a little hard to read. However, for much larger projects, it can really save a lot of difficulty of working with text-based templates, as well as making it much easier to read in terms of conditional logic/loops.

Example code can be found on GitLab.com.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #go.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.