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_utils
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"math"
    21  	"net/url"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/ant0ine/go-json-rest/rest"
    26  
    27  	micro_strings "github.com/mendersoftware/go-lib-micro/strings"
    28  )
    29  
    30  // pagination constants
    31  const (
    32  	PageName       = "page"
    33  	PerPageName    = "per_page"
    34  	PageMin        = 1
    35  	PageDefault    = 1
    36  	PerPageMin     = 1
    37  	PerPageMax     = 500
    38  	PerPageDefault = 20
    39  	LinkHdr        = "Link"
    40  	LinkTmpl       = "<%s>; rel=\"%s\""
    41  	LinkPrev       = "prev"
    42  	LinkNext       = "next"
    43  	LinkFirst      = "first"
    44  	DefaultScheme  = "http"
    45  )
    46  
    47  // error msgs
    48  func MsgQueryParmInvalid(name string) string {
    49  	return fmt.Sprintf("Can't parse param %s", name)
    50  }
    51  
    52  func MsgQueryParmMissing(name string) string {
    53  	return fmt.Sprintf("Missing required param %s", name)
    54  }
    55  
    56  func MsgQueryParmLimit(name string) string {
    57  	return fmt.Sprintf("Param %s is out of bounds", name)
    58  }
    59  
    60  func MsgQueryParmOneOf(name string, allowed []string) string {
    61  	return fmt.Sprintf("Param %s must be one of %v", name, allowed)
    62  }
    63  
    64  // query param parsing/validation
    65  func ParseQueryParmUInt(r *rest.Request, name string, required bool, min, max, def uint64) (uint64, error) {
    66  	strVal := r.URL.Query().Get(name)
    67  
    68  	if strVal == "" {
    69  		if required {
    70  			return 0, errors.New(MsgQueryParmMissing(name))
    71  		} else {
    72  			return def, nil
    73  		}
    74  	}
    75  
    76  	uintVal, err := strconv.ParseUint(strVal, 10, 32)
    77  	if err != nil {
    78  		return 0, errors.New(MsgQueryParmInvalid(name))
    79  	}
    80  
    81  	if uintVal < min || uintVal > max {
    82  		return 0, errors.New(MsgQueryParmLimit(name))
    83  	}
    84  
    85  	return uintVal, nil
    86  }
    87  
    88  func ParseQueryParmBool(r *rest.Request, name string, required bool, def *bool) (*bool, error) {
    89  	strVal := r.URL.Query().Get(name)
    90  
    91  	if strVal == "" {
    92  		if required {
    93  			return nil, errors.New(MsgQueryParmMissing(name))
    94  		} else {
    95  			return def, nil
    96  		}
    97  	}
    98  
    99  	boolVal, err := strconv.ParseBool(strVal)
   100  	if err != nil {
   101  		return nil, errors.New(MsgQueryParmInvalid(name))
   102  	}
   103  
   104  	return &boolVal, nil
   105  }
   106  
   107  func ParseQueryParmStr(r *rest.Request, name string, required bool, allowed []string) (string, error) {
   108  	val := r.URL.Query().Get(name)
   109  
   110  	if val == "" {
   111  		if required {
   112  			return "", errors.New(MsgQueryParmMissing(name))
   113  		}
   114  	} else {
   115  		if allowed != nil && !micro_strings.ContainsString(val, allowed) {
   116  			return "", errors.New(MsgQueryParmOneOf(name, allowed))
   117  		}
   118  	}
   119  
   120  	return val, nil
   121  }
   122  
   123  // pagination helpers
   124  func ParsePagination(r *rest.Request) (uint64, uint64, error) {
   125  	page, err := ParseQueryParmUInt(r, PageName, false, PageMin, math.MaxUint64, PageDefault)
   126  	if err != nil {
   127  		return 0, 0, err
   128  	}
   129  
   130  	per_page, err := ParseQueryParmUInt(r, PerPageName, false, PerPageMin, PerPageMax, PerPageDefault)
   131  	if err != nil {
   132  		return 0, 0, err
   133  	}
   134  
   135  	return page, per_page, nil
   136  }
   137  
   138  func MakePageLinkHdrs(r *rest.Request, page, per_page uint64, has_next bool) []string {
   139  	var links []string
   140  	if page > 1 {
   141  		links = append(links, MakeLink(LinkPrev, r, page-1, per_page))
   142  	}
   143  
   144  	if has_next {
   145  		links = append(links, MakeLink(LinkNext, r, page+1, per_page))
   146  	}
   147  
   148  	links = append(links, MakeLink(LinkFirst, r, 1, per_page))
   149  	return links
   150  }
   151  
   152  // MakeLink creates a relative URL for insertion in the link header URL field.
   153  func MakeLink(link_type string, r *rest.Request, page, per_page uint64) string {
   154  	q := r.URL.Query()
   155  	q.Set(PageName, strconv.Itoa(int(page)))
   156  	q.Set(PerPageName, strconv.Itoa(int(per_page)))
   157  	url := url.URL{
   158  		Path:     r.URL.Path,
   159  		RawPath:  r.URL.RawPath,
   160  		RawQuery: q.Encode(),
   161  		Fragment: r.URL.Fragment,
   162  	}
   163  
   164  	return fmt.Sprintf(LinkTmpl, url.String(), link_type)
   165  }
   166  
   167  // build URL using request 'r' and template, replace path params with
   168  // elements from 'params' using lexical match as in strings.Replace()
   169  func BuildURL(r *rest.Request, template string, params map[string]string) *url.URL {
   170  	url := r.BaseUrl()
   171  
   172  	path := template
   173  	for k, v := range params {
   174  		path = strings.Replace(path, k, v, -1)
   175  	}
   176  	url.Path = path
   177  
   178  	return url
   179  }