Skip to content
This repository was archived by the owner on Dec 8, 2021. It is now read-only.

Commit 2a96070

Browse files
authored
refactor: copy PaginationRange from spanner (#168)
1 parent f4427ce commit 2a96070

File tree

5 files changed

+405
-2
lines changed

5 files changed

+405
-2
lines changed

google/cloud/CMakeLists.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ if (GOOGLE_CLOUD_CPP_ENABLE_GRPC_UTILS)
331331
internal/background_threads_impl.cc
332332
internal/background_threads_impl.h
333333
internal/completion_queue_impl.cc
334-
internal/completion_queue_impl.h)
334+
internal/completion_queue_impl.h
335+
internal/pagination_range.h)
335336
target_link_libraries(
336337
google_cloud_cpp_grpc_utils
337338
PUBLIC googleapis-c++::rpc_status_protos google_cloud_cpp_common
@@ -360,7 +361,8 @@ if (GOOGLE_CLOUD_CPP_ENABLE_GRPC_UTILS)
360361
completion_queue_test.cc
361362
connection_options_test.cc
362363
grpc_error_delegate_test.cc
363-
internal/background_threads_impl_test.cc)
364+
internal/background_threads_impl_test.cc
365+
internal/pagination_range_test.cc)
364366

365367
# Export the list of unit tests so the Bazel BUILD file can pick it up.
366368
export_list_to_bazel("google_cloud_cpp_grpc_utils_unit_tests.bzl"

google/cloud/google_cloud_cpp_grpc_utils.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ google_cloud_cpp_grpc_utils_hdrs = [
2929
"internal/async_read_stream_impl.h",
3030
"internal/background_threads_impl.h",
3131
"internal/completion_queue_impl.h",
32+
"internal/pagination_range.h",
3233
]
3334

3435
google_cloud_cpp_grpc_utils_srcs = [

google/cloud/google_cloud_cpp_grpc_utils_unit_tests.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ google_cloud_cpp_grpc_utils_unit_tests = [
2121
"connection_options_test.cc",
2222
"grpc_error_delegate_test.cc",
2323
"internal/background_threads_impl_test.cc",
24+
"internal/pagination_range_test.cc",
2425
]
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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

Comments
 (0)