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  }