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