blob: 443a000f4cd443e81f575262ab3e5e3635da6e07 [file] [log] [blame]
Ash Wilson64d67b22014-09-05 13:04:12 -04001package gophercloud
2
3import (
4 "errors"
5
6 "github.com/racker/perigee"
7)
8
9var (
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.
19type 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 Wilsonb110fc92014-09-08 13:54:59 -040024
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 Wilson64d67b22014-09-05 13:04:12 -040028}
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.
33func 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.
56func AllPages(first Collection) (Collection, error) {
Ash Wilsonb110fc92014-09-08 13:54:59 -040057 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 Wilson64d67b22014-09-05 13:04:12 -040070}
71
Ash Wilsond67975b2014-09-09 14:57:17 -040072// 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 Wilson64d67b22014-09-05 13:04:12 -040074// Clients should not generally interact with Pagers directly.
Ash Wilsond67975b2014-09-09 14:57:17 -040075// Instead, use the more convenient collection traversal methods: AllPages and EachPage.
Ash Wilson64d67b22014-09-05 13:04:12 -040076type 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.
88type SinglePager struct{}
89
90// HasNextPage always reports false.
91func (p SinglePager) HasNextPage() bool {
92 return false
93}
94
95// NextPage always returns an ErrPageNotAvailable.
96func (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.
101type 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.
111type LinkCollection interface {
Ash Wilson612df9e2014-09-08 08:59:12 -0400112 Collection
Ash Wilson64d67b22014-09-05 13:04:12 -0400113
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".
126type LinkPager struct {
127 current LinkCollection
128}
129
130// NewLinkPager creates and initializes a pager for a LinkCollection.
131func NewLinkPager(first LinkCollection) *LinkPager {
132 return &LinkPager{current: first}
133}
134
135// HasNextPage checks the `next` link in the pagination data.
136func (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.
141func (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}