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