| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 1 | package pagination | 
|  | 2 |  | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 3 | import ( | 
|  | 4 | "errors" | 
| Jon Perritt | db319f1 | 2015-02-17 19:32:40 -0700 | [diff] [blame] | 5 | "fmt" | 
|  | 6 | "reflect" | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 7 |  | 
|  | 8 | "github.com/rackspace/gophercloud" | 
|  | 9 | ) | 
| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 10 |  | 
|  | 11 | var ( | 
|  | 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. | 
|  | 22 | type 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 Perritt | db319f1 | 2015-02-17 19:32:40 -0700 | [diff] [blame] | 30 |  | 
|  | 31 | // GetBody returns the Page Body. This is used in the `AllPages`. | 
|  | 32 | GetBody() interface{} | 
| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 33 | } | 
|  | 34 |  | 
|  | 35 | // Pager knows how to advance through a specific resource collection, one page at a time. | 
|  | 36 | type Pager struct { | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 37 | client *gophercloud.ServiceClient | 
|  | 38 |  | 
| Ash Wilson | fc4191f | 2014-10-10 15:05:27 -0400 | [diff] [blame] | 39 | initialURL string | 
| Ash Wilson | 5bc7ba8 | 2014-10-09 13:57:34 -0400 | [diff] [blame] | 40 |  | 
| Ash Wilson | b8b16f8 | 2014-10-20 10:19:49 -0400 | [diff] [blame] | 41 | createPage func(r PageResult) Page | 
| Ash Wilson | a740247 | 2014-09-16 15:18:34 -0400 | [diff] [blame] | 42 |  | 
| Jon Perritt | 9bd7bd9 | 2014-09-28 20:10:27 -0500 | [diff] [blame] | 43 | Err error | 
|  | 44 |  | 
| Ash Wilson | a740247 | 2014-09-16 15:18:34 -0400 | [diff] [blame] | 45 | // Headers supplies additional HTTP headers to populate on each paged request. | 
|  | 46 | Headers map[string]string | 
| Jon Perritt | db319f1 | 2015-02-17 19:32:40 -0700 | [diff] [blame] | 47 |  | 
|  | 48 | PageType Page | 
| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 49 | } | 
|  | 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 Wilson | b8b16f8 | 2014-10-20 10:19:49 -0400 | [diff] [blame] | 53 | func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { | 
| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 54 | return Pager{ | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 55 | client:     client, | 
| Ash Wilson | fc4191f | 2014-10-10 15:05:27 -0400 | [diff] [blame] | 56 | 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 Wilson | b8b16f8 | 2014-10-20 10:19:49 -0400 | [diff] [blame] | 63 | func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { | 
| Ash Wilson | fc4191f | 2014-10-10 15:05:27 -0400 | [diff] [blame] | 64 | return Pager{ | 
|  | 65 | client:     p.client, | 
|  | 66 | initialURL: p.initialURL, | 
|  | 67 | createPage: createPage, | 
| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 68 | } | 
|  | 69 | } | 
|  | 70 |  | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 71 | func (p Pager) fetchNextPage(url string) (Page, error) { | 
| Ash Wilson | a740247 | 2014-09-16 15:18:34 -0400 | [diff] [blame] | 72 | resp, err := Request(p.client, p.Headers, url) | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 73 | if err != nil { | 
|  | 74 | return nil, err | 
|  | 75 | } | 
|  | 76 |  | 
| Ash Wilson | b8b16f8 | 2014-10-20 10:19:49 -0400 | [diff] [blame] | 77 | remembered, err := PageResultFrom(resp) | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 78 | if err != nil { | 
|  | 79 | return nil, err | 
|  | 80 | } | 
|  | 81 |  | 
| Ash Wilson | fc4191f | 2014-10-10 15:05:27 -0400 | [diff] [blame] | 82 | return p.createPage(remembered), nil | 
| Ash Wilson | 7049af4 | 2014-09-16 13:04:48 -0400 | [diff] [blame] | 83 | } | 
|  | 84 |  | 
| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 85 | // 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. | 
|  | 87 | func (p Pager) EachPage(handler func(Page) (bool, error)) error { | 
| Jon Perritt | 6f9e4ff | 2014-09-30 13:29:47 -0500 | [diff] [blame] | 88 | if p.Err != nil { | 
|  | 89 | return p.Err | 
|  | 90 | } | 
| Ash Wilson | fc4191f | 2014-10-10 15:05:27 -0400 | [diff] [blame] | 91 | currentURL := p.initialURL | 
| Ash Wilson | c8e6887 | 2014-09-16 10:36:56 -0400 | [diff] [blame] | 92 | 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 Perritt | db319f1 | 2015-02-17 19:32:40 -0700 | [diff] [blame] | 123 |  | 
|  | 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. | 
|  | 126 | func (p Pager) AllPages() (Page, error) { | 
| Jon Perritt | 71bf00e | 2015-02-18 10:53:15 -0700 | [diff] [blame] | 127 | // 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 Perritt | db319f1 | 2015-02-17 19:32:40 -0700 | [diff] [blame] | 131 | // 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 | } |