blob: 68fe140510fa639ddf221c293f423b596f4df393 [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"
Ash Wilson7049af42014-09-16 13:04:48 -04008
9 "github.com/rackspace/gophercloud"
10)
Ash Wilsonc8e68872014-09-16 10:36:56 -040011
12var (
13 // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
14 ErrPageNotAvailable = errors.New("The requested page does not exist.")
15)
16
17// Page must be satisfied by the result type of any resource collection.
18// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated.
19// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs,
20// instead.
21// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type
22// will need to implement.
23type Page interface {
24
25 // NextPageURL generates the URL for the page of data that follows this collection.
26 // Return "" if no such page exists.
27 NextPageURL() (string, error)
28
29 // IsEmpty returns true if this Page has no items in it.
30 IsEmpty() (bool, error)
Jon Perrittdb319f12015-02-17 19:32:40 -070031
Jon Perritt46b71ba2015-02-18 22:25:55 -070032 // GetBody returns the Page Body. This is used in the `AllPages` method.
Jon Perrittdb319f12015-02-17 19:32:40 -070033 GetBody() interface{}
Ash Wilsonc8e68872014-09-16 10:36:56 -040034}
35
36// Pager knows how to advance through a specific resource collection, one page at a time.
37type Pager struct {
Ash Wilson7049af42014-09-16 13:04:48 -040038 client *gophercloud.ServiceClient
39
Ash Wilsonfc4191f2014-10-10 15:05:27 -040040 initialURL string
Ash Wilson5bc7ba82014-10-09 13:57:34 -040041
Ash Wilsonb8b16f82014-10-20 10:19:49 -040042 createPage func(r PageResult) Page
Ash Wilsona7402472014-09-16 15:18:34 -040043
Jon Perritt9bd7bd92014-09-28 20:10:27 -050044 Err error
45
Ash Wilsona7402472014-09-16 15:18:34 -040046 // Headers supplies additional HTTP headers to populate on each paged request.
47 Headers map[string]string
Jon Perrittdb319f12015-02-17 19:32:40 -070048
Jon Perrittbd34ac92015-02-18 15:04:46 -070049 // PageType is the type of `Page` the `Extract*` function expects back. This is
Jon Perritt46b71ba2015-02-18 22:25:55 -070050 // needed because a type assertion occurs in each `Extract*` function, and it will
Jon Perrittbd34ac92015-02-18 15:04:46 -070051 // fail if the `Page` doesn't have the expected type.
Jon Perrittdb319f12015-02-17 19:32:40 -070052 PageType Page
Ash Wilsonc8e68872014-09-16 10:36:56 -040053}
54
55// NewPager constructs a manually-configured pager.
56// 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 -040057func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager {
Ash Wilsonc8e68872014-09-16 10:36:56 -040058 return Pager{
Ash Wilson7049af42014-09-16 13:04:48 -040059 client: client,
Ash Wilsonfc4191f2014-10-10 15:05:27 -040060 initialURL: initialURL,
61 createPage: createPage,
62 }
63}
64
65// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
66// useful for overriding List functions in delegation.
Ash Wilsonb8b16f82014-10-20 10:19:49 -040067func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager {
Ash Wilsonfc4191f2014-10-10 15:05:27 -040068 return Pager{
69 client: p.client,
70 initialURL: p.initialURL,
71 createPage: createPage,
Ash Wilsonc8e68872014-09-16 10:36:56 -040072 }
73}
74
Ash Wilson7049af42014-09-16 13:04:48 -040075func (p Pager) fetchNextPage(url string) (Page, error) {
Ash Wilsona7402472014-09-16 15:18:34 -040076 resp, err := Request(p.client, p.Headers, url)
Ash Wilson7049af42014-09-16 13:04:48 -040077 if err != nil {
78 return nil, err
79 }
80
Ash Wilsonb8b16f82014-10-20 10:19:49 -040081 remembered, err := PageResultFrom(resp)
Ash Wilson7049af42014-09-16 13:04:48 -040082 if err != nil {
83 return nil, err
84 }
85
Ash Wilsonfc4191f2014-10-10 15:05:27 -040086 return p.createPage(remembered), nil
Ash Wilson7049af42014-09-16 13:04:48 -040087}
88
Ash Wilsonc8e68872014-09-16 10:36:56 -040089// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
90// Return "false" from the handler to prematurely stop iterating.
91func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Jon Perritt6f9e4ff2014-09-30 13:29:47 -050092 if p.Err != nil {
93 return p.Err
94 }
Ash Wilsonfc4191f2014-10-10 15:05:27 -040095 currentURL := p.initialURL
Ash Wilsonc8e68872014-09-16 10:36:56 -040096 for {
97 currentPage, err := p.fetchNextPage(currentURL)
98 if err != nil {
99 return err
100 }
101
102 empty, err := currentPage.IsEmpty()
103 if err != nil {
104 return err
105 }
106 if empty {
107 return nil
108 }
109
110 ok, err := handler(currentPage)
111 if err != nil {
112 return err
113 }
114 if !ok {
115 return nil
116 }
117
118 currentURL, err = currentPage.NextPageURL()
119 if err != nil {
120 return err
121 }
122 if currentURL == "" {
123 return nil
124 }
125 }
126}
Jon Perrittdb319f12015-02-17 19:32:40 -0700127
128// AllPages returns all the pages from a `List` operation in a single page,
129// allowing the user to retrieve all the pages at once.
130func (p Pager) AllPages() (Page, error) {
Jon Perritt71bf00e2015-02-18 10:53:15 -0700131 // Having a value of `nil` for `p.PageType` will cause a run-time error.
132 if p.PageType == nil {
133 return nil, fmt.Errorf("Pager field PageType must be set to successfully call pagination.AllPages method.")
134 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700135 // pagesSlice holds all the pages until they get converted into as Page Body.
136 var pagesSlice []interface{}
137 // body will contain the final concatenated Page body.
138 var body reflect.Value
139
140 // Grab a test page to ascertain the page body type.
141 testPage, err := p.fetchNextPage(p.initialURL)
142 if err != nil {
143 return nil, err
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{}`.
Jon Perrittdb319f12015-02-17 19:32:40 -0700148 switch testPage.GetBody().(type) {
149 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.
153 err := p.EachPage(func(page Page) (bool, error) {
154 b := page.GetBody().(map[string]interface{})
155 for k := range b {
156 // If it's a linked page, we don't want the `links`, we want the other one.
157 if k != "links" {
158 key = k
159 }
160 }
161 pagesSlice = append(pagesSlice, b[key].([]interface{})...)
162 return true, nil
163 })
164 if err != nil {
165 return nil, err
166 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700167 // Set body to value of type `map[string]interface{}`
168 body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice)))
169 body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice))
Jon Perrittdb319f12015-02-17 19:32:40 -0700170 case []byte:
171 // Iterate over the pages to concatenate the bodies.
172 err := p.EachPage(func(page Page) (bool, error) {
173 b := page.GetBody().([]byte)
174 pagesSlice = append(pagesSlice, b)
175 // seperate pages with a comma
176 pagesSlice = append(pagesSlice, []byte{10})
177 return true, nil
178 })
179 if err != nil {
180 return nil, err
181 }
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700182 // Remove the trailing comma.
Jon Perrittdb319f12015-02-17 19:32:40 -0700183 pagesSlice = pagesSlice[:len(pagesSlice)-1]
184 var b []byte
185 // Combine the slice of slices in to a single slice.
186 for _, slice := range pagesSlice {
187 b = append(b, slice.([]byte)...)
188 }
189 // Set body to value of type `bytes`.
190 body = reflect.New(reflect.TypeOf(b)).Elem()
191 body.SetBytes(b)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700192 case []interface{}:
193 // Iterate over the pages to concatenate the bodies.
194 err := p.EachPage(func(page Page) (bool, error) {
195 b := page.GetBody().([]interface{})
196 pagesSlice = append(pagesSlice, b...)
197 return true, nil
198 })
199 if err != nil {
200 return nil, err
201 }
Jon Perritt2a3f7e82015-02-18 14:11:33 -0700202 // Set body to value of type `[]interface{}`
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700203 body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice))
204 for i, s := range pagesSlice {
205 body.Index(i).Set(reflect.ValueOf(s))
206 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700207 default:
208 return nil, fmt.Errorf("Page body has unrecognized type.")
209 }
210
211 // Each `Extract*` function is expecting a specific type of page coming back,
212 // otherwise the type assertion in those functions will fail. PageType is needed
213 // to create a type in this method that has the same type that the `Extract*`
214 // function is expecting and set the Body of that object to the concatenated
215 // pages.
216 page := reflect.New(reflect.TypeOf(p.PageType))
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700217 // Set the page body to be the concatenated pages.
Jon Perrittdb319f12015-02-17 19:32:40 -0700218 page.Elem().FieldByName("Body").Set(body)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700219 // Set any additional headers that were pass along. The `objectstorage` pacakge,
220 // for example, passes a Content-Type header.
221 h := make(http.Header)
222 for k, v := range p.Headers {
223 h.Add(k, v)
224 }
225 page.Elem().FieldByName("Header").Set(reflect.ValueOf(h))
Jon Perrittdb319f12015-02-17 19:32:40 -0700226 // Type assert the page to a Page interface so that the type assertion in the
227 // `Extract*` methods will work.
228 return page.Elem().Interface().(Page), err
229}