github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/apiv3/route/paginator.go (about)

     1  package route
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"path"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/evergreen-ci/evergreen/apiv3"
    15  	"github.com/evergreen-ci/evergreen/apiv3/model"
    16  	"github.com/evergreen-ci/evergreen/apiv3/servicecontext"
    17  )
    18  
    19  const (
    20  	defaultLimit = 100
    21  )
    22  
    23  var linkMatcher = regexp.MustCompile(`^\<(\S+)\>; rel=\"(\S+)\"`)
    24  
    25  // PaginationExecutor is a struct that handles gathering necessary
    26  // information for pagination and handles executing the pagination. It is
    27  // designed to be embedded into a request handler to completely handler the
    28  // execution of endpoints with pagination.
    29  type PaginationExecutor struct {
    30  	// KeyQueryParam is the query param that a PaginationExecutor
    31  	// expects to hold the key to use for fetching results.
    32  	KeyQueryParam string
    33  	// LimitQueryParam is the query param that a PaginationExecutor
    34  	// expects to hold the limit to use for fetching results.
    35  	LimitQueryParam string
    36  
    37  	// Paginator is the function that a PaginationExector uses to
    38  	// retrieve results from the service layer.
    39  	Paginator PaginatorFunc
    40  
    41  	// Args contain all additional arguments that will be passed to the paginator
    42  	// function.
    43  	Args interface{}
    44  
    45  	limit int
    46  	key   string
    47  }
    48  
    49  // PaginationMetadata is a struct that contains all of the information for
    50  // creating the next and previous pages of data.
    51  type PaginationMetadata struct {
    52  	Pages *PageResult
    53  
    54  	KeyQueryParam   string
    55  	LimitQueryParam string
    56  }
    57  
    58  // Page contains the information about a single page of the resource.
    59  type Page struct {
    60  	Relation string
    61  	Key      string
    62  	Limit    int
    63  }
    64  
    65  // PageResult is a type that holds the two pages that pagintion handlers create
    66  type PageResult struct {
    67  	Next *Page
    68  	Prev *Page
    69  }
    70  
    71  // PaginatorFunc is a function that handles fetching results from the service
    72  // layer. It takes as parameters a string which is the key to fetch starting
    73  // from, and an int as the number of results to limit to.
    74  type PaginatorFunc func(string, int, interface{}, servicecontext.ServiceContext) ([]model.Model, *PageResult, error)
    75  
    76  // Execute serves as an implementation of the RequestHandler's 'Execute' method.
    77  // It calls the embedded PaginationFunc and then processes and returns the results.
    78  func (pe *PaginationExecutor) Execute(sc servicecontext.ServiceContext) (ResponseData, error) {
    79  	models, pages, err := pe.Paginator(pe.key, pe.limit, pe.Args, sc)
    80  	if err != nil {
    81  		return ResponseData{}, err
    82  	}
    83  
    84  	pm := PaginationMetadata{
    85  		Pages:           pages,
    86  		KeyQueryParam:   pe.KeyQueryParam,
    87  		LimitQueryParam: pe.LimitQueryParam,
    88  	}
    89  
    90  	rd := ResponseData{
    91  		Result:   models,
    92  		Metadata: &pm,
    93  	}
    94  	return rd, nil
    95  }
    96  
    97  // ParseAndValidate gets the key and limit from the request
    98  // and sets them on the PaginationExecutor.
    99  func (pe *PaginationExecutor) ParseAndValidate(r *http.Request) error {
   100  	vals := r.URL.Query()
   101  	if k, ok := vals[pe.KeyQueryParam]; ok && len(k) > 0 {
   102  		pe.key = k[0]
   103  	}
   104  
   105  	pe.limit = defaultLimit
   106  	limit := ""
   107  	if l, ok := vals[pe.LimitQueryParam]; ok && len(l) > 0 {
   108  		limit = l[0]
   109  	}
   110  
   111  	// not having a limit is not an error
   112  	if limit == "" {
   113  		return nil
   114  	}
   115  	var err error
   116  	pe.limit, err = strconv.Atoi(limit)
   117  	if err != nil {
   118  		return apiv3.APIError{
   119  			StatusCode: http.StatusBadRequest,
   120  			Message: fmt.Sprintf("Value '%v' provided for '%v' must be integer",
   121  				limit, pe.LimitQueryParam),
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  // buildLink creates the link string for a given page of the resource.
   128  func (p *Page) buildLink(keyQueryParam, limitQueryParam string,
   129  	baseURL *url.URL) string {
   130  
   131  	q := baseURL.Query()
   132  	q.Set(keyQueryParam, p.Key)
   133  	if p.Limit != 0 {
   134  		q.Set(limitQueryParam, fmt.Sprintf("%d", p.Limit))
   135  	}
   136  	baseURL.RawQuery = q.Encode()
   137  	return fmt.Sprintf("<%s>; rel=\"%s\"", baseURL.String(), p.Relation)
   138  }
   139  
   140  // ParsePaginationHeader creates a PaginationMetadata using the header
   141  // that a paginator creates.
   142  func ParsePaginationHeader(header, keyQueryParam,
   143  	limitQueryParam string) (*PaginationMetadata, error) {
   144  
   145  	pm := PaginationMetadata{
   146  		KeyQueryParam:   keyQueryParam,
   147  		LimitQueryParam: limitQueryParam,
   148  
   149  		Pages: &PageResult{},
   150  	}
   151  
   152  	scanner := bufio.NewScanner(strings.NewReader(header))
   153  
   154  	// Looks through the lines of the header and creates a new page for each
   155  	for scanner.Scan() {
   156  		matches := linkMatcher.FindStringSubmatch(scanner.Text())
   157  		if len(matches) != 3 {
   158  			return nil, fmt.Errorf("malformed link header %v", scanner.Text())
   159  		}
   160  		u, err := url.Parse(matches[1])
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		vals := u.Query()
   165  		p := Page{}
   166  		p.Relation = matches[2]
   167  		if len(vals[limitQueryParam]) > 0 {
   168  			var err error
   169  			p.Limit, err = strconv.Atoi(vals[limitQueryParam][0])
   170  			if err != nil {
   171  				return nil, err
   172  			}
   173  		}
   174  		if len(vals[keyQueryParam]) < 1 {
   175  			return nil, fmt.Errorf("key query paramater must be set")
   176  		}
   177  		p.Key = vals[keyQueryParam][0]
   178  		switch p.Relation {
   179  		case "next":
   180  			pm.Pages.Next = &p
   181  		case "prev":
   182  			pm.Pages.Prev = &p
   183  		default:
   184  			return nil, fmt.Errorf("unknown relation: %v", p.Relation)
   185  		}
   186  
   187  	}
   188  	return &pm, nil
   189  }
   190  
   191  // MakeHeader builds a list of links to different pages of the same resource
   192  // and writes out the result to the ResponseWriter.
   193  // As per the specification, the header has the form of:
   194  // Link:
   195  //		http://evergreen.mongodb.com/{route}?key={key}&limit={limit}; rel="{relation}"
   196  //    http://...
   197  func (pm *PaginationMetadata) MakeHeader(w http.ResponseWriter,
   198  	apiURL, route string) error {
   199  
   200  	//Not exactly sure what to do in this case
   201  	if pm.Pages == nil || (pm.Pages.Next == nil && pm.Pages.Prev == nil) {
   202  		return nil
   203  	}
   204  	baseURL, err := url.Parse(apiURL)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	baseURL.Path = path.Clean(fmt.Sprintf("/%s", route))
   209  
   210  	b := bytes.Buffer{}
   211  	if pm.Pages.Next != nil {
   212  		pageLink := pm.Pages.Next.buildLink(pm.KeyQueryParam, pm.LimitQueryParam,
   213  			baseURL)
   214  		_, err := b.WriteString(pageLink)
   215  		if err != nil {
   216  			return err
   217  		}
   218  	}
   219  	if pm.Pages.Prev != nil {
   220  		if pm.Pages.Next != nil {
   221  			_, err := b.WriteString("\n")
   222  			if err != nil {
   223  				return err
   224  			}
   225  		}
   226  		pageLink := pm.Pages.Prev.buildLink(pm.KeyQueryParam, pm.LimitQueryParam,
   227  			baseURL)
   228  		_, err := b.WriteString(pageLink)
   229  		if err != nil {
   230  			return err
   231  		}
   232  	}
   233  	w.Header().Set("Link", b.String())
   234  	return nil
   235  }