From e43e02a28042d4ef8702efdc6dd96b1c3c5dec9e Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:43:53 +0100 Subject: [PATCH] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../gno.land/p/moul/collection/collection.gno | 144 ++++++------------ .../p/moul/collection/collection_test.gno | 123 ++++++--------- 2 files changed, 96 insertions(+), 171 deletions(-) diff --git a/examples/gno.land/p/moul/collection/collection.gno b/examples/gno.land/p/moul/collection/collection.gno index 34fb78561a1..2a8533321ae 100644 --- a/examples/gno.land/p/moul/collection/collection.gno +++ b/examples/gno.land/p/moul/collection/collection.gno @@ -1,84 +1,48 @@ -// Package collection provides a flexible, indexed data structure for storing and retrieving objects. -// It supports multiple indexes with various options including unique constraints, case-insensitive -// matching, sparse indexing, and multi-value indexes. +// Package collection provides a generic collection implementation with support for +// multiple indexes, including unique indexes, multi-value indexes, and case-insensitive +// indexes. It is designed to be used with any type and allows efficient lookups using +// different fields or computed values. // -// Example usage: -// -// // Define a data type -// type User struct { -// Name string -// Email string -// Age int -// Username string -// Tags []string -// } +// Basic usage: // // // Create a new collection // c := collection.New() // -// // Add indexes with different options +// // Add various types of indexes // c.AddIndex("name", func(v interface{}) string { // return v.(*User).Name // }, UniqueIndex) // // c.AddIndex("email", func(v interface{}) string { // return v.(*User).Email -// }, UniqueIndex|CaseInsensitiveIndex) +// }, UniqueIndex | CaseInsensitiveIndex) // // c.AddIndex("age", func(v interface{}) string { // return strconv.Itoa(v.(*User).Age) -// }, DefaultIndex) // Non-unique index -// -// c.AddIndex("username", func(v interface{}) string { -// return v.(*User).Username -// }, UniqueIndex|SparseIndex) // Allow empty usernames +// }, DefaultIndex) // // c.AddIndex("tags", func(v interface{}) []string { // return v.(*User).Tags -// }, MultiValueIndex) // One object can have multiple tag values -// -// // Insert objects -// user1 := &User{ -// Name: "Alice Smith", -// Email: "alice@example.com", -// Age: 30, -// Username: "alice123", -// Tags: []string{"admin", "developer"}, -// } -// id1 := c.Set(user1) -// -// user2 := &User{ -// Name: "Bob Jones", -// Email: "BOB@EXAMPLE.COM", // Case-insensitive, will be stored lowercase -// Age: 30, // Same age as Alice (allowed, non-unique) -// Tags: []string{"developer"}, -// } -// id2 := c.Set(user2) -// -// // Retrieve objects in different ways -// obj1, id := c.Get("name", "Alice Smith") // By unique name -// obj2, id := c.Get("email", "bob@example.com") // Case-insensitive -// obj3, id := c.Get(IDIndex, id1.String()) // Direct by ID -// -// // Retrieve all objects matching a multi-value index -// entries, ok := c.GetAll("tags", "developer") // Get all users with "developer" tag -// if ok { -// for _, entry := range entries { -// user := entry.Obj.(*User) -// ufmt.Printf("Developer: %s (ID: %s)\n", user.Name, entry.ID) -// } -// } +// }, MultiValueIndex) // -// // Update an object -// user1.Email = "alice.smith@example.com" -// c.Update(id1, user1) +// // Store an object +// id := c.Set(&User{ +// Name: "Alice", +// Email: "alice@example.com", +// Age: 30, +// Tags: []string{"admin", "staff"}, +// }) // -// // Delete an object -// c.Delete(id2) +// // Retrieve by any index +// entry = c.Get("email", "alice@example.com") +// entries := c.GetAll("tags", "admin") // -// The package maintains consistency across all indexes and enforces uniqueness -// constraints where specified. It's particularly useful for building indexed -// collections where objects need to be retrieved efficiently by different fields. +// Index options can be combined using the bitwise OR operator: +// - UniqueIndex: Ensures values are unique within the index +// - MultiValueIndex: Allows storing multiple values for a single key +// - CaseInsensitiveIndex: Makes string comparisons case-insensitive +// - SparseIndex: Skips indexing empty values +// - DefaultIndex: Regular index with no special behavior package collection import ( @@ -329,27 +293,19 @@ func (c *Collection) Set(obj interface{}) uint64 { return uint64(id) } -// Get retrieves an object by index and key, returns (object, id). -// -// If it's a MultiValueIndex, only the first found ID is returned. -// For a more complete "GetAll", you'll need a separate method to iterate -// all stored IDs in that key's slice. -func (c *Collection) Get(indexName, key string) (interface{}, uint64) { +// Get retrieves an object by index and key, returns an Entry or nil if not found. +func (c *Collection) Get(indexName, key string) *Entry { idx, exists := c.indexes[indexName] if !exists { - return nil, 0 + return nil } if indexName == IDIndex { obj, exists := idx.tree.Get(key) if !exists { - return nil, 0 - } - id, err := seqid.FromString(key) - if err != nil { - return nil, 0 + return nil } - return obj, uint64(id) + return &Entry{ID: key, Obj: obj} } // For other indexes @@ -359,42 +315,34 @@ func (c *Collection) Get(indexName, key string) (interface{}, uint64) { idData, exists := idx.tree.Get(key) if !exists { - return nil, 0 + return nil } // If multi-value, just return the first object found if idx.options&MultiValueIndex != 0 { list, ok := idData.([]string) if !ok || len(list) == 0 { - return nil, 0 + return nil } // Get the first ID from the slice idStr := list[0] obj, exists := c.indexes[IDIndex].tree.Get(idStr) if !exists { - return nil, 0 - } - idVal, err := seqid.FromString(idStr) - if err != nil { - return nil, 0 + return nil } - return obj, uint64(idVal) + return &Entry{ID: idStr, Obj: obj} } // single-value index idStr, ok := idData.(string) if !ok { - return nil, 0 + return nil } obj, exists := c.indexes[IDIndex].tree.Get(idStr) if !exists { - return nil, 0 - } - idVal, err := seqid.FromString(idStr) - if err != nil { - return nil, 0 + return nil } - return obj, uint64(idVal) + return &Entry{ID: idStr, Obj: obj} } // Entry represents a single object with its ID in the collection @@ -404,10 +352,10 @@ type Entry struct { } // GetAll retrieves all entries matching the given key in the specified index. -func (c *Collection) GetAll(indexName, key string) ([]Entry, bool) { +func (c *Collection) GetAll(indexName, key string) []Entry { idx, exists := c.indexes[indexName] if !exists { - return nil, false + return nil } if idx.options&CaseInsensitiveIndex != 0 { @@ -417,14 +365,14 @@ func (c *Collection) GetAll(indexName, key string) ([]Entry, bool) { // Special handling for ID index if indexName == IDIndex { if obj, exists := idx.tree.Get(key); exists { - return []Entry{{ID: key, Obj: obj}}, true + return []Entry{{ID: key, Obj: obj}} } - return nil, false + return nil } idData, exists := idx.tree.Get(key) if !exists { - return nil, false + return nil } // For multi-value indexes @@ -436,18 +384,18 @@ func (c *Collection) GetAll(indexName, key string) ([]Entry, bool) { result = append(result, Entry{ID: idStr, Obj: obj}) } } - return result, len(result) > 0 + return result } - return nil, false + return nil } // For single-value indexes if idStr, ok := idData.(string); ok { if obj, exists := c.indexes[IDIndex].tree.Get(idStr); exists { - return []Entry{{ID: idStr, Obj: obj}}, true + return []Entry{{ID: idStr, Obj: obj}} } } - return nil, false + return nil } // GetIndex returns the underlying tree for an index diff --git a/examples/gno.land/p/moul/collection/collection_test.gno b/examples/gno.land/p/moul/collection/collection_test.gno index 4a03621ad10..e7329b29d7d 100644 --- a/examples/gno.land/p/moul/collection/collection_test.gno +++ b/examples/gno.land/p/moul/collection/collection_test.gno @@ -51,11 +51,11 @@ func TestBasicOperations(t *testing.T) { } // Get by ID - obj1, gotId := c.Get(IDIndex, seqid.ID(id1).String()) - if gotId == 0 { + entry := c.Get(IDIndex, seqid.ID(id1).String()) + if entry == nil { t.Error("Failed to get object by ID") } - if obj1.(*Person).Name != "Alice" { + if entry.Obj.(*Person).Name != "Alice" { t.Error("Got wrong object") } } @@ -198,9 +198,9 @@ func TestDelete(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c.Delete(tt.id) - _, gotId := c.Get(IDIndex, seqid.ID(tt.id).String()) - if (gotId != 0) != tt.wantObj { - t.Errorf("After Delete(), Get() returned object: %v, want object: %v", gotId != 0, tt.wantObj) + entry := c.Get(IDIndex, seqid.ID(tt.id).String()) + if entry != nil { + t.Errorf("After Delete(), Get() returned object: %v, want object: %v", entry != nil, tt.wantObj) } }) } @@ -215,7 +215,6 @@ func TestEdgeCases(t *testing.T) { tests := []struct { name string operation func() uint64 - wantID uint64 wantPanic bool }{ { @@ -223,7 +222,6 @@ func TestEdgeCases(t *testing.T) { operation: func() uint64 { return c.Set(nil) }, - wantID: 0, wantPanic: false, }, { @@ -231,7 +229,6 @@ func TestEdgeCases(t *testing.T) { operation: func() uint64 { return c.Set("not a person") }, - wantID: 0, wantPanic: false, }, { @@ -240,16 +237,21 @@ func TestEdgeCases(t *testing.T) { id := c.Set(&Person{Name: "Test"}) return c.Update(id, nil) }, - wantID: 0, wantPanic: false, }, { name: "Get with invalid index name", operation: func() uint64 { - _, id := c.Get("invalid_index", "key") - return id + entry := c.Get("invalid_index", "key") + if entry == nil { + return 0 + } + id, err := seqid.FromString(entry.ID) + if err != nil { + return 0 + } + return uint64(id) }, - wantID: 0, wantPanic: false, }, } @@ -271,8 +273,8 @@ func TestEdgeCases(t *testing.T) { if panicked != tt.wantPanic { t.Errorf("Operation panicked = %v, want panic = %v", panicked, tt.wantPanic) } - if !panicked && gotID != tt.wantID { - t.Errorf("Operation returned %v, want %v", gotID, tt.wantID) + if !panicked && gotID != 0 { + t.Errorf("Operation returned %v, want 0", gotID) } }) } @@ -391,10 +393,10 @@ func TestConcurrentOperations(t *testing.T) { id1 := c.Set(p1) // Get and Update simultaneously - obj, gotId := c.Get(IDIndex, seqid.ID(id1).String()) + entry := c.Get(IDIndex, seqid.ID(id1).String()) newID := c.Update(id1, &Person{Name: "Alice2"}) - if gotId == 0 || newID == 0 { + if entry == nil || newID == 0 { t.Error("Concurrent operations failed") } } @@ -560,12 +562,12 @@ func TestCaseInsensitiveGet(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - obj, gotId := c.Get("email", tt.key) - if (obj != nil) != tt.wantObj { - t.Errorf("Get() got object = %v, want object = %v", obj != nil, tt.wantObj) + entry := c.Get("email", tt.key) + if (entry != nil) != tt.wantObj { + t.Errorf("Get() got object = %v, want object = %v", entry != nil, tt.wantObj) } - if tt.wantObj && gotId != id { - t.Errorf("Get() got id = %v, want id = %v", gotId, id) + if tt.wantObj && entry.ID != seqid.ID(id).String() { + t.Errorf("Get() got id = %v, want id = %v", entry.ID, seqid.ID(id).String()) } }) } @@ -573,9 +575,9 @@ func TestCaseInsensitiveGet(t *testing.T) { func TestGetInvalidID(t *testing.T) { c := New() - obj, id := c.Get(IDIndex, "not-a-valid-id") - if obj != nil || id != 0 { - t.Errorf("Get() with invalid ID format got (obj=%v, id=%v), want (nil, 0)", obj, id) + entry := c.Get(IDIndex, "not-a-valid-id") + if entry != nil { + t.Errorf("Get() with invalid ID format got entry=%v, want nil", entry) } } @@ -687,8 +689,8 @@ func TestMultiValueDelete(t *testing.T) { } // Double-check that Alice is not retrievable by her ID - _, gotId := c.Get(IDIndex, seqid.ID(idAlice).String()) - if gotId != 0 { + entry := c.Get(IDIndex, seqid.ID(idAlice).String()) + if entry != nil { t.Errorf("Expected Alice with ID %v to be deleted but she is still present", idAlice) } } @@ -725,72 +727,54 @@ func TestGetAll(t *testing.T) { indexName string key string wantLen int - wantOK bool }{ { name: "Multi-value index with multiple matches", indexName: "tags", key: "dev", wantLen: 3, - wantOK: true, }, { name: "Multi-value index with single match", indexName: "tags", key: "go", wantLen: 1, - wantOK: true, }, { name: "Multi-value index with no matches", indexName: "tags", key: "java", wantLen: 0, - wantOK: false, }, { name: "Single-value non-unique index with multiple matches", indexName: "age", key: "30", wantLen: 1, // GetAll still returns just one entry for non-MultiValueIndex - wantOK: true, }, { name: "Single-value unique index", indexName: "name", key: "Alice", wantLen: 1, - wantOK: true, }, { name: "Non-existent index", indexName: "invalid", key: "value", wantLen: 0, - wantOK: false, }, { name: "ID index", indexName: IDIndex, key: seqid.ID(ids[0]).String(), wantLen: 1, - wantOK: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - entries, ok := c.GetAll(tt.indexName, tt.key) - if ok != tt.wantOK { - t.Errorf("GetAll() ok = %v, want %v", ok, tt.wantOK) - } - - if !ok { - if entries != nil { - t.Error("GetAll() returned non-nil list when ok is false") - } - return - } + entries := c.GetAll(tt.indexName, tt.key) if len(entries) != tt.wantLen { t.Errorf("GetAll() returned %d entries, want %d", len(entries), tt.wantLen) @@ -822,47 +806,40 @@ func TestGetAllWithCaseInsensitive(t *testing.T) { } tests := []struct { - name string - key string - wantOK bool + name string + key string + wantLen int }{ { - name: "Exact match", - key: "Test@Example.com", - wantOK: true, + name: "Exact match", + key: "Test@Example.com", + wantLen: 1, }, { - name: "Different case", - key: "test@example.COM", - wantOK: true, + name: "Different case", + key: "test@example.COM", + wantLen: 1, }, { - name: "Non-existent", - key: "other@example.com", - wantOK: false, + name: "Non-existent", + key: "other@example.com", + wantLen: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - entries, ok := c.GetAll("email", tt.key) - if ok != tt.wantOK { - t.Errorf("GetAll() ok = %v, want %v", ok, tt.wantOK) - } - if !ok { - if entries != nil { - t.Error("GetAll() returned non-nil list when ok is false") - } - return - } + entries := c.GetAll("email", tt.key) - if len(entries) != 1 { - t.Errorf("GetAll() returned %d entries, want 1", len(entries)) + if len(entries) != tt.wantLen { + t.Errorf("GetAll() returned %d entries, want %d", len(entries), tt.wantLen) } - entry := entries[0] - if entry.ID != seqid.ID(id).String() { - t.Errorf("GetAll() returned ID %s, want %s", entry.ID, seqid.ID(id).String()) + if tt.wantLen > 0 { + entry := entries[0] + if entry.ID != seqid.ID(id).String() { + t.Errorf("GetAll() returned ID %s, want %s", entry.ID, seqid.ID(id).String()) + } } }) }