diff --git a/v2/go.mod b/v2/go.mod index 63452bd..f417ec2 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -4,6 +4,11 @@ go 1.18 require github.com/nats-io/nkeys v0.4.7 +retract ( + v2.7.1 // contains retractions only + v2.7.0 // includes case insensitive changes to tags that break jetstream placement +) + require ( golang.org/x/crypto v0.19.0 // indirect golang.org/x/sys v0.17.0 // indirect diff --git a/v2/operator_claims_test.go b/v2/operator_claims_test.go index 04529ae..ed7df4f 100644 --- a/v2/operator_claims_test.go +++ b/v2/operator_claims_test.go @@ -459,6 +459,7 @@ func TestTags(t *testing.T) { } AssertTrue(oc.GenericFields.Tags.Contains("one"), t) + AssertTrue(oc.GenericFields.Tags.Contains("ONE"), t) AssertTrue(oc.GenericFields.Tags.Contains("TWO"), t) AssertTrue(oc.GenericFields.Tags.Contains("three"), t) } diff --git a/v2/types.go b/v2/types.go index f0049ab..d943bd0 100644 --- a/v2/types.go +++ b/v2/types.go @@ -21,6 +21,7 @@ import ( "net" "net/url" "reflect" + "sort" "strconv" "strings" "time" @@ -421,96 +422,166 @@ func (u *StringList) Remove(p ...string) { } } -// TagList is a unique array of lower case strings +// TagList is a unique array of strings. // All tag list methods lower case the strings in the arguments type TagList []string -// Contains returns true if the list contains the tags +// Contains returns true if the list contains the tags. func (u *TagList) Contains(p string) bool { + p = strings.ToLower(strings.TrimSpace(p)) return u.find(p) != -1 } -func (u *TagList) Equals(other *TagList) bool { - if len(*u) != len(*other) { - return false - } - for _, v := range *u { - if other.find(v) == -1 { - return false - } - } - return true +func (u *TagList) ContainsExact(p string) bool { + p = strings.TrimSpace(p) + return u.findExact(p) != -1 } func (u *TagList) find(p string) int { for idx, t := range *u { - if p == t { + if strings.EqualFold(t, p) { return idx } } return -1 } -// Add appends 1 or more tags to a list -func (u *TagList) Add(p ...string) { - for _, v := range p { - v = strings.TrimSpace(v) - if v == "" { - continue - } - if !u.Contains(v) { - *u = append(*u, v) +func (u *TagList) findExact(p string) int { + for idx, t := range *u { + if t == p { + return idx } } + return -1 } -// Remove removes 1 or more tags from a list -func (u *TagList) Remove(p ...string) error { +// Add appends 1 or more tags to a list, tags are converted to lowercase. +func (u *TagList) Add(p ...string) { for _, v := range p { - v = strings.TrimSpace(v) + v = strings.ToLower(strings.TrimSpace(v)) idx := u.find(v) if idx != -1 { a := *u - *u = append(a[:idx], a[idx+1:]...) + a[idx] = v } else { - return fmt.Errorf("unable to remove tag: %q - not found", v) + *u = append(*u, v) } } - return nil } -type CIDRList []string - -func (c *CIDRList) Contains(p string) bool { - p = strings.ToLower(strings.TrimSpace(p)) - for _, t := range *c { - if t == p { - return true +func (u *TagList) AddExact(p ...string) { + for _, v := range p { + v = strings.TrimSpace(v) + idx := u.findExact(v) + if idx == -1 { + *u = append(*u, v) } } - return false } -func (c *CIDRList) Add(p ...string) { +// Remove removes 1 or more tags from a list, removal is case-insensitive, +// and can remove any value that is EqualsFold +func (u *TagList) Remove(p ...string) { for _, v := range p { - v = strings.ToLower(strings.TrimSpace(v)) - if !c.Contains(v) && v != "" { - *c = append(*c, v) + v = strings.TrimSpace(v) + for { + idx := u.find(v) + if idx != -1 { + a := *u + *u = append(a[:idx], a[idx+1:]...) + } else { + break + } } } } -func (c *CIDRList) Remove(p ...string) { +func (u *TagList) RemoveExact(p ...string) error { for _, v := range p { - v = strings.ToLower(strings.TrimSpace(v)) - for i, t := range *c { - if t == v { - a := *c - *c = append(a[:i], a[i+1:]...) - break + v = strings.TrimSpace(v) + idx := u.findExact(v) + if idx == -1 { + return fmt.Errorf("tag \"%q\" not found", v) + } + a := *u + *u = append(a[:idx], a[idx+1:]...) + } + return nil +} + +// FindTag finds entries that start with the specified name +// followed by a colon (:). Names are case-insensitive for +// matches. This function returns the name+:+value (the entire "entry"). +func (u *TagList) FindTag(v string) *TagList { + var matches TagList + // must have a suffix that is name+":" + if !strings.HasSuffix(v, ":") { + v = fmt.Sprintf("%s:", v) + } + // to be a valid tag it must have a name + if v == ":" { + return &matches + } + for _, t := range *u { + idx := strings.Index(t, ":") + if idx != -1 { + prefix := t[:idx+1] + value := t[idx+1:] + // to be a valid tag, it must match the name and have a value + if strings.EqualFold(v, prefix) && len(value) > 0 { + matches = append(matches, t) } } } + return &matches +} + +// GetTagValueFor finds entries that start with the specified name +// followed by a colon (:). Names are case-insensitive for +// matches. This function returns the value portion of the entry +func (u *TagList) GetTagValueFor(v string) *TagList { + tags := u.FindTag(v) + if !strings.HasSuffix(v, ":") { + v = fmt.Sprintf("%s:", v) + } + start := len(v) + a := *tags + for idx, t := range a { + a[idx] = t[start:] + } + return &a +} + +func (u *TagList) Equals(other *TagList) bool { + if len(*u) != len(*other) { + return false + } + + a := sort.StringSlice(*u) + sort.Sort(a) + b := sort.StringSlice(*other) + sort.Sort(b) + + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +type CIDRList TagList + +func (c *CIDRList) Contains(p string) bool { + return (*TagList)(c).Contains(p) +} + +func (c *CIDRList) Add(p ...string) { + (*TagList)(c).Add(p...) +} + +func (c *CIDRList) Remove(p ...string) { + (*TagList)(c).Remove(p...) } func (c *CIDRList) Set(values string) { diff --git a/v2/types_test.go b/v2/types_test.go index 9d8a205..8f634b2 100644 --- a/v2/types_test.go +++ b/v2/types_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 The NATS Authors + * Copyright 2018 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -111,27 +111,138 @@ func TestTimeRangeValidation(t *testing.T) { } } -func TestTagList(t *testing.T) { +func TestTagAdd(t *testing.T) { tags := TagList{} - + tags.Add("ONE") + AssertEquals("one", tags[0], t) + // dupes are not allowed + tags.Add("ONE") tags.Add("one") + AssertEquals(1, len(tags), t) +} - AssertEquals(true, tags.Contains("one"), t) - AssertEquals(false, tags.Contains("ONE"), t) +func TestTagAddMultiple(t *testing.T) { + tags := TagList{} + tags.Add("ONE", "two", "Three") + AssertEquals(3, len(tags), t) AssertEquals("one", tags[0], t) + AssertEquals("two", tags[1], t) + AssertEquals("three", tags[2], t) +} - tags.Add("TWO") +func TestTagAddMultipleExact(t *testing.T) { + tags := TagList{} + tags.AddExact("ONE", "two", "Three") + AssertEquals(3, len(tags), t) + AssertEquals("ONE", tags[0], t) + AssertEquals("two", tags[1], t) + AssertEquals("Three", tags[2], t) +} - AssertEquals(false, tags.Contains("two"), t) - AssertEquals(true, tags.Contains("TWO"), t) - AssertEquals("TWO", tags[1], t) +func TestTagRemoveMultiple(t *testing.T) { + tags := TagList{} + tags.Add("ONE", "TWo", "Three") + AssertEquals(3, len(tags), t) + AssertEquals("one", tags[0], t) + AssertEquals("two", tags[1], t) + AssertEquals("three", tags[2], t) + + tags.Remove("onE", "threE") + AssertEquals(1, len(tags), t) + AssertEquals("two", tags[0], t) +} - err := tags.Remove("ONE") +func TestTagRemoveMultipleExact(t *testing.T) { + tags := TagList{} + tags.AddExact("ONE", "TWo", "Three") + AssertEquals(3, len(tags), t) + AssertEquals("ONE", tags[0], t) + AssertEquals("TWo", tags[1], t) + AssertEquals("Three", tags[2], t) + + // fails if not found + err := tags.RemoveExact("three") if err == nil { - t.Fatal("removing tag that doesn't exist should have failed") + t.Fatal("expected error") } + AssertEquals(3, len(tags), t) + + err = tags.RemoveExact("ONE", "Three") + AssertNoError(err, t) + AssertEquals(1, len(tags), t) + AssertEquals("TWo", tags[0], t) +} + +func TestTagList_Remove(t *testing.T) { + tags := TagList{} + tags.Add("ONE") + AssertEquals("one", tags[0], t) + tags.Remove("one") + AssertEquals(0, len(tags), t) + + tags.AddExact("one") + tags.AddExact("ONE") + AssertEquals(2, len(tags), t) + tags.Remove("One") + AssertEquals(0, len(tags), t) +} + +func TestTagList_AddExact(t *testing.T) { + tags := TagList{} + tags.AddExact("ONE") + AssertEquals("ONE", tags[0], t) + // dupes are not allowed + tags.AddExact("ONE") + AssertEquals(1, len(tags), t) + tags.AddExact("one") + AssertEquals(2, len(tags), t) + AssertEquals("one", tags[1], t) +} + +func TestTagList_RemoveExact(t *testing.T) { + tags := TagList{} + tags.AddExact("ONE") + AssertEquals("ONE", tags[0], t) + tags.AddExact("one") + AssertEquals(2, len(tags), t) + AssertEquals("one", tags[1], t) + + err := tags.RemoveExact("ONE") + AssertNoError(err, t) + AssertEquals(1, len(tags), t) AssertEquals("one", tags[0], t) - AssertEquals(true, tags.Contains("TWO"), t) +} + +func TestTagList_Contains(t *testing.T) { + tags := TagList{} + tags.AddExact("ONE") + AssertTrue(tags.Contains("one"), t) + AssertFalse(tags.ContainsExact("one"), t) + + tags.Add("HELLO") + AssertEquals("hello", tags[1], t) + AssertTrue(tags.Contains("HELLO"), t) + AssertFalse(tags.ContainsExact("HELLO"), t) +} + +func TestTagList_FindTag(t *testing.T) { + tags := TagList{"A:hello", "a:hola", "b:hi"} + matches := tags.FindTag("a") + AssertTrue(matches.Equals(&TagList{"A:hello", "a:hola"}), t) + + matches = tags.FindTag("x") + AssertEquals(0, len(*matches), t) + AssertTrue(matches.Equals(&TagList{}), t) +} + +func TestTagList_GetTagValueFor(t *testing.T) { + tags := TagList{"A:Hello", "a:hola", "b:hi"} + matches := tags.GetTagValueFor("a") + AssertTrue(matches.Equals(&TagList{"Hello", "hola"}), t) + + matches = tags.GetTagValueFor("x") + AssertEquals(0, len(*matches), t) + AssertTrue(matches.Equals(&TagList{}), t) } func TestStringList(t *testing.T) { @@ -429,77 +540,3 @@ func TestInvalidInfo(t *testing.T) { } } } - -func TestTagList_CasePreservingContains(t *testing.T) { - type test struct { - v string - a TagList - ok bool - } - - tests := []test{ - {v: "A", a: TagList{}, ok: false}, - {v: "A", a: TagList{"A"}, ok: true}, - {v: "a", a: TagList{"A"}, ok: false}, - {v: "a", a: TagList{"a:hello"}, ok: false}, - {v: "a:a", a: TagList{"a:c"}, ok: false}, - } - - for idx, test := range tests { - found := test.a.Contains(test.v) - if !found && test.ok { - t.Errorf("[%d] expected to contain %q", idx, test.v) - } - } -} - -func TestTagList_Add(t *testing.T) { - type test struct { - v string - a TagList - shouldBe TagList - } - - tests := []test{ - {v: "A", a: TagList{}, shouldBe: TagList{"A"}}, - {v: "A", a: TagList{"A"}, shouldBe: TagList{"A"}}, - {v: "a", a: TagList{"A"}, shouldBe: TagList{"A", "a"}}, - {v: "a", a: TagList{"a:hello"}, shouldBe: TagList{"a", "a:hello"}}, - {v: "a:Hello", a: TagList{"a:hello"}, shouldBe: TagList{"a:hello", "a:Hello"}}, - {v: "a:a", a: TagList{"a:c"}, shouldBe: TagList{"a:a", "a:c"}}, - } - - for idx, test := range tests { - test.a.Add(test.v) - if !test.a.Equals(&test.shouldBe) { - t.Errorf("[%d] expected lists to be equal: %v", idx, test.a) - } - } -} - -func TestTagList_Delete(t *testing.T) { - type test struct { - v string - a TagList - shouldBe TagList - shouldFail bool - } - - tests := []test{ - {v: "A", a: TagList{}, shouldBe: TagList{}, shouldFail: true}, - {v: "A", a: TagList{"A"}, shouldBe: TagList{}}, - {v: "a", a: TagList{"A"}, shouldBe: TagList{"A"}, shouldFail: true}, - {v: "a:Hello", a: TagList{"a:hello"}, shouldBe: TagList{"a:hello"}, shouldFail: true}, - {v: "a:a", a: TagList{"a:A"}, shouldBe: TagList{"a:A"}, shouldFail: true}, - } - - for idx, test := range tests { - err := test.a.Remove(test.v) - if test.shouldFail && err == nil { - t.Fatalf("[%d] expected delete to fail: %v", idx, test.a) - } - if !test.a.Equals(&test.shouldBe) { - t.Fatalf("[%d] expected lists to be equal: %v", idx, test.a) - } - } -}