commit
627ec9333a
@ -0,0 +1,17 @@ |
||||
Copyright 2017 Felix Lange <fjl@twurst.com> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this |
||||
software and associated documentation files (the "Software"), to deal in the Software |
||||
without restriction, including without limitation the rights to use, copy, modify, merge, |
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
||||
to whom the Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or |
||||
substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,44 @@ |
||||
Command gencodec generates marshaling methods for struct types. |
||||
|
||||
When gencodec is invoked on a directory and type name, it creates a Go source file |
||||
containing JSON and YAML marshaling methods for the type. The generated methods add |
||||
features which the standard json package cannot offer. |
||||
|
||||
gencodec -dir . -type MyType -out mytype_json.go |
||||
|
||||
### Struct Tags |
||||
|
||||
All fields are required unless the "optional" struct tag is present. The generated |
||||
unmarshaling method return an error if a required field is missing. Other struct tags are |
||||
carried over as is. The standard "json" and "yaml" tags can be used to rename a field when |
||||
marshaling to/from JSON. |
||||
|
||||
Example: |
||||
|
||||
type foo { |
||||
Required string |
||||
Optional string `optional:""` |
||||
Renamed string `json:"otherName"` |
||||
} |
||||
|
||||
### Field Type Overrides |
||||
|
||||
An invocation of gencodec can specify an additional 'field override' struct from which |
||||
marshaling type replacements are taken. If the override struct contains a field whose name |
||||
matches the original type, the generated marshaling methods will use the overridden type |
||||
and convert to and from the original field type. |
||||
|
||||
In this example, the specialString type implements json.Unmarshaler to enforce additional |
||||
parsing rules. When json.Unmarshal is used with type foo, the specialString unmarshaler |
||||
will be used to parse the value of SpecialField. |
||||
|
||||
//go:generate gencodec -dir . -type foo -field-override fooMarshaling -out foo_json.go |
||||
|
||||
type foo struct { |
||||
Field string |
||||
SpecialField string |
||||
} |
||||
|
||||
type fooMarshaling struct { |
||||
SpecialField specialString // overrides type of SpecialField when marshaling/unmarshaling |
||||
} |
@ -0,0 +1,535 @@ |
||||
// Copyright 2017 Felix Lange <fjl@twurst.com>.
|
||||
// Use of this source code is governed by the MIT license,
|
||||
// which can be found in the LICENSE file.
|
||||
|
||||
/* |
||||
Command gencodec generates marshaling methods for struct types. |
||||
|
||||
When gencodec is invoked on a directory and type name, it creates a Go source file |
||||
containing JSON and YAML marshaling methods for the type. The generated methods add |
||||
features which the standard json package cannot offer. |
||||
|
||||
gencodec -dir . -type MyType -out mytype_json.go |
||||
|
||||
Struct Tags |
||||
|
||||
All fields are required unless the "optional" struct tag is present. The generated |
||||
unmarshaling method return an error if a required field is missing. Other struct tags are |
||||
carried over as is. The standard "json" and "yaml" tags can be used to rename a field when |
||||
marshaling to/from JSON. |
||||
|
||||
Example: |
||||
|
||||
type foo { |
||||
Required string |
||||
Optional string `optional:""` |
||||
Renamed string `json:"otherName"` |
||||
} |
||||
|
||||
Field Type Overrides |
||||
|
||||
An invocation of gencodec can specify an additional 'field override' struct from which |
||||
marshaling type replacements are taken. If the override struct contains a field whose name |
||||
matches the original type, the generated marshaling methods will use the overridden type |
||||
and convert to and from the original field type. |
||||
|
||||
In this example, the specialString type implements json.Unmarshaler to enforce additional |
||||
parsing rules. When json.Unmarshal is used with type foo, the specialString unmarshaler |
||||
will be used to parse the value of SpecialField. |
||||
|
||||
//go:generate gencodec -dir . -type foo -field-override fooMarshaling -out foo_json.go
|
||||
|
||||
type foo struct { |
||||
Field string |
||||
SpecialField string |
||||
} |
||||
|
||||
type fooMarshaling struct { |
||||
SpecialField specialString // overrides type of SpecialField when marshaling/unmarshaling
|
||||
} |
||||
|
||||
*/ |
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"flag" |
||||
"fmt" |
||||
"go/ast" |
||||
"go/importer" |
||||
"go/parser" |
||||
"go/token" |
||||
"go/types" |
||||
"io/ioutil" |
||||
"os" |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
"text/template" |
||||
|
||||
"golang.org/x/tools/imports" |
||||
) |
||||
|
||||
func main() { |
||||
var ( |
||||
pkgdir = flag.String("dir", ".", "input package directory") |
||||
output = flag.String("out", "-", "output file") |
||||
typename = flag.String("type", "", "type to generate") |
||||
overrides = flag.String("field-override", "", "type to take field type replacements from") |
||||
) |
||||
flag.Parse() |
||||
|
||||
fs := token.NewFileSet() |
||||
pkg := loadPackage(fs, *pkgdir) |
||||
code := makeMarshalingCode(fs, pkg, *typename, *overrides) |
||||
if *output == "-" { |
||||
os.Stdout.Write(code) |
||||
} else if err := ioutil.WriteFile(*output, code, 0644); err != nil { |
||||
fatal(err) |
||||
} |
||||
} |
||||
|
||||
func loadPackage(fs *token.FileSet, dir string) *types.Package { |
||||
// Load the package.
|
||||
pkgs, err := parser.ParseDir(fs, dir, nil, parser.AllErrors) |
||||
if err != nil { |
||||
fatal(err) |
||||
} |
||||
if len(pkgs) == 0 || len(pkgs) > 1 { |
||||
fatal(err) |
||||
} |
||||
var files []*ast.File |
||||
var name string |
||||
for _, pkg := range pkgs { |
||||
for _, file := range pkg.Files { |
||||
files = append(files, file) |
||||
} |
||||
name = pkg.Name |
||||
break |
||||
} |
||||
// Type-check the package.
|
||||
cfg := types.Config{ |
||||
IgnoreFuncBodies: true, |
||||
FakeImportC: true, |
||||
Importer: importer.Default(), |
||||
} |
||||
tpkg, err := cfg.Check(name, fs, files, nil) |
||||
if err != nil { |
||||
fatal(err) |
||||
} |
||||
return tpkg |
||||
} |
||||
|
||||
func makeMarshalingCode(fs *token.FileSet, pkg *types.Package, typename, otypename string) (packageBody []byte) { |
||||
typ, err := lookupStructType(pkg.Scope(), typename) |
||||
if err != nil { |
||||
fatal(fmt.Sprintf("can't find %s: %v", typename, err)) |
||||
} |
||||
mtyp := newMarshalerType(fs, pkg, typ) |
||||
if otypename != "" { |
||||
otyp, err := lookupStructType(pkg.Scope(), otypename) |
||||
if err != nil { |
||||
fatal(fmt.Sprintf("can't find field replacement type %s: %v", otypename, err)) |
||||
} |
||||
mtyp.loadOverrides(otypename, otyp.Underlying().(*types.Struct)) |
||||
} |
||||
|
||||
w := new(bytes.Buffer) |
||||
fmt.Fprintln(w, "// generated by gencodec, do not edit.\n") |
||||
fmt.Fprintln(w, "package ", pkg.Name()) |
||||
fmt.Fprintln(w, render(mtyp.computeImports(), ` |
||||
import ( |
||||
{{- range $name, $path := . }} |
||||
{{ $name }} "{{ $path }}" |
||||
{{- end }} |
||||
)`)) |
||||
fmt.Fprintln(w) |
||||
fmt.Fprintln(w, mtyp.JSONMarshalMethod()) |
||||
fmt.Fprintln(w) |
||||
fmt.Fprintln(w, mtyp.JSONUnmarshalMethod()) |
||||
fmt.Fprintln(w) |
||||
fmt.Fprintln(w, mtyp.YAMLMarshalMethod()) |
||||
fmt.Fprintln(w) |
||||
fmt.Fprintln(w, mtyp.YAMLUnmarshalMethod()) |
||||
|
||||
// Use goimports to format the source because it separates imports.
|
||||
opt := &imports.Options{Comments: true, FormatOnly: true, TabIndent: true, TabWidth: 8} |
||||
body, err := imports.Process("", w.Bytes(), opt) |
||||
if err != nil { |
||||
fatal("can't gofmt generated code:", err, "\n"+w.String()) |
||||
} |
||||
return body |
||||
} |
||||
|
||||
// marshalerType represents the intermediate struct type used during marshaling.
|
||||
// This is the input data to all the Go code templates.
|
||||
type marshalerType struct { |
||||
OrigName string |
||||
Name string |
||||
Fields []*marshalerField |
||||
fs *token.FileSet |
||||
orig *types.Named |
||||
} |
||||
|
||||
// marshalerField represents a field of the intermediate marshaling type.
|
||||
type marshalerField struct { |
||||
parent *marshalerType |
||||
field *types.Var |
||||
typ types.Type |
||||
tag string |
||||
} |
||||
|
||||
func newMarshalerType(fs *token.FileSet, pkg *types.Package, typ *types.Named) *marshalerType { |
||||
name := typ.Obj().Name() + "JSON" |
||||
styp := typ.Underlying().(*types.Struct) |
||||
mtyp := &marshalerType{OrigName: typ.Obj().Name(), Name: name, fs: fs, orig: typ} |
||||
for i := 0; i < styp.NumFields(); i++ { |
||||
f := styp.Field(i) |
||||
if !f.Exported() { |
||||
continue |
||||
} |
||||
mf := &marshalerField{parent: mtyp, field: f, typ: ensurePointer(f.Type()), tag: styp.Tag(i)} |
||||
if f.Anonymous() { |
||||
fmt.Fprintln(os.Stderr, mf.errorf("Warning: ignoring embedded field")) |
||||
continue |
||||
} |
||||
mtyp.Fields = append(mtyp.Fields, mf) |
||||
} |
||||
return mtyp |
||||
} |
||||
|
||||
// loadOverrides sets field types of the intermediate marshaling type from
|
||||
// matching fields of otyp.
|
||||
func (mtyp *marshalerType) loadOverrides(otypename string, otyp *types.Struct) { |
||||
for i := 0; i < otyp.NumFields(); i++ { |
||||
of := otyp.Field(i) |
||||
if of.Anonymous() || !of.Exported() { |
||||
fatalf("%v: field override type cannot have embedded or unexported fields", mtyp.fs.Position(of.Pos())) |
||||
} |
||||
f := mtyp.fieldByName(of.Name()) |
||||
if f == nil { |
||||
fatalf("%v: no matching field for %s in original type %s", mtyp.fs.Position(of.Pos()), of.Name(), mtyp.OrigName) |
||||
} |
||||
if !types.ConvertibleTo(of.Type(), f.field.Type()) { |
||||
fatalf("%v: field override type %s is not convertible to %s", mtyp.fs.Position(of.Pos()), mtyp.typeString(of.Type()), mtyp.typeString(f.field.Type())) |
||||
} |
||||
f.typ = ensurePointer(of.Type()) |
||||
} |
||||
} |
||||
|
||||
func (mtyp *marshalerType) fieldByName(name string) *marshalerField { |
||||
for _, f := range mtyp.Fields { |
||||
if f.field.Name() == name { |
||||
return f |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// computeImports returns the import paths of all referenced types.
|
||||
// computeImports must be called before generating any code because it
|
||||
// renames packages to avoid name clashes.
|
||||
func (mtyp *marshalerType) computeImports() map[string]string { |
||||
seen := make(map[string]string) |
||||
counter := 0 |
||||
add := func(name string, path string, pkg *types.Package) { |
||||
if seen[name] != path { |
||||
if pkg != nil { |
||||
name = "_" + name |
||||
pkg.SetName(name) |
||||
} |
||||
if seen[name] != "" { |
||||
// Name clash, add counter.
|
||||
name += "_" + strconv.Itoa(counter) |
||||
counter++ |
||||
pkg.SetName(name) |
||||
} |
||||
seen[name] = path |
||||
} |
||||
} |
||||
addNamed := func(typ *types.Named) { |
||||
if pkg := typ.Obj().Pkg(); pkg != mtyp.orig.Obj().Pkg() { |
||||
add(pkg.Name(), pkg.Path(), pkg) |
||||
} |
||||
} |
||||
|
||||
// Add packages which always referenced by the generated code.
|
||||
add("json", "encoding/json", nil) |
||||
add("errors", "errors", nil) |
||||
for _, f := range mtyp.Fields { |
||||
// Add field types of the intermediate struct.
|
||||
walkNamedTypes(f.typ, addNamed) |
||||
// Add field types of the original struct. Note that this won't generate unused
|
||||
// imports because all fields are either referenced by a conversion or by fields
|
||||
// of the intermediate struct (if no conversion is needed).
|
||||
walkNamedTypes(f.field.Type(), addNamed) |
||||
} |
||||
return seen |
||||
} |
||||
|
||||
// JSONMarshalMethod generates MarshalJSON.
|
||||
func (mtyp *marshalerType) JSONMarshalMethod() string { |
||||
return render(mtyp, ` |
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (x *{{.OrigName}}) MarshalJSON() ([]byte, error) { |
||||
{{.TypeDecl}} |
||||
|
||||
return json.Marshal(&{{.Name}}{ |
||||
{{- range .Fields}} |
||||
{{.Name}}: {{.Convert "x"}}, |
||||
{{- end}} |
||||
}) |
||||
}`) |
||||
} |
||||
|
||||
// YAMLMarsalMethod generates MarshalYAML.
|
||||
func (mtyp *marshalerType) YAMLMarshalMethod() string { |
||||
return render(mtyp, ` |
||||
// MarshalYAML implements yaml.Marshaler
|
||||
func (x *{{.OrigName}}) MarshalYAML() (interface{}, error) { |
||||
{{.TypeDecl}} |
||||
|
||||
return &{{.Name}}{ |
||||
{{- range .Fields}} |
||||
{{.Name}}: {{.Convert "x"}}, |
||||
{{- end}} |
||||
}, nil |
||||
}`) |
||||
} |
||||
|
||||
// JSONUnmarshalMethod generates UnmarshalJSON.
|
||||
func (mtyp *marshalerType) JSONUnmarshalMethod() string { |
||||
return render(mtyp, ` |
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (x *{{.OrigName}}) UnmarshalJSON(input []byte) error { |
||||
{{.TypeDecl}} |
||||
|
||||
var dec {{.Name}} |
||||
if err := json.Unmarshal(input, &dec); err != nil { |
||||
return err |
||||
} |
||||
var v {{.OrigName}} |
||||
{{.UnmarshalConversions "json"}} |
||||
*x = v |
||||
return nil |
||||
}`) |
||||
} |
||||
|
||||
// YAMLUnmarshalMethod generates UnmarshalYAML.
|
||||
func (mtyp *marshalerType) YAMLUnmarshalMethod() string { |
||||
return render(mtyp, ` |
||||
// UnmarshalYAML implements yaml.Unmarshaler.
|
||||
func (x *{{.OrigName}}) UnmarshalYAML(fn func (interface{}) error) error { |
||||
{{.TypeDecl}} |
||||
|
||||
var dec {{.Name}} |
||||
if err := fn(&dec); err != nil { |
||||
return err |
||||
} |
||||
var v {{.OrigName}} |
||||
{{.UnmarshalConversions "yaml"}} |
||||
*x = v |
||||
return nil |
||||
}`) |
||||
} |
||||
|
||||
// TypeDecl genereates the declaration of the intermediate marshaling type.
|
||||
func (mtyp *marshalerType) TypeDecl() string { |
||||
return render(mtyp, ` |
||||
type {{.Name}} struct{ |
||||
{{- range .Fields}} |
||||
{{.Name}} {{.Type}} {{.StructTag}} |
||||
{{- end}} |
||||
}`) |
||||
} |
||||
|
||||
// UnmarshalConversion genereates field conversions and presence checks.
|
||||
func (mtyp *marshalerType) UnmarshalConversions(formatTag string) (s string) { |
||||
type fieldContext struct{ Typ, Name, EncName, Conv string } |
||||
|
||||
for _, mf := range mtyp.Fields { |
||||
ctx := fieldContext{ |
||||
Typ: strings.ToUpper(formatTag) + " " + mtyp.OrigName, |
||||
Name: mf.Name(), |
||||
EncName: mf.encodedName(formatTag), |
||||
Conv: mf.ConvertBack("dec"), |
||||
} |
||||
if mf.isOptional(formatTag) { |
||||
s += render(ctx, ` |
||||
if dec.{{.Name}} != nil { |
||||
v.{{.Name}} = {{.Conv}} |
||||
}`) |
||||
} else { |
||||
s += render(ctx, ` |
||||
if dec.{{.Name}} == nil { |
||||
return errors.New("missing required field '{{.EncName}}' in {{.Typ}}") |
||||
} |
||||
v.{{.Name}} = {{.Conv}}`) |
||||
} |
||||
s += "\n" |
||||
} |
||||
return s |
||||
} |
||||
|
||||
func (mf *marshalerField) Name() string { |
||||
return mf.field.Name() |
||||
} |
||||
|
||||
func (mf *marshalerField) Type() string { |
||||
return mf.parent.typeString(mf.typ) |
||||
} |
||||
|
||||
func (mf *marshalerField) OrigType() string { |
||||
return mf.parent.typeString(mf.typ) |
||||
} |
||||
|
||||
func (mf *marshalerField) StructTag() string { |
||||
if mf.tag == "" { |
||||
return "" |
||||
} |
||||
return "`" + mf.tag + "`" |
||||
} |
||||
|
||||
func (mf *marshalerField) Convert(variable string) string { |
||||
expr := fmt.Sprintf("%s.%s", variable, mf.field.Name()) |
||||
return mf.parent.conversionExpr(expr, mf.field.Type(), mf.typ) |
||||
} |
||||
|
||||
func (mf *marshalerField) ConvertBack(variable string) string { |
||||
expr := fmt.Sprintf("%s.%s", variable, mf.field.Name()) |
||||
return mf.parent.conversionExpr(expr, mf.typ, mf.field.Type()) |
||||
} |
||||
|
||||
func (mtyp *marshalerType) conversionExpr(valueExpr string, from, to types.Type) string { |
||||
if isPointer(from) && !isPointer(to) { |
||||
valueExpr = "*" + valueExpr |
||||
from = from.(*types.Pointer).Elem() |
||||
} else if !isPointer(from) && isPointer(to) { |
||||
valueExpr = "&" + valueExpr |
||||
from = types.NewPointer(from) |
||||
} |
||||
if types.AssignableTo(from, to) { |
||||
return valueExpr |
||||
} |
||||
return fmt.Sprintf("(%s)(%s)", mtyp.typeString(to), valueExpr) |
||||
} |
||||
|
||||
func (mf *marshalerField) errorf(format string, args ...interface{}) error { |
||||
pos := mf.parent.fs.Position(mf.field.Pos()).String() |
||||
return errors.New(pos + ": (" + mf.parent.OrigName + "." + mf.Name() + ") " + fmt.Sprintf(format, args...)) |
||||
} |
||||
|
||||
// isOptional returns whether the field is optional when decoding the given format.
|
||||
func (mf *marshalerField) isOptional(format string) bool { |
||||
rtag := reflect.StructTag(mf.tag) |
||||
if rtag.Get("optional") == "true" || rtag.Get("optional") == "yes" { |
||||
return true |
||||
} |
||||
// Fields with json:"-" must be treated as optional.
|
||||
return strings.HasPrefix(rtag.Get(format), "-") |
||||
} |
||||
|
||||
// encodedName returns the alternative field name assigned by the format's struct tag.
|
||||
func (mf *marshalerField) encodedName(format string) string { |
||||
val := reflect.StructTag(mf.tag).Get(format) |
||||
if comma := strings.Index(val, ","); comma != -1 { |
||||
val = val[:comma] |
||||
} |
||||
if val == "" || val == "-" { |
||||
return uncapitalize(mf.Name()) |
||||
} |
||||
return val |
||||
} |
||||
|
||||
func (mtyp *marshalerType) typeString(typ types.Type) string { |
||||
return types.TypeString(typ, func(pkg *types.Package) string { |
||||
if pkg == mtyp.orig.Obj().Pkg() { |
||||
return "" |
||||
} |
||||
return pkg.Name() |
||||
}) |
||||
} |
||||
|
||||
// walkNamedTypes runs the callback for all named types contained in the given type.
|
||||
func walkNamedTypes(typ types.Type, callback func(*types.Named)) { |
||||
switch typ := typ.(type) { |
||||
case *types.Basic: |
||||
case *types.Chan: |
||||
walkNamedTypes(typ.Elem(), callback) |
||||
case *types.Map: |
||||
walkNamedTypes(typ.Key(), callback) |
||||
walkNamedTypes(typ.Elem(), callback) |
||||
case *types.Named: |
||||
callback(typ) |
||||
case *types.Pointer: |
||||
walkNamedTypes(typ.Elem(), callback) |
||||
case *types.Slice: |
||||
walkNamedTypes(typ.Elem(), callback) |
||||
case *types.Struct: |
||||
for i := 0; i < typ.NumFields(); i++ { |
||||
walkNamedTypes(typ.Field(i).Type(), callback) |
||||
} |
||||
default: |
||||
panic(fmt.Errorf("can't walk %T", typ)) |
||||
} |
||||
} |
||||
|
||||
func lookupStructType(scope *types.Scope, name string) (*types.Named, error) { |
||||
typ, err := lookupType(scope, name) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
_, ok := typ.Underlying().(*types.Struct) |
||||
if !ok { |
||||
return nil, errors.New("not a struct type") |
||||
} |
||||
return typ, nil |
||||
} |
||||
|
||||
func lookupType(scope *types.Scope, name string) (*types.Named, error) { |
||||
obj := scope.Lookup(name) |
||||
if obj == nil { |
||||
return nil, errors.New("no such identifier") |
||||
} |
||||
typ, ok := obj.(*types.TypeName) |
||||
if !ok { |
||||
return nil, errors.New("not a type") |
||||
} |
||||
return typ.Type().(*types.Named), nil |
||||
} |
||||
|
||||
func isPointer(typ types.Type) bool { |
||||
_, ok := typ.(*types.Pointer) |
||||
return ok |
||||
} |
||||
|
||||
func ensurePointer(typ types.Type) types.Type { |
||||
if isPointer(typ) { |
||||
return typ |
||||
} |
||||
return types.NewPointer(typ) |
||||
} |
||||
|
||||
func uncapitalize(s string) string { |
||||
return strings.ToLower(s[:1]) + s[1:] |
||||
} |
||||
|
||||
func render(data interface{}, text string) string { |
||||
t := template.Must(template.New("").Parse(strings.TrimSpace(text))) |
||||
out := new(bytes.Buffer) |
||||
if err := t.Execute(out, data); err != nil { |
||||
panic(err) |
||||
} |
||||
return out.String() |
||||
} |
||||
|
||||
func fatal(args ...interface{}) { |
||||
fmt.Fprintln(os.Stderr, args...) |
||||
os.Exit(1) |
||||
} |
||||
|
||||
func fatalf(format string, args ...interface{}) { |
||||
fmt.Fprintf(os.Stderr, format+"\n", args...) |
||||
os.Exit(1) |
||||
} |
Loading…
Reference in new issue