github.com/huaweicloud/golangsdk@v0.0.0-20210831081626-d823fe11ceba/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/huaweicloud/golangsdk" 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 *golangsdk.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 *golangsdk.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 // pagesSlice holds all the pages until they get converted into as Page Body. 138 var pagesSlice []interface{} 139 // body will contain the final concatenated Page body. 140 var body reflect.Value 141 142 // Grab a first page to ascertain the page body type. 143 firstPage, err := p.fetchNextPage(p.initialURL) 144 if err != nil { 145 return nil, err 146 } 147 // Store the page type so we can use reflection to create a new mega-page of 148 // that type. 149 pageType := reflect.TypeOf(firstPage) 150 151 // if it's a single page, just return the firstPage (first page) 152 if _, found := pageType.FieldByName("SinglePageBase"); found { 153 return firstPage, nil 154 } 155 156 // store the first page to avoid getting it twice 157 p.firstPage = firstPage 158 159 // Switch on the page body type. Recognized types are `map[string]interface{}`, 160 // `[]byte`, and `[]interface{}`. 161 switch pb := firstPage.GetBody().(type) { 162 case map[string]interface{}: 163 // key is the map key for the page body if the body type is `map[string]interface{}`. 164 var key string 165 // Iterate over the pages to concatenate the bodies. 166 err = p.EachPage(func(page Page) (bool, error) { 167 b := page.GetBody().(map[string]interface{}) 168 for k, v := range b { 169 // If it's a linked page, we don't want the `links`, we want the other one. 170 if !strings.HasSuffix(k, "links") { 171 // check the field's type. we only want []interface{} (which is really []map[string]interface{}) 172 switch vt := v.(type) { 173 case []interface{}: 174 key = k 175 pagesSlice = append(pagesSlice, vt...) 176 } 177 } 178 } 179 return true, nil 180 }) 181 if err != nil { 182 return nil, err 183 } 184 // Set body to value of type `map[string]interface{}` 185 body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) 186 body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) 187 case []byte: 188 // Iterate over the pages to concatenate the bodies. 189 err = p.EachPage(func(page Page) (bool, error) { 190 b := page.GetBody().([]byte) 191 pagesSlice = append(pagesSlice, b) 192 // seperate pages with a comma 193 pagesSlice = append(pagesSlice, []byte{10}) 194 return true, nil 195 }) 196 if err != nil { 197 return nil, err 198 } 199 if len(pagesSlice) > 0 { 200 // Remove the trailing comma. 201 pagesSlice = pagesSlice[:len(pagesSlice)-1] 202 } 203 var b []byte 204 // Combine the slice of slices in to a single slice. 205 for _, slice := range pagesSlice { 206 b = append(b, slice.([]byte)...) 207 } 208 // Set body to value of type `bytes`. 209 body = reflect.New(reflect.TypeOf(b)).Elem() 210 body.SetBytes(b) 211 case []interface{}: 212 // Iterate over the pages to concatenate the bodies. 213 err = p.EachPage(func(page Page) (bool, error) { 214 b := page.GetBody().([]interface{}) 215 pagesSlice = append(pagesSlice, b...) 216 return true, nil 217 }) 218 if err != nil { 219 return nil, err 220 } 221 // Set body to value of type `[]interface{}` 222 body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) 223 for i, s := range pagesSlice { 224 body.Index(i).Set(reflect.ValueOf(s)) 225 } 226 default: 227 err := golangsdk.ErrUnexpectedType{} 228 err.Expected = "map[string]interface{}/[]byte/[]interface{}" 229 err.Actual = fmt.Sprintf("%T", pb) 230 return nil, err 231 } 232 233 // Each `Extract*` function is expecting a specific type of page coming back, 234 // otherwise the type assertion in those functions will fail. pageType is needed 235 // to create a type in this method that has the same type that the `Extract*` 236 // function is expecting and set the Body of that object to the concatenated 237 // pages. 238 page := reflect.New(pageType) 239 // Set the page body to be the concatenated pages. 240 page.Elem().FieldByName("Body").Set(body) 241 // Set any additional headers that were pass along. The `objectstorage` pacakge, 242 // for example, passes a Content-Type header. 243 h := make(http.Header) 244 for k, v := range p.Headers { 245 h.Add(k, v) 246 } 247 page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) 248 // Type assert the page to a Page interface so that the type assertion in the 249 // `Extract*` methods will work. 250 return page.Elem().Interface().(Page), err 251 }