From ccf4eebc3db5cc9eebc6f40ff61fc84965373604 Mon Sep 17 00:00:00 2001 From: Yonas Habteab Date: Mon, 15 Sep 2025 10:40:07 +0200 Subject: [PATCH] Make `ValueGenerator` more flexible & easy to use This commit refactors the ValueGenerator class to be a template that can work with any container type. Previously, one has to manually take care of the used container by lazily iterating over it within a lambda. Now, the `ValueGenerator` class itself takes care of all the iteration, making it easier to use and less error-prone. The new base `Generator` class is required to allow the `JsonEncoder` to handle generators in a type-erased manner. --- lib/base/generator.hpp | 87 ++++++++++++++++++++++++------- lib/base/json.cpp | 4 +- lib/base/json.hpp | 2 +- lib/remote/objectqueryhandler.cpp | 12 +---- test/base-json.cpp | 30 +++++++---- 5 files changed, 94 insertions(+), 41 deletions(-) diff --git a/lib/base/generator.hpp b/lib/base/generator.hpp index fa41e67fc..5602cde9b 100644 --- a/lib/base/generator.hpp +++ b/lib/base/generator.hpp @@ -10,39 +10,90 @@ namespace icinga { /** - * ValueGenerator is a class that defines a generator function type for producing Values on demand. + * Abstract base class for generators that produce a sequence of Values. * - * This class is used to create generator functions that can yield any values that can be represented by the - * Icinga Value type. The generator function is exhausted when it returns `std::nullopt`, indicating that there - * are no more values to produce. Subsequent calls to `Next()` will always return `std::nullopt` after exhaustion. + * @note Any instance of a Generator should be treated as an @c Array -like object that produces its elements + * on-the-fly. * * @ingroup base */ -class ValueGenerator final : public Object +class Generator : public Object +{ +public: + DECLARE_PTR_TYPEDEFS(Generator); + + /** + * Produces the next Value in the sequence. + * + * This method returns the next Value produced by the generator. If the generator is exhausted, + * it returns std::nullopt for all subsequent calls to this method. + * + * @return The next Value in the sequence, or std::nullopt if the generator is exhausted. + */ + virtual std::optional Next() = 0; +}; + +/** + * A generator that transforms elements of a container into Values using a provided transformation function. + * + * This class takes a container and a transformation function as input. It uses the transformation function + * to convert each element of the container into a Value. The generator produces Values on-the-fly as they + * are requested via the `Next()` method. If the transformation function returns `std::nullopt` for an element, + * that element is skipped, and the generator continues to the next element in the container without its caller + * being aware of it. The generator is exhausted when all elements of the container have been processed. + * + * @tparam Container The type of the container holding the elements to be transformed. + * + * @ingroup base + */ +template +class ValueGenerator final : public Generator { public: DECLARE_PTR_TYPEDEFS(ValueGenerator); - /** - * Generates a Value using the provided generator function. - * - * The generator function should return an `std::optional` which contains the produced Value or - * `std::nullopt` when there are no more values to produce. After the generator function returns `std::nullopt`, - * the generator is considered exhausted, and further calls to `Next()` will always return `std::nullopt`. - */ - using GenFunc = std::function()>; + // The type of elements in the container and the type to be passed to the transformation function. + using ValueType = typename Container::value_type; - explicit ValueGenerator(GenFunc generator): m_Generator(std::move(generator)) + // The type of the transformation function that takes a ValueType and returns an optional Value. + using TransFormFunc = std::function (const ValueType&)>; + + /** + * Constructs a ValueGenerator with the given container and transformation function. + * + * The generator will iterate over the elements of the container, applying the transformation function + * to each element to produce values on-the-fly. You must ensure that the container remains valid for + * the lifetime of the generator. + * + * @param container The container holding the elements to be transformed. + * @param generator The transformation function to convert elements into Values. + */ + ValueGenerator(Container& container, TransFormFunc generator) + : m_It{container.begin()}, m_End{container.end()}, m_Func{std::move(generator)} { } - std::optional Next() const + std::optional Next() override { - return m_Generator(); + if (m_It == m_End) { + return std::nullopt; + } + + auto next = m_Func(*m_It); + ++m_It; + + if (next == std::nullopt && m_It != m_End) { + return Next(); + } + return next; } private: - GenFunc m_Generator; // The generator function that produces Values. + using Iterator = typename Container::iterator; + Iterator m_It; // Current iterator position. + Iterator m_End; // End iterator position. + + TransFormFunc m_Func; // The transformation function. }; -} +} // namespace icinga diff --git a/lib/base/json.cpp b/lib/base/json.cpp index 531ab45b2..eed6679a5 100644 --- a/lib/base/json.cpp +++ b/lib/base/json.cpp @@ -73,7 +73,7 @@ void JsonEncoder::Encode(const Value& value, boost::asio::yield_context* yc) EncodeObject(static_pointer_cast(obj), extractor, yc); } else if (type == Array::TypeInstance) { EncodeArray(static_pointer_cast(obj), yc); - } else if (auto gen(dynamic_pointer_cast(obj)); gen) { + } else if (auto gen(dynamic_pointer_cast(obj)); gen) { EncodeValueGenerator(gen, yc); } else { // Some other non-serializable object type! @@ -126,7 +126,7 @@ void JsonEncoder::EncodeArray(const Array::Ptr& array, boost::asio::yield_contex * @param yc The optional yield context for asynchronous operations. If provided, it allows the encoder * to flush the output stream safely when it has not acquired any object lock on the parent containers. */ -void JsonEncoder::EncodeValueGenerator(const ValueGenerator::Ptr& generator, boost::asio::yield_context* yc) +void JsonEncoder::EncodeValueGenerator(const Generator::Ptr& generator, boost::asio::yield_context* yc) { BeginContainer('['); bool isEmpty = true; diff --git a/lib/base/json.hpp b/lib/base/json.hpp index a5ed46280..95bb6d80c 100644 --- a/lib/base/json.hpp +++ b/lib/base/json.hpp @@ -73,7 +73,7 @@ public: private: void EncodeArray(const Array::Ptr& array, boost::asio::yield_context* yc); - void EncodeValueGenerator(const ValueGenerator::Ptr& generator, boost::asio::yield_context* yc); + void EncodeValueGenerator(const Generator::Ptr& generator, boost::asio::yield_context* yc); template void EncodeObject(const Iterable& container, const ValExtractor& extractor, boost::asio::yield_context* yc); diff --git a/lib/remote/objectqueryhandler.cpp b/lib/remote/objectqueryhandler.cpp index 4384abb55..84520e7b4 100644 --- a/lib/remote/objectqueryhandler.cpp +++ b/lib/remote/objectqueryhandler.cpp @@ -209,15 +209,7 @@ bool ObjectQueryHandler::HandleRequest( std::unordered_map>> typePermissions; std::unordered_map objectAccessAllowed; - auto it = objs.begin(); - auto generatorFunc = [&]() -> std::optional { - if (it == objs.end()) { - return std::nullopt; - } - - ConfigObject::Ptr obj = *it; - ++it; - + auto generatorFunc = [&](const ConfigObject::Ptr& obj) -> std::optional { DictionaryData result1{ { "name", obj->GetName() }, { "type", obj->GetReflectionType()->GetName() } @@ -330,7 +322,7 @@ bool ObjectQueryHandler::HandleRequest( response.set(http::field::content_type, "application/json"); response.StartStreaming(); - Dictionary::Ptr results = new Dictionary{{"results", new ValueGenerator{generatorFunc}}}; + Dictionary::Ptr results = new Dictionary{{"results", new ValueGenerator{objs, generatorFunc}}}; results->Freeze(); bool pretty = HttpUtility::GetLastParameter(params, "pretty"); diff --git a/test/base-json.cpp b/test/base-json.cpp index 4ae17bf09..2120cf2fd 100644 --- a/test/base-json.cpp +++ b/test/base-json.cpp @@ -18,14 +18,10 @@ BOOST_AUTO_TEST_SUITE(base_json) BOOST_AUTO_TEST_CASE(encode) { - auto generate = []() -> std::optional { - static int count = 0; - if (++count == 4) { - count = 0; - return std::nullopt; - } - return Value(count); - }; + int emptyGenCounter = 0; + std::vector empty; + std::vector vec{1, 2, 3}; + auto generate = [](int count) -> std::optional { return Value(count); }; Dictionary::Ptr input (new Dictionary({ { "array", new Array({ new Namespace() }) }, @@ -42,8 +38,17 @@ BOOST_AUTO_TEST_CASE(encode) { "string", "LF\nTAB\tAUml\xC3\xA4Ill\xC3" }, { "true", true }, { "uint", 23u }, - { "generator", new ValueGenerator(generate) }, - { "empty_generator", new ValueGenerator([]() -> std::optional { return std::nullopt; }) }, + { "generator", new ValueGenerator{vec, generate} }, + { + "empty_generator", + new ValueGenerator{ + empty, + [&emptyGenCounter](int) -> std::optional { + emptyGenCounter++; + return std::nullopt; + } + } + }, })); String output (R"EOF({ @@ -72,15 +77,20 @@ BOOST_AUTO_TEST_CASE(encode) auto got(JsonEncode(input, true)); BOOST_CHECK_EQUAL(output, got); + BOOST_CHECK_EQUAL(emptyGenCounter, 0); // Ensure the transformation function was never invoked. + input->Set("generator", new ValueGenerator{vec, generate}); std::ostringstream oss; JsonEncode(input, oss, true); + BOOST_CHECK_EQUAL(emptyGenCounter, 0); // Ensure the transformation function was never invoked. BOOST_CHECK_EQUAL(output, oss.str()); boost::algorithm::replace_all(output, " ", ""); boost::algorithm::replace_all(output, "Objectoftype'Function'", "Object of type 'Function'"); boost::algorithm::replace_all(output, "\n", ""); + input->Set("generator", new ValueGenerator{vec, generate}); + BOOST_CHECK_EQUAL(emptyGenCounter, 0); // Ensure the transformation function was never invoked. BOOST_CHECK(JsonEncode(input, false) == output); }