Skip to content
This repository has been archived by the owner on Feb 5, 2021. It is now read-only.

Commit

Permalink
feat: Page has a resultCount value, rework package API
Browse files Browse the repository at this point in the history
Merge pull request #1 from qri-io/results_count
  • Loading branch information
b5 authored May 27, 2020
2 parents f2fbf0e + 198e5f3 commit 7a07891
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 42 deletions.
105 changes: 89 additions & 16 deletions page.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
package apiutil

import "net/http"
import (
"net/http"
"net/url"
"strconv"
)

// DefaultPageSize is the
const DefaultPageSize = 100
var (
// DefaultPageSize is the number NewPage
DefaultPageSize = 50
// DefaultMaxPageSize is the max size a page can be by default, set to -1 to
// ignore maximum sizes
DefaultMaxPageSize = 100
)

// Page represents pagination information
type Page struct {
Number int `json:"page"`
Size int `json:"pageSize"`
Number int `json:"page,omitempty"`
Size int `json:"pageSize,omitempty"`
ResultCount int `json:"resultCount,omitempty"`
NextURL string `json:"nextUrl"`
PrevURL string `json:"prevUrl"`
}

// NewPage is a conveince page constructor
// NewPage constructs a basic page struct, setting sensible defaults
func NewPage(number, size int) Page {
return Page{number, size}
if number <= 0 {
number = 1
}

if DefaultMaxPageSize != -1 && size > DefaultMaxPageSize {
size = DefaultMaxPageSize
}

return Page{
Number: number,
Size: size,
}
}

// Limit is a convenience accessor for page size
Expand All @@ -27,24 +50,71 @@ func (p Page) Offset() int {
return (p.Number - 1) * p.Size
}

// Next returns a page with the number advanced by one
func (p Page) Next() Page {
return Page{
Number: p.Number + 1,
Size: p.Size,
ResultCount: p.ResultCount,
NextURL: p.NextURL,
PrevURL: p.PrevURL,
}
}

// Prev returns a page with the number decremented by 1
func (p Page) Prev() Page {
return Page{
Number: p.Number - 1,
Size: p.Size,
ResultCount: p.ResultCount,
NextURL: p.NextURL,
PrevURL: p.PrevURL,
}
}

// SetQueryParams adds pagination info to a url as query parameters
func (p Page) SetQueryParams(u *url.URL) *url.URL {
q := u.Query()
q.Set("page", strconv.Itoa(p.Number))
if p.Size != DefaultPageSize {
q.Set("pageSize", strconv.Itoa(p.Size))
}

u.RawQuery = q.Encode()
return u
}

// NextPageExists returns false if the next page.ResultCount is a postive
// number and the starting offset of the next page exceeds page.ResultCount
func (p Page) NextPageExists() bool {
return p.ResultCount <= 0 || !(p.Number*p.Size >= p.ResultCount)
}

// PrevPageExists returns false if the page number is 1
func (p Page) PrevPageExists() bool {
return p.Number > 1
}

// PageFromRequest extracts pagination params from an http request
func PageFromRequest(r *http.Request) Page {
var number, size int
if i, err := ReqParamInt("page", r); err == nil {
number = i
}
number := ReqParamInt(r, "page", 1)
if number <= 0 {
number = 1
}

if i, err := ReqParamInt("pageSize", r); err == nil {
size = i
}
size := ReqParamInt(r, "pageSize", DefaultPageSize)
if size <= 0 {
size = DefaultPageSize
}

return NewPage(number, size)
if DefaultMaxPageSize != -1 && size > DefaultMaxPageSize {
size = DefaultMaxPageSize
}

return Page{
Number: number,
Size: size,
}
}

// NewPageFromOffsetAndLimit converts a offset and Limit to a Page struct
Expand All @@ -55,5 +125,8 @@ func NewPageFromOffsetAndLimit(offset, limit int) Page {
size = DefaultPageSize
}
number = offset/size + 1
return NewPage(number, size)
return Page{
Number: number,
Size: size,
}
}
39 changes: 39 additions & 0 deletions page_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package apiutil

import (
"fmt"
"net/http/httptest"
"strconv"
"testing"
Expand Down Expand Up @@ -61,3 +62,41 @@ func TestNewPageFromLimitAndOffset(t *testing.T) {
}
}
}

func TestNextPageExists(t *testing.T) {
cases := []struct {
number, size, resultCount int
expect bool
}{
{1, 50, 0, true},
{1, 50, 51, true},
{1, 50, 50, false},
{1, 50, 35, false},
}

for _, c := range cases {
t.Run(fmt.Sprintf("num_%d_size_%d_count_%d=%t", c.number, c.size, c.resultCount, c.expect), func(t *testing.T) {
p := Page{Number: c.number, Size: c.size, ResultCount: c.resultCount}
got := p.NextPageExists()
if c.expect != got {
t.Errorf("result mismatch. expected: %t got: %t", c.expect, got)
}
})
}
}

