diff --git a/src/aws-cpp-sdk-core/include/aws/core/utils/pagination/Paginator.h b/src/aws-cpp-sdk-core/include/aws/core/utils/pagination/Paginator.h new file mode 100644 index 00000000000..8ff25228129 --- /dev/null +++ b/src/aws-cpp-sdk-core/include/aws/core/utils/pagination/Paginator.h @@ -0,0 +1,101 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#pragma once + +#include +#include +#include + +namespace Aws +{ +namespace Utils +{ +namespace Pagination +{ + +template +class PagePaginator +{ +public: + using OutcomeType = typename OperationTraits::OutcomeType; + using ResultType = typename OperationTraits::ResultType; + + struct EndSentinel {}; + + class PageIterator + { + public: + using iterator_category = std::input_iterator_tag; + using value_type = OutcomeType; + using difference_type = std::ptrdiff_t; + using pointer = const OutcomeType*; + using reference = const OutcomeType&; + + PageIterator(std::shared_ptr client, const OperationRequest& firstReq) + : m_client(client), + m_request(firstReq), + m_atEnd(false) + { + FetchPage(); + } + + const OutcomeType& operator*() const { return m_currentOutcome; } + + PageIterator& operator++() + { + if (m_atEnd) return *this; + + // If last fetch failed, end iteration + if (!m_currentOutcome.IsSuccess()) + { + m_atEnd = true; + return *this; + } + + // If no more results, end iteration + if (!OperationTraits::HasMoreResults(m_currentOutcome.GetResult())) + { + m_atEnd = true; + return *this; + } + + // Mutate iterator-owned request for next page + OperationTraits::SetNextRequest(m_currentOutcome.GetResult(), m_request); + FetchPage(); + + return *this; + } + + bool operator==(EndSentinel) const { return m_atEnd; } + bool operator!=(EndSentinel) const { return !m_atEnd; } + + private: + void FetchPage() + { + m_currentOutcome = OperationTraits::Invoke(*m_client, m_request); + } + + std::shared_ptr m_client; + OperationRequest m_request{}; + OutcomeType m_currentOutcome{}; + bool m_atEnd{true}; + }; + + PagePaginator(std::shared_ptr client, const OperationRequest& firstReq) + : m_client(client), + m_firstRequest(firstReq) {} + + PageIterator begin() const { return PageIterator(m_client, m_firstRequest); } + EndSentinel end() const { return {}; } + +private: + std::shared_ptr m_client; + OperationRequest m_firstRequest{}; +}; + +} // namespace Pagination +} // namespace Utils +} // namespace Aws \ No newline at end of file diff --git a/tests/aws-cpp-sdk-core-tests/CMakeLists.txt b/tests/aws-cpp-sdk-core-tests/CMakeLists.txt index 90406667b9a..c4c997a1bf0 100644 --- a/tests/aws-cpp-sdk-core-tests/CMakeLists.txt +++ b/tests/aws-cpp-sdk-core-tests/CMakeLists.txt @@ -12,6 +12,7 @@ file(GLOB AWS_CLIENT_SRC "${CMAKE_CURRENT_SOURCE_DIR}/aws/client/*.cpp") file(GLOB AWS_NET_SRC "${CMAKE_CURRENT_SOURCE_DIR}/aws/net/*.cpp") file(GLOB HTTP_SRC "${CMAKE_CURRENT_SOURCE_DIR}/http/*.cpp") file(GLOB UTILS_SRC "${CMAKE_CURRENT_SOURCE_DIR}/utils/*.cpp") +file(GLOB UTILS_PAGINATION_SRC "${CMAKE_CURRENT_SOURCE_DIR}/utils/pagination/*.cpp") file(GLOB UTILS_CRYPTO_SRC "${CMAKE_CURRENT_SOURCE_DIR}/utils/crypto/*.cpp") file(GLOB UTILS_EVENT_SRC "${CMAKE_CURRENT_SOURCE_DIR}/utils/event/*.cpp") file(GLOB UTILS_JSON_SRC "${CMAKE_CURRENT_SOURCE_DIR}/utils/json/*.cpp") @@ -41,6 +42,7 @@ file(GLOB AWS_CPP_SDK_CORE_TESTS_SRC ${AWS_NET_SRC} ${HTTP_SRC} ${UTILS_SRC} + ${UTILS_PAGINATION_SRC} ${UTILS_CRYPTO_SRC} ${UTILS_EVENT_SRC} ${UTILS_JSON_SRC} @@ -69,6 +71,7 @@ if(PLATFORM_WINDOWS) source_group("Source Files\\http" FILES ${HTTP_SRC}) source_group("Source Files\\monitoring" FILES ${MONITORING_SRC}) source_group("Source Files\\utils" FILES ${UTILS_SRC}) + source_group("Source Files\\utils\\pagination" FILES ${UTILS_PAGINATION_SRC}) source_group("Source Files\\utils\\crypto" FILES ${UTILS_CRYPTO_SRC}) source_group("Source Files\\utils\\event" FILES ${UTILS_EVENT_SRC}) source_group("Source Files\\utils\\json" FILES ${UTILS_JSON_SRC}) diff --git a/tests/aws-cpp-sdk-core-tests/utils/pagination/PaginatorTest.cpp b/tests/aws-cpp-sdk-core-tests/utils/pagination/PaginatorTest.cpp new file mode 100644 index 00000000000..bcab9afe15a --- /dev/null +++ b/tests/aws-cpp-sdk-core-tests/utils/pagination/PaginatorTest.cpp @@ -0,0 +1,239 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include + +using namespace Aws::Utils::Pagination; +using namespace Aws::Utils; + +struct TestResult { + Aws::Vector items; + Aws::String outputToken; + bool moreResults; + + const Aws::Vector& GetItems() const { return items; } + const Aws::String& GetOutputToken() const { return outputToken; } + bool GetMoreResults() const { return moreResults; } +}; + +struct TestOutcome { + bool success; + Aws::String error; + TestResult result; + + bool IsSuccess() const { return success; } + const Aws::String& GetError() const { return error; } + const TestResult& GetResult() const { return result; } +}; + +struct TestRequest { + Aws::String inputToken; + int limitKey = 3; + + void SetInputToken(const Aws::String& t) { inputToken = t; } + const Aws::String& GetInputToken() const { return inputToken; } +}; + +class PaginatorTestClient { +private: + Aws::Vector allData; + bool shouldFail = false; + int failOnPage = -1; + int currentPage = 0; + +public: + PaginatorTestClient() : allData({"apple", "banana", "cherry", "dragon-fruit", "elderberry", "fig", "grape"}) {} + + void SetData(const Aws::Vector& data) { allData = data; } + void SetShouldFail(bool fail) { shouldFail = fail; } + void SetFailOnPage(int page) { failOnPage = page; } + void Reset() { + allData = {"apple", "banana", "cherry", "dragon-fruit", "elderberry", "fig", "grape"}; + shouldFail = false; + failOnPage = -1; + currentPage = 0; + } + + TestOutcome ListItems(const TestRequest& request) { + if (shouldFail) { + return {false, "Request failed", {}}; + } + + if (failOnPage >= 0 && currentPage == failOnPage) { + currentPage++; + return {false, "Page " + StringUtils::to_string(failOnPage) + " failed", {}}; + } + currentPage++; + + int startIdx = 0; + if (!request.GetInputToken().empty()) { + startIdx = StringUtils::ConvertToInt32(request.GetInputToken().c_str()); + } + + TestResult result; + for (size_t i = 0; i < static_cast(request.limitKey) && startIdx + i < allData.size(); ++i) { + result.items.push_back(allData[startIdx + i]); + } + + int nextIdx = startIdx + request.limitKey; + if (static_cast(nextIdx) < allData.size()) { + result.outputToken = StringUtils::to_string(nextIdx); + result.moreResults = true; + } else { + result.moreResults = false; + } + + return {true, "", result}; + } +}; + +struct ListItemsTraits { + using OutcomeType = TestOutcome; + using ResultType = TestResult; + + static OutcomeType Invoke(PaginatorTestClient& client, const TestRequest& request) { + return client.ListItems(request); + } + + static bool HasMoreResults(const TestResult& result) { + return result.GetMoreResults(); + } + + static void SetNextRequest(const TestResult& result, TestRequest& request) { + request.SetInputToken(result.GetOutputToken()); + } +}; + +class PaginatorTest : public Aws::Testing::AwsCppSdkGTestSuite +{ +protected: + void SetUp() override + { + client.Reset(); + } + + void TearDown() override + { + client.Reset(); + } + + PaginatorTestClient client; + TestRequest request; +}; + +TEST_F(PaginatorTest, TestIteratesThroughAllPages) +{ + auto clientPtr = std::make_shared(client); + PagePaginator paginator(clientPtr, request); + + Aws::Vector allItems; + int pageCount = 0; + + auto it = paginator.begin(); + auto end = paginator.end(); + while (it != end) + { + const auto& outcome = *it; + ASSERT_TRUE(outcome.IsSuccess()); + pageCount++; + + const auto& page = outcome.GetResult(); + for (const auto& item : page.GetItems()) + { + allItems.push_back(item); + } + ++it; + } + + EXPECT_EQ(pageCount, 3); // 7 items / 3 per page = 3 pages + EXPECT_EQ(allItems.size(), 7u); + EXPECT_STREQ(allItems[0].c_str(), "apple"); + EXPECT_STREQ(allItems[6].c_str(), "grape"); +} + +TEST_F(PaginatorTest, TestHandlesErrorGracefully) +{ + client.SetShouldFail(true); + auto clientPtr = std::make_shared(client); + PagePaginator paginator(clientPtr, request); + + auto it = paginator.begin(); + ASSERT_TRUE(it != paginator.end()); + + const auto& outcome = *it; + EXPECT_FALSE(outcome.IsSuccess()); + EXPECT_STREQ(outcome.GetError().c_str(), "Request failed"); + + ++it; + EXPECT_TRUE(it == paginator.end()); +} + +TEST_F(PaginatorTest, TestEmptyResultSet) +{ + client.SetData({}); + auto clientPtr = std::make_shared(client); + PagePaginator paginator(clientPtr, request); + + auto it = paginator.begin(); + ASSERT_TRUE(it != paginator.end()); + + const auto& outcome = *it; + EXPECT_TRUE(outcome.IsSuccess()); + EXPECT_TRUE(outcome.GetResult().GetItems().empty()); + EXPECT_FALSE(outcome.GetResult().GetMoreResults()); + + ++it; + EXPECT_TRUE(it == paginator.end()); +} + +TEST_F(PaginatorTest, TestBeginEndIteratorComparison) +{ + auto clientPtr = std::make_shared(client); + PagePaginator paginator(clientPtr, request); + + auto begin = paginator.begin(); + auto end = paginator.end(); + + EXPECT_TRUE(begin != end); + EXPECT_FALSE(begin == end); + + while (begin != end) + { + ++begin; + } + + EXPECT_TRUE(begin == end); + EXPECT_FALSE(begin != end); +} + +TEST_F(PaginatorTest, TestHandlesErrorOnSecondPage) +{ + client.SetFailOnPage(1); // Fail on second page (0-indexed) + auto clientPtr = std::make_shared(client); + PagePaginator paginator(clientPtr, request); + + auto it = paginator.begin(); + + // First page should succeed + ASSERT_TRUE(it != paginator.end()); + EXPECT_TRUE((*it).IsSuccess()); + EXPECT_EQ((*it).GetResult().GetItems().size(), 3u); + + // Move to second page + ++it; + + // Second page should have the error + ASSERT_TRUE(it != paginator.end()); + EXPECT_FALSE((*it).IsSuccess()); + EXPECT_TRUE((*it).GetError().find("Page 1 failed") != Aws::String::npos); + + // After error, iteration should end + ++it; + EXPECT_TRUE(it == paginator.end()); +} \ No newline at end of file