|
| 1 | +// Copyright 2020 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_PAGINATION_RANGE_H |
| 16 | +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_PAGINATION_RANGE_H |
| 17 | + |
| 18 | +#include "google/cloud/status_or.h" |
| 19 | +#include "google/cloud/version.h" |
| 20 | +#include <google/protobuf/util/message_differencer.h> |
| 21 | +#include <functional> |
| 22 | +#include <iterator> |
| 23 | +#include <string> |
| 24 | +#include <utility> |
| 25 | +#include <vector> |
| 26 | + |
| 27 | +namespace google { |
| 28 | +namespace cloud { |
| 29 | +inline namespace GOOGLE_CLOUD_CPP_NS { |
| 30 | +namespace internal { |
| 31 | + |
| 32 | +/** |
| 33 | + * An input iterator for a class with the same interface as `PaginationRange`. |
| 34 | + */ |
| 35 | +template <typename T, typename Range> |
| 36 | +class PaginationIterator { |
| 37 | + public: |
| 38 | + //@{ |
| 39 | + /// @name Iterator traits |
| 40 | + using iterator_category = std::input_iterator_tag; |
| 41 | + using value_type = StatusOr<T>; |
| 42 | + using difference_type = std::ptrdiff_t; |
| 43 | + using pointer = value_type*; |
| 44 | + using reference = value_type&; |
| 45 | + //@} |
| 46 | + |
| 47 | + PaginationIterator() : owner_(nullptr) {} |
| 48 | + |
| 49 | + PaginationIterator& operator++() { |
| 50 | + *this = owner_->GetNext(); |
| 51 | + return *this; |
| 52 | + } |
| 53 | + |
| 54 | + PaginationIterator operator++(int) { |
| 55 | + PaginationIterator tmp(*this); |
| 56 | + operator++(); |
| 57 | + return tmp; |
| 58 | + } |
| 59 | + |
| 60 | + value_type const* operator->() const { return &value_; } |
| 61 | + value_type* operator->() { return &value_; } |
| 62 | + |
| 63 | + value_type const& operator*() const& { return value_; } |
| 64 | + value_type& operator*() & { return value_; } |
| 65 | +#if GOOGLE_CLOUD_CPP_HAVE_CONST_REF_REF |
| 66 | + value_type const&& operator*() const&& { return std::move(value_); } |
| 67 | +#endif // GOOGLE_CLOUD_CPP_HAVE_CONST_REF_REF |
| 68 | + value_type&& operator*() && { return std::move(value_); } |
| 69 | + |
| 70 | + private: |
| 71 | + friend Range; |
| 72 | + |
| 73 | + friend bool operator==(PaginationIterator const& lhs, |
| 74 | + PaginationIterator const& rhs) { |
| 75 | + // Iterators on different streams are always different. |
| 76 | + if (lhs.owner_ != rhs.owner_) { |
| 77 | + return false; |
| 78 | + } |
| 79 | + // All end iterators are equal. |
| 80 | + if (lhs.owner_ == nullptr) { |
| 81 | + return true; |
| 82 | + } |
| 83 | + // Iterators on the same stream are equal if they point to the same object. |
| 84 | + if (lhs.value_.ok() && rhs.value_.ok()) { |
| 85 | + return google::protobuf::util::MessageDifferencer::Equals(*lhs.value_, |
| 86 | + *rhs.value_); |
| 87 | + } |
| 88 | + // If one is an error and the other is not then they must be different, |
| 89 | + // because only one iterator per range can have an error status. For the |
| 90 | + // same reason, if both have an error they both are pointing to the same |
| 91 | + // element. |
| 92 | + return lhs.value_.ok() == rhs.value_.ok(); |
| 93 | + } |
| 94 | + |
| 95 | + friend bool operator!=(PaginationIterator const& lhs, |
| 96 | + PaginationIterator const& rhs) { |
| 97 | + return !(lhs == rhs); |
| 98 | + } |
| 99 | + |
| 100 | + PaginationIterator(Range* owner, value_type value) |
| 101 | + : owner_(owner), value_(std::move(value)) {} |
| 102 | + |
| 103 | + Range* owner_; |
| 104 | + value_type value_; |
| 105 | +}; |
| 106 | + |
| 107 | +/** |
| 108 | + * Adapt pagination APIs to look like input ranges. |
| 109 | + * |
| 110 | + * A number of gRPC APIs iterate over the elements in a "collection" using |
| 111 | + * pagination APIs. The application calls a `List*()` RPC which returns |
| 112 | + * a "page" of elements and a token, calling the same `List*()` RPC with the |
| 113 | + * token returns the next "page". We want to expose these APIs as input ranges |
| 114 | + * in the C++ client libraries. This class performs that work. |
| 115 | + * |
| 116 | + * @tparam T the type of the items, typically a proto describing the resources |
| 117 | + * @tparam Request the type of the request object for the `List` RPC. |
| 118 | + * @tparam Response the type of the response object for the `List` RPC. |
| 119 | + */ |
| 120 | +template <typename T, typename Request, typename Response> |
| 121 | +class PaginationRange { |
| 122 | + public: |
| 123 | + /** |
| 124 | + * Create a new range to paginate over some elements. |
| 125 | + * |
| 126 | + * @param request the first request to start the iteration, the library may |
| 127 | + * initialize this request with any filtering constraints. |
| 128 | + * @param loader makes the RPC request to fetch a new page of items. |
| 129 | + * @param get_items extracts the items from the response using native C++ |
| 130 | + * types (as opposed to the proto types used in `Response`). |
| 131 | + */ |
| 132 | + PaginationRange(Request request, |
| 133 | + std::function<StatusOr<Response>(Request const& r)> loader, |
| 134 | + std::function<std::vector<T>(Response r)> get_items) |
| 135 | + : request_(std::move(request)), |
| 136 | + next_page_loader_(std::move(loader)), |
| 137 | + get_items_(std::move(get_items)), |
| 138 | + on_last_page_(false) { |
| 139 | + current_ = current_page_.begin(); |
| 140 | + } |
| 141 | + |
| 142 | + /// The iterator type for this Range. |
| 143 | + using iterator = PaginationIterator<T, PaginationRange>; |
| 144 | + |
| 145 | + /** |
| 146 | + * Return an iterator over the range of `T` objects. |
| 147 | + * |
| 148 | + * The returned iterator is a single-pass input iterator that reads new `T` |
| 149 | + * objects from the underlying `PaginationRange` when incremented. |
| 150 | + * |
| 151 | + * Creating, and particularly incrementing, multiple iterators on the same |
| 152 | + * PaginationRange<> is unsupported and can produce incorrect results. |
| 153 | + */ |
| 154 | + iterator begin() { return GetNext(); } |
| 155 | + |
| 156 | + /// Return an iterator pointing to the end of the stream. |
| 157 | + iterator end() { return PaginationIterator<T, PaginationRange>{}; } |
| 158 | + |
| 159 | + protected: |
| 160 | + friend class PaginationIterator<T, PaginationRange>; |
| 161 | + |
| 162 | + /** |
| 163 | + * Fetches (or returns if already fetched) the next object from the stream. |
| 164 | + * |
| 165 | + * @return An iterator pointing to the next element in the stream. On error, |
| 166 | + * it returns an iterator that is different from `.end()`, but has an error |
| 167 | + * status. If the stream is exhausted, it returns the `.end()` iterator. |
| 168 | + */ |
| 169 | + iterator GetNext() { |
| 170 | + static Status const kPastTheEndError( |
| 171 | + StatusCode::kFailedPrecondition, |
| 172 | + "Cannot iterating past the end of ListObjectReader"); |
| 173 | + if (current_page_.end() == current_) { |
| 174 | + if (on_last_page_) { |
| 175 | + return iterator(nullptr, kPastTheEndError); |
| 176 | + } |
| 177 | + request_.set_page_token(std::move(next_page_token_)); |
| 178 | + auto response = next_page_loader_(request_); |
| 179 | + if (!response.ok()) { |
| 180 | + next_page_token_.clear(); |
| 181 | + current_page_.clear(); |
| 182 | + on_last_page_ = true; |
| 183 | + current_ = current_page_.begin(); |
| 184 | + return iterator(this, std::move(response).status()); |
| 185 | + } |
| 186 | + next_page_token_ = std::move(*response->mutable_next_page_token()); |
| 187 | + current_page_ = get_items_(*std::move(response)); |
| 188 | + current_ = current_page_.begin(); |
| 189 | + if (next_page_token_.empty()) { |
| 190 | + on_last_page_ = true; |
| 191 | + } |
| 192 | + if (current_page_.end() == current_) { |
| 193 | + return iterator(nullptr, kPastTheEndError); |
| 194 | + } |
| 195 | + } |
| 196 | + return iterator(this, std::move(*current_++)); |
| 197 | + } |
| 198 | + |
| 199 | + private: |
| 200 | + Request request_; |
| 201 | + std::function<StatusOr<Response>(Request const& r)> next_page_loader_; |
| 202 | + std::function<std::vector<T>(Response r)> get_items_; |
| 203 | + std::vector<T> current_page_; |
| 204 | + typename std::vector<T>::iterator current_; |
| 205 | + std::string next_page_token_; |
| 206 | + bool on_last_page_; |
| 207 | +}; |
| 208 | + |
| 209 | +} // namespace internal |
| 210 | +} // namespace GOOGLE_CLOUD_CPP_NS |
| 211 | +} // namespace cloud |
| 212 | +} // namespace google |
| 213 | + |
| 214 | +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_PAGINATION_RANGE_H |
0 commit comments