├── go.mod ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── export_test.go ├── LICENSE ├── sort_test.go ├── .circleci └── config.yml ├── sort.go ├── query_test.go ├── query.go ├── example └── server.go ├── helper_test.go ├── README.md ├── pager.go └── pager_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gemcook/pagination-go 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### PR概要 2 | 3 | - どういうコードを追加したか・どういうコードに変更したか 4 | - 技術的な注意点・レビュアーに注目して欲しい点 5 | 6 | #### 追加したライブラリ 7 | 8 | - なし 9 | 10 | #### 課題とは直接関係がないが修正した項目 11 | 12 | - なし 13 | 14 | #### 今回保留した項目 15 | 16 | - なし -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | var CreateMockPager = func(limit, page, sidePagingCount, totalCount int) *Pager { 4 | pager := Pager{ 5 | limit: limit, 6 | page: page, 7 | sidePagingCount: sidePagingCount, 8 | totalCount: totalCount, 9 | } 10 | return &pager 11 | } 12 | 13 | var NewPager = newPager 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gemcook, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sort_test.go: -------------------------------------------------------------------------------- 1 | package pagination_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | pagination "github.com/gemcook/pagination-go" 8 | ) 9 | 10 | func TestParseSort(t *testing.T) { 11 | type args struct { 12 | queryStr string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want []*pagination.Order 18 | }{ 19 | {"no sort", args{""}, []*pagination.Order{}}, 20 | {"single col asc", args{"?sort=+col_a"}, []*pagination.Order{&pagination.Order{Direction: pagination.DirectionAsc, ColumnName: "col_a"}}}, 21 | {"single col desc", args{"?sort=-col_b"}, []*pagination.Order{&pagination.Order{Direction: pagination.DirectionDesc, ColumnName: "col_b"}}}, 22 | {"multi col", args{"?sort=+col_c-col_d"}, []*pagination.Order{ 23 | &pagination.Order{Direction: pagination.DirectionAsc, ColumnName: "col_c"}, 24 | &pagination.Order{Direction: pagination.DirectionDesc, ColumnName: "col_d"}, 25 | }}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | if got := pagination.ParseSort(tt.args.queryStr); !reflect.DeepEqual(got, tt.want) { 30 | t.Errorf("ParseSort() = %v, want %v", got, tt.want) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | environment: 5 | GOPATH: /home/circleci/go 6 | docker: 7 | - image: circleci/golang:1.10 8 | working_directory: /home/circleci/go/src/github.com/gemcook/pagination-go 9 | steps: 10 | - run: 11 | name: show information 12 | command: | 13 | echo pwd \"$(pwd)\" 14 | echo go version \"$(go version)\" 15 | echo go env \"$(go env)\" 16 | - run: 17 | name: add GOPATH/bin to PATH 18 | command: | 19 | echo 'export PATH=${GOPATH}/bin/:${PATH}' >> $BASH_ENV 20 | - checkout 21 | - run: 22 | name: install go tools 23 | command: | 24 | go get github.com/golang/lint/golint 25 | go get github.com/mattn/goveralls 26 | - run: 27 | name: lint 28 | command: | 29 | golint ./... 30 | go vet ./... 31 | - run: 32 | name: unit test 33 | command: | 34 | go test -v -cover -race -coverprofile=./coverage.out ./... 35 | - run: 36 | name: code coverage 37 | command: | 38 | goveralls -coverprofile=coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN 39 | 40 | workflows: 41 | version: 2 42 | build: 43 | jobs: 44 | - build: 45 | filters: 46 | branches: 47 | only: /.*/ 48 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | // Direction shows the sort direction 9 | type Direction string 10 | 11 | const ( 12 | // DirectionAsc sorts by ascending order 13 | DirectionAsc Direction = "ASC" 14 | // DirectionDesc sorts by descending order 15 | DirectionDesc Direction = "DESC" 16 | ) 17 | 18 | // Order defines sort order clause 19 | type Order struct { 20 | Direction Direction 21 | ColumnName string 22 | } 23 | 24 | // ParseSort parses sort option in the given URL query string 25 | func ParseSort(queryStr string) []*Order { 26 | u, err := url.Parse(queryStr) 27 | if err != nil { 28 | return []*Order{} 29 | } 30 | query := u.Query() 31 | 32 | if s := query.Get("sort"); s != "" { 33 | return ParseOrders(s) 34 | } 35 | return []*Order{} 36 | } 37 | 38 | // ParseOrders parses sort option string 39 | // Sort option would be like '-col_first+col_second'. 40 | func ParseOrders(sort string) []*Order { 41 | if sort == "" { 42 | return []*Order{} 43 | } 44 | 45 | orders := make([]*Order, 0) 46 | o := sort 47 | for _i := strings.IndexAny(o, "+- "); _i == 0; { 48 | col := "" 49 | _o := "" 50 | // 次に + or - が現れる位置を判定 51 | if i := strings.IndexAny(o[1:], "+- "); i == -1 { 52 | col = o[1:] 53 | } else { 54 | col = o[1 : i+1] 55 | _o = o[i+1:] 56 | } 57 | 58 | // カラム名が空の場合はループを抜ける 59 | if col == "" { 60 | break 61 | } 62 | 63 | // ソート条件を設定する 64 | var d Direction 65 | if o[0] == '+' || o[0] == ' ' { 66 | d = DirectionAsc 67 | } else if o[0] == '-' { 68 | d = DirectionDesc 69 | } 70 | 71 | orders = append(orders, &Order{d, col}) 72 | 73 | if _o == "" { 74 | break 75 | } 76 | o = _o 77 | } 78 | return orders 79 | } 80 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package pagination_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | pagination "github.com/gemcook/pagination-go" 8 | ) 9 | 10 | func TestParseQuery(t *testing.T) { 11 | type args struct { 12 | queryStr string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want *pagination.Query 18 | }{ 19 | {"default", args{"https://example.com/fruits?price_range=0,100"}, &pagination.Query{Limit: 10, Page: 1, Sort: []*pagination.Order{}, Enabled: true}}, 20 | {"limit=10, page=5", args{"https://example.com/fruits?price_range=0,100&page=5&limit=10"}, &pagination.Query{Limit: 10, Page: 5, Sort: []*pagination.Order{}, Enabled: true}}, 21 | {"pagination disabled", args{"https://example.com/fruits?price_range=0,100&pagination=false"}, &pagination.Query{Limit: 10, Page: 1, Sort: []*pagination.Order{}, Enabled: false}}, 22 | } 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | if got := pagination.ParseQuery(tt.args.queryStr); !reflect.DeepEqual(got, tt.want) { 26 | t.Errorf("ParseQuery() = %v, want %v", got, tt.want) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | func TestParseMap(t *testing.T) { 33 | tests := []struct { 34 | name string 35 | qs map[string]string 36 | want *pagination.Query 37 | }{ 38 | {"default", map[string]string{}, &pagination.Query{Limit: 10, Page: 1, Sort: []*pagination.Order{}, Enabled: true}}, 39 | {"limit=10, page=2", map[string]string{"limit": "10", "page": "2"}, &pagination.Query{Limit: 10, Page: 2, Sort: []*pagination.Order{}, Enabled: true}}, 40 | {"pagination=false", map[string]string{"pagination": "false"}, &pagination.Query{Limit: 10, Page: 1, Sort: []*pagination.Order{}, Enabled: false}}, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if got := pagination.ParseMap(tt.qs); !reflect.DeepEqual(got, tt.want) { 45 | t.Errorf("ParseMap() = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | ) 7 | 8 | // Query has pagination query parameters. 9 | type Query struct { 10 | Limit int 11 | Page int 12 | Sort []*Order 13 | Enabled bool 14 | } 15 | 16 | // Init initialize pagination query parameters. 17 | func (q *Query) Init() { 18 | q.Limit = 10 19 | q.Page = 1 20 | q.Enabled = true 21 | } 22 | 23 | // ParseQuery parses URL query string to get limit, page and sort 24 | func ParseQuery(queryStr string) *Query { 25 | 26 | // Set default values. 27 | p := &Query{} 28 | p.Init() 29 | 30 | u, err := url.Parse(queryStr) 31 | if err != nil { 32 | return p 33 | } 34 | query := u.Query() 35 | 36 | if limitStr := query.Get("limit"); limitStr != "" { 37 | if limit, err := strconv.Atoi(limitStr); err == nil { 38 | p.Limit = limit 39 | } 40 | } 41 | 42 | if pageStr := query.Get("page"); pageStr != "" { 43 | if page, err := strconv.Atoi(pageStr); err == nil { 44 | p.Page = page 45 | } 46 | } 47 | 48 | if pageStr := query.Get("pagination"); pageStr != "" { 49 | if pageStr == "false" { 50 | p.Enabled = false 51 | } 52 | } 53 | 54 | p.Sort = ParseSort(queryStr) 55 | return p 56 | } 57 | 58 | // ParseMap parses URL parameters map to get limit, page and sort 59 | func ParseMap(qs map[string]string) *Query { 60 | 61 | // Set default values. 62 | p := &Query{} 63 | p.Init() 64 | 65 | if limitStr, ok := qs["limit"]; ok { 66 | if limit, err := strconv.Atoi(limitStr); err == nil { 67 | p.Limit = limit 68 | } 69 | } 70 | 71 | if pageStr, ok := qs["page"]; ok { 72 | if page, err := strconv.Atoi(pageStr); err == nil { 73 | p.Page = page 74 | } 75 | } 76 | 77 | if pageStr, ok := qs["pagination"]; ok { 78 | if pageStr == "false" { 79 | p.Enabled = false 80 | } 81 | } 82 | 83 | orders := []*Order{} 84 | 85 | if sort, ok := qs["sort"]; ok { 86 | orders = ParseOrders(sort) 87 | } 88 | p.Sort = orders 89 | return p 90 | } 91 | -------------------------------------------------------------------------------- /example/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "sort" 9 | "strconv" 10 | "strings" 11 | 12 | pagination "github.com/gemcook/pagination-go" 13 | ) 14 | 15 | type fruit struct { 16 | Name string 17 | Price int 18 | } 19 | 20 | var dummyFruits = []fruit{ 21 | fruit{"Apple", 112}, 22 | fruit{"Pear", 245}, 23 | fruit{"Banana", 60}, 24 | fruit{"Orange", 80}, 25 | fruit{"Kiwi", 106}, 26 | fruit{"Strawberry", 350}, 27 | fruit{"Grape", 400}, 28 | fruit{"Grapefruit", 150}, 29 | fruit{"Pineapple", 200}, 30 | fruit{"Cherry", 140}, 31 | fruit{"Mango", 199}, 32 | } 33 | 34 | type fruitsRepository struct { 35 | priceLowerLimit int 36 | priceHigherLimit int 37 | } 38 | 39 | func newFruitsRepository() *fruitsRepository { 40 | return &fruitsRepository{ 41 | priceLowerLimit: -1 << 31, 42 | priceHigherLimit: 1<<31 - 1, 43 | } 44 | } 45 | 46 | func (fr *fruitsRepository) GetFruits(orders []*pagination.Order) []fruit { 47 | result := make([]fruit, 0) 48 | for _, f := range dummyFruits { 49 | if fr.priceHigherLimit >= f.Price && f.Price >= fr.priceLowerLimit { 50 | result = append(result, f) 51 | } 52 | } 53 | 54 | for _, o := range orders { 55 | if o.ColumnName != "price" { 56 | continue 57 | } 58 | sort.SliceStable(result, func(i, j int) bool { 59 | if o.Direction == pagination.DirectionAsc { 60 | return result[i].Price < result[j].Price 61 | } 62 | 63 | return result[i].Price > result[j].Price 64 | }) 65 | } 66 | 67 | return result 68 | } 69 | 70 | type fruitCondition struct { 71 | PriceLowerLimit *int 72 | PriceHigherLimit *int 73 | } 74 | 75 | func newFruitCondition(low, high int) *fruitCondition { 76 | return &fruitCondition{ 77 | PriceLowerLimit: &low, 78 | PriceHigherLimit: &high, 79 | } 80 | } 81 | 82 | func parseFruitCondition(queryStr string) *fruitCondition { 83 | u, err := url.Parse(queryStr) 84 | if err != nil { 85 | fmt.Println(err) 86 | low := -1 << 31 87 | high := 1<<31 - 1 88 | return newFruitCondition(low, high) 89 | } 90 | query := u.Query() 91 | 92 | if s := query.Get("price_range"); s != "" { 93 | prices := strings.Split(s, ",") 94 | low, err := strconv.Atoi(prices[0]) 95 | if err != nil { 96 | panic(err) 97 | } 98 | high, err := strconv.Atoi(prices[1]) 99 | if err != nil { 100 | panic(err) 101 | } 102 | return newFruitCondition(low, high) 103 | } 104 | 105 | low := -1 << 31 106 | high := 1<<31 - 1 107 | return newFruitCondition(low, high) 108 | } 109 | 110 | type fruitFetcher struct { 111 | repo *fruitsRepository 112 | } 113 | 114 | func newFruitFetcher() *fruitFetcher { 115 | return &fruitFetcher{ 116 | repo: &fruitsRepository{}, 117 | } 118 | } 119 | 120 | func (ff *fruitFetcher) applyCondition(cond *fruitCondition) { 121 | if cond.PriceHigherLimit != nil { 122 | ff.repo.priceHigherLimit = *cond.PriceHigherLimit 123 | } 124 | if cond.PriceLowerLimit != nil { 125 | ff.repo.priceLowerLimit = *cond.PriceLowerLimit 126 | } 127 | } 128 | 129 | func (ff *fruitFetcher) Count(cond interface{}) (int, error) { 130 | if cond != nil { 131 | ff.applyCondition(cond.(*fruitCondition)) 132 | } 133 | orders := make([]*pagination.Order, 0, 0) 134 | fruits := ff.repo.GetFruits(orders) 135 | return len(fruits), nil 136 | } 137 | 138 | func (ff *fruitFetcher) FetchPage(cond interface{}, input *pagination.PageFetchInput, result *pagination.PageFetchResult) error { 139 | if cond != nil { 140 | ff.applyCondition(cond.(*fruitCondition)) 141 | } 142 | fruits := ff.repo.GetFruits(input.Orders) 143 | var toIndex int 144 | toIndex = input.Offset + input.Limit 145 | if toIndex > len(fruits) { 146 | toIndex = len(fruits) 147 | } 148 | for _, fruit := range fruits[input.Offset:toIndex] { 149 | *result = append(*result, fruit) 150 | } 151 | return nil 152 | } 153 | 154 | func handler(w http.ResponseWriter, r *http.Request) { 155 | // RequestURI: https://example.com/fruits?limit=10&page=1&price_range=100,300&sort=+price 156 | p := pagination.ParseQuery(r.URL.RequestURI()) 157 | cond := parseFruitCondition(r.URL.RequestURI()) 158 | fetcher := newFruitFetcher() 159 | 160 | totalCount, totalPages, res, err := pagination.Fetch(fetcher, &pagination.Setting{ 161 | Limit: p.Limit, 162 | Page: p.Page, 163 | Cond: cond, 164 | Orders: p.Sort, 165 | }) 166 | 167 | if err != nil { 168 | w.Header().Set("Content-Type", "text/html; charset=utf8") 169 | w.WriteHeader(400) 170 | fmt.Fprintf(w, "something wrong: %v", err) 171 | return 172 | } 173 | 174 | w.Header().Set("X-Total-Count", strconv.Itoa(totalCount)) 175 | w.Header().Set("X-Total-Pages", strconv.Itoa(totalPages)) 176 | w.Header().Set("Access-Control-Expose-Headers", "X-Total-Count,X-Total-Pages") 177 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 178 | w.WriteHeader(200) 179 | resJSON, _ := json.Marshal(res) 180 | w.Write(resJSON) 181 | } 182 | 183 | func main() { 184 | http.HandleFunc("/fruits", handler) 185 | fmt.Println("server is listening on port 8080") 186 | fmt.Println("try http://localhost:8080/fruits?limit=2&page=1&price_range=100,300&sort=+price") 187 | http.ListenAndServe(":8080", nil) 188 | } 189 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package pagination_test 2 | 3 | import pagination "github.com/gemcook/pagination-go" 4 | 5 | type fruit struct { 6 | Name string 7 | Price int 8 | } 9 | 10 | type fruitCondition struct { 11 | PriceLowerLimit *int 12 | PriceHigherLimit *int 13 | } 14 | 15 | func newFruitCondition(low, high int) *fruitCondition { 16 | return &fruitCondition{ 17 | PriceLowerLimit: &low, 18 | PriceHigherLimit: &high, 19 | } 20 | } 21 | 22 | type fruitFetcher struct { 23 | priceLowerLimit int 24 | priceHigherLimit int 25 | } 26 | 27 | func newFruitFetcher() *fruitFetcher { 28 | return &fruitFetcher{ 29 | priceLowerLimit: -1 << 31, 30 | priceHigherLimit: 1<<31 - 1, 31 | } 32 | } 33 | 34 | func (ff *fruitFetcher) applyCondition(cond *fruitCondition) { 35 | if cond.PriceHigherLimit != nil { 36 | ff.priceHigherLimit = *cond.PriceHigherLimit 37 | } 38 | if cond.PriceLowerLimit != nil { 39 | ff.priceLowerLimit = *cond.PriceLowerLimit 40 | } 41 | } 42 | 43 | func (ff *fruitFetcher) Count(cond interface{}) (int, error) { 44 | if cond != nil { 45 | ff.applyCondition(cond.(*fruitCondition)) 46 | } 47 | dummyFruits := ff.GetDummy() 48 | return len(dummyFruits), nil 49 | } 50 | 51 | func (ff *fruitFetcher) FetchPage(cond interface{}, input *pagination.PageFetchInput, result *pagination.PageFetchResult) error { 52 | if cond != nil { 53 | ff.applyCondition(cond.(*fruitCondition)) 54 | } 55 | dummyFruits := ff.GetDummy() 56 | var toIndex int 57 | toIndex = input.Offset + input.Limit 58 | if toIndex > len(dummyFruits) { 59 | toIndex = len(dummyFruits) 60 | } 61 | for _, fruit := range dummyFruits[input.Offset:toIndex] { 62 | *result = append(*result, fruit) 63 | } 64 | return nil 65 | } 66 | 67 | func (ff *fruitFetcher) GetDummy() []fruit { 68 | result := make([]fruit, 0) 69 | for _, f := range dummyFruits { 70 | if ff.priceHigherLimit >= f.Price && f.Price >= ff.priceLowerLimit { 71 | result = append(result, f) 72 | } 73 | } 74 | return result 75 | } 76 | 77 | var dummyFruits = []fruit{ 78 | fruit{"Apple", 112}, 79 | fruit{"Pear", 245}, 80 | fruit{"Banana", 60}, 81 | fruit{"Orange", 80}, 82 | fruit{"Kiwi", 106}, 83 | fruit{"Strawberry", 350}, 84 | fruit{"Grape", 400}, 85 | fruit{"Grapefruit", 150}, 86 | fruit{"Pineapple", 200}, 87 | fruit{"Cherry", 140}, 88 | fruit{"Mango", 199}, 89 | } 90 | 91 | type LargeData struct { 92 | ID int 93 | } 94 | 95 | type LargeDataFetcher struct{} 96 | 97 | func newLargeDataFetcher() *LargeDataFetcher { 98 | return &LargeDataFetcher{} 99 | } 100 | 101 | func (ff *LargeDataFetcher) Count(cond interface{}) (int, error) { 102 | return len(dummyLargeList), nil 103 | } 104 | 105 | func (ff *LargeDataFetcher) FetchPage(cond interface{}, input *pagination.PageFetchInput, result *pagination.PageFetchResult) error { 106 | var toIndex int 107 | toIndex = input.Offset + input.Limit 108 | if toIndex > len(dummyLargeList) { 109 | toIndex = len(dummyLargeList) 110 | } 111 | for _, LargeData := range dummyLargeList[input.Offset:toIndex] { 112 | *result = append(*result, LargeData) 113 | } 114 | return nil 115 | } 116 | 117 | var dummyLargeList = []LargeData{ 118 | LargeData{1}, 119 | LargeData{2}, 120 | LargeData{3}, 121 | LargeData{4}, 122 | LargeData{5}, 123 | LargeData{6}, 124 | LargeData{7}, 125 | LargeData{8}, 126 | LargeData{9}, 127 | LargeData{10}, 128 | LargeData{11}, 129 | LargeData{12}, 130 | LargeData{13}, 131 | LargeData{14}, 132 | LargeData{15}, 133 | LargeData{16}, 134 | LargeData{17}, 135 | LargeData{18}, 136 | LargeData{19}, 137 | LargeData{20}, 138 | LargeData{21}, 139 | LargeData{22}, 140 | LargeData{23}, 141 | LargeData{24}, 142 | LargeData{25}, 143 | LargeData{26}, 144 | LargeData{27}, 145 | LargeData{28}, 146 | LargeData{29}, 147 | LargeData{30}, 148 | LargeData{31}, 149 | LargeData{32}, 150 | LargeData{33}, 151 | LargeData{34}, 152 | LargeData{35}, 153 | LargeData{36}, 154 | LargeData{37}, 155 | LargeData{38}, 156 | LargeData{39}, 157 | LargeData{40}, 158 | LargeData{41}, 159 | LargeData{42}, 160 | LargeData{43}, 161 | LargeData{44}, 162 | LargeData{45}, 163 | LargeData{46}, 164 | LargeData{47}, 165 | LargeData{48}, 166 | LargeData{49}, 167 | LargeData{50}, 168 | LargeData{51}, 169 | LargeData{52}, 170 | LargeData{53}, 171 | LargeData{54}, 172 | LargeData{55}, 173 | LargeData{56}, 174 | LargeData{57}, 175 | LargeData{58}, 176 | LargeData{59}, 177 | LargeData{60}, 178 | LargeData{61}, 179 | LargeData{62}, 180 | LargeData{63}, 181 | LargeData{64}, 182 | LargeData{65}, 183 | LargeData{66}, 184 | LargeData{67}, 185 | LargeData{68}, 186 | LargeData{69}, 187 | LargeData{70}, 188 | LargeData{71}, 189 | LargeData{72}, 190 | LargeData{73}, 191 | LargeData{74}, 192 | LargeData{75}, 193 | LargeData{76}, 194 | LargeData{77}, 195 | LargeData{78}, 196 | LargeData{79}, 197 | LargeData{80}, 198 | LargeData{81}, 199 | LargeData{82}, 200 | LargeData{83}, 201 | LargeData{84}, 202 | LargeData{85}, 203 | LargeData{86}, 204 | LargeData{87}, 205 | LargeData{88}, 206 | LargeData{89}, 207 | LargeData{90}, 208 | LargeData{91}, 209 | LargeData{92}, 210 | LargeData{93}, 211 | LargeData{94}, 212 | LargeData{95}, 213 | LargeData{96}, 214 | LargeData{97}, 215 | LargeData{98}, 216 | LargeData{99}, 217 | LargeData{100}, 218 | LargeData{101}, 219 | LargeData{102}, 220 | LargeData{103}, 221 | } 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pagination-go 2 | 3 | [![CircleCI](https://circleci.com/gh/gemcook/pagination-go/tree/master.svg?style=shield)](https://circleci.com/gh/gemcook/pagination-go/tree/master) [![Coverage Status](https://coveralls.io/repos/github/gemcook/pagination-go/badge.svg?branch=master)](https://coveralls.io/github/gemcook/pagination-go?branch=master) 4 | 5 | This is a helper library which perfectly matches for server-side implementation of [@gemcook/pagination](https://github.com/gemcook/pagination) 6 | 7 | ## Installation 8 | 9 | ```sh 10 | go get -u github.com/gemcook/pagination-go 11 | ``` 12 | 13 | If you use `dep` 14 | 15 | ```sh 16 | dep ensure -add github.com/gemcook/pagination-go 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### fetcher interface 22 | 23 | Your fetching object must implement fetcher interface. 24 | 25 | ```go 26 | type PageFetcher interface { 27 | Count(cond interface{}) (int, error) 28 | FetchPage(cond interface{}, input *PageFetchInput, result *PageFetchResult) error 29 | } 30 | 31 | type PageFetchInput struct { 32 | Limit int 33 | Offset int 34 | Orders []*Order 35 | } 36 | ``` 37 | 38 | ### parse Function 39 | 40 | Package `pagination` provides `ParseQuery` and `ParseMap` functions that parses Query Parameters from request URL. 41 | Those query parameters below will be parsed. 42 | 43 | | query parameter | Mapped field | required | expected value | default value | 44 | | --------------- | ------------ | -------- | --------------------- | ------------- | 45 | | `limit` | `Limit` | no | positive integer | `10` | 46 | | `page` | `Page` | no | positive integer (1~) | `1` | 47 | | `pagination` | `Enabled` | no | boolean | `true` | 48 | 49 | #### Query String from URL 50 | 51 | ```go 52 | // RequestURI: https://example.com/fruits?limit=10&page=1&price_range=100,300&sort=+price&pagination=true 53 | p := pagination.ParseQuery(r.URL.RequestURI()) 54 | 55 | fmt.Println("limit =", p.Limit) 56 | fmt.Println("page =", p.Page) 57 | fmt.Println("pagination =", p.Enabled) 58 | ``` 59 | 60 | #### Query Parameters from AWS API Gateway - Lambda 61 | 62 | ```go 63 | import "github.com/aws/aws-lambda-go/events" 64 | 65 | func Handler(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 66 | // event.QueryStringParameters 67 | // map[string]string{"limit": "10", "page": "1", "pagination": "false"} 68 | 69 | p := pagination.ParseMap(event.QueryStringParameters) 70 | fmt.Println("limit =", p.Limit) 71 | fmt.Println("page =", p.Page) 72 | fmt.Println("pagination =", p.Enabled) 73 | } 74 | ``` 75 | 76 | ### fetching condition [OPTIONAL] 77 | 78 | Tell pagination the condition to filter resources. 79 | Then use `cond interface{}` in `Count` and `FetchPage` function. 80 | Use type assertion for `cond` to restore your fetching condition object. 81 | 82 | ### Orders [OPTIONAL] 83 | 84 | Optionally, pagination takes orders. 85 | Use `pagination.ParseQuery` or `pagination.ParseMap` to parse sort parameter in query string. 86 | Then, just pass `Query.Sort` to `Setting.Orders`. 87 | 88 | Those query parameters below will be parsed. 89 | 90 | | query parameter | Mapped field | required | expected value | default value | 91 | | --------------- | ------------ | -------- | ---------------------------------------------------------------------------- | ------------- | 92 | | `sort` | `Sort` | no | `+column_name` for ascending sort.
`-column_name` for descending sort. | `nil` | 93 | 94 | ## Example 95 | 96 | ```go 97 | 98 | import ( 99 | "http/net" 100 | "encoding/json" 101 | "strconv" 102 | 103 | "github.com/gemcook/pagination-go" 104 | ) 105 | 106 | type fruitFetcher struct{} 107 | 108 | type FruitCondition struct{ 109 | PriceLowerLimit int 110 | PriceHigherLimit int 111 | } 112 | 113 | func ParseFruitCondition(uri string) *FruitCondition { 114 | // parse uri and initialize struct 115 | } 116 | 117 | func handler(w http.ResponseWriter, r *http.Request) { 118 | // RequestURI: https://example.com/fruits?limit=10&page=1&price_range=100,300&sort=+price 119 | p := pagination.ParseQuery(r.URL.RequestURI()) 120 | cond := parseFruitCondition(r.URL.RequestURI()) 121 | fetcher := newFruitFetcher() 122 | 123 | totalCount, totalPages, res, err := pagination.Fetch(fetcher, &pagination.Setting{ 124 | Limit: p.Limit, 125 | Page: p.Page, 126 | Cond: cond, 127 | Orders: p.Sort, 128 | }) 129 | 130 | if err != nil { 131 | w.Header().Set("Content-Type", "text/html; charset=utf8") 132 | w.WriteHeader(400) 133 | fmt.Fprintf(w, "something wrong: %v", err) 134 | return 135 | } 136 | 137 | w.Header().Set("X-Total-Count", strconv.Itoa(totalCount)) 138 | w.Header().Set("X-Total-Pages", strconv.Itoa(totalPages)) 139 | w.Header().Set("Access-Control-Expose-Headers", "X-Total-Count,X-Total-Pages") 140 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 141 | w.WriteHeader(200) 142 | resJSON, _ := json.Marshal(res) 143 | w.Write(resJSON) 144 | } 145 | ``` 146 | 147 | For full source code, see [example/server.go](./example/server.go). 148 | 149 | Run example. 150 | 151 | ```sh 152 | cd example 153 | go run server.go 154 | ``` 155 | 156 | Then open `http://localhost:8080/fruits?limit=2&page=1&price_range=100,300&sort=+price` 157 | -------------------------------------------------------------------------------- /pager.go: -------------------------------------------------------------------------------- 1 | package pagination 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | ) 8 | 9 | // Setting is pagination setting 10 | type Setting struct { 11 | // data record count per single page 12 | Limit int `json:"limit"` 13 | // active page number (1〜) 14 | Page int `json:"page"` 15 | Cond interface{} 16 | Orders []*Order 17 | } 18 | 19 | // Pager has pagination parameters 20 | type Pager struct { 21 | limit int 22 | page int 23 | sidePagingCount int 24 | totalCount int 25 | Condition interface{} 26 | Orders []*Order 27 | fetcher PageFetcher 28 | } 29 | 30 | // PageFetcher is the interface to fetch the desired range of record. 31 | type PageFetcher interface { 32 | Count(cond interface{}) (int, error) 33 | FetchPage(cond interface{}, input *PageFetchInput, result *PageFetchResult) error 34 | } 35 | 36 | // PageFetchInput input for page fetcher 37 | type PageFetchInput struct { 38 | Limit int 39 | Offset int 40 | Orders []*Order 41 | } 42 | 43 | // GetPageName returns named page 44 | func GetPageName(i int) string { 45 | switch i { 46 | case 0: 47 | return "before_distant" 48 | case 1: 49 | return "before_near" 50 | case 2: 51 | return "after_near" 52 | case 3: 53 | return "after_distant" 54 | default: 55 | return strconv.Itoa(i) 56 | } 57 | } 58 | 59 | // Fetch returns paging response using arbitrary record fetcher. 60 | func Fetch(fetcher PageFetcher, setting *Setting) (totalCount, pageCount int, res *PagingResponse, err error) { 61 | pager, err := newPager(fetcher, setting) 62 | if err != nil { 63 | return 0, 0, nil, err 64 | } 65 | res, err = pager.GetPages() 66 | if err != nil { 67 | return 0, 0, nil, err 68 | } 69 | 70 | return pager.totalCount, pager.GetPageCount(), res, nil 71 | } 72 | 73 | func newPager(fetcher PageFetcher, setting *Setting) (*Pager, error) { 74 | pager := Pager{} 75 | pager.init() 76 | pager.fetcher = fetcher 77 | 78 | if setting.Limit != 0 { 79 | pager.limit = setting.Limit 80 | } 81 | 82 | if setting.Page != 0 { 83 | if setting.Page < 1 { 84 | return nil, fmt.Errorf("page must be >= 1") 85 | } 86 | pager.page = setting.Page 87 | } 88 | 89 | // currently side pages count is fixed to 2 90 | pager.sidePagingCount = 2 91 | 92 | pager.Condition = setting.Cond 93 | pager.Orders = setting.Orders 94 | 95 | return &pager, nil 96 | } 97 | 98 | // init は Pager パラメータの初期値をセットする 99 | func (p *Pager) init() { 100 | p.limit = 10 101 | p.page = 1 102 | p.sidePagingCount = 2 103 | } 104 | 105 | // ActivePageIndex はアクティブのページ番号を取得する 106 | func (p *Pager) ActivePageIndex() int { 107 | return p.page - 1 108 | } 109 | 110 | // StartPageIndex は最初のページ番号を取得する 111 | func (p *Pager) StartPageIndex() int { 112 | startPageIndex := (p.page - 1) - p.sidePagingCount 113 | 114 | // 最終ページを含む場合は取得開始位置を調整する 115 | endPageIndex := startPageIndex + (p.sidePagingCount * 2) 116 | if endPageIndex > p.LastPageIndex() { 117 | startPageIndex = startPageIndex - (endPageIndex - p.LastPageIndex()) 118 | } 119 | 120 | if startPageIndex < 0 { 121 | startPageIndex = 0 122 | } 123 | 124 | return startPageIndex 125 | } 126 | 127 | // LastPageIndex returns the last page index. 128 | func (p *Pager) LastPageIndex() int { 129 | if p.totalCount < 1 || p.limit == 0 { 130 | return 0 131 | } 132 | 133 | // calculate the last page index 134 | lastPageIndex := (p.totalCount - 1) / p.limit 135 | return lastPageIndex 136 | } 137 | 138 | // GetActiveAndSidesLimit gets records count and offset of pages chunk. 139 | func (p *Pager) GetActiveAndSidesLimit() (limit, offset int) { 140 | // start record index of side pages chunk 141 | offset = p.StartPageIndex() * p.limit 142 | 143 | if offset > p.totalCount { 144 | offset = p.totalCount - 1 145 | } 146 | 147 | // data record limit of side pages chunk 148 | limit = ((p.sidePagingCount * 2) + 1) * p.limit 149 | 150 | if limit > p.totalCount { 151 | limit = p.totalCount 152 | } 153 | 154 | return limit, offset 155 | } 156 | 157 | // GetPages gets formated paging response. 158 | func (p *Pager) GetPages() (*PagingResponse, error) { 159 | 160 | count, err := p.fetcher.Count(p.Condition) 161 | if err != nil { 162 | return nil, err 163 | } 164 | p.totalCount = count 165 | 166 | pageCount := p.GetPageCount() 167 | if pageCount == 0 { 168 | return p.formatResponse(PageFetchResult{}, PageFetchResult{}, PageFetchResult{}), nil 169 | } 170 | if p.page > pageCount { 171 | return nil, fmt.Errorf("page is out of range. page range is 1-%v", pageCount) 172 | } 173 | 174 | // active と sides に相当する範囲をまとめて取得する 175 | limit, offset := p.GetActiveAndSidesLimit() 176 | activeAndSides := make(PageFetchResult, 0, limit) 177 | fetchActiveInput := &PageFetchInput{ 178 | Limit: limit, 179 | Offset: offset, 180 | Orders: p.Orders, 181 | } 182 | err = p.fetcher.FetchPage(p.Condition, fetchActiveInput, &activeAndSides) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | // 最初のページが範囲外の場合は取得する 188 | first := make(PageFetchResult, 0, p.limit) 189 | if p.StartPageIndex() > 0 { 190 | fetchFirstInput := &PageFetchInput{ 191 | Limit: p.limit, 192 | Offset: 0, 193 | Orders: p.Orders, 194 | } 195 | err = p.fetcher.FetchPage(p.Condition, fetchFirstInput, &first) 196 | if err != nil { 197 | return nil, err 198 | } 199 | } 200 | 201 | // 最後のページが範囲外の場合は取得する 202 | last := make(PageFetchResult, 0, p.limit) 203 | if p.StartPageIndex()+(p.sidePagingCount*2) < p.LastPageIndex() { 204 | 205 | fetchLastInput := &PageFetchInput{ 206 | Limit: p.limit, 207 | Offset: p.LastPageIndex() * p.limit, 208 | Orders: p.Orders, 209 | } 210 | err = p.fetcher.FetchPage(p.Condition, fetchLastInput, &last) 211 | if err != nil { 212 | return nil, err 213 | } 214 | } 215 | 216 | return p.formatResponse(first, activeAndSides, last), nil 217 | } 218 | 219 | // GetPageCount はページの総数を返します 220 | func (p *Pager) GetPageCount() int { 221 | if p.limit == 0 { 222 | return 0 223 | } 224 | count := math.Ceil(float64(p.totalCount) / float64(p.limit)) 225 | return int(count) 226 | } 227 | 228 | // PageFetchResult has a single page chunk. 229 | type PageFetchResult []interface{} 230 | 231 | // Pages is a named map of pager. 232 | type Pages map[string]PageFetchResult 233 | 234 | // PagingResponse is a response of pager. 235 | type PagingResponse struct { 236 | Pages Pages `json:"pages"` 237 | } 238 | 239 | func (p *Pager) formatResponse(first PageFetchResult, activeAndSides PageFetchResult, last PageFetchResult) *PagingResponse { 240 | active := make(PageFetchResult, 0) 241 | sidesLen := p.sidePagingCount * 2 242 | sides := make([]PageFetchResult, sidesLen, sidesLen) 243 | 244 | page := p.StartPageIndex() + 1 245 | pageIndex := 0 246 | for i, item := range activeAndSides { 247 | 248 | // fill the active page data 249 | if page == p.page { 250 | active = append(active, item) 251 | } 252 | // fill the side pages sequentially 253 | if page != p.page { 254 | sides[pageIndex] = append(sides[pageIndex], item) 255 | } 256 | 257 | // fill the first, if the chunk data has the first page. 258 | if page == 1 { 259 | first = append(first, item) 260 | } 261 | // fill the last, if the chunk data has the last page. 262 | if (p.LastPageIndex() + 1) == page { 263 | last = append(last, item) 264 | } 265 | 266 | // ページの区切り 267 | if (i+1)%p.limit == 0 { 268 | page++ 269 | if pageIndex < sidesLen && len(sides[pageIndex]) > 0 { 270 | pageIndex++ 271 | } 272 | } 273 | } 274 | 275 | // name pages 276 | responsePage := make(Pages) 277 | responsePage["active"] = active 278 | responsePage["first"] = first 279 | responsePage["last"] = last 280 | 281 | for i, sampleItems := range sides { 282 | pageName := GetPageName(i) 283 | responsePage[pageName] = sampleItems 284 | } 285 | 286 | return &PagingResponse{ 287 | Pages: responsePage, 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /pager_test.go: -------------------------------------------------------------------------------- 1 | package pagination_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | pagination "github.com/gemcook/pagination-go" 8 | ) 9 | 10 | func TestPager_GetActiveAndSidesLimit(t *testing.T) { 11 | type fields struct { 12 | Limit int 13 | Page int 14 | SidePagingCount int 15 | totalCount int 16 | } 17 | 18 | tests := []struct { 19 | name string 20 | fields fields 21 | wantLimit int 22 | wantPage int 23 | }{ 24 | {"limit=5&page=1 in 100 record", fields{Limit: 5, Page: 1, SidePagingCount: 2, totalCount: 100}, 25, 0}, 25 | {"limit=2&page=5 in 100 record", fields{Limit: 2, Page: 5, SidePagingCount: 2, totalCount: 100}, 10, 2 * 2}, 26 | {"limit=5&page=6 in 100 record", fields{Limit: 5, Page: 6, SidePagingCount: 2, totalCount: 58}, 25, 3 * 5}, 27 | {"limit=1&page=1 in 1 record", fields{Limit: 1, Page: 1, SidePagingCount: 2, totalCount: 1}, 1, 0}, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | p := pagination.CreateMockPager( 32 | tt.fields.Limit, 33 | tt.fields.Page, 34 | tt.fields.SidePagingCount, 35 | tt.fields.totalCount, 36 | ) 37 | gotLimit, gotPage := p.GetActiveAndSidesLimit() 38 | if gotLimit != tt.wantLimit { 39 | t.Errorf("Pager.GetActiveAndSidesLimit() gotLimit = %v, want %v", gotLimit, tt.wantLimit) 40 | } 41 | if gotPage != tt.wantPage { 42 | t.Errorf("Pager.GetActiveAndSidesLimit() gotPage = %v, want %v", gotPage, tt.wantPage) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestPager_LastPage(t *testing.T) { 49 | type fields struct { 50 | Limit int 51 | totalCount int 52 | } 53 | tests := []struct { 54 | name string 55 | fields fields 56 | want int 57 | }{ 58 | {"limit=10 in 100 record", fields{Limit: 10, totalCount: 100}, 9}, 59 | {"limit=2 in 100 record", fields{Limit: 2, totalCount: 100}, 49}, 60 | {"limit=5 in 59 record", fields{Limit: 5, totalCount: 59}, 11}, 61 | {"limit=5 in 5 record", fields{Limit: 5, totalCount: 1}, 0}, 62 | } 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | p := pagination.CreateMockPager( 66 | tt.fields.Limit, 67 | 1, 68 | 2, 69 | tt.fields.totalCount, 70 | ) 71 | if got := p.LastPageIndex(); got != tt.want { 72 | t.Errorf("Pager.LastPage() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func TestPager_StartPage(t *testing.T) { 79 | type fields struct { 80 | Limit int 81 | Page int 82 | SidePagingCount int 83 | totalCount int 84 | } 85 | tests := []struct { 86 | name string 87 | fields fields 88 | want int 89 | }{ 90 | {"limit=10", fields{Limit: 10, Page: 1, SidePagingCount: 2, totalCount: 100}, 0}, 91 | {"limit=10", fields{Limit: 10, Page: 2, SidePagingCount: 2, totalCount: 100}, 0}, 92 | {"limit=10", fields{Limit: 10, Page: 3, SidePagingCount: 2, totalCount: 100}, 0}, 93 | {"limit=10", fields{Limit: 10, Page: 4, SidePagingCount: 2, totalCount: 100}, 1}, 94 | {"limit=5", fields{Limit: 5, Page: 6, SidePagingCount: 2, totalCount: 58}, 3}, 95 | {"limit=5", fields{Limit: 5, Page: 1, SidePagingCount: 2, totalCount: 1}, 0}, 96 | } 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | p := pagination.CreateMockPager( 100 | tt.fields.Limit, 101 | tt.fields.Page, 102 | tt.fields.SidePagingCount, 103 | tt.fields.totalCount, 104 | ) 105 | if got := p.StartPageIndex(); got != tt.want { 106 | t.Errorf("Pager.StartPage() = %v, want %v", got, tt.want) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestPager_GetPageCount(t *testing.T) { 113 | type fields struct { 114 | limit int 115 | totalCount int 116 | } 117 | tests := []struct { 118 | name string 119 | fields fields 120 | want int 121 | }{ 122 | {"limit=0", fields{0, 100}, 0}, 123 | {"limit=10, total=100 -> 10 pages", fields{10, 100}, 10}, 124 | {"limit=10, total=101 -> 11 pages", fields{10, 101}, 11}, 125 | } 126 | for _, tt := range tests { 127 | t.Run(tt.name, func(t *testing.T) { 128 | p := pagination.CreateMockPager( 129 | tt.fields.limit, 130 | 1, 131 | 2, 132 | tt.fields.totalCount, 133 | ) 134 | if got := p.GetPageCount(); got != tt.want { 135 | t.Errorf("Pager.GetPageCount() = %v, want %v", got, tt.want) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestFetch(t *testing.T) { 142 | type args struct { 143 | fetcher pagination.PageFetcher 144 | setting *pagination.Setting 145 | } 146 | tests := []struct { 147 | name string 148 | args args 149 | wantTotalCount int 150 | wantPageCount int 151 | wantRes *pagination.PagingResponse 152 | wantErr bool 153 | }{ 154 | {"active is out of range", args{newFruitFetcher(), &pagination.Setting{ 155 | Limit: 2, 156 | Page: 100, 157 | }}, 0, 0, nil, true}, 158 | {"no response", args{newFruitFetcher(), &pagination.Setting{ 159 | Limit: 2, 160 | Page: 1, 161 | Cond: newFruitCondition(-1, -1), 162 | }}, 0, 0, &pagination.PagingResponse{ 163 | Pages: pagination.Pages{ 164 | "active": pagination.PageFetchResult{}, 165 | "first": pagination.PageFetchResult{}, 166 | "last": pagination.PageFetchResult{}, 167 | "before_distant": nil, 168 | "before_near": nil, 169 | "after_near": nil, 170 | "after_distant": nil, 171 | }, 172 | }, false}, 173 | {"no condition", args{newFruitFetcher(), &pagination.Setting{ 174 | Limit: 2, 175 | Page: 1, 176 | }}, 11, 6, 177 | &pagination.PagingResponse{ 178 | Pages: pagination.Pages{ 179 | "active": pagination.PageFetchResult{ 180 | dummyFruits[0], 181 | dummyFruits[1], 182 | }, 183 | "first": pagination.PageFetchResult{ 184 | dummyFruits[0], 185 | dummyFruits[1], 186 | }, 187 | "last": pagination.PageFetchResult{ 188 | dummyFruits[10], 189 | }, 190 | "before_distant": pagination.PageFetchResult{ 191 | dummyFruits[2], 192 | dummyFruits[3], 193 | }, 194 | "before_near": pagination.PageFetchResult{ 195 | dummyFruits[4], 196 | dummyFruits[5], 197 | }, 198 | "after_near": pagination.PageFetchResult{ 199 | dummyFruits[6], 200 | dummyFruits[7], 201 | }, 202 | "after_distant": pagination.PageFetchResult{ 203 | dummyFruits[8], 204 | dummyFruits[9], 205 | }, 206 | }, 207 | }, 208 | false, 209 | }, 210 | {"price 100-300", args{newFruitFetcher(), &pagination.Setting{ 211 | Limit: 1, 212 | Page: 4, 213 | Cond: newFruitCondition(100, 300), 214 | }}, 7, 7, 215 | &pagination.PagingResponse{ 216 | Pages: pagination.Pages{ 217 | "active": pagination.PageFetchResult{ 218 | dummyFruits[7], 219 | }, 220 | "first": pagination.PageFetchResult{ 221 | dummyFruits[0], 222 | }, 223 | "last": pagination.PageFetchResult{ 224 | dummyFruits[10], 225 | }, 226 | "before_distant": pagination.PageFetchResult{ 227 | dummyFruits[1], 228 | }, 229 | "before_near": pagination.PageFetchResult{ 230 | dummyFruits[4], 231 | }, 232 | "after_near": pagination.PageFetchResult{ 233 | dummyFruits[8], 234 | }, 235 | "after_distant": pagination.PageFetchResult{ 236 | dummyFruits[9], 237 | }, 238 | }, 239 | }, 240 | false, 241 | }, 242 | {"totalCount is even number", args{newFruitFetcher(), &pagination.Setting{ 243 | Limit: 5, 244 | Page: 1, 245 | Cond: newFruitCondition(0, 360), 246 | }}, 10, 2, 247 | &pagination.PagingResponse{ 248 | Pages: pagination.Pages{ 249 | "active": pagination.PageFetchResult{ 250 | dummyFruits[0], 251 | dummyFruits[1], 252 | dummyFruits[2], 253 | dummyFruits[3], 254 | dummyFruits[4], 255 | }, 256 | "first": pagination.PageFetchResult{ 257 | dummyFruits[0], 258 | dummyFruits[1], 259 | dummyFruits[2], 260 | dummyFruits[3], 261 | dummyFruits[4], 262 | }, 263 | "last": pagination.PageFetchResult{ 264 | dummyFruits[5], 265 | dummyFruits[7], 266 | dummyFruits[8], 267 | dummyFruits[9], 268 | dummyFruits[10], 269 | }, 270 | "before_distant": pagination.PageFetchResult{ 271 | dummyFruits[5], 272 | dummyFruits[7], 273 | dummyFruits[8], 274 | dummyFruits[9], 275 | dummyFruits[10], 276 | }, 277 | "before_near": nil, 278 | "after_near": nil, 279 | "after_distant": nil, 280 | }, 281 | }, 282 | false, 283 | }, 284 | } 285 | for _, tt := range tests { 286 | t.Run(tt.name, func(t *testing.T) { 287 | gotTotalCount, gotPageCount, gotRes, err := pagination.Fetch(tt.args.fetcher, tt.args.setting) 288 | if (err != nil) != tt.wantErr { 289 | t.Errorf("Fetch() error = %v, wantErr %v", err, tt.wantErr) 290 | return 291 | } 292 | if gotTotalCount != tt.wantTotalCount { 293 | t.Errorf("Fetch() gotTotalCount = %v, want %v", gotTotalCount, tt.wantTotalCount) 294 | } 295 | if gotPageCount != tt.wantPageCount { 296 | t.Errorf("Fetch() gotPageCount = %v, want %v", gotPageCount, tt.wantPageCount) 297 | } 298 | if err != nil { 299 | return 300 | } 301 | for key := range tt.wantRes.Pages { 302 | gotVal, ok := gotRes.Pages[key] 303 | if !ok { 304 | t.Errorf("Fetch() gotRes.Pages must have key = %v, but got %v", key, gotRes) 305 | } 306 | if !reflect.DeepEqual(tt.wantRes.Pages[key], gotVal) { 307 | t.Errorf("Fetch() gotRes.Pages[%v] = %v, want %v", key, gotVal, tt.wantRes.Pages[key]) 308 | } 309 | } 310 | }) 311 | } 312 | } 313 | 314 | func TestPager_GetPages_LargeData_Last(t *testing.T) { 315 | type args struct { 316 | Limit int 317 | Page int 318 | Condition interface{} 319 | Orders []*pagination.Order 320 | Fetcher pagination.PageFetcher 321 | } 322 | tests := []struct { 323 | name string 324 | args args 325 | wantLast []LargeData 326 | wantErr bool 327 | }{ 328 | {"largeData", 329 | args{ 330 | Limit: 10, Page: 1, Condition: nil, Orders: []*pagination.Order{}, 331 | Fetcher: newLargeDataFetcher(), 332 | }, 333 | []LargeData{LargeData{101}, LargeData{102}, LargeData{103}}, 334 | false, 335 | }, 336 | } 337 | for _, tt := range tests { 338 | t.Run(tt.name, func(t *testing.T) { 339 | fetcher := newLargeDataFetcher() 340 | p, err := pagination.NewPager(fetcher, &pagination.Setting{ 341 | Limit: tt.args.Limit, 342 | Page: tt.args.Page, 343 | Cond: tt.args.Condition, 344 | Orders: tt.args.Orders, 345 | }) 346 | if (err != nil) != tt.wantErr { 347 | t.Errorf("Pager.GetPages() error = %v, wantErr %v", err, tt.wantErr) 348 | return 349 | } 350 | 351 | got, err := p.GetPages() 352 | if (err != nil) != tt.wantErr { 353 | t.Errorf("Pager.GetPages() error = %v, wantErr %v", err, tt.wantErr) 354 | return 355 | } 356 | 357 | wantLast := make(pagination.PageFetchResult, 0) 358 | for _, data := range tt.wantLast { 359 | wantLast = append(wantLast, data) 360 | } 361 | gotLast := got.Pages["last"] 362 | if !reflect.DeepEqual(gotLast, wantLast) { 363 | t.Errorf("Pager.GetPages() pages.last = %+v, wantLast %+v", gotLast, wantLast) 364 | } 365 | }) 366 | } 367 | } 368 | --------------------------------------------------------------------------------