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  }