blob: 9f378d46786b45fcf547588a55c07c49bd7e03b1 [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
32 // GetBody returns the Page Body. This is used in the `AllPages`.
33 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
49 PageType Page
Ash Wilsonc8e68872014-09-16 10:36:56 -040050}
51
52// NewPager constructs a manually-configured pager.
53// 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 -040054func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager {
Ash Wilsonc8e68872014-09-16 10:36:56 -040055 return Pager{
Ash Wilson7049af42014-09-16 13:04:48 -040056 client: client,
Ash Wilsonfc4191f2014-10-10 15:05:27 -040057 initialURL: initialURL,
58 createPage: createPage,
59 }
60}
61
62// WithPageCreator returns a new Pager that substitutes a different page creation function. This is
63// useful for overriding List functions in delegation.
Ash Wilsonb8b16f82014-10-20 10:19:49 -040064func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager {
Ash Wilsonfc4191f2014-10-10 15:05:27 -040065 return Pager{
66 client: p.client,
67 initialURL: p.initialURL,
68 createPage: createPage,
Ash Wilsonc8e68872014-09-16 10:36:56 -040069 }
70}
71
Ash Wilson7049af42014-09-16 13:04:48 -040072func (p Pager) fetchNextPage(url string) (Page, error) {
Ash Wilsona7402472014-09-16 15:18:34 -040073 resp, err := Request(p.client, p.Headers, url)
Ash Wilson7049af42014-09-16 13:04:48 -040074 if err != nil {
75 return nil, err
76 }
77
Ash Wilsonb8b16f82014-10-20 10:19:49 -040078 remembered, err := PageResultFrom(resp)
Ash Wilson7049af42014-09-16 13:04:48 -040079 if err != nil {
80 return nil, err
81 }
82
Ash Wilsonfc4191f2014-10-10 15:05:27 -040083 return p.createPage(remembered), nil
Ash Wilson7049af42014-09-16 13:04:48 -040084}
85
Ash Wilsonc8e68872014-09-16 10:36:56 -040086// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function.
87// Return "false" from the handler to prematurely stop iterating.
88func (p Pager) EachPage(handler func(Page) (bool, error)) error {
Jon Perritt6f9e4ff2014-09-30 13:29:47 -050089 if p.Err != nil {
90 return p.Err
91 }
Ash Wilsonfc4191f2014-10-10 15:05:27 -040092 currentURL := p.initialURL
Ash Wilsonc8e68872014-09-16 10:36:56 -040093 for {
94 currentPage, err := p.fetchNextPage(currentURL)
95 if err != nil {
96 return err
97 }
98
99 empty, err := currentPage.IsEmpty()
100 if err != nil {
101 return err
102 }
103 if empty {
104 return nil
105 }
106
107 ok, err := handler(currentPage)
108 if err != nil {
109 return err
110 }
111 if !ok {
112 return nil
113 }
114
115 currentURL, err = currentPage.NextPageURL()
116 if err != nil {
117 return err
118 }
119 if currentURL == "" {
120 return nil
121 }
122 }
123}
Jon Perrittdb319f12015-02-17 19:32:40 -0700124
125// AllPages returns all the pages from a `List` operation in a single page,
126// allowing the user to retrieve all the pages at once.
127func (p Pager) AllPages() (Page, error) {
Jon Perritt71bf00e2015-02-18 10:53:15 -0700128 // Having a value of `nil` for `p.PageType` will cause a run-time error.
129 if p.PageType == nil {
130 return nil, fmt.Errorf("Pager field PageType must be set to successfully call pagination.AllPages method.")
131 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700132 // pagesSlice holds all the pages until they get converted into as Page Body.
133 var pagesSlice []interface{}
134 // body will contain the final concatenated Page body.
135 var body reflect.Value
136
137 // Grab a test page to ascertain the page body type.
138 testPage, err := p.fetchNextPage(p.initialURL)
139 if err != nil {
140 return nil, err
141 }
142
Jon Perritt2a3f7e82015-02-18 14:11:33 -0700143 // Switch on the page body type. Recognized types are `map[string]interface{}`,
144 // `[]byte`, and `[]interface{}`.
Jon Perrittdb319f12015-02-17 19:32:40 -0700145 switch testPage.GetBody().(type) {
146 case map[string]interface{}:
147 // key is the map key for the page body if the body type is `map[string]interface{}`.
148 var key string
149 // Iterate over the pages to concatenate the bodies.
150 err := p.EachPage(func(page Page) (bool, error) {
151 b := page.GetBody().(map[string]interface{})
152 for k := range b {
153 // If it's a linked page, we don't want the `links`, we want the other one.
154 if k != "links" {
155 key = k
156 }
157 }
158 pagesSlice = append(pagesSlice, b[key].([]interface{})...)
159 return true, nil
160 })
161 if err != nil {
162 return nil, err
163 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700164 // Set body to value of type `map[string]interface{}`
165 body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice)))
166 body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice))
Jon Perrittdb319f12015-02-17 19:32:40 -0700167 case []byte:
168 // Iterate over the pages to concatenate the bodies.
169 err := p.EachPage(func(page Page) (bool, error) {
170 b := page.GetBody().([]byte)
171 pagesSlice = append(pagesSlice, b)
172 // seperate pages with a comma
173 pagesSlice = append(pagesSlice, []byte{10})
174 return true, nil
175 })
176 if err != nil {
177 return nil, err
178 }
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700179 // Remove the trailing comma.
Jon Perrittdb319f12015-02-17 19:32:40 -0700180 pagesSlice = pagesSlice[:len(pagesSlice)-1]
181 var b []byte
182 // Combine the slice of slices in to a single slice.
183 for _, slice := range pagesSlice {
184 b = append(b, slice.([]byte)...)
185 }
186 // Set body to value of type `bytes`.
187 body = reflect.New(reflect.TypeOf(b)).Elem()
188 body.SetBytes(b)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700189 case []interface{}:
190 // Iterate over the pages to concatenate the bodies.
191 err := p.EachPage(func(page Page) (bool, error) {
192 b := page.GetBody().([]interface{})
193 pagesSlice = append(pagesSlice, b...)
194 return true, nil
195 })
196 if err != nil {
197 return nil, err
198 }
Jon Perritt2a3f7e82015-02-18 14:11:33 -0700199 // Set body to value of type `[]interface{}`
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700200 body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice))
201 for i, s := range pagesSlice {
202 body.Index(i).Set(reflect.ValueOf(s))
203 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700204 default:
205 return nil, fmt.Errorf("Page body has unrecognized type.")
206 }
207
208 // Each `Extract*` function is expecting a specific type of page coming back,
209 // otherwise the type assertion in those functions will fail. PageType is needed
210 // to create a type in this method that has the same type that the `Extract*`
211 // function is expecting and set the Body of that object to the concatenated
212 // pages.
213 page := reflect.New(reflect.TypeOf(p.PageType))
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700214 // Set the page body to be the concatenated pages.
Jon Perrittdb319f12015-02-17 19:32:40 -0700215 page.Elem().FieldByName("Body").Set(body)
Jon Perritt0ed1fa92015-02-18 13:50:43 -0700216 // Set any additional headers that were pass along. The `objectstorage` pacakge,
217 // for example, passes a Content-Type header.
218 h := make(http.Header)
219 for k, v := range p.Headers {
220 h.Add(k, v)
221 }
222 page.Elem().FieldByName("Header").Set(reflect.ValueOf(h))
Jon Perrittdb319f12015-02-17 19:32:40 -0700223 // Type assert the page to a Page interface so that the type assertion in the
224 // `Extract*` methods will work.
225 return page.Elem().Interface().(Page), err
226}