github.com/mendersoftware/go-lib-micro@v0.0.0-20240304135804-e8e39c59b148/rest.utils/paging.go (about) 1 // Copyright 2023 Northern.tech AS 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package rest 16 17 import ( 18 "fmt" 19 "net/http" 20 "net/url" 21 "strconv" 22 23 "github.com/pkg/errors" 24 ) 25 26 const ( 27 PerPageDefault = 20 28 PerPageMax = 500 29 30 pageQueryParam = "page" 31 perPageQueryParam = "per_page" 32 ) 33 34 var ( 35 ErrPerPageLimit = errors.Errorf( 36 `parameter "per_page" above limit (max: %d)`, PerPageMax, 37 ) 38 ) 39 40 // ParsePagingParameters parses the paging parameters from the URL query 41 // string and returns the parsed page, per_page or a parsing error respectively. 42 func ParsePagingParameters(r *http.Request) (int64, int64, error) { 43 q := r.URL.Query() 44 var ( 45 err error 46 page int64 47 perPage int64 48 ) 49 qPage := q.Get(pageQueryParam) 50 if qPage == "" { 51 page = 1 52 } else { 53 page, err = strconv.ParseInt(qPage, 10, 64) 54 if err != nil { 55 return -1, -1, errors.Errorf( 56 "invalid page query: \"%s\"", 57 qPage, 58 ) 59 } else if page < 1 { 60 return -1, -1, errors.New("invalid page query: " + 61 "value must be a non-zero positive integer", 62 ) 63 } 64 } 65 66 qPerPage := q.Get(perPageQueryParam) 67 if qPerPage == "" { 68 perPage = PerPageDefault 69 } else { 70 perPage, err = strconv.ParseInt(qPerPage, 10, 64) 71 if err != nil { 72 return -1, -1, errors.Errorf( 73 "invalid per_page query: \"%s\"", 74 qPerPage, 75 ) 76 } else if perPage < 1 { 77 return -1, -1, errors.New("invalid per_page query: " + 78 "value must be a non-zero positive integer", 79 ) 80 } else if perPage > PerPageMax { 81 return page, perPage, ErrPerPageLimit 82 } 83 } 84 return page, perPage, nil 85 } 86 87 type PagingHints struct { 88 // TotalCount provides the total count of elements available, 89 // if provided adds another link to the last page available. 90 TotalCount *int64 91 92 // HasNext instructs adding the "next" link header. This option 93 // has no effect if TotalCount is given. 94 HasNext *bool 95 96 // Pagination parameters 97 Page, PerPage *int64 98 } 99 100 func NewPagingHints() *PagingHints { 101 return new(PagingHints) 102 } 103 104 func (h *PagingHints) SetTotalCount(totalCount int64) *PagingHints { 105 h.TotalCount = &totalCount 106 return h 107 } 108 109 func (h *PagingHints) SetHasNext(hasNext bool) *PagingHints { 110 h.HasNext = &hasNext 111 return h 112 } 113 114 func (h *PagingHints) SetPage(page int64) *PagingHints { 115 h.Page = &page 116 return h 117 } 118 119 func (h *PagingHints) SetPerPage(perPage int64) *PagingHints { 120 h.PerPage = &perPage 121 return h 122 } 123 124 func MakePagingHeaders(r *http.Request, hints ...*PagingHints) ([]string, error) { 125 // Parse hints 126 hint := new(PagingHints) 127 for _, h := range hints { 128 if h == nil { 129 continue 130 } 131 if h.HasNext != nil { 132 hint.HasNext = h.HasNext 133 } 134 if h.TotalCount != nil { 135 hint.TotalCount = h.TotalCount 136 } 137 if h.Page != nil { 138 hint.Page = h.Page 139 } 140 if h.PerPage != nil { 141 hint.PerPage = h.PerPage 142 } 143 } 144 if hint.Page == nil || hint.PerPage == nil { 145 page, perPage, err := ParsePagingParameters(r) 146 if err != nil { 147 return nil, err 148 } 149 hint.Page, hint.PerPage = &page, &perPage 150 } 151 locationURL := url.URL{ 152 Path: r.URL.Path, 153 RawQuery: r.URL.RawQuery, 154 Fragment: r.URL.Fragment, 155 } 156 q := locationURL.Query() 157 // Ensure per_page is set 158 q.Set(perPageQueryParam, strconv.FormatInt(*hint.PerPage, 10)) 159 links := make([]string, 0, 4) 160 q.Set(pageQueryParam, "1") 161 locationURL.RawQuery = q.Encode() 162 links = append(links, fmt.Sprintf( 163 "<%s>; rel=\"first\"", locationURL.String(), 164 )) 165 if (*hint.Page) > 1 { 166 q.Set(pageQueryParam, strconv.FormatInt(*hint.Page-1, 10)) 167 locationURL.RawQuery = q.Encode() 168 links = append(links, fmt.Sprintf( 169 "<%s>; rel=\"prev\"", locationURL.String(), 170 )) 171 } 172 173 // TotalCount takes precedence over HasNext 174 if hint.TotalCount != nil && *hint.TotalCount > 0 { 175 lastPage := (*hint.TotalCount-1) / *hint.PerPage + 1 176 if *hint.Page < lastPage { 177 // Add "next" link 178 q.Set(pageQueryParam, strconv.FormatUint(uint64(*hint.Page)+1, 10)) 179 locationURL.RawQuery = q.Encode() 180 links = append(links, fmt.Sprintf( 181 "<%s>; rel=\"next\"", locationURL.String(), 182 )) 183 } 184 // Add "last" link 185 q.Set(pageQueryParam, strconv.FormatInt(lastPage, 10)) 186 locationURL.RawQuery = q.Encode() 187 links = append(links, fmt.Sprintf( 188 "<%s>; rel=\"last\"", locationURL.String(), 189 )) 190 } else if hint.HasNext != nil && *hint.HasNext { 191 q.Set(pageQueryParam, strconv.FormatUint(uint64(*hint.Page)+1, 10)) 192 locationURL.RawQuery = q.Encode() 193 links = append(links, fmt.Sprintf( 194 "<%s>; rel=\"next\"", locationURL.String(), 195 )) 196 } 197 198 return links, nil 199 }