Skip to content

Commit 7af7a51

Browse files
authored
feat(datastore) remove generics from the top level api (#149)
This PR introduces a leaner custom metadata handling model by replacing generics with `any`. While generics are still used to define interfaces, we now replace the use of generics for contract and environment metadata with any. This simplifies the API and removes the need for two conversion steps per changeset. Users can continue to work with their own metadata types. The only trade-off is that when reading from the datastore (e.g., after a Get()), they’ll need to convert from any, which is easily handled using the provided `As` helper. Overall, this change reduces boilerplate and improves usability without sacrificing flexibility.
1 parent 1134c51 commit 7af7a51

20 files changed

+305
-617
lines changed

.changeset/purple-needles-retire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
feat(datastore) remove generics from the top level api

datastore/contract_metadata.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,30 @@ var ErrContractMetadataNotFound = errors.New("no contract metadata record can be
66
var ErrContractMetadataExists = errors.New("a contract metadata record with the supplied key already exists")
77

88
// ContractMetadata implements the Record interface
9-
var _ UniqueRecord[ContractMetadataKey, ContractMetadata[DefaultMetadata]] = ContractMetadata[DefaultMetadata]{}
9+
var _ UniqueRecord[ContractMetadataKey, ContractMetadata] = ContractMetadata{}
1010

1111
// ContractMetadata is a generic struct that holds the metadata for a contract on a specific chain.
1212
// It implements the Record interface and is used to store contract metadata in the datastore.
1313
// The metadata is generic and can be of any type that implements the Cloneable interface.
14-
type ContractMetadata[M any] struct {
14+
type ContractMetadata struct {
1515
// Address is the address of the contract on the chain.
1616
Address string `json:"address"`
1717
// ChainSelector is the chain-selector of the chain where the contract is deployed.
1818
ChainSelector uint64 `json:"chainSelector"`
1919
// Metadata is the metadata associated with the contract.
2020
// It is a generic type that can be of any type that implements the Cloneable interface.
21-
Metadata M `json:"metadata"`
21+
Metadata any `json:"metadata"`
2222
}
2323

2424
// Clone creates a copy of the ContractMetadata.
2525
// The Metadata field is cloned using the Clone method of the Cloneable interface.
26-
func (r ContractMetadata[M]) Clone() (ContractMetadata[M], error) {
26+
func (r ContractMetadata) Clone() (ContractMetadata, error) {
2727
metaClone, err := clone(r.Metadata)
2828
if err != nil {
29-
return ContractMetadata[M]{}, err
29+
return ContractMetadata{}, err
3030
}
3131

32-
return ContractMetadata[M]{
32+
return ContractMetadata{
3333
ChainSelector: r.ChainSelector,
3434
Address: r.Address,
3535
Metadata: metaClone,
@@ -38,6 +38,6 @@ func (r ContractMetadata[M]) Clone() (ContractMetadata[M], error) {
3838

3939
// Key returns the ContractMetadataKey for the ContractMetadata.
4040
// It is used to uniquely identify the contract metadata in the datastore.
41-
func (r ContractMetadata[M]) Key() ContractMetadataKey {
41+
func (r ContractMetadata) Key() ContractMetadataKey {
4242
return NewContractMetadataKey(r.ChainSelector, r.Address)
4343
}

datastore/contract_metadata_test.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,55 @@ import (
44
"testing"
55

66
"github.com/stretchr/testify/require"
7+
8+
chain_selectors "github.com/smartcontractkit/chain-selectors"
79
)
810

911
func TestContractMetadata_Clone(t *testing.T) {
1012
t.Parallel()
1113

12-
original := ContractMetadata[DefaultMetadata]{
14+
original := ContractMetadata{
1315
ChainSelector: 1,
1416
Address: "0x123",
15-
Metadata: DefaultMetadata{Data: "test data"},
17+
Metadata: testMetadata{
18+
Field: "test field",
19+
ChainSelector: chain_selectors.APTOS_MAINNET.Selector,
20+
},
1621
}
1722

1823
cloned, err := original.Clone()
1924
require.NoError(t, err, "Clone should not return an error")
2025

2126
require.Equal(t, original.ChainSelector, cloned.ChainSelector)
2227
require.Equal(t, original.Address, cloned.Address)
23-
require.Equal(t, original.Metadata, cloned.Metadata)
28+
29+
concrete, err := As[testMetadata](cloned.Metadata)
30+
require.NoError(t, err, "As should not return an error for CustomMetadata")
31+
require.Equal(t, original.Metadata, concrete)
2432

2533
// Modify the original and ensure the cloned remains unchanged
2634
original.ChainSelector = 2
2735
original.Address = "0x456"
28-
original.Metadata = DefaultMetadata{Data: "updated data"}
36+
original.Metadata = testMetadata{
37+
Field: "updated field",
38+
ChainSelector: chain_selectors.APTOS_MAINNET.Selector,
39+
}
2940

3041
require.NotEqual(t, original.ChainSelector, cloned.ChainSelector)
3142
require.NotEqual(t, original.Address, cloned.Address)
32-
require.NotEqual(t, original.Metadata, cloned.Metadata)
43+
44+
concrete, err = As[testMetadata](cloned.Metadata)
45+
require.NoError(t, err, "As should not return an error for CustomMetadata")
46+
require.NotEqual(t, original.Metadata, concrete, "Cloned metadata should not be equal to modified original")
3347
}
3448

3549
func TestContractMetadata_Key(t *testing.T) {
3650
t.Parallel()
3751

38-
metadata := ContractMetadata[DefaultMetadata]{
52+
metadata := ContractMetadata{
3953
ChainSelector: 1,
4054
Address: "0x123",
41-
Metadata: DefaultMetadata{Data: "test data"},
55+
Metadata: testMetadata{Field: "test data", ChainSelector: 0},
4256
}
4357

4458
key := metadata.Key()

datastore/default_metadata.go

Lines changed: 0 additions & 9 deletions
This file was deleted.

datastore/env_metadata.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ import "errors"
44

55
var ErrEnvMetadataNotSet = errors.New("no environment metadata set")
66

7-
type EnvMetadata[M any] struct {
7+
type EnvMetadata struct {
88
// Metadata is the metadata associated with the domain and environment.
99
// It is a generic type that can be of any type that implements the Cloneable interface.
10-
Metadata M `json:"metadata"`
10+
Metadata any `json:"metadata"`
1111
}
1212

1313
// Clone creates a copy of the EnvMetadata.
1414
// The Metadata field is cloned using the Clone method of the Cloneable interface.
15-
func (r EnvMetadata[M]) Clone() (EnvMetadata[M], error) {
15+
func (r EnvMetadata) Clone() (EnvMetadata, error) {
1616
metaClone, err := clone(r.Metadata)
1717
if err != nil {
1818
// If cloning fails, we return an empty EnvMetadata with the error.
19-
return EnvMetadata[M]{}, err
19+
return EnvMetadata{}, err
2020
}
2121

22-
return EnvMetadata[M]{
22+
return EnvMetadata{
2323
Metadata: metaClone,
2424
}, nil
2525
}

datastore/env_metadata_test.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@ import (
99
func TestEnvMetadata_Clone(t *testing.T) {
1010
t.Parallel()
1111

12-
original := EnvMetadata[DefaultMetadata]{
13-
Metadata: DefaultMetadata{Data: "test-value"},
14-
}
12+
var (
13+
metaOne = testMetadata{Field: "test-value-one", ChainSelector: 0}
14+
metaTwo = testMetadata{Field: "test-value-two", ChainSelector: 0}
15+
original = EnvMetadata{
16+
Metadata: metaOne,
17+
}
18+
)
1519

1620
cloned, err := original.Clone()
1721
require.NoError(t, err, "Clone should not return an error")
1822

19-
require.Equal(t, original.Metadata, cloned.Metadata)
20-
require.NotSame(t, &original.Metadata, &cloned.Metadata) // Ensure Metadata is a deep copy
23+
concrete, err := As[testMetadata](cloned.Metadata)
24+
require.NoError(t, err, "As should not return an error for CustomMetadata")
25+
require.Equal(t, metaOne, concrete)
26+
27+
original.Metadata = metaTwo
28+
29+
concreteTwo, err := As[testMetadata](original.Metadata)
30+
require.NoError(t, err, "As should not return an error for CustomMetadata after modification")
31+
require.NotEqual(t, concrete, concreteTwo, "Cloned metadata should not be equal to modified original metadata")
2132
}

datastore/filters.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ func AddressRefByQualifier(qualifier string) FilterFunc[AddressRefKey, AddressRe
7575
}
7676

7777
// ContractMetadataByChainSelector returns a filter that only includes records with the provided chain.
78-
func ContractMetadataByChainSelector[M any](chainSelector uint64) FilterFunc[ContractMetadataKey, ContractMetadata[M]] {
79-
return func(records []ContractMetadata[M]) []ContractMetadata[M] {
80-
filtered := make([]ContractMetadata[M], 0, len(records)) // Pre-allocate capacity
78+
func ContractMetadataByChainSelector(chainSelector uint64) FilterFunc[ContractMetadataKey, ContractMetadata] {
79+
return func(records []ContractMetadata) []ContractMetadata {
80+
filtered := make([]ContractMetadata, 0, len(records)) // Pre-allocate capacity
8181
for _, record := range records {
8282
if record.ChainSelector == chainSelector {
8383
filtered = append(filtered, record)

datastore/filters_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -351,56 +351,56 @@ func TestContractMetadataByChainSelector(t *testing.T) {
351351
t.Parallel()
352352

353353
var (
354-
recordOne = ContractMetadata[DefaultMetadata]{
354+
recordOne = ContractMetadata{
355355
ChainSelector: 1,
356-
Metadata: DefaultMetadata{Data: "Record1"},
356+
Metadata: testMetadata{Field: "Record1", ChainSelector: 0},
357357
}
358-
recordTwo = ContractMetadata[DefaultMetadata]{
358+
recordTwo = ContractMetadata{
359359
ChainSelector: 2,
360-
Metadata: DefaultMetadata{Data: "Record2"},
360+
Metadata: testMetadata{Field: "Record2", ChainSelector: 0},
361361
}
362-
recordThree = ContractMetadata[DefaultMetadata]{
362+
recordThree = ContractMetadata{
363363
ChainSelector: 1,
364-
Metadata: DefaultMetadata{Data: "Record3"},
364+
Metadata: testMetadata{Field: "Record3", ChainSelector: 0},
365365
}
366366
)
367367

368368
tests := []struct {
369369
name string
370-
givenState []ContractMetadata[DefaultMetadata]
370+
givenState []ContractMetadata
371371
giveChain uint64
372-
expectedResult []ContractMetadata[DefaultMetadata]
372+
expectedResult []ContractMetadata
373373
}{
374374
{
375375
name: "success: returns records with given chain",
376-
givenState: []ContractMetadata[DefaultMetadata]{
376+
givenState: []ContractMetadata{
377377
recordOne,
378378
recordTwo,
379379
recordThree,
380380
},
381381
giveChain: 1,
382-
expectedResult: []ContractMetadata[DefaultMetadata]{
382+
expectedResult: []ContractMetadata{
383383
recordOne,
384384
recordThree,
385385
},
386386
},
387387
{
388388
name: "success: returns no records with given chain",
389-
givenState: []ContractMetadata[DefaultMetadata]{
389+
givenState: []ContractMetadata{
390390
recordOne,
391391
recordTwo,
392392
recordThree,
393393
},
394394
giveChain: 3,
395-
expectedResult: []ContractMetadata[DefaultMetadata]{},
395+
expectedResult: []ContractMetadata{},
396396
},
397397
}
398398

399399
for _, tt := range tests {
400400
t.Run(tt.name, func(t *testing.T) {
401401
t.Parallel()
402402

403-
filter := ContractMetadataByChainSelector[DefaultMetadata](tt.giveChain)
403+
filter := ContractMetadataByChainSelector(tt.giveChain)
404404
filteredRecords := filter(tt.givenState)
405405
assert.Equal(t, tt.expectedResult, filteredRecords)
406406
})

datastore/memory_contract_metadata_store.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,54 @@ import (
66

77
// ContractMetadataStore is an interface that represents an immutable view over a set
88
// of ContractMetadata records identified by ContractMetadataKey.
9-
type ContractMetadataStore[M any] interface {
10-
Store[ContractMetadataKey, ContractMetadata[M]]
9+
type ContractMetadataStore interface {
10+
Store[ContractMetadataKey, ContractMetadata]
1111
}
1212

1313
// MutableContractMetadataStore is an interface that represents a mutable ContractMetadataStore
1414
// of ContractMetadata records identified by ContractMetadataKey.
15-
type MutableContractMetadataStore[M any] interface {
16-
MutableStore[ContractMetadataKey, ContractMetadata[M]]
15+
type MutableContractMetadataStore interface {
16+
MutableStore[ContractMetadataKey, ContractMetadata]
1717
}
1818

1919
// MemoryContractMetadataStore is an in-memory implementation of the ContractMetadataStore and
2020
// MutableContractMetadataStore interfaces.
21-
type MemoryContractMetadataStore[M any] struct {
21+
type MemoryContractMetadataStore struct {
2222
mu sync.RWMutex
23-
Records []ContractMetadata[M] `json:"records"`
23+
Records []ContractMetadata `json:"records"`
2424
}
2525

2626
// MemoryContractMetadataStore implements ContractMetadataStore interface.
27-
var _ ContractMetadataStore[DefaultMetadata] = &MemoryContractMetadataStore[DefaultMetadata]{}
27+
var _ ContractMetadataStore = &MemoryContractMetadataStore{}
2828

2929
// MemoryContractMetadataStore implements MutableContractMetadataStore interface.
30-
var _ MutableContractMetadataStore[DefaultMetadata] = &MemoryContractMetadataStore[DefaultMetadata]{}
30+
var _ MutableContractMetadataStore = &MemoryContractMetadataStore{}
3131

3232
// NewMemoryContractMetadataStore creates a new MemoryContractMetadataStore instance.
3333
// It is a generic function that takes a type parameter M which must implement the Cloneable interface.
34-
func NewMemoryContractMetadataStore[M any]() *MemoryContractMetadataStore[M] {
35-
return &MemoryContractMetadataStore[M]{Records: []ContractMetadata[M]{}}
34+
func NewMemoryContractMetadataStore() *MemoryContractMetadataStore {
35+
return &MemoryContractMetadataStore{Records: []ContractMetadata{}}
3636
}
3737

3838
// Get returns the ContractMetadata for the provided key, or an error if no such record exists.
39-
func (s *MemoryContractMetadataStore[M]) Get(key ContractMetadataKey) (ContractMetadata[M], error) {
39+
func (s *MemoryContractMetadataStore) Get(key ContractMetadataKey) (ContractMetadata, error) {
4040
s.mu.RLock()
4141
defer s.mu.RUnlock()
4242

4343
idx := s.indexOf(key)
4444
if idx == -1 {
45-
return ContractMetadata[M]{}, ErrContractMetadataNotFound
45+
return ContractMetadata{}, ErrContractMetadataNotFound
4646
}
4747

4848
return s.Records[idx].Clone()
4949
}
5050

5151
// Fetch returns a copy of all ContractMetadata in the store.
52-
func (s *MemoryContractMetadataStore[M]) Fetch() ([]ContractMetadata[M], error) {
52+
func (s *MemoryContractMetadataStore) Fetch() ([]ContractMetadata, error) {
5353
s.mu.RLock()
5454
defer s.mu.RUnlock()
5555

56-
records := []ContractMetadata[M]{}
56+
records := []ContractMetadata{}
5757
for _, record := range s.Records {
5858
clone, err := record.Clone()
5959
if err != nil {
@@ -68,11 +68,11 @@ func (s *MemoryContractMetadataStore[M]) Fetch() ([]ContractMetadata[M], error)
6868
// Filter returns a copy of all ContractMetadata in the store that pass all of the provided filters.
6969
// Filters are applied in the order they are provided.
7070
// If no filters are provided, all records are returned.
71-
func (s *MemoryContractMetadataStore[M]) Filter(filters ...FilterFunc[ContractMetadataKey, ContractMetadata[M]]) []ContractMetadata[M] {
71+
func (s *MemoryContractMetadataStore) Filter(filters ...FilterFunc[ContractMetadataKey, ContractMetadata]) []ContractMetadata {
7272
s.mu.RLock()
7373
defer s.mu.RUnlock()
7474

75-
records := append([]ContractMetadata[M]{}, s.Records...)
75+
records := append([]ContractMetadata{}, s.Records...)
7676
for _, filter := range filters {
7777
records = filter(records)
7878
}
@@ -81,7 +81,7 @@ func (s *MemoryContractMetadataStore[M]) Filter(filters ...FilterFunc[ContractMe
8181
}
8282

8383
// indexOf returns the index of the record with the provided key, or -1 if no such record exists.
84-
func (s *MemoryContractMetadataStore[M]) indexOf(key ContractMetadataKey) int {
84+
func (s *MemoryContractMetadataStore) indexOf(key ContractMetadataKey) int {
8585
for i, record := range s.Records {
8686
if record.Key().Equals(key) {
8787
return i
@@ -93,7 +93,7 @@ func (s *MemoryContractMetadataStore[M]) indexOf(key ContractMetadataKey) int {
9393

9494
// Add inserts a new record into the store.
9595
// If a record with the same key already exists, an error is returned.
96-
func (s *MemoryContractMetadataStore[M]) Add(record ContractMetadata[M]) error {
96+
func (s *MemoryContractMetadataStore) Add(record ContractMetadata) error {
9797
s.mu.Lock()
9898
defer s.mu.Unlock()
9999

@@ -108,7 +108,7 @@ func (s *MemoryContractMetadataStore[M]) Add(record ContractMetadata[M]) error {
108108

109109
// Upsert inserts a new record into the store if no record with the same key already exists.
110110
// If a record with the same key already exists, it is updated.
111-
func (s *MemoryContractMetadataStore[M]) Upsert(record ContractMetadata[M]) error {
111+
func (s *MemoryContractMetadataStore) Upsert(record ContractMetadata) error {
112112
s.mu.Lock()
113113
defer s.mu.Unlock()
114114

@@ -125,7 +125,7 @@ func (s *MemoryContractMetadataStore[M]) Upsert(record ContractMetadata[M]) erro
125125
// Update edits an existing record whose fields match the primary key elements of the supplied ContractMetadata, with
126126
// the non-primary-key values of the supplied ContractMetadata.
127127
// If no such record exists, an error is returned.
128-
func (s *MemoryContractMetadataStore[M]) Update(record ContractMetadata[M]) error {
128+
func (s *MemoryContractMetadataStore) Update(record ContractMetadata) error {
129129
s.mu.Lock()
130130
defer s.mu.Unlock()
131131

@@ -140,7 +140,7 @@ func (s *MemoryContractMetadataStore[M]) Update(record ContractMetadata[M]) erro
140140

141141
// Delete deletes an existing record whose primary key elements match the supplied ContractMetadata, returning an error if no
142142
// such record exists.
143-
func (s *MemoryContractMetadataStore[M]) Delete(key ContractMetadataKey) error {
143+
func (s *MemoryContractMetadataStore) Delete(key ContractMetadataKey) error {
144144
s.mu.Lock()
145145
defer s.mu.Unlock()
146146

0 commit comments

Comments
 (0)