Просмотр исходного кода

cmd/viewer: add codegen tool for Views

Signed-off-by: Maisem Ali <[email protected]>
Maisem Ali 3 лет назад
Родитель
Сommit
c4e9739251

+ 7 - 2
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)
 				}

+ 48 - 0
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
+}

+ 120 - 0
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
+}{})

+ 268 - 0
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
+}{})

+ 316 - 0
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")
+	}
+}

+ 73 - 10
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.