From d214d18bc9d13c65ab73c1dc5a081467519066ab Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 15 Jan 2025 22:47:54 +0100 Subject: [PATCH] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../gno.land/p/moul/pageable/pageable.gno | 130 ++++++++++-------- .../p/moul/pageable/pageable_test.gno | 107 ++++++++++++++ 2 files changed, 181 insertions(+), 56 deletions(-) diff --git a/examples/gno.land/p/moul/pageable/pageable.gno b/examples/gno.land/p/moul/pageable/pageable.gno index 01032c51f73..a4dd8a3eca0 100644 --- a/examples/gno.land/p/moul/pageable/pageable.gno +++ b/examples/gno.land/p/moul/pageable/pageable.gno @@ -62,74 +62,92 @@ func (p *Pager) GetPage(pageNumber int) *Page { return p.GetPageWithSize(pageNumber, p.DefaultPageSize) } -func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { +// Helper function to create a basic page +func (p *Pager) newBasePage(pageSize int) *Page { totalItems := p.Source.Size() - - // Handle invalid page size - if pageSize <= 0 { - return &Page{ - TotalItems: totalItems, - TotalPages: 0, - PageSize: pageSize, - Pager: p, - } + totalPages := 0 + if pageSize > 0 { + totalPages = int(math.Ceil(float64(totalItems) / float64(pageSize))) } - totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize))) - - page := &Page{ + return &Page{ TotalItems: totalItems, TotalPages: totalPages, PageSize: pageSize, Pager: p, } +} + +func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { + // Handle invalid page size + if pageSize <= 0 { + return p.newBasePage(pageSize) + } - // pages without content - if pageSize < 1 { + page := p.newBasePage(pageSize) + + // For empty source, return empty page with no navigation + if page.TotalItems == 0 { + page.HasPrev = false + page.HasNext = false return page } // page number provided is not available if pageNumber < 1 { - page.HasNext = totalPages > 0 + page.HasNext = page.TotalPages > 0 return page } // page number provided is outside the range of total pages - if pageNumber > totalPages { + if pageNumber > page.TotalPages { page.PageNumber = pageNumber - page.HasPrev = pageNumber > 0 + page.HasPrev = page.TotalPages > 0 return page } - // Calculate offset based on page number - offset := (pageNumber - 1) * pageSize + // Calculate offset and size + var offset, iterCount int if p.Reversed { - offset = totalItems - pageSize - offset - if offset < 0 { - pageSize += offset // Reduce pageSize if we're going past the start - offset = 0 - } + // For reversed order, calculate from the end + iterCount = min(pageSize, page.TotalItems-((pageNumber-1)*pageSize)) + offset = max(0, page.TotalItems-(pageNumber*pageSize)) + } else { + offset = (pageNumber - 1) * pageSize + iterCount = pageSize } // Collect items for the current page var items []Item - iterCount := pageSize if p.Reversed { - iterCount = -pageSize - } - p.Source.IterateByOffset(offset, iterCount, func(index interface{}, value interface{}) bool { - items = append(items, Item{ - Index: index, - Value: value, + // For reversed order, iterate from the end of the array + if offset == 0 && iterCount == 1 { + // Special case for last partial page + p.Source.IterateByOffset(0, 1, func(index interface{}, value interface{}) bool { + items = append(items, Item{Index: index, Value: value}) + return true + }) + } else { + startIndex := page.TotalItems - offset - 1 + endIndex := startIndex - iterCount + 1 + for i := startIndex; i >= endIndex && i >= 0; i-- { + p.Source.IterateByOffset(i, 1, func(index interface{}, value interface{}) bool { + items = append(items, Item{Index: index, Value: value}) + return true + }) + } + } + } else { + p.Source.IterateByOffset(offset, iterCount, func(index interface{}, value interface{}) bool { + items = append(items, Item{Index: index, Value: value}) + return false }) - return false - }) + } page.Items = items page.PageNumber = pageNumber page.HasPrev = pageNumber > 1 - page.HasNext = pageNumber < totalPages + page.HasNext = pageNumber < page.TotalPages return page } @@ -208,7 +226,18 @@ func (p *Page) Picker() string { return md } -// ParseQuery parses the URL to extract the page number and page size. +// Helper to safely parse positive integers with a default value +func parsePositiveIntOrDefault(str string, defaultValue int) int { + if str == "" { + return defaultValue + } + val, err := strconv.Atoi(str) + if err != nil || val < 1 { + return defaultValue + } + return val +} + func (p *Pager) ParseQuery(rawURL string) (int, int, error) { u, err := url.Parse(rawURL) if err != nil { @@ -216,26 +245,8 @@ func (p *Pager) ParseQuery(rawURL string) (int, int, error) { } 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 - } - } - } + pageNumber := parsePositiveIntOrDefault(query.Get(p.PageQueryParam), 1) + pageSize := parsePositiveIntOrDefault(query.Get(p.SizeQueryParam), p.DefaultPageSize) return pageNumber, pageSize, nil } @@ -246,3 +257,10 @@ func max(a, b int) int { } return b } + +func min(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 index 7f8c2298c1a..2cbce82c7f5 100644 --- a/examples/gno.land/p/moul/pageable/pageable_test.gno +++ b/examples/gno.land/p/moul/pageable/pageable_test.gno @@ -271,3 +271,110 @@ func TestInvalidInputs(t *testing.T) { }) } } + +func TestEmptySource(t *testing.T) { + mock := &MockPageable{items: []int{}} + pager := NewPager(mock, 10, false) + + page := pager.GetPage(1) + if page.TotalItems != 0 { + t.Errorf("got total items %d, want 0", page.TotalItems) + } + if page.TotalPages != 0 { + t.Errorf("got total pages %d, want 0", page.TotalPages) + } + if page.HasNext { + t.Error("got HasNext=true, want false") + } + if page.HasPrev { + t.Error("got HasPrev=true, want false") + } + if len(page.Items) != 0 { + t.Errorf("got %d items, want 0", len(page.Items)) + } +} + +func TestReversedPagerEdgeCases(t *testing.T) { + mock := &MockPageable{ + items: []int{1, 2, 3, 4, 5}, + } + pager := NewPager(mock, 2, true) + + tests := []struct { + name string + pageNum int + pageSize int + want []int + }{ + {"last partial page", 3, 2, []int{1}}, + {"oversized page", 1, 10, []int{5, 4, 3, 2, 1}}, + {"empty result", 4, 2, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + page := pager.GetPageWithSize(tt.pageNum, tt.pageSize) + got := make([]int, len(page.Items)) + for i, item := range page.Items { + got[i] = item.Value.(int) + } + + if len(got) != len(tt.want) { + t.Errorf("got length %d, want length %d", len(got), len(tt.want)) + return + } + + for i := 0; i < len(got); i++ { + if got[i] != tt.want[i] { + t.Errorf("at index %d: got %d, want %d", i, got[i], tt.want[i]) + } + } + }) + } +} + +func TestCustomQueryParams(t *testing.T) { + mock := &MockPageable{items: []int{1, 2, 3}} + pager := NewPager(mock, 10, false) + pager.PageQueryParam = "p" + pager.SizeQueryParam = "s" + + tests := []struct { + name string + url string + wantPage int + wantSize int + }{ + {"custom params", "/?p=2&s=1", 2, 1}, + {"old params ignored", "/?page=3&size=2", 1, 10}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + page, size, err := pager.ParseQuery(tt.url) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + 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 TestMustGetPageByPath(t *testing.T) { + mock := &MockPageable{items: []int{1, 2, 3}} + pager := NewPager(mock, 10, false) + + t.Run("panic on invalid url", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic but got none") + } + }() + pager.MustGetPageByPath(":%invalid") + }) +}