Rendering Go struct tags other than json with oto code generation

Revision history
Tags: golang

Preface

If you have no idea what oto is, it is a RPC code generation tool which lets you generate server and client code based on Go interfaces. I recommend to take a look, not just to learn the tool itself, but to get some new perspectives on what simple can mean in programming. Mat Ryer (one of the authors) has two videos on the subject I quite liked (1, 2).

When working with oto and defining the interfaces for my data structures I felt a need to include validate tags for my struct fields so that I could run validate.Validate(request) in all my Go server HTTP handlers. However, this requires some extra work in the plush templates. If you don’t know the plush template language; I didn’t either. But the language has few surprises and good documentation, so I don’t mind.

The validate tags are used with the goplayground/validator package which let’s us write validation rules for fields in a struct.

oto parser

Looking at the upstream code for the oto parser, I could see that all tags for each field are actually parsed and are available through def.Objects[].Fields[].ParsedTags["tagName"]. And the value and options for the tag is in the .Value and .Options fields within the ...ParsedTags["tagName"].FieldTag.

Plush templates

Access to fields of structs residing in a map or an array is not straight forward with the version of plush oto is currently depending on. (As of writing, oto v0.11.1 depends on plush v3.8.3.)

plush landed a fix for this in v4.1.4, which makes it possible to access the nested fields directly with a["myaddress"].Street, however it’s in a version much newer than what oto is using.

<p><%= a["myaddress"].Street %></p>

Directly accessing the field, like in the above example, in plush version <4.1.4 will result in an error resembling:

line 58: no prefix parse function for DOT found

But a workaround is possible in oto (as noted by @pvelder in this issue) by first assigning the map or array item to a variable:

<p><% let addr = a["myaddress"] %><%= addr.Street %></p>

An example definition and rendering

This means that we can put validate struct tags in the oto definitions (and all other kinds of tags if we want). Note that the service objects are redacted from this plush template. See the examples in the oto repo for full templates.

# def/my.go
package definitions

type ProductsService interface {
	AddProduct(AddProductRequest) AddProductResponse
}

type Product struct {
	ID           int32
	OrgID        string
	Name         string
	Description  string
	Price        int32
	CategoryID   *int32
	SortPriority int
	CreatedAt    string
	ModifiedAt   string
}

type AddProductRequest struct {
	OrgID       string `validate:"required"`
	Name        string `validate:"required"`
	Description string `validate:"required"`
	Price       int32  `validate:"required,gte=0,lte=9999"`
	CategoryID  *int32
}

type AddProductResponse struct {
	Product Product
}

The plush templates typically start with the // DO NOT EDIT comment to help you rembember that the generated files shouldn’t be touched by hand. But the templates themselves should of course be editable!

# templates/my.go.plush
// Code generated by oto; DO NOT EDIT.

package <%= def.PackageName %>

<%= for (object) in def.Objects { %>
<%= format_comment_text(object.Comment) %>type <%= object.Name %> struct {
	<%= for (field) in object.Fields { let validateTag = field.ParsedTags["validate"] %><%= format_comment_text(field.Comment) %><%= field.Name %> <%= if (field.Type.Multiple == true) { %>[]<% } %><%= field.Type.TypeName %> `json:"<%= field.NameLowerCamel %><%= if (field.OmitEmpty) { %>,omitempty<% } %>"<%= if (field.ParsedTags["validate"]) { %> validate:"<%= validateTag.Value %><%= for (v) in validateTag.Options { %><%= if (v) { %>,<%= v %><% } %><% } %>"<% } %>`
<% } %>
}
<% } %>

Then let’s generate the template

$ oto -template ./templates/my.go.plush \
      -out ./my.gen.go \
      -pkg main \
      -ignore Ignorer \
      ./def/my.go \
      && gofmt -w ./my.gen.go
# my.gen.go
// Code generated by oto; DO NOT EDIT.

package main

type AddProductRequest struct {
	OrgID       string `json:"orgID" validate:"required"`
	Name        string `json:"name" validate:"required"`
	Description string `json:"description" validate:"required"`
	Price       int32  `json:"price" validate:"required,gte=0,lte=9999"`
	CategoryID  *int32 `json:"categoryID"`
}

type AddProductResponse struct {
	Product Product `json:"product"`
	// Error is string explaining what went wrong. Empty if everything was fine.
	Error string `json:"error,omitempty"`
}

type Product struct {
	ID           int32  `json:"id"`
	OrgID        string `json:"orgID"`
	Name         string `json:"name"`
	Description  string `json:"description"`
	Price        int32  `json:"price"`
	CategoryID   *int32 `json:"categoryID"`
	SortPriority int    `json:"sortPriority"`
	CreatedAt    string `json:"createdAt"`
	ModifiedAt   string `json:"modifiedAt"`
}

Now it’s easier to validate the request structs in the handlers while still having the validation logic at a single place, namely at the structs definitions.

type productsService struct {
    validator *validator.Validate
}

func (svc productsService) AddProduct(ctx context.Context, preq AddProductRequest) (*AddProductResponse, error) {
    // Validate the struct against our validation rules!
    if err := svc.validator.Struct(preq); err != nil {
        return nil, err
    }

    // do something to get the product here
    p := Product{}

    return &AddProductResponse{Product: p}, nil
}

There we have it! Not a beautiful plush template, but it sure works! :)

References

If you have any comments or feedback, please send me an e-mail. (stig at stigok dotcom).

Did you find any typos, incorrect information, or have something to add? Then please propose a change to this post.

Creative Commons License This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.