After making some changes to an API realized I had to manually rewrite too many sections of code. That started me looking at different code generation techniques. I did a bit with Swagger/OpenAPI but it was overkill for what I needed. I found this binding package to help bind http.FormValues
to structs
but it requires writing redundant code for each struct to work. To simplify things I wrote a small CLI tool that accepts a file, parses it for Go structs
, and then generates the necessary binding function based on tags so that if you use //go:generate
it will work as seamlessly as json
or db
tags.
This is my first CLI with Go so I'm a bit iffy on my implementation main()
but given how small a piece that is compared to the rest I didn't spend any time refactoring int.
main.go:
package main
import (
"flag"
"go/ast"
"go/parser"
"go/token"
"os"
"reflect"
"strings"
"text/template"
)
// BindTemplate is the top-level struct passed to template.Execute()
type BindTemplate struct {
Package string
Structs []*ParsedStruct
}
// ParsedStruct represents a single tagged struct to generate bindings for
type ParsedStruct struct {
Name string
Token string
Fields map[string]*ParsedTag
}
// ParsedTag includes the tag value and a bool Required for each field
type ParsedTag struct {
Value string
Required bool
}
// Example code generated from template with required and optional fields:
//
// func (p *Person) FieldMap(r *http.Request) binding.FieldMap {
// return binding.FieldMap {
// &p.Age: "age",
// &p.Name: binding.Field{
// Form: "name",
// Required: true,
// },
// }
// }
const bindTemplateBase = (
`package {{.Package}}
import (
"net/http"
"github.com/mholt/binding"
)
{{ range .Structs }}{{ $token := .Token }}{{ $name := .Name }}
func ({{ $token }} *{{ $name }}) FieldMap(r *http.Request) binding.FieldMap {
return binding.FieldMap {
{{- range $field, $tag := .Fields }}
&{{ $token }}.{{ $field }}:
{{- if not $tag.Required }} "{{ $tag.Value }}",
{{- else }} binding.Field{
Form: "{{ $tag.Value }}",
Required: true,
},{{ end }}{{ end }}
}
}
{{ end -}}
`)
const requiredFieldFlag = ",required"
func generateStructBindings(file, pkg, targetTag string) (*BindTemplate, bool) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, file, nil, 0)
if err != nil {
panic(err)
}
bindTemplate := &BindTemplate{
Package: pkg,
Structs: make([]*ParsedStruct, 0),
}
// Parse file Abstract Syntax Tree of file to find any Structs
ast.Inspect(f, func(n ast.Node) bool {
if typeSpec, ok := n.(*ast.TypeSpec); ok {
if structType, ok := typeSpec.Type.(*ast.StructType); ok {
// if Struct has any tagged fields add it to bindTemplate.Structs
if parsedFields := parseFieldTags(structType, targetTag); len(parsedFields) > 0 {
bindTemplate.Structs = append(bindTemplate.Structs,
&ParsedStruct{
Name: typeSpec.Name.Name,
Token: strings.ToLower(string(typeSpec.Name.Name[0])),
Fields: parsedFields,
})
}
}
}
return true
})
// Return ok == true as long as bindTemplate contains any structs
return bindTemplate, len(bindTemplate.Structs) > 0
}
func parseFieldTags(structType *ast.StructType, target string) map[string]*ParsedTag {
parsedTags := make(map[string]*ParsedTag)
// For each field cast non-nil tags to reflect.StructTag and add target value to map
for _, field := range structType.Fields.List {
if tag := field.Tag; tag != nil {
if value, ok := (reflect.StructTag)(strings.Trim(tag.Value, "`")).Lookup(target); ok {
required := false
// mholt/binding includes option for required fields that needs to be evaluated
if strings.HasSuffix(value, requiredFieldFlag) {
value = strings.TrimSuffix(value, requiredFieldFlag)
required = true
}
parsedTags[field.Names[0].Name] = &ParsedTag{value, required}
}
}
}
return parsedTags
}
func main() {
pkg := flag.String("package", "main", "package to export bindings to; default = main")
fileIn := flag.String("f", "models.go", "file to generate bindings for; default = models.go")
fileOut := flag.String("out", "static.go", "file to save bindings in; default = bindings.go")
targetTag := flag.String("tag", "request", "tag key to evalute; default = request")
flag.Parse()
bindTemplateStruct, ok := generateStructBindings(*fileIn, *pkg, *targetTag)
if !ok {
panic("Couldn't bind template")
}
f, err := os.OpenFile(*fileOut, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
panic(err)
}
defer f.Close()
if tpl, err := template.New("BindTemplate").Parse(bindTemplateBase); err == nil {
tpl.ExecuteTemplate(f, "BindTemplate", bindTemplateStruct)
}
}
models.go:
package main
type PostRequest struct {
Summary bool `request:"summary,required"`
IDs []int `request:"ids"`
Num int `request:"num"`
Raw bool `request:"raw"`
}
type Person struct {
Name string `request:"name,required"`
Age int `request:"age"`
}
and running them together with the default cli settings they generate bindings.go:
package main
import (
"net/http"
"github.com/mholt/binding"
)
func (p *PostRequest) FieldMap(r *http.Request) binding.FieldMap {
return binding.FieldMap {
&p.IDs: "ids",
&p.Num: "num",
&p.Raw: "raw",
&p.Summary: binding.Field{
Form: "summary",
Required: true,
},
}
}
func (p *Person) FieldMap(r *http.Request) binding.FieldMap {
return binding.FieldMap {
&p.Age: "age",
&p.Name: binding.Field{
Form: "name",
Required: true,
},
}
}