github.com/gophercloud/gophercloud@v1.11.0/pagination/pager.go (about) 1 package pagination 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "reflect" 8 "strings" 9 10 "github.com/gophercloud/gophercloud" 11 ) 12 13 var ( 14 // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. 15 ErrPageNotAvailable = errors.New("The requested page does not exist.") 16 ) 17 18 // Page must be satisfied by the result type of any resource collection. 19 // It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. 20 // Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, 21 // instead. 22 // Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type 23 // will need to implement. 24 type Page interface { 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) 31 32 // GetBody returns the Page Body. This is used in the `AllPages` method. 33 GetBody() interface{} 34 } 35 36 // Pager knows how to advance through a specific resource collection, one page at a time. 37 type Pager struct { 38 client *gophercloud.ServiceClient 39 40 initialURL string 41 42 createPage func(r PageResult) Page 43 44 firstPage Page 45 46 Err error 47 48 // Headers supplies additional HTTP headers to populate on each paged request. 49 Headers map[string]string 50 } 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. 54 func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { 55 return Pager{ 56 client: client, 57 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. 64 func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { 65 return Pager{ 66 client: p.client, 67 initialURL: p.initialURL, 68 createPage: createPage, 69 } 70 } 71 72 func (p Pager) fetchNextPage(url string) (Page, error) { 73 resp, err := Request(p.client, p.Headers, url) 74 if err != nil { 75 return nil, err 76 } 77 78 remembered, err := PageResultFrom(resp) 79 if err != nil { 80 return nil, err 81 } 82 83 return p.createPage(remembered), nil 84 } 85 86 // 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. 88 func (p Pager) EachPage(handler func(Page) (bool, error)) error { 89 if p.Err != nil { 90 return p.Err 91 } 92 currentURL := p.initialURL 93 for { 94 var currentPage Page 95 96 // if first page has already been fetched, no need to fetch it again 97 if p.firstPage != nil { 98 currentPage = p.firstPage 99 p.firstPage = nil 100 } else { 101 var err error 102 currentPage, err = p.fetchNextPage(currentURL) 103 if err != nil { 104 return err 105 } 106 } 107 108 empty, err := currentPage.IsEmpty() 109 if err != nil { 110 return err 111 } 112 if empty { 113 return nil 114 } 115 116 ok, err := handler(currentPage) 117 if err != nil { 118 return err 119 } 120 if !ok { 121 return nil 122 } 123 124 currentURL, err = currentPage.NextPageURL() 125 if err != nil { 126 return err 127 } 128 if currentURL == "" { 129 return nil 130 } 131 } 132 } 133 134 // AllPages returns all the pages from a `List` operation in a single page, 135 // allowing the user to retrieve all the pages at once. 136 func (p Pager) AllPages() (Page, error) { 137 if p.Err != nil { 138 return nil, p.Err 139 } 140 // pagesSlice holds all the pages until they get converted into as Page Body. 141 var pagesSlice []interface{} 142 // body will contain the final concatenated Page body. 143 var body reflect.Value 144 145 // Grab a first page to ascertain the page body type. 146 firstPage, err := p.fetchNextPage(p.initialURL) 147 if err != nil { 148 return nil, err 149 } 150 // Store the page type so we can use reflection to create a new mega-page of 151 // that type. 152 pageType := reflect.TypeOf(firstPage) 153 154 // if it's a single page, just return the firstPage (first page) 155 if _, found := pageType.FieldByName("SinglePageBase"); found { 156 return firstPage, nil 157 } 158 159 // store the first page to avoid getting it twice 160 p.firstPage = firstPage 161 162 // Switch on the page body type. Recognized types are `map[string]interface{}`, 163 // `[]byte`, and `[]interface{}`. 164 switch pb := firstPage.GetBody().(type) { 165 case map[string]interface{}: 166 // key is the map key for the page body if the body type is `map[string]interface{}`. 167 var key string 168 // Iterate over the pages to concatenate the bodies. 169 err = p.EachPage(func(page Page) (bool, error) { 170 b := page.GetBody().(map[string]interface{}) 171 for k, v := range b { 172 // If it's a linked page, we don't want the `links`, we want the other one. 173 if !strings.HasSuffix(k, "links") { 174 // check the field's type. we only want []interface{} (which is really []map[string]interface{}) 175 switch vt := v.(type) { 176 case []interface{}: 177 key = k 178 pagesSlice = append(pagesSlice, vt...) 179 } 180 } 181 } 182 return true, nil 183 }) 184 if err != nil { 185 return nil, err 186 } 187 // Set body to value of type `map[string]interface{}` 188 body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) 189 body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) 190 case []byte: 191 // Iterate over the pages to concatenate the bodies. 192 err = p.EachPage(func(page Page) (bool, error) { 193 b := page.GetBody().([]byte) 194 pagesSlice = append(pagesSlice, b) 195 // seperate pages with a comma 196 pagesSlice = append(pagesSlice, []byte{10}) 197 return true, nil 198 }) 199 if err != nil { 200 return nil, err 201 } 202 if len(pagesSlice) > 0 { 203 // Remove the trailing comma. 204 pagesSlice = pagesSlice[:len(pagesSlice)-1] 205 } 206 var b []byte 207 // Combine the slice of slices in to a single slice. 208 for _, slice := range pagesSlice { 209 b = append(b, slice.([]byte)...) 210 } 211 // Set body to value of type `bytes`. 212 body = reflect.New(reflect.TypeOf(b)).Elem() 213 body.SetBytes(b) 214 case []interface{}: 215 // Iterate over the pages to concatenate the bodies. 216 err = p.EachPage(func(page Page) (bool, error) { 217 b := page.GetBody().([]interface{}) 218 pagesSlice = append(pagesSlice, b...) 219 return true, nil 220 }) 221 if err != nil { 222 return nil, err 223 } 224 // Set body to value of type `[]interface{}` 225 body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) 226 for i, s := range pagesSlice { 227 body.Index(i).Set(reflect.ValueOf(s)) 228 } 229 default: 230 err := gophercloud.ErrUnexpectedType{} 231 err.Expected = "map[string]interface{}/[]byte/[]interface{}" 232 err.Actual = fmt.Sprintf("%T", pb) 233 return nil, err 234 } 235 236 // Each `Extract*` function is expecting a specific type of page coming back, 237 // otherwise the type assertion in those functions will fail. pageType is needed 238 // to create a type in this method that has the same type that the `Extract*` 239 // function is expecting and set the Body of that object to the concatenated 240 // pages. 241 page := reflect.New(pageType) 242 // Set the page body to be the concatenated pages. 243 page.Elem().FieldByName("Body").Set(body) 244 // Set any additional headers that were pass along. The `objectstorage` pacakge, 245 // for example, passes a Content-Type header. 246 h := make(http.Header) 247 for k, v := range p.Headers { 248 h.Add(k, v) 249 } 250 page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) 251 // Type assert the page to a Page interface so that the type assertion in the 252 // `Extract*` methods will work. 253 return page.Elem().Interface().(Page), err 254 }