mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-27 12:05:40 +00:00
491483d599
This abstraction provides a nicer way to work with maps of slices without having to write out three long type params. This also allows it to provide an AsMap implementation which copies the map and the slices at least. Updates tailscale/corp#20910 Signed-off-by: Maisem Ali <maisem@tailscale.com>
398 lines
11 KiB
Go
398 lines
11 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Viewer is a tool to automate the creation of "view" wrapper types that
|
|
// provide read-only accessor methods to underlying fields.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/types"
|
|
"html/template"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"tailscale.com/util/codegen"
|
|
)
|
|
|
|
const viewTemplateStr = `{{define "common"}}
|
|
// View returns a readonly view of {{.StructName}}.
|
|
func (p *{{.StructName}}) View() {{.ViewName}} {
|
|
return {{.ViewName}}{ж: p}
|
|
}
|
|
|
|
// {{.ViewName}} provides a read-only view over {{.StructName}}.
|
|
//
|
|
// Its methods should only be called if ` + "`Valid()`" + ` returns true.
|
|
type {{.ViewName}} struct {
|
|
// ж is the underlying mutable value, named with a hard-to-type
|
|
// character that looks pointy like a pointer.
|
|
// It is named distinctively to make you think of how dangerous it is to escape
|
|
// to callers. You must not let callers be able to mutate it.
|
|
ж *{{.StructName}}
|
|
}
|
|
|
|
// Valid reports whether underlying value is non-nil.
|
|
func (v {{.ViewName}}) Valid() bool { return v.ж != nil }
|
|
|
|
// AsStruct returns a clone of the underlying value which aliases no memory with
|
|
// the original.
|
|
func (v {{.ViewName}}) AsStruct() *{{.StructName}}{
|
|
if v.ж == nil {
|
|
return nil
|
|
}
|
|
return v.ж.Clone()
|
|
}
|
|
|
|
func (v {{.ViewName}}) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
|
|
|
func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
|
|
if v.ж != nil {
|
|
return errors.New("already initialized")
|
|
}
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
var x {{.StructName}}
|
|
if err := json.Unmarshal(b, &x); err != nil {
|
|
return err
|
|
}
|
|
v.ж=&x
|
|
return nil
|
|
}
|
|
|
|
{{end}}
|
|
{{define "valueField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} { return v.ж.{{.FieldName}} }
|
|
{{end}}
|
|
{{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.ByteSlice[{{.FieldType}}] { return views.ByteSliceOf(v.ж.{{.FieldName}}) }
|
|
{{end}}
|
|
{{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
|
|
{{end}}
|
|
{{define "viewSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
|
|
{{end}}
|
|
{{define "viewField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}}View { return v.ж.{{.FieldName}}.View() }
|
|
{{end}}
|
|
{{define "valuePointerField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {
|
|
if v.ж.{{.FieldName}} == nil {
|
|
return nil
|
|
}
|
|
x := *v.ж.{{.FieldName}}
|
|
return &x
|
|
}
|
|
|
|
{{end}}
|
|
{{define "mapField"}}
|
|
func(v {{.ViewName}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})}
|
|
{{end}}
|
|
{{define "mapFnField"}}
|
|
func(v {{.ViewName}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} {
|
|
return {{.MapFn}}
|
|
})}
|
|
{{end}}
|
|
{{define "mapSliceField"}}
|
|
func(v {{.ViewName}}) {{.FieldName}}() views.MapSlice[{{.MapKeyType}},{{.MapValueType}}] { return views.MapSliceOf(v.ж.{{.FieldName}}) }
|
|
{{end}}
|
|
{{define "unsupportedField"}}func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")}
|
|
{{end}}
|
|
{{define "stringFunc"}}func(v {{.ViewName}}) String() string { return v.ж.String() }
|
|
{{end}}
|
|
{{define "equalFunc"}}func(v {{.ViewName}}) Equal(v2 {{.ViewName}}) bool { return v.ж.Equal(v2.ж) }
|
|
{{end}}
|
|
`
|
|
|
|
var viewTemplate *template.Template
|
|
|
|
func init() {
|
|
viewTemplate = template.Must(template.New("view").Parse(viewTemplateStr))
|
|
}
|
|
|
|
func requiresCloning(t types.Type) (shallow, deep bool, base types.Type) {
|
|
switch v := t.(type) {
|
|
case *types.Pointer:
|
|
_, deep, base = requiresCloning(v.Elem())
|
|
return true, deep, base
|
|
case *types.Slice:
|
|
_, deep, base = requiresCloning(v.Elem())
|
|
return true, deep, base
|
|
}
|
|
p := codegen.ContainsPointers(t)
|
|
return p, p, t
|
|
}
|
|
|
|
func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thisPkg *types.Package) {
|
|
t, ok := typ.Underlying().(*types.Struct)
|
|
if !ok || codegen.IsViewType(t) {
|
|
return
|
|
}
|
|
it.Import("encoding/json")
|
|
it.Import("errors")
|
|
|
|
args := struct {
|
|
StructName string
|
|
ViewName string
|
|
FieldName string
|
|
FieldType string
|
|
FieldViewName string
|
|
|
|
MapKeyType string
|
|
MapValueType string
|
|
MapValueView string
|
|
MapFn string
|
|
}{
|
|
StructName: typ.Obj().Name(),
|
|
ViewName: typ.Obj().Name() + "View",
|
|
}
|
|
|
|
writeTemplate := func(name string) {
|
|
if err := viewTemplate.ExecuteTemplate(buf, name, args); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
writeTemplate("common")
|
|
for i := range t.NumFields() {
|
|
f := t.Field(i)
|
|
fname := f.Name()
|
|
if !f.Exported() {
|
|
continue
|
|
}
|
|
args.FieldName = fname
|
|
fieldType := f.Type()
|
|
if codegen.IsInvalid(fieldType) {
|
|
continue
|
|
}
|
|
if !codegen.ContainsPointers(fieldType) || codegen.IsViewType(fieldType) || codegen.HasNoClone(t.Tag(i)) {
|
|
args.FieldType = it.QualifiedName(fieldType)
|
|
writeTemplate("valueField")
|
|
continue
|
|
}
|
|
switch underlying := fieldType.Underlying().(type) {
|
|
case *types.Slice:
|
|
slice := underlying
|
|
elem := slice.Elem()
|
|
switch elem.String() {
|
|
case "byte":
|
|
args.FieldType = it.QualifiedName(fieldType)
|
|
it.Import("tailscale.com/types/views")
|
|
writeTemplate("byteSliceField")
|
|
default:
|
|
args.FieldType = it.QualifiedName(elem)
|
|
it.Import("tailscale.com/types/views")
|
|
shallow, deep, base := requiresCloning(elem)
|
|
if deep {
|
|
if _, isPtr := elem.(*types.Pointer); isPtr {
|
|
args.FieldViewName = it.QualifiedName(base) + "View"
|
|
writeTemplate("viewSliceField")
|
|
} else {
|
|
writeTemplate("unsupportedField")
|
|
}
|
|
continue
|
|
} else if shallow {
|
|
if _, isBasic := base.(*types.Basic); isBasic {
|
|
writeTemplate("unsupportedField")
|
|
} else {
|
|
args.FieldViewName = it.QualifiedName(base) + "View"
|
|
writeTemplate("viewSliceField")
|
|
}
|
|
continue
|
|
}
|
|
writeTemplate("sliceField")
|
|
}
|
|
continue
|
|
case *types.Struct, *types.Named:
|
|
strucT := underlying
|
|
args.FieldType = it.QualifiedName(fieldType)
|
|
if codegen.ContainsPointers(strucT) {
|
|
writeTemplate("viewField")
|
|
continue
|
|
}
|
|
writeTemplate("valueField")
|
|
continue
|
|
case *types.Map:
|
|
m := underlying
|
|
args.FieldType = it.QualifiedName(fieldType)
|
|
shallow, deep, key := requiresCloning(m.Key())
|
|
if shallow || deep {
|
|
writeTemplate("unsupportedField")
|
|
continue
|
|
}
|
|
args.MapKeyType = it.QualifiedName(key)
|
|
mElem := m.Elem()
|
|
var template string
|
|
switch u := mElem.(type) {
|
|
case *types.Struct, *types.Named:
|
|
strucT := u
|
|
args.FieldType = it.QualifiedName(fieldType)
|
|
if codegen.ContainsPointers(strucT) {
|
|
args.MapFn = "t.View()"
|
|
template = "mapFnField"
|
|
args.MapValueType = it.QualifiedName(mElem)
|
|
args.MapValueView = args.MapValueType + "View"
|
|
} else {
|
|
template = "mapField"
|
|
args.MapValueType = it.QualifiedName(mElem)
|
|
}
|
|
case *types.Basic:
|
|
template = "mapField"
|
|
args.MapValueType = it.QualifiedName(mElem)
|
|
case *types.Slice:
|
|
slice := u
|
|
sElem := slice.Elem()
|
|
switch x := sElem.(type) {
|
|
case *types.Basic, *types.Named:
|
|
sElem := it.QualifiedName(sElem)
|
|
args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem)
|
|
args.MapValueType = sElem
|
|
template = "mapSliceField"
|
|
case *types.Pointer:
|
|
ptr := x
|
|
pElem := ptr.Elem()
|
|
switch pElem.(type) {
|
|
case *types.Struct, *types.Named:
|
|
ptrType := it.QualifiedName(ptr)
|
|
viewType := it.QualifiedName(pElem) + "View"
|
|
args.MapFn = fmt.Sprintf("views.SliceOfViews[%v,%v](t)", ptrType, viewType)
|
|
args.MapValueView = fmt.Sprintf("views.SliceView[%v,%v]", ptrType, viewType)
|
|
args.MapValueType = "[]" + ptrType
|
|
template = "mapFnField"
|
|
default:
|
|
template = "unsupportedField"
|
|
}
|
|
default:
|
|
template = "unsupportedField"
|
|
}
|
|
case *types.Pointer:
|
|
ptr := u
|
|
pElem := ptr.Elem()
|
|
switch pElem.(type) {
|
|
case *types.Struct, *types.Named:
|
|
args.MapValueType = it.QualifiedName(ptr)
|
|
args.MapValueView = it.QualifiedName(pElem) + "View"
|
|
args.MapFn = "t.View()"
|
|
template = "mapFnField"
|
|
default:
|
|
template = "unsupportedField"
|
|
}
|
|
default:
|
|
template = "unsupportedField"
|
|
}
|
|
writeTemplate(template)
|
|
continue
|
|
case *types.Pointer:
|
|
ptr := underlying
|
|
_, deep, base := requiresCloning(ptr)
|
|
if deep {
|
|
args.FieldType = it.QualifiedName(base)
|
|
writeTemplate("viewField")
|
|
} else {
|
|
args.FieldType = it.QualifiedName(ptr)
|
|
writeTemplate("valuePointerField")
|
|
}
|
|
continue
|
|
}
|
|
writeTemplate("unsupportedField")
|
|
}
|
|
for i := range typ.NumMethods() {
|
|
f := typ.Method(i)
|
|
if !f.Exported() {
|
|
continue
|
|
}
|
|
sig, ok := f.Type().(*types.Signature)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
switch f.Name() {
|
|
case "Clone", "View":
|
|
continue // "AsStruct"
|
|
case "String":
|
|
writeTemplate("stringFunc")
|
|
continue
|
|
case "Equal":
|
|
if sig.Results().Len() == 1 && sig.Results().At(0).Type().String() == "bool" {
|
|
writeTemplate("equalFunc")
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
fmt.Fprintf(buf, "\n")
|
|
buf.Write(codegen.AssertStructUnchanged(t, args.StructName, "View", it))
|
|
}
|
|
|
|
var (
|
|
flagTypes = flag.String("type", "", "comma-separated list of types; required")
|
|
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
|
|
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
|
|
|
|
flagCloneOnlyTypes = flag.String("clone-only-type", "", "comma-separated list of types (a subset of --type) that should only generate a go:generate clone line and not actual views")
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(0)
|
|
log.SetPrefix("viewer: ")
|
|
flag.Parse()
|
|
if len(*flagTypes) == 0 {
|
|
flag.Usage()
|
|
os.Exit(2)
|
|
}
|
|
typeNames := strings.Split(*flagTypes, ",")
|
|
|
|
var flagArgs []string
|
|
flagArgs = append(flagArgs, fmt.Sprintf("-clonefunc=%v", *flagCloneFunc))
|
|
if *flagTypes != "" {
|
|
flagArgs = append(flagArgs, "-type="+*flagTypes)
|
|
}
|
|
if *flagBuildTags != "" {
|
|
flagArgs = append(flagArgs, "-tags="+*flagBuildTags)
|
|
}
|
|
pkg, namedTypes, err := codegen.LoadTypes(*flagBuildTags, ".")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
it := codegen.NewImportTracker(pkg.Types)
|
|
|
|
cloneOnlyType := map[string]bool{}
|
|
for _, t := range strings.Split(*flagCloneOnlyTypes, ",") {
|
|
cloneOnlyType[t] = true
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
fmt.Fprintf(buf, "//go:generate go run tailscale.com/cmd/cloner %s\n\n", strings.Join(flagArgs, " "))
|
|
runCloner := false
|
|
for _, typeName := range typeNames {
|
|
if cloneOnlyType[typeName] {
|
|
continue
|
|
}
|
|
typ, ok := namedTypes[typeName]
|
|
if !ok {
|
|
log.Fatalf("could not find type %s", typeName)
|
|
}
|
|
var hasClone bool
|
|
for i, n := 0, typ.NumMethods(); i < n; i++ {
|
|
if typ.Method(i).Name() == "Clone" {
|
|
hasClone = true
|
|
break
|
|
}
|
|
}
|
|
if !hasClone {
|
|
runCloner = true
|
|
}
|
|
genView(buf, it, typ, pkg.Types)
|
|
}
|
|
out := pkg.Name + "_view.go"
|
|
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, it, buf); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if runCloner {
|
|
// When a new package is added or when existing generated files have
|
|
// been deleted, we might run into a case where tailscale.com/cmd/cloner
|
|
// has not run yet. We detect this by verifying that all the structs we
|
|
// interacted with have had Clone method already generated. If they
|
|
// haven't we ask the caller to rerun generation again so that those get
|
|
// generated.
|
|
log.Printf("%v requires regeneration. Please run go generate again", pkg.Name+"_clone.go")
|
|
}
|
|
}
|