diff --git a/cmd/viewer/tests/tests.go b/cmd/viewer/tests/tests.go index 18115ea0f..ec202ccde 100644 --- a/cmd/viewer/tests/tests.go +++ b/cmd/viewer/tests/tests.go @@ -19,7 +19,18 @@ type StructWithoutPtrs struct { } type Map struct { - M map[string]int + Int map[string]int + SliceInt map[string][]int + StructWithPtr map[string]*StructWithPtrs + StructWithoutPtr map[string]*StructWithoutPtrs + SlicesWithPtrs map[string][]*StructWithPtrs + SlicesWithoutPtrs map[string][]*StructWithoutPtrs + StructWithoutPtrKey map[StructWithoutPtrs]int `json:"-"` + + // Unsupported. + SliceIntPtr map[string][]*int + PointerKey map[*string]int `json:"-"` + StructWithPtrKey map[StructWithPtrs]int `json:"-"` } type StructWithPtrs struct { diff --git a/cmd/viewer/tests/tests_clone.go b/cmd/viewer/tests/tests_clone.go index 1fb12746f..357fa20a5 100644 --- a/cmd/viewer/tests/tests_clone.go +++ b/cmd/viewer/tests/tests_clone.go @@ -61,10 +61,64 @@ func (src *Map) Clone() *Map { } dst := new(Map) *dst = *src - if dst.M != nil { - dst.M = map[string]int{} - for k, v := range src.M { - dst.M[k] = v + if dst.Int != nil { + dst.Int = map[string]int{} + for k, v := range src.Int { + dst.Int[k] = v + } + } + if dst.SliceInt != nil { + dst.SliceInt = map[string][]int{} + for k := range src.SliceInt { + dst.SliceInt[k] = append([]int{}, src.SliceInt[k]...) + } + } + if dst.StructWithPtr != nil { + dst.StructWithPtr = map[string]*StructWithPtrs{} + for k, v := range src.StructWithPtr { + dst.StructWithPtr[k] = v.Clone() + } + } + if dst.StructWithoutPtr != nil { + dst.StructWithoutPtr = map[string]*StructWithoutPtrs{} + for k, v := range src.StructWithoutPtr { + dst.StructWithoutPtr[k] = v.Clone() + } + } + if dst.SlicesWithPtrs != nil { + dst.SlicesWithPtrs = map[string][]*StructWithPtrs{} + for k := range src.SlicesWithPtrs { + dst.SlicesWithPtrs[k] = append([]*StructWithPtrs{}, src.SlicesWithPtrs[k]...) + } + } + if dst.SlicesWithoutPtrs != nil { + dst.SlicesWithoutPtrs = map[string][]*StructWithoutPtrs{} + for k := range src.SlicesWithoutPtrs { + dst.SlicesWithoutPtrs[k] = append([]*StructWithoutPtrs{}, src.SlicesWithoutPtrs[k]...) + } + } + if dst.StructWithoutPtrKey != nil { + dst.StructWithoutPtrKey = map[StructWithoutPtrs]int{} + for k, v := range src.StructWithoutPtrKey { + dst.StructWithoutPtrKey[k] = v + } + } + if dst.SliceIntPtr != nil { + dst.SliceIntPtr = map[string][]*int{} + for k := range src.SliceIntPtr { + dst.SliceIntPtr[k] = append([]*int{}, src.SliceIntPtr[k]...) + } + } + if dst.PointerKey != nil { + dst.PointerKey = map[*string]int{} + for k, v := range src.PointerKey { + dst.PointerKey[k] = v + } + } + if dst.StructWithPtrKey != nil { + dst.StructWithPtrKey = map[StructWithPtrs]int{} + for k, v := range src.StructWithPtrKey { + dst.StructWithPtrKey[k] = v } } return dst @@ -72,7 +126,16 @@ func (src *Map) Clone() *Map { // 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 + Int map[string]int + SliceInt map[string][]int + StructWithPtr map[string]*StructWithPtrs + StructWithoutPtr map[string]*StructWithoutPtrs + SlicesWithPtrs map[string][]*StructWithPtrs + SlicesWithoutPtrs map[string][]*StructWithoutPtrs + StructWithoutPtrKey map[StructWithoutPtrs]int + SliceIntPtr map[string][]*int + PointerKey map[*string]int + StructWithPtrKey map[StructWithPtrs]int }{}) // Clone makes a deep copy of StructWithSlices. diff --git a/cmd/viewer/tests/tests_view.go b/cmd/viewer/tests/tests_view.go index a3abe6f1e..30d6b1af3 100644 --- a/cmd/viewer/tests/tests_view.go +++ b/cmd/viewer/tests/tests_view.go @@ -188,9 +188,57 @@ func (v *MapView) UnmarshalJSON(b []byte) error { return nil } +func (v MapView) Int() views.Map[string, int] { return views.MapOf(v.ж.Int) } + +func (v MapView) SliceInt() views.MapFn[string, []int, views.Slice[int]] { + return views.MapFnOf(v.ж.SliceInt, func(t []int) views.Slice[int] { + return views.SliceOf(t) + }) +} + +func (v MapView) StructWithPtr() views.MapFn[string, *StructWithPtrs, StructWithPtrsView] { + return views.MapFnOf(v.ж.StructWithPtr, func(t *StructWithPtrs) StructWithPtrsView { + return t.View() + }) +} + +func (v MapView) StructWithoutPtr() views.MapFn[string, *StructWithoutPtrs, StructWithoutPtrsView] { + return views.MapFnOf(v.ж.StructWithoutPtr, func(t *StructWithoutPtrs) StructWithoutPtrsView { + return t.View() + }) +} + +func (v MapView) SlicesWithPtrs() views.MapFn[string, []*StructWithPtrs, views.SliceView[*StructWithPtrs, StructWithPtrsView]] { + return views.MapFnOf(v.ж.SlicesWithPtrs, func(t []*StructWithPtrs) views.SliceView[*StructWithPtrs, StructWithPtrsView] { + return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](t) + }) +} + +func (v MapView) SlicesWithoutPtrs() views.MapFn[string, []*StructWithoutPtrs, views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView]] { + return views.MapFnOf(v.ж.SlicesWithoutPtrs, func(t []*StructWithoutPtrs) views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView] { + return views.SliceOfViews[*StructWithoutPtrs, StructWithoutPtrsView](t) + }) +} + +func (v MapView) StructWithoutPtrKey() views.Map[StructWithoutPtrs, int] { + return views.MapOf(v.ж.StructWithoutPtrKey) +} +func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported") } +func (v MapView) PointerKey() map[*string]int { panic("unsupported") } +func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") } + // 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 + Int map[string]int + SliceInt map[string][]int + StructWithPtr map[string]*StructWithPtrs + StructWithoutPtr map[string]*StructWithoutPtrs + SlicesWithPtrs map[string][]*StructWithPtrs + SlicesWithoutPtrs map[string][]*StructWithoutPtrs + StructWithoutPtrKey map[StructWithoutPtrs]int + SliceIntPtr map[string][]*int + PointerKey map[*string]int + StructWithPtrKey map[StructWithPtrs]int }{}) // View returns a readonly view of StructWithSlices. diff --git a/cmd/viewer/viewer.go b/cmd/viewer/viewer.go index ad3f02696..c9909453a 100644 --- a/cmd/viewer/viewer.go +++ b/cmd/viewer/viewer.go @@ -88,8 +88,12 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error { {{end}} {{define "mapField"}} -// Unsupported, panics. -func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")} +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 "unsupportedField"}}func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")} {{end}} @@ -132,6 +136,11 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi FieldName string FieldType string FieldViewName string + + MapKeyType string + MapValueType string + MapValueView string + MapFn string }{ StructName: typ.Obj().Name(), ViewName: typ.Obj().Name() + "View", @@ -194,7 +203,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi writeTemplate("sliceField") } continue - case *types.Struct: + case *types.Struct, *types.Named: strucT := underlying args.FieldType = it.QualifiedName(fieldType) if codegen.ContainsPointers(strucT) { @@ -204,9 +213,62 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi writeTemplate("valueField") continue case *types.Map: - // TODO(maisem): support this. - // args.FieldType = importedName(ft) - // writeTemplate("mapField") + 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.Basic: + template = "mapField" + args.MapValueType = it.QualifiedName(mElem) + case *types.Slice: + slice := u + sElem := slice.Elem() + switch x := sElem.(type) { + case *types.Basic: + args.MapValueView = fmt.Sprintf("views.Slice[%v]", sElem) + args.MapValueType = "[]" + sElem.String() + args.MapFn = "views.SliceOf(t)" + template = "mapFnField" + 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 diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index b313a5ca9..b7ddb9dec 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -531,13 +531,6 @@ type NetInfo struct { // Update BasicallyEqual when adding fields. } -// DERPLatencyForEach calls fn for each value in the DERPLatency map. -func (v NetInfoView) DERPLatencyForEach(fn func(k string, v float64)) { - for k, v := range v.ж.DERPLatency { - fn(k, v) - } -} - func (ni *NetInfo) String() string { if ni == nil { return "NetInfo(nil)" diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index a5f95457b..be466d41b 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -343,7 +343,9 @@ func (v NetInfoView) PMP() opt.Bool { return v.ж.PMP } func (v NetInfoView) PCP() opt.Bool { return v.ж.PCP } func (v NetInfoView) PreferredDERP() int { return v.ж.PreferredDERP } func (v NetInfoView) LinkType() string { return v.ж.LinkType } -func (v NetInfoView) String() string { return v.ж.String() } + +func (v NetInfoView) DERPLatency() views.Map[string, float64] { return views.MapOf(v.ж.DERPLatency) } +func (v NetInfoView) String() string { return v.ж.String() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _NetInfoViewNeedsRegeneration = NetInfo(struct { @@ -471,6 +473,12 @@ func (v *DNSConfigView) UnmarshalJSON(b []byte) error { func (v DNSConfigView) Resolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers) } + +func (v DNSConfigView) Routes() views.MapFn[string, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] { + return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { + return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t) + }) +} func (v DNSConfigView) FallbackResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.FallbackResolvers) } @@ -667,6 +675,11 @@ func (v *DERPMapView) UnmarshalJSON(b []byte) error { return nil } +func (v DERPMapView) Regions() views.MapFn[int, *DERPRegion, DERPRegionView] { + return views.MapFnOf(v.ж.Regions, func(t *DERPRegion) DERPRegionView { + return t.View() + }) +} func (v DERPMapView) OmitDefaultRegions() bool { return v.ж.OmitDefaultRegions } // A compilation failure here means this code must be regenerated, with the command at the top of this file. diff --git a/types/views/views.go b/types/views/views.go index af037f0a0..466462460 100644 --- a/types/views/views.go +++ b/types/views/views.go @@ -53,6 +53,8 @@ func SliceOfViews[T ViewCloner[T, V], V StructView[T]](x []T) SliceView[T, V] { // 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 { + // ж 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. ж []T @@ -88,6 +90,8 @@ func (v SliceView[T, V]) AsSlice() []V { // Slice is a read-only accessor for a slice. type Slice[T any] 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. ж []T @@ -181,3 +185,102 @@ func (v IPPrefixSlice) MarshalJSON() ([]byte, error) { func (v *IPPrefixSlice) UnmarshalJSON(b []byte) error { return v.ж.UnmarshalJSON(b) } + +// MapOf returns a read-only view over m. +func MapOf[K comparable, V comparable](m map[K]V) Map[K, V] { + return Map[K, V]{m} +} + +// Map is a read-only accessor over a map whose values are immutable. +type Map[K comparable, V any] 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[K]V +} + +// Has reports whether k has an entry in the map. +func (m Map[K, V]) Has(k K) bool { + _, ok := m.ж[k] + return ok +} + +// IsNil reports whether the underlying map is nil. +func (m Map[K, V]) IsNil() bool { + return m.ж == nil +} + +// Len returns the number of elements in the map. +func (m Map[K, V]) Len() int { return len(m.ж) } + +// Get returns the element with key k. +func (m Map[K, V]) Get(k K) V { + return m.ж[k] +} + +// GetOk returns the element with key k and a bool representing whether the key +// is in map. +func (m Map[K, V]) GetOk(k K) (V, bool) { + v, ok := m.ж[k] + return v, ok +} + +// ForEach calls f for every k,v pair in the underlying map. +func (m Map[K, V]) ForEach(f func(k K, v V)) { + for k, v := range m.ж { + f(k, v) + } +} + +// MapFnOf returns a MapFn for m. +func MapFnOf[K comparable, T any, V any](m map[K]T, f func(T) V) MapFn[K, T, V] { + return MapFn[K, T, V]{ + ж: m, + wrapv: f, + } +} + +// MapFn is like Map but with a func to convert values from T to V. +// It is used to provide map of slices and views. +type MapFn[K comparable, T any, V any] 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[K]T + wrapv func(T) V +} + +// Has reports whether k has an entry in the map. +func (m MapFn[K, T, V]) Has(k K) bool { + _, ok := m.ж[k] + return ok +} + +// Get returns the element with key k. +func (m MapFn[K, T, V]) Get(k K) V { + return m.wrapv(m.ж[k]) +} + +// IsNil reports whether the underlying map is nil. +func (m MapFn[K, T, V]) IsNil() bool { + return m.ж == nil +} + +// Len returns the number of elements in the map. +func (m MapFn[K, T, V]) Len() int { return len(m.ж) } + +// GetOk returns the element with key k and a bool representing whether the key +// is in map. +func (m MapFn[K, T, V]) GetOk(k K) (V, bool) { + v, ok := m.ж[k] + return m.wrapv(v), ok +} + +// ForEach calls f for every k,v pair in the underlying map. +func (m MapFn[K, T, V]) ForEach(f func(k K, v V)) { + for k, v := range m.ж { + f(k, m.wrapv(v)) + } +}