Automating boilerplate/scaffolding code with custom code generation in Go, with jen
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.