Rendering Go struct tags other than json with oto code generation
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 interface
s. 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! :)
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.