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 }