common/pagination_range.h (140 lines of code) (raw):
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file was ported from the Google Cloud Platform C++ Client Libraries,
// the original source can be found here:
// https://github.com/googleapis/google-cloud-cpp/blob/9d9591913f342bafcd83a34df8e96d4dd5d87534/google/cloud/internal/pagination_range.h
#ifndef COMMON_PAGINATION_RANGE_H_
#define COMMON_PAGINATION_RANGE_H_
#include <functional>
#include <iterator>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "absl/status/statusor.h"
#include "google/protobuf/util/message_differencer.h"
namespace cloud_kms {
inline bool ComparePaginationValues(const google::protobuf::Message& lhs,
const google::protobuf::Message& rhs) {
return google::protobuf::util::MessageDifferencer::Equals(lhs, rhs);
}
template <typename T, typename std::enable_if<
!std::is_base_of<google::protobuf::Message, T>::value,
int>::type = 0>
bool ComparePaginationValues(const T& lhs, const T& rhs) {
return lhs == rhs;
}
/**
* An input iterator for a class with the same interface as `PaginationRange`.
*/
template <typename T, typename Range>
class PaginationIterator {
public:
//@{
/// @name Iterator traits
using iterator_category = std::input_iterator_tag;
using value_type = absl::StatusOr<T>;
using difference_type = std::ptrdiff_t;
using pointer = value_type*;
using reference = value_type&;
//@}
PaginationIterator() : owner_(nullptr) {}
PaginationIterator& operator++() {
*this = owner_->GetNext();
return *this;
}
PaginationIterator operator++(int) {
PaginationIterator tmp(*this);
operator++();
return tmp;
}
const value_type* operator->() const { return &value_; }
value_type* operator->() { return &value_; }
const value_type& operator*() const& { return value_; }
value_type& operator*() & { return value_; }
const value_type&& operator*() const&& { return std::move(value_); }
value_type&& operator*() && { return std::move(value_); }
private:
friend Range;
friend bool operator==(const PaginationIterator& lhs,
const PaginationIterator& rhs) {
// Iterators on different streams are always different.
if (lhs.owner_ != rhs.owner_) {
return false;
}
// All end iterators are equal.
if (lhs.owner_ == nullptr) {
return true;
}
// Iterators on the same stream are equal if they point to the same object.
if (lhs.value_.ok() && rhs.value_.ok()) {
return ComparePaginationValues(*lhs.value_, *rhs.value_);
}
// If one is an error and the other is not then they must be different,
// because only one iterator per range can have an error status. For the
// same reason, if both have an error they both are pointing to the same
// element.
return lhs.value_.ok() == rhs.value_.ok();
}
friend bool operator!=(const PaginationIterator& lhs,
const PaginationIterator& rhs) {
return !(lhs == rhs);
}
PaginationIterator(Range* owner, value_type value)
: owner_(owner), value_(std::move(value)) {}
Range* owner_;
value_type value_;
};
/**
* Adapt pagination APIs to look like input ranges.
*
* A number of gRPC APIs iterate over the elements in a "collection" using
* pagination APIs. The application calls a `List*()` RPC which returns
* a "page" of elements and a token, calling the same `List*()` RPC with the
* token returns the next "page". We want to expose these APIs as input ranges
* in the C++ client libraries. This class performs that work.
*
* @tparam T the type of the items, typically a proto describing the resources
* @tparam Request the type of the request object for the `List` RPC.
* @tparam Response the type of the response object for the `List` RPC.
*/
template <typename T, typename Request, typename Response>
class PaginationRange {
public:
/**
* Create a new range to paginate over some elements.
*
* @param request the first request to start the iteration, the library may
* initialize this request with any filtering constraints.
* @param loader makes the RPC request to fetch a new page of items.
* @param get_items extracts the items from the response using native C++
* types (as opposed to the proto types used in `Response`).
*/
PaginationRange(
Request request,
std::function<absl::StatusOr<Response>(const Request& r)> loader,
std::function<std::vector<T>(Response r)> get_items)
: request_(std::move(request)),
next_page_loader_(std::move(loader)),
get_items_(std::move(get_items)),
on_last_page_(false) {
current_ = current_page_.begin();
}
/// The iterator type for this Range.
using iterator = PaginationIterator<T, PaginationRange>;
/**
* Return an iterator over the range of `T` objects.
*
* The returned iterator is a single-pass input iterator that reads new `T`
* objects from the underlying `PaginationRange` when incremented.
*
* Creating, and particularly incrementing, multiple iterators on the same
* PaginationRange<> is unsupported and can produce incorrect results.
*/
iterator begin() { return GetNext(); }
/// Return an iterator pointing to the end of the stream.
iterator end() { return PaginationIterator<T, PaginationRange>{}; }
protected:
friend class PaginationIterator<T, PaginationRange>;
/**
* Fetches (or returns if already fetched) the next object from the stream.
*
* @return An iterator pointing to the next element in the stream. On error,
* it returns an iterator that is different from `.end()`, but has an error
* status. If the stream is exhausted, it returns the `.end()` iterator.
*/
iterator GetNext() {
static absl::Status const kPastTheEndError(absl::FailedPreconditionError(
"Cannot iterating past the end of ListObjectReader"));
if (current_page_.end() == current_) {
if (on_last_page_) {
return iterator(nullptr, kPastTheEndError);
}
request_.set_page_token(std::move(next_page_token_));
auto response = next_page_loader_(request_);
if (!response.ok()) {
next_page_token_.clear();
current_page_.clear();
on_last_page_ = true;
current_ = current_page_.begin();
return iterator(this, std::move(response).status());
}
next_page_token_ = std::move(*response->mutable_next_page_token());
current_page_ = get_items_(*std::move(response));
current_ = current_page_.begin();
if (next_page_token_.empty()) {
on_last_page_ = true;
}
if (current_page_.end() == current_) {
return iterator(nullptr, kPastTheEndError);
}
}
return iterator(this, std::move(*current_++));
}
private:
Request request_;
std::function<absl::StatusOr<Response>(const Request& r)> next_page_loader_;
std::function<std::vector<T>(Response r)> get_items_;
std::vector<T> current_page_;
typename std::vector<T>::iterator current_;
std::string next_page_token_;
bool on_last_page_;
};
template <typename T>
struct UnimplementedPaginationRange {};
template <typename T, typename Request, typename Response>
struct UnimplementedPaginationRange<PaginationRange<T, Request, Response>> {
static PaginationRange<T, Request, Response> Create() {
return PaginationRange<T, Request, Response>(
Request{},
[](const Request&) {
return absl::StatusOr<Response>(
absl::UnimplementedError("needs-override"));
},
[](Response) { return std::vector<T>{}; });
}
};
} // namespace cloud_kms
#endif // COMMON_PAGINATION_RANGE_H_