[Golang]ページャーに便利なライブラリ、「pagination-go」を触ってみた

# はじめに

今回、pagination-goというライブラリを知ったので、公式のサンプルコードを実行してあそんでみました。
公式のドキュメントが英語で、Google翻訳と相性が悪い感じだったので、公式のサンプルの実行手順をまとめて共有させて頂きます。[公式はこちら](https://github.com/gemcook/pagination-go/tree/master/example)。

# インストール方法

```console:go　getでのインストールコマンド
$ go get -u github.com/gemcook/pagination-go
```

```console:depでのインストールコマンド
$ dep ensure -add github.com/gemcook/pagination-go
```

# 動作確認方法

**以下のサンプルコードを全文コピペ**して、`main.go`など適当な名前で保存してください。

## 公式のサンプルコード

```golang:公式より全文
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"

	pagination "github.com/gemcook/pagination-go"
)

type fruit struct {
	Name  string
	Price int
}

var dummyFruits = []fruit{
	fruit{"Apple", 112},
	fruit{"Pear", 245},
	fruit{"Banana", 60},
	fruit{"Orange", 80},
	fruit{"Kiwi", 106},
	fruit{"Strawberry", 350},
	fruit{"Grape", 400},
	fruit{"Grapefruit", 150},
	fruit{"Pineapple", 200},
	fruit{"Cherry", 140},
	fruit{"Mango", 199},
}

type fruitsRepository struct {
	priceLowerLimit  int
	priceHigherLimit int
}

func newFruitsRepository() *fruitsRepository {
	return &fruitsRepository{
		priceLowerLimit:  -1 << 31,
		priceHigherLimit: 1<<31 - 1,
	}
}

func (fr *fruitsRepository) GetFruits(orders []*pagination.Order) []fruit {
	result := make([]fruit, 0)
	for _, f := range dummyFruits {
		if fr.priceHigherLimit >= f.Price && f.Price >= fr.priceLowerLimit {
			result = append(result, f)
		}
	}

	for _, o := range orders {
		if o.ColumnName != "price" {
			continue
		}
		sort.SliceStable(result, func(i, j int) bool {
			if o.Direction == pagination.DirectionAsc {
				return result[i].Price < result[j].Price
			}

			return result[i].Price > result[j].Price
		})
	}

	return result
}

type fruitCondition struct {
	PriceLowerLimit  *int
	PriceHigherLimit *int
}

func newFruitCondition(low, high int) *fruitCondition {
	return &fruitCondition{
		PriceLowerLimit:  &low,
		PriceHigherLimit: &high,
	}
}

func parseFruitCondition(queryStr string) *fruitCondition {
	u, err := url.Parse(queryStr)
	if err != nil {
		fmt.Println(err)
		low := -1 << 31
		high := 1<<31 - 1
		return newFruitCondition(low, high)
	}
	query := u.Query()

	if s := query.Get("price_range"); s != "" {
		prices := strings.Split(s, ",")
		low, err := strconv.Atoi(prices[0])
		if err != nil {
			panic(err)
		}
		high, err := strconv.Atoi(prices[1])
		if err != nil {
			panic(err)
		}
		return newFruitCondition(low, high)
	}

	low := -1 << 31
	high := 1<<31 - 1
	return newFruitCondition(low, high)
}

type fruitFetcher struct {
	repo *fruitsRepository
}

func newFruitFetcher() *fruitFetcher {
	return &fruitFetcher{
		repo: &fruitsRepository{},
	}
}

func (ff *fruitFetcher) applyCondition(cond *fruitCondition) {
	if cond.PriceHigherLimit != nil {
		ff.repo.priceHigherLimit = *cond.PriceHigherLimit
	}
	if cond.PriceLowerLimit != nil {
		ff.repo.priceLowerLimit = *cond.PriceLowerLimit
	}
}

func (ff *fruitFetcher) Count(cond interface{}) (int, error) {
	if cond != nil {
		ff.applyCondition(cond.(*fruitCondition))
	}
	orders := make([]*pagination.Order, 0, 0)
	fruits := ff.repo.GetFruits(orders)
	return len(fruits), nil
}

func (ff *fruitFetcher) FetchPage(cond interface{}, input *pagination.PageFetchInput, result *pagination.PageFetchResult) error {
	if cond != nil {
		ff.applyCondition(cond.(*fruitCondition))
	}
	fruits := ff.repo.GetFruits(input.Orders)
	var toIndex int
	toIndex = input.Offset + input.Limit
	if toIndex > len(fruits) {
		toIndex = len(fruits)
	}
	for _, fruit := range fruits[input.Offset:toIndex] {
		*result = append(*result, fruit)
	}
	return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
	// RequestURI: https://example.com/fruits?limit=10&page=1&price_range=100,300&sort=+price
	p := pagination.ParseQuery(r.URL.RequestURI())
	cond := parseFruitCondition(r.URL.RequestURI())
	fetcher := newFruitFetcher()

	totalCount, totalPages, res, err := pagination.Fetch(fetcher, &pagination.Setting{
		Limit:  p.Limit,
		Page:   p.Page,
		Cond:   cond,
		Orders: p.Sort,
	})

	if err != nil {
		w.Header().Set("Content-Type", "text/html; charset=utf8")
		w.WriteHeader(400)
		fmt.Fprintf(w, "something wrong: %v", err)
		return
	}

	w.Header().Set("X-Total-Count", strconv.Itoa(totalCount))
	w.Header().Set("X-Total-Pages", strconv.Itoa(totalPages))
	w.Header().Set("Access-Control-Expose-Headers", "X-Total-Count,X-Total-Pages")
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(200)
	resJSON, _ := json.Marshal(res)
	w.Write(resJSON)
}

func main() {
	http.HandleFunc("/fruits", handler)
	fmt.Println("server is listening on port 8080")
	fmt.Println("try http://localhost:8080/fruits?limit=2&page=1&price_range=100,300&sort=+price")
	http.ListenAndServe(":8080", nil)
}
```

## 手順

ターミナルやコマンドプロンプト等で、カレントディレクトリをファイル保存したところまで移動し、以下のコマンドを入力して実行します。

```console:実行コマンド
$ go run main.go
```

`main.go`のところは、違う名前で保存されている方は、そのファイル名で実行して下さい。

```console:実行したら表示されるもの
server is listening on port 8080
try http://localhost:8080/fruits?limit=2&page=1&price_range=100,300&sort=+price
```

一瞬エラーコードかと思いきや、URLが書かれていてそこにアクセスすればOKです。
抜粋すると下のURLです。

`http://localhost:8080/fruits?limit=2&page=1&price_range=100,300&sort=+price`

```json:ブラウザ出力結果（JSON整形済）
{
	"pages": {
		"active": [
			{
				"Name": "Kiwi",
				"Price": 106
			},
			{
				"Name": "Apple",
				"Price": 112
			}
		],
		"after_distant": null,
		"after_near": [
			{
				"Name": "Pear",
				"Price": 245
			}
		],
		"before_distant": [
			{
				"Name": "Cherry",
				"Price": 140
			},
			{
				"Name": "Grapefruit",
				"Price": 150
			}
		],
		"before_near": [
			{
				"Name": "Mango",
				"Price": 199
			},
			{
				"Name": "Pineapple",
				"Price": 200
			}
		],
		"first": [
			{
				"Name": "Kiwi",
				"Price": 106
			},
			{
				"Name": "Apple",
				"Price": 112
			}
		],
		"last": [
			{
				"Name": "Pear",
				"Price": 245
			}
		]
	}
}
```

ページネーションに使いやすそうな形で、きっちりとJSONファイルが返っています。
これは便利そう。

## ページネーションの詳細を設定するには

先ほどアクセスしたURLのうち、
`limit=2&page=1&price_range=100,300&sort=+price`
のパラメーターを、以下表に沿って変更するだけでOKです。

| query parameter | Mapped field | required | expected value | default value |
|:--|:--|:--|:--|:--|
| limit | Limit | no | positive integer | 10 |
| page | Page | no | positive integer (1~) | 1 |
| pagination | Enabled | no | boolean | true |

パラメータをいじって、再度アクセスすると、ページネーションするのにいい感じに整形されて返されているのが確認できるかと思います。
色々触ってみて遊んでみました。

```json:limit=2&page=1&price_range=300,350&sort=+price
{
	"pages": {
		"active": [
			{
				"Name": "Strawberry",
				"Price": 350
			}
		],
		"after_distant": null,
		"after_near": null,
		"before_distant": null,
		"before_near": null,
		"first": [
			{
				"Name": "Strawberry",
				"Price": 350
			}
		],
		"last": [
			{
				"Name": "Strawberry",
				"Price": 350
			}
		]
	}
}
```

```golang:limit=4&page=1&price_range=0,500&sort=+price
{
	"pages": {
		"active": [
			{
				"Name": "Banana",
				"Price": 60
			},
			{
				"Name": "Orange",
				"Price": 80
			},
			{
				"Name": "Kiwi",
				"Price": 106
			},
			{
				"Name": "Apple",
				"Price": 112
			}
		],
		"after_distant": null,
		"after_near": null,
		"before_distant": [
			{
				"Name": "Cherry",
				"Price": 140
			},
			{
				"Name": "Grapefruit",
				"Price": 150
			},
			{
				"Name": "Mango",
				"Price": 199
			},
			{
				"Name": "Pineapple",
				"Price": 200
			}
		],
		"before_near": [
			{
				"Name": "Pear",
				"Price": 245
			},
			{
				"Name": "Strawberry",
				"Price": 350
			},
			{
				"Name": "Grape",
				"Price": 400
			}
		],
		"first": [
			{
				"Name": "Banana",
				"Price": 60
			},
			{
				"Name": "Orange",
				"Price": 80
			},
			{
				"Name": "Kiwi",
				"Price": 106
			},
			{
				"Name": "Apple",
				"Price": 112
			}
		],
		"last": [
			{
				"Name": "Pear",
				"Price": 245
			},
			{
				"Name": "Strawberry",
				"Price": 350
			},
			{
				"Name": "Grape",
				"Price": 400
			}
		]
	}
}
```

簡単に色々できておもしろい。

# 最後に

今回は、公式のライブラ入りを触ってみただけですが、共有させて頂きました。
もう少しGo言語の理解を深めて、ライブラリの中身の解説とかも、できるようになりたいところです。

最後まで、ありがとうございました。