Ash Wilson | 64d67b2 | 2014-09-05 13:04:12 -0400 | [diff] [blame] | 1 | package gophercloud |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | |
| 6 | "github.com/racker/perigee" |
| 7 | ) |
| 8 | |
| 9 | var ( |
| 10 | // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. |
| 11 | ErrPageNotAvailable = errors.New("The requested Collection page does not exist.") |
| 12 | ) |
| 13 | |
| 14 | // Collection describes the minimum functionality that any collection resource must implement to be able to use |
| 15 | // the global paging and iteration functions. |
| 16 | // Every resource that returns a list of multiple results must implement this functionality, whether or not it is paged. |
| 17 | // In addition to the methods provided here, each collection should also provide an AsItem(Page) method that |
| 18 | // casts the Page to its more specific type and returns the Page's contents as a slice. |
| 19 | type Collection interface { |
| 20 | |
| 21 | // Pager returns one of the concrete Pager implementations from this package, or a custom one. |
| 22 | // The style of Pager returned determines how the collection is paged. |
| 23 | Pager() Pager |
Ash Wilson | b110fc9 | 2014-09-08 13:54:59 -0400 | [diff] [blame] | 24 | |
| 25 | // Concat the contents of another collection on to the end of this one. |
| 26 | // Return a new collection that contains elements from both. |
| 27 | Concat(Collection) Collection |
Ash Wilson | 64d67b2 | 2014-09-05 13:04:12 -0400 | [diff] [blame] | 28 | } |
| 29 | |
| 30 | // EachPage iterates through a Collection one page at a time. |
| 31 | // The handler function will be invoked with a Collection containing each page. |
| 32 | // If the handler returns true, iteration will continue. If it returns false, no more pages will be fetched. |
| 33 | func EachPage(first Collection, handler func(Collection) bool) error { |
| 34 | p := first.Pager() |
| 35 | var err error |
| 36 | current := first |
| 37 | |
| 38 | for { |
| 39 | if !handler(current) { |
| 40 | return nil |
| 41 | } |
| 42 | |
| 43 | if !p.HasNextPage() { |
| 44 | return nil |
| 45 | } |
| 46 | |
| 47 | current, err = p.NextPage() |
| 48 | if err != nil { |
| 49 | return err |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // AllPages consolidates all pages reachable from a provided starting point into a single mega-Page. |
| 55 | // Use this only when you know that the full set will always fit within memory. |
| 56 | func AllPages(first Collection) (Collection, error) { |
Ash Wilson | b110fc9 | 2014-09-08 13:54:59 -0400 | [diff] [blame] | 57 | megaPage := first |
| 58 | isFirst := true |
| 59 | |
| 60 | err := EachPage(first, func(page Collection) bool { |
| 61 | if isFirst { |
| 62 | isFirst = false |
| 63 | } else { |
| 64 | megaPage = megaPage.Concat(page) |
| 65 | } |
| 66 | return true |
| 67 | }) |
| 68 | |
| 69 | return megaPage, err |
Ash Wilson | 64d67b2 | 2014-09-05 13:04:12 -0400 | [diff] [blame] | 70 | } |
| 71 | |
Ash Wilson | d67975b | 2014-09-09 14:57:17 -0400 | [diff] [blame] | 72 | // Pager describes a specific paging idiom for a Collection resource. |
| 73 | // Generally, to use a Pager, the Collection must also implement a more specialized interface than Collection. |
Ash Wilson | 64d67b2 | 2014-09-05 13:04:12 -0400 | [diff] [blame] | 74 | // Clients should not generally interact with Pagers directly. |
Ash Wilson | d67975b | 2014-09-09 14:57:17 -0400 | [diff] [blame] | 75 | // Instead, use the more convenient collection traversal methods: AllPages and EachPage. |
Ash Wilson | 64d67b2 | 2014-09-05 13:04:12 -0400 | [diff] [blame] | 76 | type Pager interface { |
| 77 | |
| 78 | // HasNextPage returns true if a call to NextPage will return an additional Page of results. |
| 79 | HasNextPage() bool |
| 80 | |
| 81 | // NextPage returns the next Page in the sequence. |
| 82 | // Panics if no page is available, so always check HasNextPage first. |
| 83 | NextPage() (Collection, error) |
| 84 | } |
| 85 | |
| 86 | // SinglePager is used by collections that are not actually paged. |
| 87 | // It has no additional interface requirements for its host Page. |
| 88 | type SinglePager struct{} |
| 89 | |
| 90 | // HasNextPage always reports false. |
| 91 | func (p SinglePager) HasNextPage() bool { |
| 92 | return false |
| 93 | } |
| 94 | |
| 95 | // NextPage always returns an ErrPageNotAvailable. |
| 96 | func (p SinglePager) NextPage() (Collection, error) { |
| 97 | return nil, ErrPageNotAvailable |
| 98 | } |
| 99 | |
| 100 | // PaginationLinks stores the `next` and `previous` links that are provided by some (but not all) paginated resources. |
| 101 | type PaginationLinks struct { |
| 102 | |
| 103 | // Next is the full URL to the next page of results, or nil if this is the last page. |
| 104 | Next *string `json:"next,omitempty"` |
| 105 | |
| 106 | // Previous is the full URL to the previous page of results, or nil if this is the first page. |
| 107 | Previous *string `json:"previous,omitempty"` |
| 108 | } |
| 109 | |
| 110 | // LinkCollection must be satisfied by a Page that uses a LinkPager. |
| 111 | type LinkCollection interface { |
Ash Wilson | 612df9e | 2014-09-08 08:59:12 -0400 | [diff] [blame] | 112 | Collection |
Ash Wilson | 64d67b2 | 2014-09-05 13:04:12 -0400 | [diff] [blame] | 113 | |
| 114 | // Service returns the client used to make further requests. |
| 115 | Service() *ServiceClient |
| 116 | |
| 117 | // Links returns the pagination links from a single page. |
| 118 | Links() PaginationLinks |
| 119 | |
| 120 | // Interpret an arbitrary JSON result as a new LinkCollection. |
| 121 | Interpret(interface{}) (LinkCollection, error) |
| 122 | } |
| 123 | |
| 124 | // LinkPager implements paging for collections that provide a link structure in their response JSON. |
| 125 | // It follows explicit `next` links and stops when the `next` link is "null". |
| 126 | type LinkPager struct { |
| 127 | current LinkCollection |
| 128 | } |
| 129 | |
| 130 | // NewLinkPager creates and initializes a pager for a LinkCollection. |
| 131 | func NewLinkPager(first LinkCollection) *LinkPager { |
| 132 | return &LinkPager{current: first} |
| 133 | } |
| 134 | |
| 135 | // HasNextPage checks the `next` link in the pagination data. |
| 136 | func (p *LinkPager) HasNextPage() bool { |
| 137 | return p.current.Links().Next != nil |
| 138 | } |
| 139 | |
| 140 | // NextPage follows the `next` link to construct the next page of data. |
| 141 | func (p *LinkPager) NextPage() (Collection, error) { |
| 142 | url := p.current.Links().Next |
| 143 | if url == nil { |
| 144 | return nil, ErrPageNotAvailable |
| 145 | } |
| 146 | |
| 147 | var response interface{} |
| 148 | _, err := perigee.Request("GET", *url, perigee.Options{ |
| 149 | MoreHeaders: p.current.Service().Provider.AuthenticatedHeaders(), |
| 150 | Results: &response, |
| 151 | OkCodes: []int{200}, |
| 152 | }) |
| 153 | if err != nil { |
| 154 | return nil, err |
| 155 | } |
| 156 | |
| 157 | interpreted, err := p.current.Interpret(response) |
| 158 | if err != nil { |
| 159 | return nil, err |
| 160 | } |
| 161 | |
| 162 | p.current = interpreted |
| 163 | return interpreted, nil |
| 164 | } |