diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index f5f909a473d..cb48e8f5ba4 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -1,223 +1,36 @@ package pager import ( - "math" - "net/url" - "strconv" - "gno.land/p/demo/avl" - "gno.land/p/demo/ufmt" + "gno.land/p/moul/pageable" ) -// Pager is a struct that holds the AVL tree and pagination parameters. -type Pager struct { - Tree avl.ITree - PageQueryParam string - SizeQueryParam string - DefaultPageSize int - Reversed bool -} - -// Page represents a single page of results. -type Page struct { - Items []Item - PageNumber int - PageSize int - TotalItems int - TotalPages int - HasPrev bool - HasNext bool - Pager *Pager // Reference to the parent Pager -} - -// Item represents a key-value pair in the AVL tree. -type Item struct { - Key string - Value interface{} -} - // NewPager creates a new Pager with default values. -func NewPager(tree avl.ITree, defaultPageSize int, reversed bool) *Pager { - return &Pager{ - Tree: tree, - PageQueryParam: "page", - SizeQueryParam: "size", - DefaultPageSize: defaultPageSize, - Reversed: reversed, - } +func NewPager(tree avl.ITree, defaultPageSize int, reversed bool) *pageable.Pager { + wrappedTree := NewAVLWrapper(tree) + return pageable.NewPager(wrappedTree, defaultPageSize, reversed) } -// GetPage retrieves a page of results from the AVL tree. -func (p *Pager) GetPage(pageNumber int) *Page { - return p.GetPageWithSize(pageNumber, p.DefaultPageSize) +// AVLWrapper adapts an avl.ITree to implement Pageable +type AVLWrapper struct { + tree avl.ITree } -func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { - totalItems := p.Tree.Size() - totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize))) - - page := &Page{ - TotalItems: totalItems, - TotalPages: totalPages, - PageSize: pageSize, - Pager: p, - } - - // pages without content - if pageSize < 1 { - return page - } - - // page number provided is not available - if pageNumber < 1 { - page.HasNext = totalPages > 0 - return page - } - - // page number provided is outside the range of total pages - if pageNumber > totalPages { - page.PageNumber = pageNumber - page.HasPrev = pageNumber > 0 - return page - } - - startIndex := (pageNumber - 1) * pageSize - endIndex := startIndex + pageSize - if endIndex > totalItems { - endIndex = totalItems - } - - items := []Item{} - - if p.Reversed { - p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { - items = append(items, Item{Key: key, Value: value}) - return false - }) - } else { - p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { - items = append(items, Item{Key: key, Value: value}) - return false - }) - } - - page.Items = items - page.PageNumber = pageNumber - page.HasPrev = pageNumber > 1 - page.HasNext = pageNumber < totalPages - return page +func NewAVLWrapper(tree avl.ITree) *AVLWrapper { + return &AVLWrapper{tree: tree} } -func (p *Pager) MustGetPageByPath(rawURL string) *Page { - page, err := p.GetPageByPath(rawURL) - if err != nil { - panic("invalid path") - } - return page +func (w *AVLWrapper) Size() int { + return w.tree.Size() } -// GetPageByPath retrieves a page of results based on the query parameters in the URL path. -func (p *Pager) GetPageByPath(rawURL string) (*Page, error) { - pageNumber, pageSize, err := p.ParseQuery(rawURL) - if err != nil { - return nil, err - } - return p.GetPageWithSize(pageNumber, pageSize), nil -} - -// Picker generates the Markdown UI for the page Picker -func (p *Page) Picker() string { - pageNumber := p.PageNumber - pageNumber = max(pageNumber, 1) - - if p.TotalPages <= 1 { - return "" - } - - md := "" - - if p.HasPrev { - // Always show the first page link - md += ufmt.Sprintf("[%d](?%s=%d) | ", 1, p.Pager.PageQueryParam, 1) - - // Before - if p.PageNumber > 4 { - md += "… | " - } - - if p.PageNumber > 3 { - md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2) - } - - if p.PageNumber > 2 { - md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1) - } - } - - if p.PageNumber > 0 && p.PageNumber <= p.TotalPages { - // Current page - md += ufmt.Sprintf("**%d**", p.PageNumber) - } else { - md += ufmt.Sprintf("_%d_", p.PageNumber) - } - - if p.HasNext { - md += " | " - - if p.PageNumber < p.TotalPages-1 { - md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1) - } - - if p.PageNumber < p.TotalPages-2 { - md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2) - } - - if p.PageNumber < p.TotalPages-3 { - md += "… | " - } - - // Always show the last page link - md += ufmt.Sprintf("[%d](?%s=%d)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages) - } - - return md -} - -// ParseQuery parses the URL to extract the page number and page size. -func (p *Pager) ParseQuery(rawURL string) (int, int, error) { - u, err := url.Parse(rawURL) - if err != nil { - return 1, p.DefaultPageSize, err - } - - query := u.Query() - pageNumber := 1 - pageSize := p.DefaultPageSize - - if p.PageQueryParam != "" { - if pageStr := query.Get(p.PageQueryParam); pageStr != "" { - pageNumber, err = strconv.Atoi(pageStr) - if err != nil || pageNumber < 1 { - pageNumber = 1 - } - } - } - - if p.SizeQueryParam != "" { - if sizeStr := query.Get(p.SizeQueryParam); sizeStr != "" { - pageSize, err = strconv.Atoi(sizeStr) - if err != nil || pageSize < 1 { - pageSize = p.DefaultPageSize - } - } - } - - return pageNumber, pageSize, nil -} - -func max(a, b int) int { - if a > b { - return a +func (w *AVLWrapper) IterateByOffset(offset int, count int, cb func(index interface{}, value interface{}) bool) bool { + if count < 0 { + return w.tree.ReverseIterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, value) + }) } - return b + return w.tree.IterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, value) + }) } diff --git a/examples/gno.land/p/demo/avl/pager/pager_test.gno b/examples/gno.land/p/demo/avl/pager/pager_test.gno index 9869924e5b5..de65572890b 100644 --- a/examples/gno.land/p/demo/avl/pager/pager_test.gno +++ b/examples/gno.land/p/demo/avl/pager/pager_test.gno @@ -7,6 +7,7 @@ import ( "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" + "gno.land/p/moul/pageable" ) func TestPager_GetPage(t *testing.T) { @@ -26,15 +27,15 @@ func TestPager_GetPage(t *testing.T) { tests := []struct { pageNumber int pageSize int - expected []Item + expected []pageable.Item }{ - {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, - {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, - {3, 2, []Item{{Key: "e", Value: 5}}}, - {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, - {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {2, 5, []Item{}}, + {1, 2, []pageable.Item{{Index: "a", Value: 1}, {Index: "b", Value: 2}}}, + {2, 2, []pageable.Item{{Index: "c", Value: 3}, {Index: "d", Value: 4}}}, + {3, 2, []pageable.Item{{Index: "e", Value: 5}}}, + {1, 3, []pageable.Item{{Index: "a", Value: 1}, {Index: "b", Value: 2}, {Index: "c", Value: 3}}}, + {2, 3, []pageable.Item{{Index: "d", Value: 4}, {Index: "e", Value: 5}}}, + {1, 5, []pageable.Item{{Index: "a", Value: 1}, {Index: "b", Value: 2}, {Index: "c", Value: 3}, {Index: "d", Value: 4}, {Index: "e", Value: 5}}}, + {2, 5, []pageable.Item{}}, } for _, tt := range tests { @@ -43,7 +44,7 @@ func TestPager_GetPage(t *testing.T) { uassert.Equal(t, len(tt.expected), len(page.Items)) for i, item := range page.Items { - uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Index, item.Index) uassert.Equal(t, tt.expected[i].Value, item.Value) } } @@ -57,15 +58,15 @@ func TestPager_GetPage(t *testing.T) { tests := []struct { pageNumber int pageSize int - expected []Item + expected []pageable.Item }{ - {1, 2, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}}}, - {2, 2, []Item{{Key: "c", Value: 3}, {Key: "b", Value: 2}}}, - {3, 2, []Item{{Key: "a", Value: 1}}}, - {1, 3, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}}}, - {2, 3, []Item{{Key: "b", Value: 2}, {Key: "a", Value: 1}}}, - {1, 5, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}, {Key: "b", Value: 2}, {Key: "a", Value: 1}}}, - {2, 5, []Item{}}, + {1, 2, []pageable.Item{{Index: "e", Value: 5}, {Index: "d", Value: 4}}}, + {2, 2, []pageable.Item{{Index: "c", Value: 3}, {Index: "b", Value: 2}}}, + {3, 2, []pageable.Item{{Index: "a", Value: 1}}}, + {1, 3, []pageable.Item{{Index: "e", Value: 5}, {Index: "d", Value: 4}, {Index: "c", Value: 3}}}, + {2, 3, []pageable.Item{{Index: "b", Value: 2}, {Index: "a", Value: 1}}}, + {1, 5, []pageable.Item{{Index: "e", Value: 5}, {Index: "d", Value: 4}, {Index: "c", Value: 3}, {Index: "b", Value: 2}, {Index: "a", Value: 1}}}, + {2, 5, []pageable.Item{}}, } for _, tt := range tests { @@ -74,7 +75,7 @@ func TestPager_GetPage(t *testing.T) { uassert.Equal(t, len(tt.expected), len(page.Items)) for i, item := range page.Items { - uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Index, item.Index) uassert.Equal(t, tt.expected[i].Value, item.Value) } } diff --git a/examples/gno.land/p/moul/pageable/pageable.gno b/examples/gno.land/p/moul/pageable/pageable.gno new file mode 100644 index 00000000000..6d3687071b5 --- /dev/null +++ b/examples/gno.land/p/moul/pageable/pageable.gno @@ -0,0 +1,230 @@ +// Package pageable provides a generic pagination system +package pageable + +import ( + "math" + "net/url" + "strconv" + + "gno.land/p/demo/ufmt" +) + +// Pageable defines the minimal interface required for pagination +type Pageable interface { + // Size returns the total number of items + Size() int + + // IterateByOffset performs iteration starting from offset for count elements + // The callback receives an index and a value, returns true to stop iteration + IterateByOffset(offset int, count int, cb func(index interface{}, value interface{}) bool) bool +} + +// Pager provides pagination functionality for any Pageable source +type Pager struct { + Source Pageable + PageQueryParam string + SizeQueryParam string + DefaultPageSize int + Reversed bool +} + +// Page represents a single page of results +type Page struct { + Items []Item + PageNumber int + PageSize int + TotalItems int + TotalPages int + HasPrev bool + HasNext bool + Pager *Pager +} + +// Item represents a generic item in the page +type Item struct { + Index interface{} + Value interface{} +} + +// NewPager creates a new Pager with default values +func NewPager(source Pageable, defaultPageSize int, reversed bool) *Pager { + return &Pager{ + Source: source, + PageQueryParam: "page", + SizeQueryParam: "size", + DefaultPageSize: defaultPageSize, + Reversed: reversed, + } +} + +// GetPage retrieves a page of results from the AVL tree. +func (p *Pager) GetPage(pageNumber int) *Page { + return p.GetPageWithSize(pageNumber, p.DefaultPageSize) +} + +func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { + totalItems := p.Source.Size() + totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize))) + + page := &Page{ + TotalItems: totalItems, + TotalPages: totalPages, + PageSize: pageSize, + Pager: p, + } + + // pages without content + if pageSize < 1 { + return page + } + + // page number provided is not available + if pageNumber < 1 { + page.HasNext = totalPages > 0 + return page + } + + // page number provided is outside the range of total pages + if pageNumber > totalPages { + page.PageNumber = pageNumber + page.HasPrev = pageNumber > 0 + return page + } + + startIndex := (pageNumber - 1) * pageSize + endIndex := startIndex + pageSize + if endIndex > totalItems { + endIndex = totalItems + } + + items := []Item{} + + count := endIndex - startIndex + if p.Reversed { + count = -count + } + p.Source.IterateByOffset(startIndex, count, func(index interface{}, value interface{}) bool { + items = append(items, Item{Index: index, Value: value}) + return false + }) + + page.Items = items + page.PageNumber = pageNumber + page.HasPrev = pageNumber > 1 + page.HasNext = pageNumber < totalPages + return page +} + +func (p *Pager) MustGetPageByPath(rawURL string) *Page { + page, err := p.GetPageByPath(rawURL) + if err != nil { + panic("invalid path") + } + return page +} + +// GetPageByPath retrieves a page of results based on the query parameters in the URL path. +func (p *Pager) GetPageByPath(rawURL string) (*Page, error) { + pageNumber, pageSize, err := p.ParseQuery(rawURL) + if err != nil { + return nil, err + } + return p.GetPageWithSize(pageNumber, pageSize), nil +} + +// Picker generates the Markdown UI for the page Picker +func (p *Page) Picker() string { + pageNumber := p.PageNumber + pageNumber = max(pageNumber, 1) + + if p.TotalPages <= 1 { + return "" + } + + md := "" + + if p.HasPrev { + // Always show the first page link + md += ufmt.Sprintf("[%d](?%s=%d) | ", 1, p.Pager.PageQueryParam, 1) + + // Before + if p.PageNumber > 4 { + md += "… | " + } + + if p.PageNumber > 3 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2) + } + + if p.PageNumber > 2 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1) + } + } + + if p.PageNumber > 0 && p.PageNumber <= p.TotalPages { + // Current page + md += ufmt.Sprintf("**%d**", p.PageNumber) + } else { + md += ufmt.Sprintf("_%d_", p.PageNumber) + } + + if p.HasNext { + md += " | " + + if p.PageNumber < p.TotalPages-1 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1) + } + + if p.PageNumber < p.TotalPages-2 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2) + } + + if p.PageNumber < p.TotalPages-3 { + md += "… | " + } + + // Always show the last page link + md += ufmt.Sprintf("[%d](?%s=%d)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages) + } + + return md +} + +// ParseQuery parses the URL to extract the page number and page size. +func (p *Pager) ParseQuery(rawURL string) (int, int, error) { + u, err := url.Parse(rawURL) + if err != nil { + return 1, p.DefaultPageSize, err + } + + query := u.Query() + pageNumber := 1 + pageSize := p.DefaultPageSize + + if p.PageQueryParam != "" { + if pageStr := query.Get(p.PageQueryParam); pageStr != "" { + pageNumber, err = strconv.Atoi(pageStr) + if err != nil || pageNumber < 1 { + pageNumber = 1 + } + } + } + + if p.SizeQueryParam != "" { + if sizeStr := query.Get(p.SizeQueryParam); sizeStr != "" { + pageSize, err = strconv.Atoi(sizeStr) + if err != nil || pageSize < 1 { + pageSize = p.DefaultPageSize + } + } + } + + return pageNumber, pageSize, nil +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/examples/gno.land/p/moul/pageable/pageable_test.gno b/examples/gno.land/p/moul/pageable/pageable_test.gno new file mode 100644 index 00000000000..c664c4f3487 --- /dev/null +++ b/examples/gno.land/p/moul/pageable/pageable_test.gno @@ -0,0 +1,265 @@ +package pageable + +import ( + "testing" +) + +// MockPageable implements the Pageable interface for testing +type MockPageable struct { + items []int +} + +func (m *MockPageable) Size() int { + return len(m.items) +} + +func (m *MockPageable) IterateByOffset(offset int, count int, cb func(index interface{}, value interface{}) bool) bool { + if count < 0 { + // Handle reversed iteration + start := offset + count + 1 + for i := offset; i >= start; i-- { + if cb(i, m.items[i]) { + return true + } + } + return false + } + + end := offset + count + if end > len(m.items) { + end = len(m.items) + } + + for i := offset; i < end; i++ { + if cb(i, m.items[i]) { + return true + } + } + return false +} + +func TestPager(t *testing.T) { + // Create test data + mock := &MockPageable{ + items: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + } + + tests := []struct { + name string + pageSize int + pageNum int + wantItems int + wantTotal int + wantPages int + wantHasPrev bool + wantHasNext bool + }{ + {"first page", 3, 1, 3, 10, 4, false, true}, + {"middle page", 3, 2, 3, 10, 4, true, true}, + {"last page", 3, 4, 1, 10, 4, true, false}, + {"invalid page", 3, 5, 0, 10, 4, true, false}, + {"zero page", 3, 0, 0, 10, 4, false, true}, + {"all items", 10, 1, 10, 10, 1, false, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pager := NewPager(mock, tt.pageSize, false) + page := pager.GetPage(tt.pageNum) + + if len(page.Items) != tt.wantItems { + t.Errorf("got %d items, want %d", len(page.Items), tt.wantItems) + } + if page.TotalItems != tt.wantTotal { + t.Errorf("got %d total items, want %d", page.TotalItems, tt.wantTotal) + } + if page.TotalPages != tt.wantPages { + t.Errorf("got %d total pages, want %d", page.TotalPages, tt.wantPages) + } + if page.HasPrev != tt.wantHasPrev { + t.Errorf("got HasPrev=%v, want %v", page.HasPrev, tt.wantHasPrev) + } + if page.HasNext != tt.wantHasNext { + t.Errorf("got HasNext=%v, want %v", page.HasNext, tt.wantHasNext) + } + }) + } +} + +func TestPicker(t *testing.T) { + mock := &MockPageable{ + items: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + } + + tests := []struct { + name string + pageSize int + pageNum int + want string + }{ + {"first page", 3, 1, "**1** | [2](?page=2) | [3](?page=3) | … | [4](?page=4)"}, + {"middle page", 3, 2, "[1](?page=1) | [1](?page=1) | **2** | [3](?page=3) | [4](?page=4)"}, + {"last page", 3, 4, "[1](?page=1) | … | [2](?page=2) | [3](?page=3) | **4**"}, + {"single page", 10, 1, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pager := NewPager(mock, tt.pageSize, false) + page := pager.GetPage(tt.pageNum) + got := page.Picker() + if got != tt.want { + t.Errorf("\ngot: %s\nwant: %s", got, tt.want) + } + }) + } +} + +func TestReversedPager(t *testing.T) { + mock := &MockPageable{ + items: []int{1, 2, 3, 4, 5}, + } + + pager := NewPager(mock, 2, true) + page := pager.GetPage(1) + + if len(page.Items) != 2 { + t.Errorf("got %d items, want 2", len(page.Items)) + } + + // Check if items are in reverse order + if page.Items[0].Value.(int) != 2 || page.Items[1].Value.(int) != 1 { + t.Errorf("items not in reverse order: got %v", page.Items) + } +} + +func TestParseQuery(t *testing.T) { + mock := &MockPageable{items: []int{1, 2, 3}} + pager := NewPager(mock, 10, false) + + tests := []struct { + name string + url string + wantPage int + wantSize int + wantError bool + }{ + {"valid query", "/?page=2&size=5", 2, 5, false}, + {"missing params", "/", 1, 10, false}, + {"invalid page", "/?page=invalid", 1, 10, false}, + {"invalid size", "/?size=invalid", 1, 10, false}, + {"negative values", "/?page=-1&size=-5", 1, 10, false}, + {"invalid url", ":%invalid", 1, 10, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + page, size, err := pager.ParseQuery(tt.url) + if (err != nil) != tt.wantError { + t.Errorf("got error=%v, want error=%v", err != nil, tt.wantError) + } + if page != tt.wantPage { + t.Errorf("got page=%d, want %d", page, tt.wantPage) + } + if size != tt.wantSize { + t.Errorf("got size=%d, want %d", size, tt.wantSize) + } + }) + } +} + +func TestPager_UI_WithManyPages(t *testing.T) { + mock := &MockPageable{ + items: make([]int, 100), + } + for i := 0; i < 100; i++ { + mock.items[i] = i + } + + pager := NewPager(mock, 10, false) + + tests := []struct { + name string + pageNum int + pageSize int + want string + }{ + {"first page", 1, 10, "**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)"}, + {"second page", 2, 10, "[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)"}, + {"middle page", 5, 10, "[1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) | [7](?page=7) | … | [10](?page=10)"}, + {"penultimate page", 9, 10, "[1](?page=1) | … | [7](?page=7) | [8](?page=8) | **9** | [10](?page=10)"}, + {"last page", 10, 10, "[1](?page=1) | … | [8](?page=8) | [9](?page=9) | **10**"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + page := pager.GetPageWithSize(tt.pageNum, tt.pageSize) + got := page.Picker() + if got != tt.want { + t.Errorf("\ngot: %s\nwant: %s", got, tt.want) + } + }) + } +} + +func TestGetPageWithSize(t *testing.T) { + mock := &MockPageable{ + items: []int{1, 2, 3, 4, 5}, + } + + pager := NewPager(mock, 10, false) + + tests := []struct { + name string + pageNum int + pageSize int + wantLen int + wantFirst int + }{ + {"custom size smaller", 1, 2, 2, 1}, + {"custom size exact", 1, 5, 5, 1}, + {"custom size larger", 1, 10, 5, 1}, + {"second page partial", 2, 3, 2, 4}, + {"empty page", 3, 3, 0, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + page := pager.GetPageWithSize(tt.pageNum, tt.pageSize) + if len(page.Items) != tt.wantLen { + t.Errorf("got %d items, want %d", len(page.Items), tt.wantLen) + } + if tt.wantLen > 0 && page.Items[0].Value.(int) != tt.wantFirst { + t.Errorf("first item got %d, want %d", page.Items[0].Value.(int), tt.wantFirst) + } + }) + } +} + +func TestInvalidInputs(t *testing.T) { + mock := &MockPageable{ + items: []int{1, 2, 3}, + } + + tests := []struct { + name string + pageSize int + pageNum int + wantLen int + }{ + {"zero page size", 0, 1, 0}, + {"negative page size", -1, 1, 0}, + {"negative page number", 10, -1, 0}, + {"too large page number", 10, 999, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pager := NewPager(mock, tt.pageSize, false) + page := pager.GetPage(tt.pageNum) + if len(page.Items) != tt.wantLen { + t.Errorf("got %d items, want %d", len(page.Items), tt.wantLen) + } + }) + } +} diff --git a/examples/gno.land/p/moul/ulist/gno.mod b/examples/gno.land/p/moul/ulist/gno.mod new file mode 100644 index 00000000000..077f8c556f3 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/ulist diff --git a/examples/gno.land/p/moul/ulist/pager/gno.mod b/examples/gno.land/p/moul/ulist/pager/gno.mod new file mode 100644 index 00000000000..87a4f65d7ac --- /dev/null +++ b/examples/gno.land/p/moul/ulist/pager/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/ulist/pager \ No newline at end of file diff --git a/examples/gno.land/p/moul/ulist/pager/pager.gno b/examples/gno.land/p/moul/ulist/pager/pager.gno new file mode 100644 index 00000000000..d0e6a06e7df --- /dev/null +++ b/examples/gno.land/p/moul/ulist/pager/pager.gno @@ -0,0 +1,30 @@ +package pager + +import ( + "gno.land/p/moul/pageable" + "gno.land/p/moul/ulist" +) + +func NewPager(list ulist.IList, defaultPageSize int, reversed bool) *pageable.Pager { + wrappedList := NewUListWrapper(list) + return pageable.NewPager(wrappedList, defaultPageSize, reversed) +} + +// UListWrapper adapts a ulist.IList to implement Pageable +type UListWrapper struct { + list ulist.IList +} + +func NewUListWrapper(list ulist.IList) *UListWrapper { + return &UListWrapper{list: list} +} + +func (w *UListWrapper) Size() int { + return w.list.TotalSize() +} + +func (w *UListWrapper) IterateByOffset(offset int, count int, cb func(index interface{}, value interface{}) bool) bool { + return w.list.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + return cb(index, value) + }) +} diff --git a/examples/gno.land/p/moul/ulist/pager/pager_test.gno b/examples/gno.land/p/moul/ulist/pager/pager_test.gno new file mode 100644 index 00000000000..ea4489216dc --- /dev/null +++ b/examples/gno.land/p/moul/ulist/pager/pager_test.gno @@ -0,0 +1,75 @@ +package pager + +import ( + "testing" + + "gno.land/p/moul/pageable" + "gno.land/p/moul/ulist" +) + +func TestNewPager(t *testing.T) { + // Create a list and add some items + list := ulist.New() + list.Append("a", "b", "c", "d", "e") + + // Test forward paging + pager := NewPager(list, 2, false) + + // First page + page := pager.GetPage(0) + assertPage(t, page, []string{"a", "b"}, "first page") + + // Second page + page = pager.GetPage(1) + assertPage(t, page, []string{"c", "d"}, "second page") + + // Last page (partial) + page = pager.GetPage(2) + assertPage(t, page, []string{"e"}, "last page") + + // Test reverse paging + reversePager := NewPager(list, 2, true) + + // First page (from end) + page = reversePager.GetPage(0) + assertPage(t, page, []string{"e", "d"}, "first reverse page") + + // Second page + page = reversePager.GetPage(1) + assertPage(t, page, []string{"c", "b"}, "second reverse page") + + // Last page (partial) + page = reversePager.GetPage(2) + assertPage(t, page, []string{"a"}, "last reverse page") +} + +func TestUListWrapperWithDeletedItems(t *testing.T) { + // Create a list with some deleted items + list := ulist.New() + list.Append("a", "b", "c", "d", "e") + list.Delete(1, 3) // Delete "b" and "d" + + pager := NewPager(list, 2, false) + + // First page should skip deleted items + page := pager.GetPage(0) + assertPage(t, page, []string{"a", "c"}, "first page with deleted items") + + // Second page + page = pager.GetPage(1) + assertPage(t, page, []string{"e"}, "second page with deleted items") +} + +// Helper function to assert page contents +func assertPage(t *testing.T, got *pageable.Page, want []string, desc string) { + if len(got.Items) != len(want) { + t.Errorf("%s: expected length %d, got %d", desc, len(want), len(got.Items)) + return + } + + for i := 0; i < len(want); i++ { + if got.Items[i].Value.(string) != want[i] { + t.Errorf("%s: at index %d expected %q, got %q", desc, i, want[i], got.Items[i]) + } + } +} diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno new file mode 100644 index 00000000000..45eac2372c3 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -0,0 +1,403 @@ +// Package ulist provides an append-only list implementation using a binary tree structure, +// optimized for scenarios requiring sequential inserts with auto-incrementing indices. +// +// The implementation uses a binary tree where new elements are added by following a path +// determined by the binary representation of the index. This provides automatic balancing +// for append operations without requiring any balancing logic. +// +// Key characteristics: +// * O(log n) append and access operations +// * Perfect balance for power-of-2 sizes +// * No balancing needed +// * Memory efficient +// * Natural support for range queries +// * Support for soft deletion of elements +// * Forward and reverse iteration capabilities +// * Offset-based iteration with count control +package ulist + +// TODO: Make avl/pager compatible in some way. Explain the limitations (not always 10 items because of nil ones). +// TODO: Use this ulist in moul/collection for the primary index. + +import ( + "errors" +) + +// List represents an append-only binary tree list +type List struct { + root *treeNode + totalSize int + activeSize int +} + +// Entry represents a key-value pair in the list, where Index is the position +// and Value is the stored data +type Entry struct { + Index int + Value interface{} +} + +// treeNode represents a node in the binary tree +type treeNode struct { + data interface{} + left *treeNode + right *treeNode +} + +// Error variables +var ( + ErrOutOfBounds = errors.New("index out of bounds") + ErrDeleted = errors.New("element already deleted") +) + +// New creates a new empty List instance +func New() *List { + return &List{} +} + +// Append adds one or more values to the end of the list. +// Values are added sequentially, and the list grows automatically. +func (l *List) Append(values ...interface{}) { + for _, value := range values { + index := l.totalSize + node := l.findNode(index, true) + node.data = value + l.totalSize++ + l.activeSize++ + } +} + +// Get retrieves the value at the specified index. +// Returns nil if the index is out of bounds or if the element was deleted. +func (l *List) Get(index int) interface{} { + node := l.findNode(index, false) + if node == nil { + return nil + } + return node.data +} + +// Delete marks the elements at the specified indices as deleted. +// Returns ErrOutOfBounds if any index is invalid or ErrDeleted if +// the element was already deleted. +func (l *List) Delete(indices ...int) error { + if len(indices) == 0 { + return nil + } + if l == nil || l.totalSize == 0 { + return ErrOutOfBounds + } + + for _, index := range indices { + if index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + + node := l.findNode(index, false) + if node == nil || node.data == nil { + return ErrDeleted + } + node.data = nil + l.activeSize-- + } + + return nil +} + +// Set updates or restores a value at the specified index if within bounds +// Returns ErrOutOfBounds if the index is invalid +func (l *List) Set(index int, value interface{}) error { + if l == nil || index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + + node := l.findNode(index, false) + if node == nil { + return ErrOutOfBounds + } + + // If this is restoring a deleted element + if value != nil && node.data == nil { + l.activeSize++ + } + + // If this is deleting an element + if value == nil && node.data != nil { + l.activeSize-- + } + + node.data = value + return nil +} + +// Size returns the number of active (non-deleted) elements in the list +func (l *List) Size() int { + if l == nil { + return 0 + } + return l.activeSize +} + +// TotalSize returns the total number of elements ever added to the list, +// including deleted elements +func (l *List) TotalSize() int { + if l == nil { + return 0 + } + return l.totalSize +} + +// IterCbFn is a callback function type used in iteration methods. +// Return true to stop iteration, false to continue. +type IterCbFn func(index int, value interface{}) bool + +// Iterator performs iteration between start and end indices, calling cb for each entry. +// If start > end, iteration is performed in reverse order. +// Returns true if iteration was stopped early by the callback returning true. +// Skips deleted elements. +func (l *List) Iterator(start, end int, cb IterCbFn) bool { + // For empty list or invalid range + if l == nil || l.totalSize == 0 { + return false + } + if start < 0 && end < 0 { + return false + } + if start >= l.totalSize && end >= l.totalSize { + return false + } + + // Normalize indices + if start < 0 { + start = 0 + } + if end < 0 { + end = 0 + } + if end >= l.totalSize { + end = l.totalSize - 1 + } + if start >= l.totalSize { + start = l.totalSize - 1 + } + + // Handle reverse iteration + if start > end { + for i := start; i >= end; i-- { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false + } + + // Handle forward iteration + for i := start; i <= end; i++ { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false +} + +// IteratorByOffset performs iteration starting from offset for count elements. +// If count is positive, iterates forward; if negative, iterates backward. +// The iteration stops after abs(count) elements or when reaching list bounds. +// Skips deleted elements. +func (l *List) IteratorByOffset(offset int, count int, cb IterCbFn) bool { + if count == 0 || l == nil || l.totalSize == 0 { + return false + } + + // Normalize offset + if offset < 0 { + offset = 0 + } + if offset >= l.totalSize { + offset = l.totalSize - 1 + } + + // Determine end based on count direction + var end int + if count > 0 { + end = l.totalSize - 1 + } else { + end = 0 + } + + wrapperReturned := false + + // Wrap the callback to limit iterations + remaining := abs(count) + wrapper := func(index int, value interface{}) bool { + if remaining <= 0 { + wrapperReturned = true + return true + } + remaining-- + return cb(index, value) + } + ret := l.Iterator(offset, end, wrapper) + if wrapperReturned { + return false + } + return ret +} + +// abs returns the absolute value of x +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +// Add this helper method to the List struct +func (l *List) findNode(index int, create bool) *treeNode { + // For read operations, check bounds strictly + if !create && (l == nil || index < 0 || index >= l.totalSize) { + return nil + } + + // For create operations, allow index == totalSize for append + if create && (l == nil || index < 0 || index > l.totalSize) { + return nil + } + + // Initialize root if needed + if l.root == nil { + if !create { + return nil + } + l.root = &treeNode{} + return l.root + } + + node := l.root + + // Special case for root node + if index == 0 { + return node + } + + // Calculate the number of bits needed (inline highestBit logic) + bits := 0 + n := index + 1 + for n > 0 { + n >>= 1 + bits++ + } + + // Start from the second highest bit + for level := bits - 2; level >= 0; level-- { + bit := (index & (1 << uint(level))) != 0 + + if bit { + if node.right == nil { + if !create { + return nil + } + node.right = &treeNode{} + } + node = node.right + } else { + if node.left == nil { + if !create { + return nil + } + node.left = &treeNode{} + } + node = node.left + } + } + + return node +} + +// MustDelete deletes elements at the specified indices. +// Panics if any index is invalid or if any element was already deleted. +func (l *List) MustDelete(indices ...int) { + if err := l.Delete(indices...); err != nil { + panic(err) + } +} + +// MustGet retrieves the value at the specified index. +// Panics if the index is out of bounds or if the element was deleted. +func (l *List) MustGet(index int) interface{} { + if l == nil || index < 0 || index >= l.totalSize { + panic(ErrOutOfBounds) + } + value := l.Get(index) + if value == nil { + panic(ErrDeleted) + } + return value +} + +// MustSet updates or restores a value at the specified index. +// Panics if the index is out of bounds. +func (l *List) MustSet(index int, value interface{}) { + if err := l.Set(index, value); err != nil { + panic(err) + } +} + +// GetRange returns a slice of Entry containing elements between start and end indices. +// If start > end, elements are returned in reverse order. +// Deleted elements are skipped. +func (l *List) GetRange(start, end int) []Entry { + var entries []Entry + l.Iterator(start, end, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// GetRangeByOffset returns a slice of Entry starting from offset for count elements. +// If count is positive, returns elements forward; if negative, returns elements backward. +// The operation stops after abs(count) elements or when reaching list bounds. +// Deleted elements are skipped. +func (l *List) GetRangeByOffset(offset int, count int) []Entry { + var entries []Entry + l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// IList defines the interface for list operations. +// It provides methods for appending, accessing, deleting, and iterating over elements. +type IList interface { + // Basic operations + Append(values ...interface{}) + Get(index int) interface{} + Delete(indices ...int) error + Size() int + TotalSize() int + Set(index int, value interface{}) error + + // Must variants that panic instead of returning errors + MustDelete(indices ...int) + MustGet(index int) interface{} + MustSet(index int, value interface{}) + + // Range operations + GetRange(start, end int) []Entry + GetRangeByOffset(offset int, count int) []Entry + + // Iterator operations + Iterator(start, end int, cb IterCbFn) bool + IteratorByOffset(offset int, count int, cb IterCbFn) bool +} + +// Verify that List implements IList +var _ IList = (*List)(nil) diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno new file mode 100644 index 00000000000..1654368b080 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -0,0 +1,1422 @@ +package ulist + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/typeutil" +) + +func TestNew(t *testing.T) { + l := New() + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) +} + +func TestListAppendAndGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + }{ + { + name: "empty list", + setup: func() *List { + return New() + }, + index: 0, + expected: nil, + }, + { + name: "single append and get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + }, + { + name: "multiple appends and get first", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 0, + expected: 1, + }, + { + name: "multiple appends and get last", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 2, + expected: 3, + }, + { + name: "get with invalid index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + expected: nil, + }, + { + name: "31 items get first", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 0, + expected: 0, + }, + { + name: "31 items get last", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 30, + expected: 30, + }, + { + name: "31 items get middle", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values around power of 2 boundary", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values at power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 16, + expected: 16, + }, + { + name: "values after power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 17, + expected: 17, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + got := l.Get(tt.index) + if got != tt.expected { + t.Errorf("List.Get() = %v, want %v", got, tt.expected) + } + }) + } +} + +// generateSequence creates a slice of integers from 0 to n-1 +func generateSequence(n int) []interface{} { + result := make([]interface{}, n) + for i := 0; i < n; i++ { + result[i] = i + } + return result +} + +func TestListDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + deleteIndices []int + expectedErr error + expectedSize int + }{ + { + name: "delete single element", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + deleteIndices: []int{1}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete multiple elements", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{0, 2, 4}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete with negative index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{-1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete beyond size", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete already deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + deleteIndices: []int{0}, + expectedErr: ErrDeleted, + expectedSize: 0, + }, + { + name: "delete multiple elements in reverse", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{4, 2, 0}, + expectedErr: nil, + expectedSize: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + initialSize := l.Size() + err := l.Delete(tt.deleteIndices...) + if err != nil && tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, tt.expectedErr, err) + } + uassert.Equal(t, tt.expectedSize, l.Size(), + ufmt.Sprintf("Expected size %d after deleting %d elements from size %d, got %d", + tt.expectedSize, len(tt.deleteIndices), initialSize, l.Size())) + }) + } +} + +func TestListSizeAndTotalSize(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + list := New() + uassert.Equal(t, 0, list.Size()) + uassert.Equal(t, 0, list.TotalSize()) + }) + + t.Run("list with elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + uassert.Equal(t, 3, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) + + t.Run("list with deleted elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + list.Delete(1) + uassert.Equal(t, 2, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) +} + +func TestIterator(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + wantStop bool + stopAfter int // stop after N elements, -1 for no stop + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 0, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "single element forward", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + stopAfter: -1, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "partial range forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + stopAfter: -1, + }, + { + name: "partial range reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + stopAfter: -1, + }, + { + name: "stop iteration early", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + wantStop: true, + stopAfter: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "negative start", + values: []interface{}{1, 2, 3}, + start: -1, + end: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "negative end", + values: []interface{}{1, 2, 3}, + start: 0, + end: -2, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start beyond size", + values: []interface{}{1, 2, 3}, + start: 5, + end: 6, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "end beyond size", + values: []interface{}{1, 2, 3}, + start: 0, + end: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements", + values: []interface{}{1, 2, nil, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements reverse", + values: []interface{}{1, nil, 3, nil, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 2, Value: 3}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start equals end", + values: []interface{}{1, 2, 3}, + start: 1, + end: 1, + expected: []Entry{{Index: 1, Value: 2}}, + stopAfter: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + stopped := list.Iterator(tt.start, tt.end, func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return tt.stopAfter >= 0 && len(result) >= tt.stopAfter + }) + + uassert.Equal(t, len(result), len(tt.expected), "comparing length") + + for i := range result { + uassert.Equal(t, result[i].Index, tt.expected[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(result[i].Value), typeutil.ToString(tt.expected[i].Value), "comparing value") + } + + uassert.Equal(t, stopped, tt.wantStop, "comparing stopped") + }) + } +} + +func TestLargeListAppendGetAndDelete(t *testing.T) { + l := New() + size := 100 + + // Append values from 0 to 99 + for i := 0; i < size; i++ { + l.Append(i) + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Verify size + uassert.Equal(t, size, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Get and verify each value + for i := 0; i < size; i++ { + err := l.Delete(i) + uassert.Equal(t, nil, err) + } + + // Verify size + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, nil, val) + } +} + +func TestEdgeCases(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "nil list operations", + test: func(t *testing.T) { + var l *List + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) + uassert.Equal(t, nil, l.Get(0)) + err := l.Delete(0) + uassert.Equal(t, ErrOutOfBounds.Error(), err.Error()) + }, + }, + { + name: "delete empty indices slice", + test: func(t *testing.T) { + l := New() + l.Append(1) + err := l.Delete() + uassert.Equal(t, nil, err) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "append nil values", + test: func(t *testing.T) { + l := New() + l.Append(nil, nil) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, nil, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + }, + }, + { + name: "delete same index multiple times", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + err := l.Delete(1) + uassert.Equal(t, nil, err) + err = l.Delete(1) + uassert.Equal(t, ErrDeleted.Error(), err.Error()) + }, + }, + { + name: "iterator with all deleted elements", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + l.Delete(0, 1, 2) + var count int + l.Iterator(0, 2, func(index int, value interface{}) bool { + count++ + return false + }) + uassert.Equal(t, 0, count) + }, + }, + { + name: "append after delete", + test: func(t *testing.T) { + l := New() + l.Append(1, 2) + l.Delete(1) + l.Append(3) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, 3, l.TotalSize()) + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + uassert.Equal(t, 3, l.Get(2)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.test(t) + }) + } +} + +func TestIteratorByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + wantStop bool + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "positive count forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "negative count backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements forward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements backward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: -5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + wantStop: false, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + wantStop: false, + }, + { + name: "early stop in forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "early stop in backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 4, + count: -5, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "single element forward", + values: []interface{}{1}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "single element backward", + values: []interface{}{1}, + offset: 0, + count: -5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "all deleted elements", + values: []interface{}{nil, nil, nil}, + offset: 0, + count: 3, + expected: []Entry{}, + wantStop: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + var cb IterCbFn + if tt.wantStop { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return len(result) >= 2 // Stop after 2 elements for early stop tests + } + } else { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return false + } + } + + stopped := list.IteratorByOffset(tt.offset, tt.count, cb) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + uassert.Equal(t, tt.wantStop, stopped, "comparing stopped") + }) + } +} + +func TestMustDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + indices []int + shouldPanic bool + panicMsg string + }{ + { + name: "successful delete", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + indices: []int{1}, + shouldPanic: false, + }, + { + name: "out of bounds", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + indices: []int{1}, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "already deleted", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + indices: []int{0}, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + l.MustDelete(tt.indices...) + if tt.shouldPanic { + t.Error("Expected panic") + } + }) + } +} + +func TestMustGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + shouldPanic bool + panicMsg string + }{ + { + name: "successful get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + shouldPanic: false, + }, + { + name: "out of bounds negative", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "out of bounds positive", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + index: 0, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + { + name: "nil list", + setup: func() *List { + return nil + }, + index: 0, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + result := l.MustGet(tt.index) + if tt.shouldPanic { + t.Error("Expected panic") + } + uassert.Equal(t, typeutil.ToString(tt.expected), typeutil.ToString(result)) + }) + } +} + +func TestGetRange(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + }, + { + name: "single element", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 5, + expected: []Entry{}, + }, + { + name: "negative indices", + values: []interface{}{1, 2, 3}, + start: -1, + end: -2, + expected: []Entry{}, + }, + { + name: "indices beyond size", + values: []interface{}{1, 2, 3}, + start: 1, + end: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetRange(tt.start, tt.end) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} + +func TestGetRangeByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + }, + { + name: "positive count forward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "negative count backward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + }, + { + name: "count exceeds available elements", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetRangeByOffset(tt.offset, tt.count) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} + +func TestMustSet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + value interface{} + shouldPanic bool + panicMsg string + }{ + { + name: "successful set", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + value: 99, + shouldPanic: false, + }, + { + name: "restore deleted element", + setup: func() *List { + l := New() + l.Append(42) + l.Delete(0) + return l + }, + index: 0, + value: 99, + shouldPanic: false, + }, + { + name: "out of bounds negative", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "out of bounds positive", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "nil list", + setup: func() *List { + return nil + }, + index: 0, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + l.MustSet(tt.index, tt.value) + if tt.shouldPanic { + t.Error("Expected panic") + } + // Verify the value was set correctly for non-panic cases + if !tt.shouldPanic { + result := l.Get(tt.index) + uassert.Equal(t, typeutil.ToString(tt.value), typeutil.ToString(result)) + } + }) + } +} + +func TestSet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + value interface{} + expectedErr error + verify func(t *testing.T, l *List) + }{ + { + name: "set value in empty list", + setup: func() *List { + return New() + }, + index: 0, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set value at valid index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + uassert.Equal(t, 1, l.TotalSize()) + }, + }, + { + name: "set value at negative index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 1, l.Get(0)) + }, + }, + { + name: "set value beyond size", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "set nil value", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: nil, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, nil, l.Get(0)) + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set value at deleted index", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + l.Delete(1) + return l + }, + index: 1, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(1)) + uassert.Equal(t, 3, l.Size()) + uassert.Equal(t, 3, l.TotalSize()) + }, + }, + { + name: "set value in nil list", + setup: func() *List { + return nil + }, + index: 0, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set multiple values at same index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(0)) + err := l.Set(0, 99) + uassert.Equal(t, nil, err) + uassert.Equal(t, 99, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "set value at last index", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + index: 2, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(2)) + uassert.Equal(t, 3, l.Size()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + err := l.Set(tt.index, tt.value) + + if tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, nil, err) + } + + tt.verify(t, l) + }) + } +} diff --git a/examples/gno.land/r/nemanya/config/gno.mod b/examples/gno.land/r/nemanya/config/gno.mod index 8ffbc32f571..4388b5bd525 100644 --- a/examples/gno.land/r/nemanya/config/gno.mod +++ b/examples/gno.land/r/nemanya/config/gno.mod @@ -1 +1 @@ -module gno.land/r/nemanya/config \ No newline at end of file +module gno.land/r/nemanya/config diff --git a/examples/gno.land/r/nemanya/home/gno.mod b/examples/gno.land/r/nemanya/home/gno.mod index 1994cf7c11b..d0220197489 100644 --- a/examples/gno.land/r/nemanya/home/gno.mod +++ b/examples/gno.land/r/nemanya/home/gno.mod @@ -1 +1 @@ -module gno.land/r/nemanya/home \ No newline at end of file +module gno.land/r/nemanya/home diff --git a/examples/gno.land/r/nemanya/home/home.gno b/examples/gno.land/r/nemanya/home/home.gno index 08b831b0d17..08e24baecfd 100644 --- a/examples/gno.land/r/nemanya/home/home.gno +++ b/examples/gno.land/r/nemanya/home/home.gno @@ -27,12 +27,12 @@ type Project struct { } var ( - textArt string - aboutMe string - sponsorInfo string - socialLinks map[string]SocialLink - gnoProjects map[string]Project - otherProjects map[string]Project + textArt string + aboutMe string + sponsorInfo string + socialLinks map[string]SocialLink + gnoProjects map[string]Project + otherProjects map[string]Project totalDonations std.Coins ) @@ -266,15 +266,15 @@ func Withdraw() string { panic(config.ErrUnauthorized) } - banker := std.GetBanker(std.BankerTypeRealmSend) - realmAddress := std.GetOrigPkgAddr() - coins := banker.GetCoins(realmAddress) + banker := std.GetBanker(std.BankerTypeRealmSend) + realmAddress := std.GetOrigPkgAddr() + coins := banker.GetCoins(realmAddress) - if len(coins) == 0 { - return "No coins available to withdraw" - } + if len(coins) == 0 { + return "No coins available to withdraw" + } - banker.SendCoins(realmAddress, config.Address(), coins) + banker.SendCoins(realmAddress, config.Address(), coins) - return "Successfully withdrew all coins to config address" + return "Successfully withdrew all coins to config address" }