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  }