diff --git a/.changeset/three-melons-stand.md b/.changeset/three-melons-stand.md new file mode 100644 index 0000000..269dc74 --- /dev/null +++ b/.changeset/three-melons-stand.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": patch +--- + +Datastore labels are now serialized into JSON as an array of strings diff --git a/datastore/address_ref.go b/datastore/address_ref.go index 2e1d83f..1d8661b 100644 --- a/datastore/address_ref.go +++ b/datastore/address_ref.go @@ -2,7 +2,6 @@ package datastore import ( "errors" - "maps" "github.com/Masterminds/semver/v3" ) @@ -51,7 +50,7 @@ func (r AddressRef) Clone() AddressRef { Version: r.Version, Qualifier: r.Qualifier, Address: r.Address, - Labels: maps.Clone(r.Labels), + Labels: r.Labels.Clone(), } } diff --git a/datastore/label_set.go b/datastore/label_set.go index 25e2880..35fdf80 100644 --- a/datastore/label_set.go +++ b/datastore/label_set.go @@ -1,43 +1,51 @@ package datastore import ( - "sort" + "encoding/json" + "maps" + "slices" "strings" ) // LabelSet represents a set of labels on an address book entry. -type LabelSet map[string]struct{} +type LabelSet struct { + elements map[string]struct{} +} // NewLabelSet initializes a new LabelSet with any number of labels. func NewLabelSet(labels ...string) LabelSet { - set := make(LabelSet) - for _, lb := range labels { - set[lb] = struct{}{} + set := make(map[string]struct{}, len(labels)) + for _, l := range labels { + set[l] = struct{}{} } - return set + return LabelSet{ + elements: set, + } } // Add inserts a label into the set. -func (ls LabelSet) Add(label string) { - ls[label] = struct{}{} +func (s LabelSet) Add(label string) { + s.elements[label] = struct{}{} } // Remove deletes a label from the set, if it exists. -func (ls LabelSet) Remove(label string) { - delete(ls, label) +func (s LabelSet) Remove(label string) { + delete(s.elements, label) } // Contains checks if the set contains the given label. -func (ls LabelSet) Contains(label string) bool { - _, ok := ls[label] +func (s LabelSet) Contains(label string) bool { + _, ok := s.elements[label] + return ok } // String returns the labels as a sorted, space-separated string. -// It implements the fmt.Stringer interface. -func (ls LabelSet) String() string { - labels := ls.List() +// +// Implements the fmt.Stringer interface. +func (s LabelSet) String() string { + labels := s.List() if len(labels) == 0 { return "" } @@ -47,38 +55,60 @@ func (ls LabelSet) String() string { } // List returns the labels as a sorted slice of strings. -func (ls LabelSet) List() []string { - if len(ls) == 0 { +func (s LabelSet) List() []string { + if len(s.elements) == 0 { return []string{} } // Collect labels into a slice - labels := make([]string, 0, len(ls)) - for label := range ls { - labels = append(labels, label) - } + labels := slices.Collect(maps.Keys(s.elements)) // Sort the labels to ensure consistent ordering - sort.Strings(labels) + slices.Sort(labels) return labels } // Equal checks if two LabelSets are equal. -func (ls LabelSet) Equal(other LabelSet) bool { - if len(ls) != len(other) { - return false - } - for label := range ls { - if _, ok := other[label]; !ok { - return false - } - } +func (s LabelSet) Equal(other LabelSet) bool { + return maps.Equal(s.elements, other.elements) +} - return true +// Len returns the number of labels in the set. +func (s LabelSet) Length() int { + return len(s.elements) } // IsEmpty checks if the LabelSet is empty. -func (ls LabelSet) IsEmpty() bool { - return len(ls) == 0 +func (s LabelSet) IsEmpty() bool { + return s.Length() == 0 +} + +// Clone creates a copy of the LabelSet. +func (s LabelSet) Clone() LabelSet { + return LabelSet{ + elements: maps.Clone(s.elements), + } +} + +// MarshalJSON marshals the LabelSet as a JSON array of strings. +// +// Implements the json.Marshaler interface. +func (s LabelSet) MarshalJSON() ([]byte, error) { + return json.Marshal(s.List()) +} + +// UnmarshalJSON unmarshals a JSON array of strings into the LabelSet. +// +// Implements the json.Unmarshaler interface. +func (s *LabelSet) UnmarshalJSON(data []byte) error { + var labels []string + if err := json.Unmarshal(data, &labels); err != nil { + return err + } + + // Initialize the LabelSet with the unmarshaled labels + *s = NewLabelSet(labels...) + + return nil } diff --git a/datastore/label_set_test.go b/datastore/label_set_test.go index 71f9840..7f9ca1e 100644 --- a/datastore/label_set_test.go +++ b/datastore/label_set_test.go @@ -1,28 +1,35 @@ package datastore import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestNewLabelSet(t *testing.T) { +func Test_NewLabelSet(t *testing.T) { t.Parallel() tests := []struct { - name string - input []string - expected []string + name string + give []string + want []string }{ { - name: "no labels", - input: []string{}, - expected: []string{}, + name: "no labels", + give: []string{}, + want: []string{}, }, { - name: "some labels", - input: []string{"foo", "bar"}, - expected: []string{"foo", "bar"}, + name: "some labels", + give: []string{"foo", "bar"}, + want: []string{"foo", "bar"}, + }, + { + name: "non unique labels", + give: []string{"foo", "bar", "foo"}, + want: []string{"foo", "bar"}, }, } @@ -30,72 +37,128 @@ func TestNewLabelSet(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - ms := NewLabelSet(tt.input...) - assert.Len(t, ms, len(tt.expected), "unexpected number of labels in the set") - for _, label := range tt.expected { - assert.True(t, ms.Contains(label), "expected label '%s' in the set", label) + got := NewLabelSet(tt.give...) + + require.Equal(t, len(tt.want), got.Length()) + for _, l := range tt.want { + assert.True(t, got.Contains(l), "expected label '%s' in the set", l) } }) } } -func TestLabelSet_Add(t *testing.T) { +func Test_LabelSet_Add(t *testing.T) { t.Parallel() - ms := NewLabelSet("initial") - ms.Add("new") + labels := NewLabelSet("initial") + labels.Add("new") - assert.True(t, ms.Contains("initial"), "expected 'initial' in set") - assert.True(t, ms.Contains("new"), "expected 'new' in set") - assert.Len(t, ms, 2, "expected 2 distinct labels in set") + require.Equal(t, 2, labels.Length()) + assert.True(t, labels.Contains("initial")) + assert.True(t, labels.Contains("new")) // Add duplicate "new" again; size should remain 2 - ms.Add("new") - assert.Len(t, ms, 2, "expected size to remain 2 after adding a duplicate") + labels.Add("new") + require.Equal(t, 2, labels.Length()) } -func TestLabelSet_Remove(t *testing.T) { +func Test_LabelSet_Remove(t *testing.T) { t.Parallel() - ms := NewLabelSet("remove_me", "keep") - ms.Remove("remove_me") + labels := NewLabelSet("remove_me", "keep") + labels.Remove("remove_me") - assert.False(t, ms.Contains("remove_me"), "expected 'remove_me' to be removed") - assert.True(t, ms.Contains("keep"), "expected 'keep' to remain") - assert.Len(t, ms, 1, "expected set size to be 1 after removal") + require.Equal(t, 1, labels.Length()) + assert.False(t, labels.Contains("remove_me")) + assert.True(t, labels.Contains("keep")) // Removing a non-existent item shouldn't change the size - ms.Remove("non_existent") - assert.Len(t, ms, 1, "expected size to remain 1 after removing a non-existent item") + labels.Remove("non_existent") + require.Equal(t, 1, labels.Length()) +} + +func Test_LabelSet_Contains(t *testing.T) { + t.Parallel() + + got := NewLabelSet("foo", "bar") + + assert.True(t, got.Contains("foo")) + assert.True(t, got.Contains("bar")) + assert.False(t, got.Contains("baz")) } -func TestLabelSet_Contains(t *testing.T) { +func Test_LabelSet_String(t *testing.T) { t.Parallel() - ms := NewLabelSet("foo", "bar") + tests := []struct { + name string + labels LabelSet + want string + }{ + { + name: "Empty LabelSet", + labels: NewLabelSet(), + want: "", + }, + { + name: "Single label", + labels: NewLabelSet("alpha"), + want: "alpha", + }, + { + name: "Multiple labels in random order", + labels: NewLabelSet("beta", "gamma", "alpha"), + want: "alpha beta gamma", + }, + { + name: "Labels with special characters", + labels: NewLabelSet("beta", "gamma!", "@alpha"), + want: "@alpha beta gamma!", + }, + { + name: "Labels with spaces", + labels: NewLabelSet("beta", "gamma delta", "alpha"), + want: "alpha beta gamma delta", + }, + { + name: "Labels added in different orders", + labels: NewLabelSet("delta", "beta", "alpha"), + want: "alpha beta delta", + }, + { + name: "Labels with duplicate additions", + labels: NewLabelSet("alpha", "beta", "alpha", "gamma", "beta"), + want: "alpha beta gamma", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() - assert.True(t, ms.Contains("foo")) - assert.True(t, ms.Contains("bar")) - assert.False(t, ms.Contains("baz")) + got := tt.labels.String() + assert.Equal(t, tt.want, got, "LabelSet.String() should return the expected sorted string") + }) + } } -func TestLabelSet_List(t *testing.T) { +func Test_LabelSet_List(t *testing.T) { t.Parallel() tests := []struct { - name string - input []string - expected []string + name string + give []string + want []string }{ { - name: "list with items", - input: []string{"foo", "bar", "baz"}, - expected: []string{"bar", "baz", "foo"}, + name: "list with items", + give: []string{"foo", "bar", "baz"}, + want: []string{"bar", "baz", "foo"}, }, { - name: "empty list", - input: []string{}, - expected: []string{}, + name: "empty list", + give: []string{}, + want: []string{}, }, } @@ -103,58 +166,77 @@ func TestLabelSet_List(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - ms := NewLabelSet(tt.input...) - labels := ms.List() + ms := NewLabelSet(tt.give...) + got := ms.List() - assert.Len(t, labels, len(tt.expected), "unexpected number of labels in the list") - assert.ElementsMatch(t, tt.expected, labels, "unexpected labels in the list") + assert.Len(t, got, len(tt.want), "unexpected number of labels in the list") + assert.ElementsMatch(t, tt.want, got, "unexpected labels in the list") }) } } -// TestLabelSet_String tests the String() method of the LabelSet type. -func TestLabelSet_String(t *testing.T) { +func Test_LabelSet_Equal(t *testing.T) { t.Parallel() tests := []struct { - name string - labels LabelSet - expected string + name string + set1 LabelSet + set2 LabelSet + want bool }{ { - name: "Empty LabelSet", - labels: NewLabelSet(), - expected: "", + name: "Both sets empty", + set1: NewLabelSet(), + set2: NewLabelSet(), + want: true, }, { - name: "Single label", - labels: NewLabelSet("alpha"), - expected: "alpha", + name: "First set empty, second set non-empty", + set1: NewLabelSet(), + set2: NewLabelSet("label1"), + want: false, }, { - name: "Multiple labels in random order", - labels: NewLabelSet("beta", "gamma", "alpha"), - expected: "alpha beta gamma", + name: "First set non-empty, second set empty", + set1: NewLabelSet("label1"), + set2: NewLabelSet(), + want: false, }, { - name: "Labels with special characters", - labels: NewLabelSet("beta", "gamma!", "@alpha"), - expected: "@alpha beta gamma!", + name: "Identical sets with single label", + set1: NewLabelSet("label1"), + set2: NewLabelSet("label1"), + want: true, }, { - name: "Labels with spaces", - labels: NewLabelSet("beta", "gamma delta", "alpha"), - expected: "alpha beta gamma delta", + name: "Identical sets with multiple labels", + set1: NewLabelSet("label1", "label2", "label3"), + set2: NewLabelSet("label3", "label2", "label1"), // Different order + want: true, }, { - name: "Labels added in different orders", - labels: NewLabelSet("delta", "beta", "alpha"), - expected: "alpha beta delta", + name: "Different sets, same size", + set1: NewLabelSet("label1", "label2", "label3"), + set2: NewLabelSet("label1", "label2", "label4"), + want: false, }, { - name: "Labels with duplicate additions", - labels: NewLabelSet("alpha", "beta", "alpha", "gamma", "beta"), - expected: "alpha beta gamma", + name: "Different sets, different sizes", + set1: NewLabelSet("label1", "label2"), + set2: NewLabelSet("label1", "label2", "label3"), + want: false, + }, + { + name: "Subset sets", + set1: NewLabelSet("label1", "label2"), + set2: NewLabelSet("label1", "label2", "label3"), + want: false, + }, + { + name: "Disjoint sets", + set1: NewLabelSet("label1", "label2"), + set2: NewLabelSet("label3", "label4"), + want: false, }, } @@ -162,74 +244,121 @@ func TestLabelSet_String(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result := tt.labels.String() - assert.Equal(t, tt.expected, result, "LabelSet.String() should return the expected sorted string") + got := tt.set1.Equal(tt.set2) + assert.Equal(t, tt.want, got, "Equal(%v, %v) should be %v", tt.set1, tt.set2, tt.want) }) } } -func TestLabelSet_Equal(t *testing.T) { +func Test_LabelSet_Length(t *testing.T) { + t.Parallel() + + got := NewLabelSet("foo", "bar", "baz") + require.Equal(t, 3, got.Length()) +} + +func Test_LabelSet_IsEmpty(t *testing.T) { t.Parallel() tests := []struct { - name string - set1 LabelSet - set2 LabelSet - expected bool + name string + give LabelSet + want bool }{ { - name: "Both sets empty", - set1: NewLabelSet(), - set2: NewLabelSet(), - expected: true, - }, - { - name: "First set empty, second set non-empty", - set1: NewLabelSet(), - set2: NewLabelSet("label1"), - expected: false, + name: "Empty set", + give: NewLabelSet(), + want: true, }, { - name: "First set non-empty, second set empty", - set1: NewLabelSet("label1"), - set2: NewLabelSet(), - expected: false, + name: "Non-empty set", + give: NewLabelSet("foo"), + want: false, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := tt.give.IsEmpty() + assert.Equal(t, tt.want, got, "IsEmpty() should return %v", tt.want) + }) + } +} + +func Test_LabelSet_Clone(t *testing.T) { + t.Parallel() + + original := NewLabelSet("foo", "bar", "baz") + clone := original.Clone() + + assert.Equal(t, original, clone, "Clone() should return an equal LabelSet") + assert.NotSame(t, &original, &clone, "Clone() should return a different LabelSet instance") + + // Modify the clone and check that the original is unchanged + clone.Add("new") + assert.NotEqual(t, original, clone, "Modifying the clone should not affect the original") +} + +func Test_LabelSet_MarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + give LabelSet + want string + }{ { - name: "Identical sets with single label", - set1: NewLabelSet("label1"), - set2: NewLabelSet("label1"), - expected: true, + name: "Empty set", + give: NewLabelSet(), + want: `[]`, }, { - name: "Identical sets with multiple labels", - set1: NewLabelSet("label1", "label2", "label3"), - set2: NewLabelSet("label3", "label2", "label1"), // Different order - expected: true, + name: "Single label", + give: NewLabelSet("foo"), + want: `["foo"]`, }, { - name: "Different sets, same size", - set1: NewLabelSet("label1", "label2", "label3"), - set2: NewLabelSet("label1", "label2", "label4"), - expected: false, + name: "Multiple labels", + give: NewLabelSet("foo", "bar"), + want: `["bar","foo"]`, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := json.Marshal(tt.give) + require.NoError(t, err) + assert.JSONEq(t, tt.want, string(got)) + }) + } +} + +func Test_LabelSet_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + give string + want LabelSet + }{ { - name: "Different sets, different sizes", - set1: NewLabelSet("label1", "label2"), - set2: NewLabelSet("label1", "label2", "label3"), - expected: false, + name: "Empty set", + give: `[]`, + want: NewLabelSet(), }, { - name: "Subset sets", - set1: NewLabelSet("label1", "label2"), - set2: NewLabelSet("label1", "label2", "label3"), - expected: false, + name: "Single label", + give: `["foo"]`, + want: NewLabelSet("foo"), }, { - name: "Disjoint sets", - set1: NewLabelSet("label1", "label2"), - set2: NewLabelSet("label3", "label4"), - expected: false, + name: "Multiple labels", + give: `["foo", "bar"]`, + want: NewLabelSet("bar", "foo"), }, } @@ -237,8 +366,10 @@ func TestLabelSet_Equal(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result := tt.set1.Equal(tt.set2) - assert.Equal(t, tt.expected, result, "Equal(%v, %v) should be %v", tt.set1, tt.set2, tt.expected) + var got LabelSet + err := json.Unmarshal([]byte(tt.give), &got) + require.NoError(t, err) + assert.Equal(t, tt.want, got) }) } }