blob: 587d62758810be507f34d703a3d2939515f9f171 [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) {
Jon Perritt71bf00e2015-02-18 10:53:15 -0700127 // Having a value of `nil` for `p.PageType` will cause a run-time error.
128 if p.PageType == nil {
129 return nil, fmt.Errorf("Pager field PageType must be set to successfully call pagination.AllPages method.")
130 }
Jon Perrittdb319f12015-02-17 19:32:40 -0700131 // pagesSlice holds all the pages until they get converted into as Page Body.
132 var pagesSlice []interface{}
133 // body will contain the final concatenated Page body.
134 var body reflect.Value
135
136 // Grab a test page to ascertain the page body type.
137 testPage, err := p.fetchNextPage(p.initialURL)
138 if err != nil {
139 return nil, err
140 }
141
142 // Switch on the page body type. Recognized types are `map[string]interface{}`
143 // and `[]byte`.
144 switch testPage.GetBody().(type) {
145 case map[string]interface{}:
146 // key is the map key for the page body if the body type is `map[string]interface{}`.
147 var key string
148 // Iterate over the pages to concatenate the bodies.
149 err := p.EachPage(func(page Page) (bool, error) {
150 b := page.GetBody().(map[string]interface{})
151 for k := range b {
152 // If it's a linked page, we don't want the `links`, we want the other one.
153 if k != "links" {
154 key = k
155 }
156 }
157 pagesSlice = append(pagesSlice, b[key].([]interface{})...)
158 return true, nil
159 })
160 if err != nil {
161 return nil, err
162 }
163
164 // 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))
167
168 case []byte:
169 // Iterate over the pages to concatenate the bodies.
170 err := p.EachPage(func(page Page) (bool, error) {
171 b := page.GetBody().([]byte)
172 pagesSlice = append(pagesSlice, b)
173 // seperate pages with a comma
174 pagesSlice = append(pagesSlice, []byte{10})
175 return true, nil
176 })
177 if err != nil {
178 return nil, err
179 }
180
181 // Remove the last comma.
182 pagesSlice = pagesSlice[:len(pagesSlice)-1]
183 var b []byte
184 // Combine the slice of slices in to a single slice.
185 for _, slice := range pagesSlice {
186 b = append(b, slice.([]byte)...)
187 }
188 // Set body to value of type `bytes`.
189 body = reflect.New(reflect.TypeOf(b)).Elem()
190 body.SetBytes(b)
191
192 default:
193 return nil, fmt.Errorf("Page body has unrecognized type.")
194 }
195
196 // Each `Extract*` function is expecting a specific type of page coming back,
197 // otherwise the type assertion in those functions will fail. PageType is needed
198 // to create a type in this method that has the same type that the `Extract*`
199 // function is expecting and set the Body of that object to the concatenated
200 // pages.
201 page := reflect.New(reflect.TypeOf(p.PageType))
202 page.Elem().FieldByName("Body").Set(body)
203
204 // Type assert the page to a Page interface so that the type assertion in the
205 // `Extract*` methods will work.
206 return page.Elem().Interface().(Page), err
207}