0
\$\begingroup\$

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,
        },
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.