func TestPrevPageExists(t *testing.T) {
for _, num := range []int{2, 3, 4} {
p := Page{Number: num}
if !p.PrevPageExists() {
t.Errorf("expected true for %d value, got false", num)
}
}

for _, num := range []int{-1, 0, -20} {
p := Page{Number: num}
if p.PrevPageExists() {
t.Errorf("expected false for %d value, got true", num)
}
}
}
27 changes: 20 additions & 7 deletions requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,27 @@ import (
)

// ReqParamInt extracts an integer parameter from a request form value
// TODO - consider providing a default param & removing the error
func ReqParamInt(key string, r *http.Request) (int, error) {
i, err := strconv.ParseInt(r.FormValue(key), 10, 0)
return int(i), err
func ReqParamInt(r *http.Request, key string, def int) int {
v := r.FormValue(key)
if v == "" {
return def
}
i, err := strconv.ParseInt(v, 10, 0)
if err != nil {
return def
}
return int(i)
}

// ReqParamBool pulls a boolean parameter from a request form value
// TODO - consider providing a default param & removing the error
func ReqParamBool(key string, r *http.Request) (bool, error) {
return strconv.ParseBool(r.FormValue(key))
func ReqParamBool(r *http.Request, key string, def bool) bool {
v := r.FormValue(key)
if v == "" {
return def
}
b, err := strconv.ParseBool(v)
if err != nil {
return def
}
return b
}
70 changes: 70 additions & 0 deletions requests_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package apiutil

import (
"fmt"
"net/http"
"testing"
)

func TestRequestParamInt(t *testing.T) {
cases := []struct {
value string
expect, def int
}{
{"", 0, 0},
{"", 1, 1},
{"", -1, -1},
{"-1", -1, 0},
{"10", 10, 0},
}

for _, c := range cases {
t.Run(fmt.Sprintf("def_%d_input_%s", c.def, c.value), func(t *testing.T) {
r, err := http.NewRequest("GET", "https://example.com", nil)
if err != nil {
t.Fatal(err)
}

q := r.URL.Query()
q.Set("key", c.value)
r.URL.RawQuery = q.Encode()

got := ReqParamInt(r, "key", c.def)

if c.expect != got {
t.Errorf("result mismatch. expected: %d got: %d", c.expect, got)
}
})
}
}

func TestRequestParamBool(t *testing.T) {
cases := []struct {
value string
expect, def bool
}{
{"", false, false},
{"", true, true},
{"false", false, true},
{"true", true, false},
}

for _, c := range cases {
t.Run(fmt.Sprintf("def_%t_input_%s", c.def, c.value), func(t *testing.T) {
r, err := http.NewRequest("GET", "https://example.com", nil)
if err != nil {
t.Fatal(err)
}

q := r.URL.Query()
q.Set("key", c.value)
r.URL.RawQuery = q.Encode()

got := ReqParamBool(r, "key", c.def)

if c.expect != got {
t.Errorf("result mismatch. expected: %t got: %t", c.expect, got)
}
})
}
}
28 changes: 9 additions & 19 deletions responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package apiutil
import (
"encoding/json"
"net/http"
"strconv"
)

// WriteResponse wraps response data in an envelope & writes it
Expand All @@ -20,32 +19,23 @@ func WriteResponse(w http.ResponseWriter, data interface{}) error {
// WritePageResponse wraps response data and pagination information in an
// envelope and writes it
func WritePageResponse(w http.ResponseWriter, data interface{}, r *http.Request, p Page) error {
if p.PrevPageExists() {
p.PrevURL = p.Prev().SetQueryParams(r.URL).String()
}
if p.NextPageExists() {
p.NextURL = p.Next().SetQueryParams(r.URL).String()
}

env := map[string]interface{}{
"meta": map[string]interface{}{
"code": http.StatusOK,
},
"data": data,
"pagination": map[string]interface{}{
"nextUrl": nextPageURL(r, p),
},
"data": data,
"pagination": p,
}
return jsonResponse(w, env)
}

func nextPageURL(r *http.Request, p Page) string {
q := r.URL.Query()
pageNum := 1
if val, ok := q["page"]; ok {
var err error
if pageNum, err = strconv.Atoi(val[0]); err != nil {
return r.URL.String()
}
}
q.Set("page", strconv.Itoa(pageNum+1))
r.URL.RawQuery = q.Encode()
return r.URL.String()
}

// WriteMessageResponse includes a message with a data response
func WriteMessageResponse(w http.ResponseWriter, message string, data interface{}) error {
env := map[string]interface{}{
Expand Down
Loading

0 comments on commit 7a07891

Please sign in to comment.