feat: allow multiple rel with matching value
This commit is contained in:
parent
b8c2f9f510
commit
12a3e7b1ff
4
Makefile
4
Makefile
|
@ -35,7 +35,3 @@ ifeq (, $(shell which gqlgen))
|
||||||
endif
|
endif
|
||||||
gqlgen
|
gqlgen
|
||||||
|
|
||||||
|
|
||||||
EV_HOST?=localhost:8080
|
|
||||||
load:
|
|
||||||
watch -n .1 "http POST $(EV_HOST)/inbox/asdf/test a=b one=1 two:='{\"v\":2}' | jq"
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ type SubjectSet struct {
|
||||||
Aliases []string `json:"aliases,omitempty"`
|
Aliases []string `json:"aliases,omitempty"`
|
||||||
Properties map[string]*string `json:"properties,omitempty"`
|
Properties map[string]*string `json:"properties,omitempty"`
|
||||||
|
|
||||||
event.IsEvent
|
event.IsEvent `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ event.Event = (*SubjectSet)(nil)
|
var _ event.Event = (*SubjectSet)(nil)
|
||||||
|
@ -17,27 +17,29 @@ var _ event.Event = (*SubjectSet)(nil)
|
||||||
type SubjectDeleted struct {
|
type SubjectDeleted struct {
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
|
|
||||||
event.IsEvent
|
event.IsEvent `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ event.Event = (*SubjectDeleted)(nil)
|
var _ event.Event = (*SubjectDeleted)(nil)
|
||||||
|
|
||||||
type LinkSet struct {
|
type LinkSet struct {
|
||||||
|
Index uint64 `json:"idx"`
|
||||||
Rel string `json:"rel"`
|
Rel string `json:"rel"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
HRef string `json:"href,omitempty"`
|
HRef string `json:"href,omitempty"`
|
||||||
Titles map[string]string `json:"titles,omitempty"`
|
Titles map[string]string `json:"titles,omitempty"`
|
||||||
Properties map[string]*string `json:"properties,omitempty"`
|
Properties map[string]*string `json:"properties,omitempty"`
|
||||||
|
|
||||||
event.IsEvent
|
event.IsEvent `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ event.Event = (*LinkSet)(nil)
|
var _ event.Event = (*LinkSet)(nil)
|
||||||
|
|
||||||
type LinkDeleted struct {
|
type LinkDeleted struct {
|
||||||
|
Index uint64 `json:"idx"`
|
||||||
Rel string `json:"rel"`
|
Rel string `json:"rel"`
|
||||||
|
|
||||||
event.IsEvent
|
event.IsEvent `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ event.Event = (*LinkDeleted)(nil)
|
var _ event.Event = (*LinkDeleted)(nil)
|
||||||
|
|
|
@ -27,9 +27,8 @@ type JRD struct {
|
||||||
Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"`
|
Aliases []string `json:"aliases,omitempty" yaml:"aliases,omitempty"`
|
||||||
Properties map[string]*string `json:"properties,omitempty" yaml:"properties,omitempty"`
|
Properties map[string]*string `json:"properties,omitempty" yaml:"properties,omitempty"`
|
||||||
Links Links `json:"links,omitempty" yaml:"links,omitempty"`
|
Links Links `json:"links,omitempty" yaml:"links,omitempty"`
|
||||||
|
|
||||||
deleted bool
|
deleted bool
|
||||||
event.IsAggregate `yaml:"-"`
|
event.IsAggregate `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *JRD) CloneValues() *JRD {
|
func (a *JRD) CloneValues() *JRD {
|
||||||
|
@ -49,6 +48,7 @@ var _ event.Aggregate = (*JRD)(nil)
|
||||||
|
|
||||||
// Link is a link to a related resource.
|
// Link is a link to a related resource.
|
||||||
type Link struct {
|
type Link struct {
|
||||||
|
Index uint64 `json:"-" yaml:"-"`
|
||||||
Rel string `json:"rel,omitempty"`
|
Rel string `json:"rel,omitempty"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
HRef string `json:"href,omitempty"`
|
HRef string `json:"href,omitempty"`
|
||||||
|
@ -71,6 +71,9 @@ func (l Links) Less(i int, j int) bool {
|
||||||
if l[i] == nil || l[j] == nil {
|
if l[i] == nil || l[j] == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if l[i].Rel == l[j].Rel {
|
||||||
|
return l[i].Type < l[j].Type
|
||||||
|
}
|
||||||
return l[i].Rel < l[j].Rel
|
return l[i].Rel < l[j].Rel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +92,9 @@ func ParseJRD(blob []byte) (*JRD, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for i := range jrd.Links {
|
||||||
|
jrd.Links[i].Index = uint64(i)
|
||||||
|
}
|
||||||
return &jrd, nil
|
return &jrd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +108,7 @@ func (jrd *JRD) GetLinkByRel(rel string) *Link {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLinksByRel returns the first *Link with the specified rel value.
|
// GetLinksByRel returns each *Link with the specified rel value.
|
||||||
func (jrd *JRD) GetLinksByRel(rel ...string) []*Link {
|
func (jrd *JRD) GetLinksByRel(rel ...string) []*Link {
|
||||||
var lis []*Link
|
var lis []*Link
|
||||||
rels := set.New(rel...)
|
rels := set.New(rel...)
|
||||||
|
@ -180,20 +186,21 @@ func (a *JRD) ApplyEvent(events ...event.Event) {
|
||||||
a.Properties = map[string]*string{}
|
a.Properties = map[string]*string{}
|
||||||
|
|
||||||
case *LinkSet:
|
case *LinkSet:
|
||||||
link, ok := slice.FindFn(func(l *Link) bool { return l.Rel == e.Rel }, a.Links...)
|
link, ok := slice.FindFn(func(l *Link) bool { return l.Index == e.Index }, a.Links...)
|
||||||
if !ok {
|
if !ok {
|
||||||
link = &Link{}
|
link = &Link{}
|
||||||
link.Rel = e.Rel
|
link.Index = uint64(len(a.Links))
|
||||||
a.Links = append(a.Links, link)
|
a.Links = append(a.Links, link)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
link.Rel = e.Rel
|
||||||
link.HRef = e.HRef
|
link.HRef = e.HRef
|
||||||
link.Type = e.Type
|
link.Type = e.Type
|
||||||
link.Titles = e.Titles
|
link.Titles = e.Titles
|
||||||
link.Properties = e.Properties
|
link.Properties = e.Properties
|
||||||
|
|
||||||
case *LinkDeleted:
|
case *LinkDeleted:
|
||||||
a.Links = slice.FilterFn(func(link *Link) bool { return link.Rel != e.Rel }, a.Links...)
|
a.Links = slice.FilterFn(func(link *Link) bool { return link.Index != e.Index }, a.Links...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,17 +248,15 @@ func (a *JRD) OnClaims(jrd *JRD) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(jrd.Links)
|
|
||||||
sort.Sort(a.Links)
|
|
||||||
for _, z := range slice.Align(
|
for _, z := range slice.Align(
|
||||||
jrd.Links,
|
jrd.Links,
|
||||||
a.Links,
|
a.Links,
|
||||||
func(l, r *Link) bool { return l.Rel < r.Rel },
|
func(l, r *Link) bool { return l.Index < r.Index },
|
||||||
) {
|
) {
|
||||||
// Not in new == delete
|
// Not in new == delete
|
||||||
if z.Key == nil {
|
if z.Key == nil {
|
||||||
link := *z.Value
|
link := *z.Value
|
||||||
event.Raise(a, &LinkDeleted{Rel: link.Rel})
|
event.Raise(a, &LinkDeleted{Index: link.Index, Rel: link.Rel})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,6 +264,7 @@ func (a *JRD) OnClaims(jrd *JRD) error {
|
||||||
if z.Value == nil {
|
if z.Value == nil {
|
||||||
link := *z.Key
|
link := *z.Key
|
||||||
event.Raise(a, &LinkSet{
|
event.Raise(a, &LinkSet{
|
||||||
|
Index: link.Index,
|
||||||
Rel: link.Rel,
|
Rel: link.Rel,
|
||||||
Type: link.Type,
|
Type: link.Type,
|
||||||
HRef: link.HRef,
|
HRef: link.HRef,
|
||||||
|
@ -324,6 +330,7 @@ func (a *JRD) OnSubjectSet(subject string, aliases []string, props map[string]*s
|
||||||
func (a *JRD) OnLinkSet(o, n *Link) error {
|
func (a *JRD) OnLinkSet(o, n *Link) error {
|
||||||
modified := false
|
modified := false
|
||||||
e := &LinkSet{
|
e := &LinkSet{
|
||||||
|
Index: n.Index,
|
||||||
Rel: n.Rel,
|
Rel: n.Rel,
|
||||||
Type: n.Type,
|
Type: n.Type,
|
||||||
HRef: n.HRef,
|
HRef: n.HRef,
|
||||||
|
@ -331,39 +338,66 @@ func (a *JRD) OnLinkSet(o, n *Link) error {
|
||||||
Properties: n.Properties,
|
Properties: n.Properties,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if n.Index != o.Index {
|
||||||
|
// fmt.Println(342)
|
||||||
|
// modified = true
|
||||||
|
// }
|
||||||
if n.Rel != o.Rel {
|
if n.Rel != o.Rel {
|
||||||
|
fmt.Println(346)
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
if n.Type != o.Type {
|
if n.Type != o.Type {
|
||||||
|
fmt.Println(350)
|
||||||
|
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
if n.HRef != o.HRef {
|
if n.HRef != o.HRef {
|
||||||
|
fmt.Println(355)
|
||||||
|
|
||||||
modified = true
|
modified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nKeys := slice.FromMapKeys(n.Properties)
|
||||||
|
sort.Strings(nKeys)
|
||||||
|
|
||||||
|
oKeys := slice.FromMapKeys(o.Properties)
|
||||||
|
sort.Strings(oKeys)
|
||||||
|
|
||||||
for _, z := range slice.Zip(
|
for _, z := range slice.Zip(
|
||||||
slice.Zip(slice.FromMap(n.Titles)),
|
slice.Zip(nKeys, slice.FromMapValues(n.Titles, nKeys)),
|
||||||
slice.Zip(slice.FromMap(o.Titles)),
|
slice.Zip(oKeys, slice.FromMapValues(o.Titles, oKeys)),
|
||||||
) {
|
) {
|
||||||
if z.Key != z.Value {
|
if z.Key != z.Value {
|
||||||
|
fmt.Println(365)
|
||||||
|
|
||||||
modified = true
|
modified = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nKeys = slice.FromMapKeys(n.Properties)
|
||||||
|
sort.Strings(nKeys)
|
||||||
|
|
||||||
|
oKeys = slice.FromMapKeys(o.Properties)
|
||||||
|
sort.Strings(oKeys)
|
||||||
|
|
||||||
for _, z := range slice.Zip(
|
for _, z := range slice.Zip(
|
||||||
slice.Zip(slice.FromMap(n.Properties)),
|
slice.Zip(nKeys, slice.FromMapValues(n.Properties, nKeys)),
|
||||||
slice.Zip(slice.FromMap(o.Properties)),
|
slice.Zip(oKeys, slice.FromMapValues(o.Properties, oKeys)),
|
||||||
) {
|
) {
|
||||||
newValue := z.Key
|
newValue := z.Key
|
||||||
curValue := z.Value
|
curValue := z.Value
|
||||||
|
|
||||||
if newValue.Key != curValue.Key {
|
if newValue.Key != curValue.Key {
|
||||||
|
fmt.Println(380, newValue.Key, curValue.Key)
|
||||||
|
|
||||||
modified = true
|
modified = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cmpPtr(newValue.Value, curValue.Value) {
|
if !cmpPtr(newValue.Value, curValue.Value) {
|
||||||
|
fmt.Println(387)
|
||||||
|
|
||||||
modified = true
|
modified = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,14 +111,17 @@ func TestApplyEvents(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&webfinger.LinkSet{
|
&webfinger.LinkSet{
|
||||||
|
Index: 0,
|
||||||
Rel: "salty:public",
|
Rel: "salty:public",
|
||||||
Type: "application/json+salty",
|
Type: "application/json+salty",
|
||||||
},
|
},
|
||||||
&webfinger.LinkSet{
|
&webfinger.LinkSet{
|
||||||
|
Index: 1,
|
||||||
Rel: "salty:private",
|
Rel: "salty:private",
|
||||||
Type: "application/json+salty",
|
Type: "application/json+salty",
|
||||||
},
|
},
|
||||||
&webfinger.LinkSet{
|
&webfinger.LinkSet{
|
||||||
|
Index: 0,
|
||||||
Rel: "salty:public",
|
Rel: "salty:public",
|
||||||
Type: "application/json+salty",
|
Type: "application/json+salty",
|
||||||
HRef: "https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5",
|
HRef: "https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5",
|
||||||
|
@ -127,6 +130,7 @@ func TestApplyEvents(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
&webfinger.LinkDeleted{
|
&webfinger.LinkDeleted{
|
||||||
|
Index: 1,
|
||||||
Rel: "salty:private",
|
Rel: "salty:private",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -167,7 +171,6 @@ func TestCommands(t *testing.T) {
|
||||||
pub, priv, err := ed25519.GenerateKey(nil)
|
pub, priv, err := ed25519.GenerateKey(nil)
|
||||||
is.NoErr(err)
|
is.NoErr(err)
|
||||||
|
|
||||||
// fmt.Println(base64.RawURLEncoding.EncodeToString(key))
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
|
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
|
||||||
"sub": "acct:me@sour.is",
|
"sub": "acct:me@sour.is",
|
||||||
"pub": enc(pub),
|
"pub": enc(pub),
|
||||||
|
|
|
@ -112,6 +112,10 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return nil, fmt.Errorf("wrong type of claim")
|
return nil, fmt.Errorf("wrong type of claim")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.JRD == nil {
|
||||||
|
c.JRD = &JRD{}
|
||||||
|
}
|
||||||
|
|
||||||
c.JRD.Subject = c.RegisteredClaims.Subject
|
c.JRD.Subject = c.RegisteredClaims.Subject
|
||||||
|
|
||||||
c.SetProperty(NSpubkey, &c.PubKey)
|
c.SetProperty(NSpubkey, &c.PubKey)
|
||||||
|
@ -149,9 +153,17 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range c.JRD.Links {
|
||||||
|
c.JRD.Links[i].Index = uint64(i)
|
||||||
|
}
|
||||||
|
|
||||||
a, err := ev.Upsert(ctx, s.es, StreamID(c.JRD.Subject), func(ctx context.Context, a *JRD) error {
|
a, err := ev.Upsert(ctx, s.es, StreamID(c.JRD.Subject), func(ctx context.Context, a *JRD) error {
|
||||||
var auth *JRD
|
var auth *JRD
|
||||||
|
|
||||||
|
for i := range a.Links {
|
||||||
|
a.Links[i].Index = uint64(i)
|
||||||
|
}
|
||||||
|
|
||||||
// does the target have a pubkey for self auth?
|
// does the target have a pubkey for self auth?
|
||||||
if _, ok := a.Properties[NSpubkey]; ok {
|
if _, ok := a.Properties[NSpubkey]; ok {
|
||||||
auth = a
|
auth = a
|
||||||
|
@ -237,7 +249,7 @@ func (s *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
redirect.Host = u.URL.Host
|
redirect.Host = u.URL.Host
|
||||||
redirect.RawQuery = r.URL.RawQuery
|
redirect.RawQuery = r.URL.RawQuery
|
||||||
redirect.Path = "/.well-known/webfinger"
|
redirect.Path = "/.well-known/webfinger"
|
||||||
fmt.Println(redirect)
|
|
||||||
w.Header().Set("location", redirect.String())
|
w.Header().Set("location", redirect.String())
|
||||||
w.WriteHeader(http.StatusSeeOther)
|
w.WriteHeader(http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
|
@ -300,31 +312,32 @@ func dec(s string) ([]byte, error) {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
return base64.RawURLEncoding.DecodeString(s)
|
return base64.RawURLEncoding.DecodeString(s)
|
||||||
}
|
}
|
||||||
func splitHostPort(hostPort string) (host, port string) {
|
|
||||||
host = hostPort
|
|
||||||
|
|
||||||
colon := strings.LastIndexByte(host, ':')
|
// func splitHostPort(hostPort string) (host, port string) {
|
||||||
if colon != -1 && validOptionalPort(host[colon:]) {
|
// host = hostPort
|
||||||
host, port = host[:colon], host[colon+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
// colon := strings.LastIndexByte(host, ':')
|
||||||
host = host[1 : len(host)-1]
|
// if colon != -1 && validOptionalPort(host[colon:]) {
|
||||||
}
|
// host, port = host[:colon], host[colon+1:]
|
||||||
|
// }
|
||||||
|
|
||||||
return
|
// if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||||
}
|
// host = host[1 : len(host)-1]
|
||||||
func validOptionalPort(port string) bool {
|
// }
|
||||||
if port == "" {
|
|
||||||
return true
|
// return
|
||||||
}
|
// }
|
||||||
if port[0] != ':' {
|
// func validOptionalPort(port string) bool {
|
||||||
return false
|
// if port == "" {
|
||||||
}
|
// return true
|
||||||
for _, b := range port[1:] {
|
// }
|
||||||
if b < '0' || b > '9' {
|
// if port[0] != ':' {
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
}
|
// for _, b := range port[1:] {
|
||||||
return true
|
// if b < '0' || b > '9' {
|
||||||
}
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
|
|
@ -38,7 +38,7 @@ var _ = apps.Register(50, func(ctx context.Context, svc *service.Harness) error
|
||||||
cache.SetDefault(s, true)
|
cache.SetDefault(s, true)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
var withHostnames webfinger.WithHostnames = strings.Fields(env.Default("WEBFINGER_DOMAINS", "sour.is"))
|
var withHostnames webfinger.WithHostnames = strings.Fields(env.Default(" ", "sour.is"))
|
||||||
|
|
||||||
wf, err := webfinger.New(ctx, eventstore, withCache, withHostnames)
|
wf, err := webfinger.New(ctx, eventstore, withCache, withHostnames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -44,11 +44,11 @@ func First[T any](in ...T) (T, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map applys func to each element s and returns results as slice.
|
// Map applys func to each element s and returns results as slice.
|
||||||
func Map[T, U any](f func(T) U) func(...T) []U {
|
func Map[T, U any](f func(int, T) U) func(...T) []U {
|
||||||
return func(lis ...T) []U {
|
return func(lis ...T) []U {
|
||||||
r := make([]U, len(lis))
|
r := make([]U, len(lis))
|
||||||
for i, v := range lis {
|
for i, v := range lis {
|
||||||
r[i] = f(v)
|
r[i] = f(i, v)
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -73,19 +73,38 @@ func FromMap[K comparable, V any](m map[K]V) (keys []K, values []V) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keys = FromMapKeys(m)
|
||||||
|
return keys, FromMapValues(m, keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromMapKeys[K comparable, V any](m map[K]V) (keys []K) {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
keys = make([]K, 0, len(m))
|
keys = make([]K, 0, len(m))
|
||||||
values = make([]V, 0, len(m))
|
|
||||||
|
|
||||||
for k := range m {
|
for k := range m {
|
||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromMapValues[K comparable, V any](m map[K]V, keys []K) (values []V) {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
values = make([]V, 0, len(keys))
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
values = append(values, m[k])
|
values = append(values, m[k])
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys, values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func ToMap[K comparable, V any](keys []K, values []V) (m map[K]V) {
|
func ToMap[K comparable, V any](keys []K, values []V) (m map[K]V) {
|
||||||
m = make(map[K]V, len(keys))
|
m = make(map[K]V, len(keys))
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user