blob: 6f1609ef2e3c074ff5ced6b5e6adee88d4abdec2 [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
Jon Perritt27249f42016-02-18 10:35:59 -060010 "github.com/gophercloud/gophercloud"
Ash Wilson7049af42014-09-16 13:04:48 -040011)
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
Ash Wilsonc8e68872014-09-16 10:36:56 -040049}
50
51// NewPager constructs a manually-configured pager.
52// 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 -040053func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager {
Ash Wilsonc8e68872014-09-16 10:36:56 -040054 return Pager{
Ash Wilson7049af42014-09-16 13:04:48 -040055 client: client,
Ash Wilsonfc4191f2014-10-10 15:05:27 -040056 initialURL: initialURL,
57 createPage: createPage,
58 }
59}
60
61// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
62// useful for overriding List functions in delegation.
Ash Wilsonb8b16f82014-10-20 10:19:49 -040063func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager {
Ash Wilsonfc4191f2014-10-10 15:05:27 -040064 return Pager{
65 client: p.client,
66 initialURL: p.initialURL,
67 createPage: createPage,
Ash Wilsonc8e68872014-09-16 10:36:56 -040068 }
69}
70
Ash Wilson7049af42014-09-16 13:04:48 -040071func (p Pager) fetchNextPage(url string) (Page, error) {
Ash Wilsona7402472014-09-16 15:18:34 -040072 resp, err := Request(p.client, p.Headers, url)
Ash Wilson7049af42014-09-16 13:04:48 -040073 if err != nil {
74 return nil, err
75 }
76
Ash Wilsonb8b16f82014-10-20 10:19:49 -040077 remembered, err := PageResultFrom(resp)
Ash Wilson7049af42014-09-16 13:04:48 -040078 if err != nil {
79 return nil, err
80 }
81
Ash Wilsonfc4191f2014-10-10 15:05:27 -040082 return p.createPage(remembered), nil
Ash Wilson7049af42014-09-16 13:04:48 -040083}
84
Ash Wilsonc8e68872014-09-16 10:36:56 -040085// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
86// Return "false" from the handler to prematurely stop iterating.
87func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Jon Perritt6f9e4ff2014-09-30 13:29:47 -050088 if p.Err != nil {
89 return p.Err
90 }
Ash Wilsonfc4191f2014-10-10 15:05:27 -040091 currentURL := p.initialURL
Ash Wilsonc8e68872014-09-16 10:36:56 -040092 for {
93 currentPage, err := p.fetchNextPage(currentURL)
94 if err != nil {
95 return err
96 }
97
98 empty, err := currentPage.IsEmpty()
99 if err != nil {
100 return err
101 }
102 if empty {
103 return nil
104 }
105
106 ok, err := handler(currentPage)
107 if err != nil {
108 return err
109 }
110 if !ok {
111 return nil
112 }
113
114 currentURL, err = currentPage.NextPageURL()
115 if err != nil {
116 return err
117 }
118 if currentURL == "" {
119 return nil
120 }
121 }
122}
Jon Perrittdb319f12015-02-17 19:32:40 -0700123
124// AllPages returns all the pages from a `List` operation in a single page,
125// allowing the user to retrieve all the pages at once.
126func (p Pager) AllPages() (Page, error) {
127 // pagesSlice holds all the pages until they get converted into as Page Body.
128 var pagesSlice []interface{}
129 // body will contain the final concatenated Page body.
130 var body reflect.Value
131
132 // Grab a test page to ascertain the page body type.
133 testPage, err := p.fetchNextPage(p.initialURL)
134 if err != nil {
135 return nil, err
136 }
Jon Perritt3d0a1852015-02-19 08:51:39 -0700137 // Store the page type so we can use reflection to create a new mega-page of
138 // that type.
139 pageType := reflect.TypeOf(testPage)
Jon Perrittdb319f12015-02-17 19:32:40 -0700140
jrperritt057373d2016-05-02 14:01:39 -0500141 // if it's a single page, just return the testPage (first page)
142 if _, found := pageType.FieldByName("SinglePageBase"); found {
143 return testPage, nil
144 }
145
Jon Perritt2a3f7e82015-02-18 14:11:33 -0700146 // Switch on the page body type. Recognized types are `map[string]interface{}`,
147 // `[]byte`, and `[]interface{}`.
jrperritt2aaed7f2017-02-20 16:19:24 -0600148 switch pb := testPage.GetBody().(type) {
Jon Perrittdb319f12015-02-17 19:32:40 -0700149 case map[string]interface{}:
150 // key is the map key for the page body if the body type is `map[string]interface{}`.
151 var key string
152 // Iterate over the pages to concatenate the bodies.
Jon Perritt2be387a2016-03-31 09:31:58 -0500153 err = p.EachPage(func(page Page) (bool, error) {
Jon Perrittdb319f12015-02-17 19:32:40 -0700154 b := page.GetBody().(map[string]interface{})
jrperritt2aaed7f2017-02-20 16:19:24 -0600155 for k, v := range b {
Jon Perrittdb319f12015-02-17 19:32:40 -0700156 // If it's a linked page, we don't want the `links`, we want the other one.
Jon Perritt521cc682015-02-19 08:39:01 -0700157 if !strings.HasSuffix(k, "links") {
jrperritt303bf8a2017-03-02 12:23:34 -0600158 // check the field's type. we only want []interface{} (which is really []map[string]interface{})
jrperritt2aaed7f2017-02-20 16:19:24 -0600159 switch vt := v.(type) {
jrperritt2aaed7f2017-02-20 16:19:24 -0600160 case []interface{}:
161 key = k
162 pagesSlice = append(pagesSlice, vt...)
163 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700164 }
165 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700166 return true, nil
167 })
168 if err != nil {
169 return nil, err
170 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700171 // Set body to value of type `map[string]interface{}`
172 body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice)))
173 body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice))
Jon Perrittdb319f12015-02-17 19:32:40 -0700174 case []byte:
175 // Iterate over the pages to concatenate the bodies.
Jon Perritt2be387a2016-03-31 09:31:58 -0500176 err = p.EachPage(func(page Page) (bool, error) {
Jon Perrittdb319f12015-02-17 19:32:40 -0700177 b := page.GetBody().([]byte)
178 pagesSlice = append(pagesSlice, b)
179 // seperate pages with a comma
180 pagesSlice = append(pagesSlice, []byte{10})
181 return true, nil
182 })
183 if err != nil {
184 return nil, err
185 }
Jon Perritta11f5db2015-06-21 21:17:06 -0600186 if len(pagesSlice) > 0 {
187 // Remove the trailing comma.
188 pagesSlice = pagesSlice[:len(pagesSlice)-1]
189 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700190 var b []byte
191 // Combine the slice of slices in to a single slice.
192 for _, slice := range pagesSlice {
193 b = append(b, slice.([]byte)...)
194 }
195 // Set body to value of type `bytes`.
196 body = reflect.New(reflect.TypeOf(b)).Elem()
197 body.SetBytes(b)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700198 case []interface{}:
199 // Iterate over the pages to concatenate the bodies.
Jon Perritt2be387a2016-03-31 09:31:58 -0500200 err = p.EachPage(func(page Page) (bool, error) {
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700201 b := page.GetBody().([]interface{})
202 pagesSlice = append(pagesSlice, b...)
203 return true, nil
204 })
205 if err != nil {
206 return nil, err
207 }
Jon Perritt2a3f7e82015-02-18 14:11:33 -0700208 // Set body to value of type `[]interface{}`
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700209 body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice))
210 for i, s := range pagesSlice {
211 body.Index(i).Set(reflect.ValueOf(s))
212 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700213 default:
jrperritt29ae6b32016-04-13 12:59:37 -0500214 err := gophercloud.ErrUnexpectedType{}
Jon Perritt80251972016-03-09 00:32:30 -0600215 err.Expected = "map[string]interface{}/[]byte/[]interface{}"
jrperritt2aaed7f2017-02-20 16:19:24 -0600216 err.Actual = fmt.Sprintf("%T", pb)
Jon Perritt80251972016-03-09 00:32:30 -0600217 return nil, err
Jon Perrittdb319f12015-02-17 19:32:40 -0700218 }
219
220 // Each `Extract*` function is expecting a specific type of page coming back,
Jon Perritt3d0a1852015-02-19 08:51:39 -0700221 // otherwise the type assertion in those functions will fail. pageType is needed
Jon Perrittdb319f12015-02-17 19:32:40 -0700222 // to create a type in this method that has the same type that the `Extract*`
223 // function is expecting and set the Body of that object to the concatenated
224 // pages.
Jon Perritt3d0a1852015-02-19 08:51:39 -0700225 page := reflect.New(pageType)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700226 // Set the page body to be the concatenated pages.
Jon Perrittdb319f12015-02-17 19:32:40 -0700227 page.Elem().FieldByName("Body").Set(body)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700228 // Set any additional headers that were pass along. The `objectstorage` pacakge,
229 // for example, passes a Content-Type header.
230 h := make(http.Header)
231 for k, v := range p.Headers {
232 h.Add(k, v)
233 }
234 page.Elem().FieldByName("Header").Set(reflect.ValueOf(h))
Jon Perrittdb319f12015-02-17 19:32:40 -0700235 // Type assert the page to a Page interface so that the type assertion in the
236 // `Extract*` methods will work.
237 return page.Elem().Interface().(Page), err
238}