blob: 8e55837bfc5ef1899fd9efc2d0d0cf5769b42824 [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"
6 "reflect"
Ash Wilson7049af42014-09-16 13:04:48 -04007
8 "github.com/rackspace/gophercloud"
9)
Ash Wilsonc8e68872014-09-16 10:36:56 -040010
11var (
12 // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist.
13 ErrPageNotAvailable = errors.New("The requested page does not exist.")
14)
15
16// Page must be satisfied by the result type of any resource collection.
17// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated.
18// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs,
19// instead.
20// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type
21// will need to implement.
22type Page interface {
23
24 // NextPageURL generates the URL for the page of data that follows this collection.
25 // Return "" if no such page exists.
26 NextPageURL() (string, error)
27
28 // IsEmpty returns true if this Page has no items in it.
29 IsEmpty() (bool, error)
Jon Perrittdb319f12015-02-17 19:32:40 -070030
31 // GetBody returns the Page Body. This is used in the `AllPages`.
32 GetBody() interface{}
Ash Wilsonc8e68872014-09-16 10:36:56 -040033}
34
35// Pager knows how to advance through a specific resource collection, one page at a time.
36type Pager struct {
Ash Wilson7049af42014-09-16 13:04:48 -040037 client *gophercloud.ServiceClient
38
Ash Wilsonfc4191f2014-10-10 15:05:27 -040039 initialURL string
Ash Wilson5bc7ba82014-10-09 13:57:34 -040040
Ash Wilsonb8b16f82014-10-20 10:19:49 -040041 createPage func(r PageResult) Page
Ash Wilsona7402472014-09-16 15:18:34 -040042
Jon Perritt9bd7bd92014-09-28 20:10:27 -050043 Err error
44
Ash Wilsona7402472014-09-16 15:18:34 -040045 // Headers supplies additional HTTP headers to populate on each paged request.
46 Headers map[string]string
Jon Perrittdb319f12015-02-17 19:32:40 -070047
48 PageType Page
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 }
137
138 // Switch on the page body type. Recognized types are `map[string]interface{}`
139 // and `[]byte`.
140 switch testPage.GetBody().(type) {
141 case map[string]interface{}:
142 // key is the map key for the page body if the body type is `map[string]interface{}`.
143 var key string
144 // Iterate over the pages to concatenate the bodies.
145 err := p.EachPage(func(page Page) (bool, error) {
146 b := page.GetBody().(map[string]interface{})
147 for k := range b {
148 // If it's a linked page, we don't want the `links`, we want the other one.
149 if k != "links" {
150 key = k
151 }
152 }
153 pagesSlice = append(pagesSlice, b[key].([]interface{})...)
154 return true, nil
155 })
156 if err != nil {
157 return nil, err
158 }
159
160 // Set body to value of type `map[string]interface{}`
161 body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice)))
162 body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice))
163
164 case []byte:
165 // Iterate over the pages to concatenate the bodies.
166 err := p.EachPage(func(page Page) (bool, error) {
167 b := page.GetBody().([]byte)
168 pagesSlice = append(pagesSlice, b)
169 // seperate pages with a comma
170 pagesSlice = append(pagesSlice, []byte{10})
171 return true, nil
172 })
173 if err != nil {
174 return nil, err
175 }
176
177 // Remove the last comma.
178 pagesSlice = pagesSlice[:len(pagesSlice)-1]
179 var b []byte
180 // Combine the slice of slices in to a single slice.
181 for _, slice := range pagesSlice {
182 b = append(b, slice.([]byte)...)
183 }
184 // Set body to value of type `bytes`.
185 body = reflect.New(reflect.TypeOf(b)).Elem()
186 body.SetBytes(b)
187
188 default:
189 return nil, fmt.Errorf("Page body has unrecognized type.")
190 }
191
192 // Each `Extract*` function is expecting a specific type of page coming back,
193 // otherwise the type assertion in those functions will fail. PageType is needed
194 // to create a type in this method that has the same type that the `Extract*`
195 // function is expecting and set the Body of that object to the concatenated
196 // pages.
197 page := reflect.New(reflect.TypeOf(p.PageType))
198 page.Elem().FieldByName("Body").Set(body)
199
200 // Type assert the page to a Page interface so that the type assertion in the
201 // `Extract*` methods will work.
202 return page.Elem().Interface().(Page), err
203}