blob: 817b806e91ab1bbc78ce94483a6665eeb2f9a85d [file] [log] [blame]
Ash Wilsonc8e68872014-09-16 10:36:56 -04001package pagination
2
Ash Wilson7049af42014-09-16 13:04:48 -04003import (
4 "errors"
Jon Perrittdb319f12015-02-17 19:32:40 -07005 "fmt"
Jon Perritt0ed1fa92015-02-18 13:50:43 -07006 "net/http"
Jon Perrittdb319f12015-02-17 19:32:40 -07007 "reflect"
Jon Perritt521cc682015-02-19 08:39:01 -07008 "strings"
Ash Wilson7049af42014-09-16 13:04:48 -04009
10 "github.com/rackspace/gophercloud"
11)
Ash Wilsonc8e68872014-09-16 10:36:56 -040012
13var (
14 // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
15 ErrPageNotAvailable = errors.New("The requested page does not exist.")
16)
17
18// Page must be satisfied by the result type of any resource collection.
19// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated.
20// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs,
21// instead.
22// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type
23// will need to implement.
24type Page interface {
25
26 // NextPageURL generates the URL for the page of data that follows this collection.
27 // Return "" if no such page exists.
28 NextPageURL() (string, error)
29
30 // IsEmpty returns true if this Page has no items in it.
31 IsEmpty() (bool, error)
Jon Perrittdb319f12015-02-17 19:32:40 -070032
Jon Perritt46b71ba2015-02-18 22:25:55 -070033 // GetBody returns the Page Body. This is used in the `AllPages` method.
Jon Perrittdb319f12015-02-17 19:32:40 -070034 GetBody() interface{}
Ash Wilsonc8e68872014-09-16 10:36:56 -040035}
36
37// Pager knows how to advance through a specific resource collection, one page at a time.
38type Pager struct {
Ash Wilson7049af42014-09-16 13:04:48 -040039 client *gophercloud.ServiceClient
40
Ash Wilsonfc4191f2014-10-10 15:05:27 -040041 initialURL string
Ash Wilson5bc7ba82014-10-09 13:57:34 -040042
Ash Wilsonb8b16f82014-10-20 10:19:49 -040043 createPage func(r PageResult) Page
Ash Wilsona7402472014-09-16 15:18:34 -040044
Jon Perritt9bd7bd92014-09-28 20:10:27 -050045 Err error
46
Ash Wilsona7402472014-09-16 15:18:34 -040047 // Headers supplies additional HTTP headers to populate on each paged request.
48 Headers map[string]string
Jon Perrittdb319f12015-02-17 19:32:40 -070049
Jon Perrittbd34ac92015-02-18 15:04:46 -070050 // PageType is the type of `Page` the `Extract*` function expects back. This is
Jon Perritt46b71ba2015-02-18 22:25:55 -070051 // needed because a type assertion occurs in each `Extract*` function, and it will
Jon Perrittbd34ac92015-02-18 15:04:46 -070052 // fail if the `Page` doesn't have the expected type.
Jon Perrittdb319f12015-02-17 19:32:40 -070053 PageType Page
Ash Wilsonc8e68872014-09-16 10:36:56 -040054}
55
56// NewPager constructs a manually-configured pager.
57// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page.
Ash Wilsonb8b16f82014-10-20 10:19:49 -040058func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager {
Ash Wilsonc8e68872014-09-16 10:36:56 -040059 return Pager{
Ash Wilson7049af42014-09-16 13:04:48 -040060 client: client,
Ash Wilsonfc4191f2014-10-10 15:05:27 -040061 initialURL: initialURL,
62 createPage: createPage,
63 }
64}
65
66// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
67// useful for overriding List functions in delegation.
Ash Wilsonb8b16f82014-10-20 10:19:49 -040068func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager {
Ash Wilsonfc4191f2014-10-10 15:05:27 -040069 return Pager{
70 client: p.client,
71 initialURL: p.initialURL,
72 createPage: createPage,
Ash Wilsonc8e68872014-09-16 10:36:56 -040073 }
74}
75
Ash Wilson7049af42014-09-16 13:04:48 -040076func (p Pager) fetchNextPage(url string) (Page, error) {
Ash Wilsona7402472014-09-16 15:18:34 -040077 resp, err := Request(p.client, p.Headers, url)
Ash Wilson7049af42014-09-16 13:04:48 -040078 if err != nil {
79 return nil, err
80 }
81
Ash Wilsonb8b16f82014-10-20 10:19:49 -040082 remembered, err := PageResultFrom(resp)
Ash Wilson7049af42014-09-16 13:04:48 -040083 if err != nil {
84 return nil, err
85 }
86
Ash Wilsonfc4191f2014-10-10 15:05:27 -040087 return p.createPage(remembered), nil
Ash Wilson7049af42014-09-16 13:04:48 -040088}
89
Ash Wilsonc8e68872014-09-16 10:36:56 -040090// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
91// Return "false" from the handler to prematurely stop iterating.
92func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Jon Perritt6f9e4ff2014-09-30 13:29:47 -050093 if p.Err != nil {
94 return p.Err
95 }
Ash Wilsonfc4191f2014-10-10 15:05:27 -040096 currentURL := p.initialURL
Ash Wilsonc8e68872014-09-16 10:36:56 -040097 for {
98 currentPage, err := p.fetchNextPage(currentURL)
99 if err != nil {
100 return err
101 }
102
103 empty, err := currentPage.IsEmpty()
104 if err != nil {
105 return err
106 }
107 if empty {
108 return nil
109 }
110
111 ok, err := handler(currentPage)
112 if err != nil {
113 return err
114 }
115 if !ok {
116 return nil
117 }
118
119 currentURL, err = currentPage.NextPageURL()
120 if err != nil {
121 return err
122 }
123 if currentURL == "" {
124 return nil
125 }
126 }
127}
Jon Perrittdb319f12015-02-17 19:32:40 -0700128
129// AllPages returns all the pages from a `List` operation in a single page,
130// allowing the user to retrieve all the pages at once.
131func (p Pager) AllPages() (Page, error) {
Jon Perritt71bf00e2015-02-18 10:53:15 -0700132 // Having a value of `nil` for `p.PageType` will cause a run-time error.
133 if p.PageType == nil {
134 return nil, fmt.Errorf("Pager field PageType must be set to successfully call pagination.AllPages method.")
135 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700136 // pagesSlice holds all the pages until they get converted into as Page Body.
137 var pagesSlice []interface{}
138 // body will contain the final concatenated Page body.
139 var body reflect.Value
140
141 // Grab a test page to ascertain the page body type.
142 testPage, err := p.fetchNextPage(p.initialURL)
143 if err != nil {
144 return nil, err
145 }
146
Jon Perritt2a3f7e82015-02-18 14:11:33 -0700147 // Switch on the page body type. Recognized types are `map[string]interface{}`,
148 // `[]byte`, and `[]interface{}`.
Jon Perrittdb319f12015-02-17 19:32:40 -0700149 switch testPage.GetBody().(type) {
150 case map[string]interface{}:
151 // key is the map key for the page body if the body type is `map[string]interface{}`.
152 var key string
153 // Iterate over the pages to concatenate the bodies.
154 err := p.EachPage(func(page Page) (bool, error) {
155 b := page.GetBody().(map[string]interface{})
156 for k := range b {
157 // If it's a linked page, we don't want the `links`, we want the other one.
Jon Perritt521cc682015-02-19 08:39:01 -0700158 if !strings.HasSuffix(k, "links") {
Jon Perrittdb319f12015-02-17 19:32:40 -0700159 key = k
160 }
161 }
162 pagesSlice = append(pagesSlice, b[key].([]interface{})...)
163 return true, nil
164 })
165 if err != nil {
166 return nil, err
167 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700168 // Set body to value of type `map[string]interface{}`
169 body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice)))
170 body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice))
Jon Perrittdb319f12015-02-17 19:32:40 -0700171 case []byte:
172 // Iterate over the pages to concatenate the bodies.
173 err := p.EachPage(func(page Page) (bool, error) {
174 b := page.GetBody().([]byte)
175 pagesSlice = append(pagesSlice, b)
176 // seperate pages with a comma
177 pagesSlice = append(pagesSlice, []byte{10})
178 return true, nil
179 })
180 if err != nil {
181 return nil, err
182 }
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700183 // Remove the trailing comma.
Jon Perrittdb319f12015-02-17 19:32:40 -0700184 pagesSlice = pagesSlice[:len(pagesSlice)-1]
185 var b []byte
186 // Combine the slice of slices in to a single slice.
187 for _, slice := range pagesSlice {
188 b = append(b, slice.([]byte)...)
189 }
190 // Set body to value of type `bytes`.
191 body = reflect.New(reflect.TypeOf(b)).Elem()
192 body.SetBytes(b)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700193 case []interface{}:
194 // Iterate over the pages to concatenate the bodies.
195 err := p.EachPage(func(page Page) (bool, error) {
196 b := page.GetBody().([]interface{})
197 pagesSlice = append(pagesSlice, b...)
198 return true, nil
199 })
200 if err != nil {
201 return nil, err
202 }
Jon Perritt2a3f7e82015-02-18 14:11:33 -0700203 // Set body to value of type `[]interface{}`
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700204 body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice))
205 for i, s := range pagesSlice {
206 body.Index(i).Set(reflect.ValueOf(s))
207 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700208 default:
209 return nil, fmt.Errorf("Page body has unrecognized type.")
210 }
211
212 // Each `Extract*` function is expecting a specific type of page coming back,
213 // otherwise the type assertion in those functions will fail. PageType is needed
214 // to create a type in this method that has the same type that the `Extract*`
215 // function is expecting and set the Body of that object to the concatenated
216 // pages.
217 page := reflect.New(reflect.TypeOf(p.PageType))
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700218 // Set the page body to be the concatenated pages.
Jon Perrittdb319f12015-02-17 19:32:40 -0700219 page.Elem().FieldByName("Body").Set(body)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700220 // Set any additional headers that were pass along. The `objectstorage` pacakge,
221 // for example, passes a Content-Type header.
222 h := make(http.Header)
223 for k, v := range p.Headers {
224 h.Add(k, v)
225 }
226 page.Elem().FieldByName("Header").Set(reflect.ValueOf(h))
Jon Perrittdb319f12015-02-17 19:32:40 -0700227 // Type assert the page to a Page interface so that the type assertion in the
228 // `Extract*` methods will work.
229 return page.Elem().Interface().(Page), err
230}