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