From c4e9739251387841c554d361747c266175014c0d Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Sun, 1 May 2022 16:15:20 -0700 Subject: [PATCH] cmd/viewer: add codegen tool for Views Signed-off-by: Maisem Ali --- cmd/cloner/cloner.go | 9 +- cmd/viewer/tests/tests.go | 48 +++++ cmd/viewer/tests/tests_clone.go | 120 ++++++++++++ cmd/viewer/tests/tests_view.go | 268 +++++++++++++++++++++++++++ cmd/viewer/viewer.go | 316 ++++++++++++++++++++++++++++++++ types/views/views.go | 83 ++++++++- 6 files changed, 832 insertions(+), 12 deletions(-) create mode 100644 cmd/viewer/tests/tests.go create mode 100644 cmd/viewer/tests/tests_clone.go create mode 100644 cmd/viewer/tests/tests_view.go create mode 100644 cmd/viewer/viewer.go diff --git a/cmd/cloner/cloner.go b/cmd/cloner/cloner.go index 7a1da24e0..3e8d76c21 100644 --- a/cmd/cloner/cloner.go +++ b/cmd/cloner/cloner.go @@ -125,8 +125,13 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) { n := it.QualifiedName(ft.Elem()) writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname) writef("for i := range dst.%s {", fname) - if _, isPtr := ft.Elem().(*types.Pointer); isPtr { - writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname) + if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr { + if _, isBasic := ptr.Elem().Underlying().(*types.Basic); isBasic { + writef("\tx := *src.%s[i]", fname) + writef("\tdst.%s[i] = &x", fname) + } else { + writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname) + } } else { writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname) } diff --git a/cmd/viewer/tests/tests.go b/cmd/viewer/tests/tests.go new file mode 100644 index 000000000..18115ea0f --- /dev/null +++ b/cmd/viewer/tests/tests.go @@ -0,0 +1,48 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package tests serves a list of tests for tailscale.com/cmd/viewer. +package tests + +import ( + "fmt" + + "inet.af/netaddr" +) + +//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices + +type StructWithoutPtrs struct { + Int int + Pfx netaddr.IPPrefix +} + +type Map struct { + M map[string]int +} + +type StructWithPtrs struct { + Value *StructWithoutPtrs + Int *int + + NoCloneValue *StructWithoutPtrs `codegen:"noclone"` +} + +func (v *StructWithPtrs) String() string { return fmt.Sprintf("%v", v.Int) } + +func (v *StructWithPtrs) Equal(v2 *StructWithPtrs) bool { + return v.Value == v2.Value +} + +type StructWithSlices struct { + Values []StructWithoutPtrs + ValuePointers []*StructWithoutPtrs + StructPointers []*StructWithPtrs + Structs []StructWithPtrs + Ints []*int + + Slice []string + Prefixes []netaddr.IPPrefix + Data []byte +} diff --git a/cmd/viewer/tests/tests_clone.go b/cmd/viewer/tests/tests_clone.go new file mode 100644 index 000000000..1fb12746f --- /dev/null +++ b/cmd/viewer/tests/tests_clone.go @@ -0,0 +1,120 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT. + +package tests + +import ( + "inet.af/netaddr" +) + +// Clone makes a deep copy of StructWithPtrs. +// The result aliases no memory with the original. +func (src *StructWithPtrs) Clone() *StructWithPtrs { + if src == nil { + return nil + } + dst := new(StructWithPtrs) + *dst = *src + if dst.Value != nil { + dst.Value = new(StructWithoutPtrs) + *dst.Value = *src.Value + } + if dst.Int != nil { + dst.Int = new(int) + *dst.Int = *src.Int + } + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _StructWithPtrsCloneNeedsRegeneration = StructWithPtrs(struct { + Value *StructWithoutPtrs + Int *int + NoCloneValue *StructWithoutPtrs +}{}) + +// Clone makes a deep copy of StructWithoutPtrs. +// The result aliases no memory with the original. +func (src *StructWithoutPtrs) Clone() *StructWithoutPtrs { + if src == nil { + return nil + } + dst := new(StructWithoutPtrs) + *dst = *src + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _StructWithoutPtrsCloneNeedsRegeneration = StructWithoutPtrs(struct { + Int int + Pfx netaddr.IPPrefix +}{}) + +// Clone makes a deep copy of Map. +// The result aliases no memory with the original. +func (src *Map) Clone() *Map { + if src == nil { + return nil + } + dst := new(Map) + *dst = *src + if dst.M != nil { + dst.M = map[string]int{} + for k, v := range src.M { + dst.M[k] = v + } + } + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _MapCloneNeedsRegeneration = Map(struct { + M map[string]int +}{}) + +// Clone makes a deep copy of StructWithSlices. +// The result aliases no memory with the original. +func (src *StructWithSlices) Clone() *StructWithSlices { + if src == nil { + return nil + } + dst := new(StructWithSlices) + *dst = *src + dst.Values = append(src.Values[:0:0], src.Values...) + dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers)) + for i := range dst.ValuePointers { + dst.ValuePointers[i] = src.ValuePointers[i].Clone() + } + dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers)) + for i := range dst.StructPointers { + dst.StructPointers[i] = src.StructPointers[i].Clone() + } + dst.Structs = make([]StructWithPtrs, len(src.Structs)) + for i := range dst.Structs { + dst.Structs[i] = *src.Structs[i].Clone() + } + dst.Ints = make([]*int, len(src.Ints)) + for i := range dst.Ints { + x := *src.Ints[i] + dst.Ints[i] = &x + } + dst.Slice = append(src.Slice[:0:0], src.Slice...) + dst.Prefixes = append(src.Prefixes[:0:0], src.Prefixes...) + dst.Data = append(src.Data[:0:0], src.Data...) + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _StructWithSlicesCloneNeedsRegeneration = StructWithSlices(struct { + Values []StructWithoutPtrs + ValuePointers []*StructWithoutPtrs + StructPointers []*StructWithPtrs + Structs []StructWithPtrs + Ints []*int + Slice []string + Prefixes []netaddr.IPPrefix + Data []byte +}{}) diff --git a/cmd/viewer/tests/tests_view.go b/cmd/viewer/tests/tests_view.go new file mode 100644 index 000000000..a3abe6f1e --- /dev/null +++ b/cmd/viewer/tests/tests_view.go @@ -0,0 +1,268 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by tailscale/cmd/viewer; DO NOT EDIT. + +package tests + +import ( + "encoding/json" + "errors" + + "go4.org/mem" + "inet.af/netaddr" + "tailscale.com/types/views" +) + +//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices + +// View returns a readonly view of StructWithPtrs. +func (p *StructWithPtrs) View() StructWithPtrsView { + return StructWithPtrsView{ж: p} +} + +// StructWithPtrsView provides a read-only view over StructWithPtrs. +// +// Its methods should only be called if `Valid()` returns true. +type StructWithPtrsView 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. + ж *StructWithPtrs +} + +// Valid reports whether underlying value is non-nil. +func (v StructWithPtrsView) Valid() bool { return v.ж != nil } + +// AsStruct returns a clone of the underlying value which aliases no memory with +// the original. +func (v StructWithPtrsView) AsStruct() *StructWithPtrs { + if v.ж == nil { + return nil + } + return v.ж.Clone() +} + +func (v StructWithPtrsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +func (v *StructWithPtrsView) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + var x StructWithPtrs + if err := json.Unmarshal(b, &x); err != nil { + return err + } + v.ж = &x + return nil +} + +func (v StructWithPtrsView) Value() *StructWithoutPtrs { + if v.ж.Value == nil { + return nil + } + x := *v.ж.Value + return &x +} + +func (v StructWithPtrsView) Int() *int { + if v.ж.Int == nil { + return nil + } + x := *v.ж.Int + return &x +} + +func (v StructWithPtrsView) NoCloneValue() *StructWithoutPtrs { return v.ж.NoCloneValue } +func (v StructWithPtrsView) String() string { return v.ж.String() } +func (v StructWithPtrsView) Equal(v2 StructWithPtrsView) bool { return v.ж.Equal(v2.ж) } + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _StructWithPtrsViewNeedsRegeneration = StructWithPtrs(struct { + Value *StructWithoutPtrs + Int *int + NoCloneValue *StructWithoutPtrs +}{}) + +// View returns a readonly view of StructWithoutPtrs. +func (p *StructWithoutPtrs) View() StructWithoutPtrsView { + return StructWithoutPtrsView{ж: p} +} + +// StructWithoutPtrsView provides a read-only view over StructWithoutPtrs. +// +// Its methods should only be called if `Valid()` returns true. +type StructWithoutPtrsView 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. + ж *StructWithoutPtrs +} + +// Valid reports whether underlying value is non-nil. +func (v StructWithoutPtrsView) Valid() bool { return v.ж != nil } + +// AsStruct returns a clone of the underlying value which aliases no memory with +// the original. +func (v StructWithoutPtrsView) AsStruct() *StructWithoutPtrs { + if v.ж == nil { + return nil + } + return v.ж.Clone() +} + +func (v StructWithoutPtrsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +func (v *StructWithoutPtrsView) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + var x StructWithoutPtrs + if err := json.Unmarshal(b, &x); err != nil { + return err + } + v.ж = &x + return nil +} + +func (v StructWithoutPtrsView) Int() int { return v.ж.Int } +func (v StructWithoutPtrsView) Pfx() netaddr.IPPrefix { return v.ж.Pfx } + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _StructWithoutPtrsViewNeedsRegeneration = StructWithoutPtrs(struct { + Int int + Pfx netaddr.IPPrefix +}{}) + +// View returns a readonly view of Map. +func (p *Map) View() MapView { + return MapView{ж: p} +} + +// MapView provides a read-only view over Map. +// +// Its methods should only be called if `Valid()` returns true. +type MapView 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. + ж *Map +} + +// Valid reports whether underlying value is non-nil. +func (v MapView) Valid() bool { return v.ж != nil } + +// AsStruct returns a clone of the underlying value which aliases no memory with +// the original. +func (v MapView) AsStruct() *Map { + if v.ж == nil { + return nil + } + return v.ж.Clone() +} + +func (v MapView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +func (v *MapView) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + var x Map + if err := json.Unmarshal(b, &x); err != nil { + return err + } + v.ж = &x + return nil +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _MapViewNeedsRegeneration = Map(struct { + M map[string]int +}{}) + +// View returns a readonly view of StructWithSlices. +func (p *StructWithSlices) View() StructWithSlicesView { + return StructWithSlicesView{ж: p} +} + +// StructWithSlicesView provides a read-only view over StructWithSlices. +// +// Its methods should only be called if `Valid()` returns true. +type StructWithSlicesView 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. + ж *StructWithSlices +} + +// Valid reports whether underlying value is non-nil. +func (v StructWithSlicesView) Valid() bool { return v.ж != nil } + +// AsStruct returns a clone of the underlying value which aliases no memory with +// the original. +func (v StructWithSlicesView) AsStruct() *StructWithSlices { + if v.ж == nil { + return nil + } + return v.ж.Clone() +} + +func (v StructWithSlicesView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +func (v *StructWithSlicesView) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + var x StructWithSlices + if err := json.Unmarshal(b, &x); err != nil { + return err + } + v.ж = &x + return nil +} + +func (v StructWithSlicesView) Values() views.Slice[StructWithoutPtrs] { + return views.SliceOf(v.ж.Values) +} +func (v StructWithSlicesView) ValuePointers() views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView] { + return views.SliceOfViews[*StructWithoutPtrs, StructWithoutPtrsView](v.ж.ValuePointers) +} +func (v StructWithSlicesView) StructPointers() views.SliceView[*StructWithPtrs, StructWithPtrsView] { + return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](v.ж.StructPointers) +} +func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") } +func (v StructWithSlicesView) Ints() *int { panic("unsupported") } +func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf(v.ж.Slice) } +func (v StructWithSlicesView) Prefixes() views.IPPrefixSlice { + return views.IPPrefixSliceOf(v.ж.Prefixes) +} +func (v StructWithSlicesView) Data() mem.RO { return mem.B(v.ж.Data) } + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct { + Values []StructWithoutPtrs + ValuePointers []*StructWithoutPtrs + StructPointers []*StructWithPtrs + Structs []StructWithPtrs + Ints []*int + Slice []string + Prefixes []netaddr.IPPrefix + Data []byte +}{}) diff --git a/cmd/viewer/viewer.go b/cmd/viewer/viewer.go new file mode 100644 index 000000000..ad3f02696 --- /dev/null +++ b/cmd/viewer/viewer.go @@ -0,0 +1,316 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// 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}}() mem.RO { return mem.B(v.ж.{{.FieldName}}) } +{{end}} +{{define "ipPrefixSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.IPPrefixSlice { return views.IPPrefixSliceOf(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"}} +// Unsupported, panics. +func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")} +{{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 + }{ + 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 := 0; i < t.NumFields(); i++ { + 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() + args.FieldType = it.QualifiedName(elem) + switch elem.String() { + case "byte": + it.Import("go4.org/mem") + writeTemplate("byteSliceField") + case "inet.af/netaddr.IPPrefix": + it.Import("tailscale.com/types/views") + writeTemplate("ipPrefixSliceField") + default: + 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: + strucT := underlying + args.FieldType = it.QualifiedName(fieldType) + if codegen.ContainsPointers(strucT) { + writeTemplate("viewField") + continue + } + writeTemplate("valueField") + continue + case *types.Map: + // TODO(maisem): support this. + // args.FieldType = importedName(ft) + // writeTemplate("mapField") + 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 := 0; i < typ.NumMethods(); i++ { + 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") +) + +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) + + buf := new(bytes.Buffer) + fmt.Fprintf(buf, `//go:generate go run tailscale.com/cmd/cloner %s`, strings.Join(flagArgs, " ")) + fmt.Fprintln(buf) + runCloner := false + for _, typeName := range typeNames { + 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 pacakge 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") + } +} diff --git a/types/views/views.go b/types/views/views.go index 1431c4bde..af037f0a0 100644 --- a/types/views/views.go +++ b/types/views/views.go @@ -14,6 +14,78 @@ import ( "tailscale.com/net/tsaddr" ) +func unmarshalJSON[T any](b []byte, x *[]T) error { + if *x != nil { + return errors.New("already initialized") + } + if len(b) == 0 { + return nil + } + return json.Unmarshal(b, x) +} + +// StructView represents the corresponding StructView of a Viewable. The concrete types are +// typically generated by tailscale.com/cmd/viewer. +type StructView[T any] interface { + // Valid reports whether the underlying Viewable is nil. + Valid() bool + // AsStruct returns a deep-copy of the underlying value. + // It returns nil, if Valid() is false. + AsStruct() T +} + +// ViewCloner is any type that has had View and Clone funcs generated using +// tailscale.com/cmd/viewer. +type ViewCloner[T any, V StructView[T]] interface { + // View returns a read-only view of Viewable. + // If Viewable is nil, View().Valid() reports false. + View() V + // Clone returns a deep-clone of Viewable. + // It returns nil, when Viewable is nil. + Clone() T +} + +// SliceOfViews returns a ViewSlice for x. +func SliceOfViews[T ViewCloner[T, V], V StructView[T]](x []T) SliceView[T, V] { + return SliceView[T, V]{x} +} + +// SliceView is a read-only wrapper around a struct which should only be exposed +// as a View. +type SliceView[T ViewCloner[T, V], V StructView[T]] struct { + // 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. + ж []T +} + +// MarshalJSON implements json.Marshaler. +func (v SliceView[T, V]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +// UnmarshalJSON implements json.Unmarshaler. +func (v *SliceView[T, V]) UnmarshalJSON(b []byte) error { return unmarshalJSON(b, &v.ж) } + +// IsNil reports whether the underlying slice is nil. +func (v SliceView[T, V]) IsNil() bool { return v.ж == nil } + +// Len returns the length of the slice. +func (v SliceView[T, V]) Len() int { return len(v.ж) } + +// At returns a View of the element at index `i` of the slice. +func (v SliceView[T, V]) At(i int) V { return v.ж[i].View() } + +// AppendTo appends the underlying slice values to dst. +func (v SliceView[T, V]) AppendTo(dst []V) []V { + for _, x := range v.ж { + dst = append(dst, x.View()) + } + return dst +} + +// AsSlice returns a copy of underlying slice. +func (v SliceView[T, V]) AsSlice() []V { + return v.AppendTo(nil) +} + // Slice is a read-only accessor for a slice. type Slice[T any] struct { // It is named distinctively to make you think of how dangerous it is to escape @@ -31,16 +103,7 @@ func (v Slice[T]) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements json.Unmarshaler. func (v *Slice[T]) UnmarshalJSON(b []byte) error { - if v.ж != nil { - return errors.New("Slice is already initialized") - } - if len(b) == 0 { - return nil - } - if err := json.Unmarshal(b, &v.ж); err != nil { - return err - } - return nil + return unmarshalJSON(b, &v.ж) } // IsNil reports whether the underlying slice is nil.