diff --git a/include/gimo/algorithm/AndForward.hpp b/include/gimo/algorithm/AndForward.hpp new file mode 100644 index 0000000..030deea --- /dev/null +++ b/include/gimo/algorithm/AndForward.hpp @@ -0,0 +1,99 @@ +// Copyright Dominic (DNKpp) Koepke 2026. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef GIMO_ALGORITHM_AND_FORWARD_HPP +#define GIMO_ALGORITHM_AND_FORWARD_HPP + +#pragma once + +#include "gimo/Common.hpp" +#include "gimo/Pipeline.hpp" +#include "gimo/algorithm/BasicAlgorithm.hpp" + +#include +#include +#include +#include + +namespace gimo::detail::and_forward +{ + template + consteval void print_diagnostics() + { + if constexpr (!std::invocable>) + { + static_assert(always_false_v, "The and_forward algorithm requires an action invocable with the nullable's value."); + } + } + + struct traits + { + template + static constexpr bool is_applicable_on = requires { + requires std::invocable>; + }; + + template + static constexpr void on_value(Action&& action, Nullable&& opt) + { + GIMO_ASSERT(detail::has_value(opt), "Nullable is empty while it's expected to contain a value."); + + if constexpr (is_applicable_on) + { + std::invoke( + std::forward(action), + detail::forward_value(opt)); + } + else + { + and_forward::print_diagnostics(); + } + } + + template + static constexpr void on_null(Action&& /*action*/, [[maybe_unused]] Nullable&& opt) + { + GIMO_ASSERT(!detail::has_value(opt), "Nullable contains a value while it's expected to be empty."); + + if constexpr (!is_applicable_on) + { + and_forward::print_diagnostics(); + } + } + }; +} + +namespace gimo +{ + namespace detail + { + template + using and_forward_t = BasicAlgorithm< + and_forward::traits, + std::remove_cvref_t>; + } + + /** + * \brief Creates a terminating pipeline step that forwards the contained value to the specified action. + * \ingroup ALGORITHM + * \tparam Action The action type. + * \param action A unary operation. + * \return A Pipeline step containing the `and_forward` algorithm. + * \details + * - **On Value**: Invokes the `action` with the underlying value of the input. + * Any returned value by `action` will be discarded, thus `action` may return `void`. + * - **On Null**: Silently terminates the pipeline. + */ + template + [[nodiscard]] + constexpr auto and_forward(Action&& action) + { + using Algorithm = detail::and_forward_t; + + return Pipeline{std::tuple{std::forward(action)}}; + } +} + +#endif diff --git a/include/gimo/algorithm/AndThen.hpp b/include/gimo/algorithm/AndThen.hpp index 411b6cf..d9206fc 100644 --- a/include/gimo/algorithm/AndThen.hpp +++ b/include/gimo/algorithm/AndThen.hpp @@ -1,4 +1,4 @@ -// Copyright Dominic (DNKpp) Koepke 2025. +// Copyright Dominic (DNKpp) Koepke 2025-2026. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -64,14 +64,14 @@ namespace gimo::detail::and_then template [[nodiscard]] - constexpr result_t on_null([[maybe_unused]] Action&& action, [[maybe_unused]] Nullable&& opt) + constexpr result_t on_null(Action&& /*action*/, Nullable&& /*opt*/) { return detail::construct_empty>(); } template [[nodiscard]] - constexpr result_t on_null([[maybe_unused]] Action&& action, Expected&& expected) + constexpr result_t on_null(Action&& /*action*/, Expected&& expected) { return detail::rebind_error, Expected>(expected); } @@ -96,6 +96,8 @@ namespace gimo::detail::and_then [[nodiscard]] static constexpr auto on_value(Action&& action, Nullable&& opt, Steps&&... steps) { + GIMO_ASSERT(detail::has_value(opt), "Nullable is empty while it's expected to contain a value."); + if constexpr (is_applicable_on) { return and_then::on_value( @@ -113,6 +115,8 @@ namespace gimo::detail::and_then [[nodiscard]] static constexpr auto on_null(Action&& action, Nullable&& opt, Steps&&... steps) { + GIMO_ASSERT(!detail::has_value(opt), "Nullable contains a value while it's expected to be empty."); + if constexpr (is_applicable_on) { return and_then::on_null( diff --git a/include/gimo/algorithm/OrElse.hpp b/include/gimo/algorithm/OrElse.hpp index c96595b..fd239a0 100644 --- a/include/gimo/algorithm/OrElse.hpp +++ b/include/gimo/algorithm/OrElse.hpp @@ -62,7 +62,7 @@ namespace gimo::detail::or_else if constexpr (std::is_void_v>) { std::invoke(std::forward(action)); - return null_v; + return detail::construct_empty(); } else { @@ -104,6 +104,8 @@ namespace gimo::detail::or_else [[nodiscard]] static constexpr auto on_value(Action&& action, Nullable&& opt, Steps&&... steps) { + GIMO_ASSERT(detail::has_value(opt), "Nullable is empty while it's expected to contain a value."); + if constexpr (is_applicable_on) { return or_else::on_value( @@ -121,6 +123,8 @@ namespace gimo::detail::or_else [[nodiscard]] static constexpr auto on_null(Action&& action, Nullable&& opt, Steps&&... steps) { + GIMO_ASSERT(!detail::has_value(opt), "Nullable contains a value while it's expected to be empty."); + if constexpr (is_applicable_on) { return or_else::on_null( diff --git a/include/gimo/algorithm/Transform.hpp b/include/gimo/algorithm/Transform.hpp index 34df64a..511a5dc 100644 --- a/include/gimo/algorithm/Transform.hpp +++ b/include/gimo/algorithm/Transform.hpp @@ -1,4 +1,4 @@ -// Copyright Dominic (DNKpp) Koepke 2025. +// Copyright Dominic (DNKpp) Koepke 2025-2026. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -41,7 +41,7 @@ namespace gimo::detail::transform template [[nodiscard]] - constexpr result_t on_value([[maybe_unused]] Action&& action, Nullable&& opt) + constexpr result_t on_value(Action&& action, Nullable&& opt) { return construct_from_value>( std::invoke( @@ -52,7 +52,7 @@ namespace gimo::detail::transform template [[nodiscard]] constexpr auto on_value( - [[maybe_unused]] Action&& action, + Action&& action, Nullable&& opt, Next&& next, Steps&&... steps) @@ -64,14 +64,14 @@ namespace gimo::detail::transform template [[nodiscard]] - constexpr result_t on_null([[maybe_unused]] Action&& action, [[maybe_unused]] Nullable&& opt) + constexpr result_t on_null(Action&& /*action*/, Nullable&& /*opt*/) { return detail::construct_empty>(); } template [[nodiscard]] - constexpr result_t on_null([[maybe_unused]] Action&& action, Expected&& expected) + constexpr result_t on_null(Action&& /*action*/, Expected&& expected) { return detail::rebind_error, Expected>(expected); } @@ -98,6 +98,8 @@ namespace gimo::detail::transform [[nodiscard]] static constexpr auto on_value(Action&& action, Nullable&& opt, Steps&&... steps) { + GIMO_ASSERT(detail::has_value(opt), "Nullable is empty while it's expected to contain a value."); + if constexpr (is_applicable_on) { return transform::on_value( @@ -115,6 +117,8 @@ namespace gimo::detail::transform [[nodiscard]] static constexpr auto on_null(Action&& action, Nullable&& opt, Steps&&... steps) { + GIMO_ASSERT(!detail::has_value(opt), "Nullable contains a value while it's expected to be empty."); + if constexpr (is_applicable_on) { return transform::on_null( diff --git a/include/gimo/algorithm/TransformError.hpp b/include/gimo/algorithm/TransformError.hpp index 86aa3f0..a0d5506 100644 --- a/include/gimo/algorithm/TransformError.hpp +++ b/include/gimo/algorithm/TransformError.hpp @@ -1,4 +1,4 @@ -// Copyright Dominic (DNKpp) Koepke 2025. +// Copyright Dominic (DNKpp) Koepke 2025-2026. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) @@ -45,7 +45,7 @@ namespace gimo::detail::transform_error template [[nodiscard]] - constexpr result_t on_value([[maybe_unused]] Action&& action, Expected&& closure) + constexpr result_t on_value(Action&& /*action*/, Expected&& closure) { return detail::rebind_value, Expected>(closure); } @@ -95,6 +95,8 @@ namespace gimo::detail::transform_error [[nodiscard]] static constexpr auto on_value(Action&& action, Expected&& closure, Steps&&... steps) { + GIMO_ASSERT(detail::has_value(opt), "Nullable is empty while it's expected to contain a value."); + if constexpr (is_applicable_on) { return transform_error::on_value( @@ -112,6 +114,8 @@ namespace gimo::detail::transform_error [[nodiscard]] static constexpr auto on_null(Action&& action, Expected&& closure, Steps&&... steps) { + GIMO_ASSERT(!detail::has_value(opt), "Nullable contains a value while it's expected to be empty."); + if constexpr (is_applicable_on) { return transform_error::on_null( diff --git a/include/gimo/algorithm/ValueOrElse.hpp b/include/gimo/algorithm/ValueOrElse.hpp index e0c19b5..7bc04dd 100644 --- a/include/gimo/algorithm/ValueOrElse.hpp +++ b/include/gimo/algorithm/ValueOrElse.hpp @@ -44,8 +44,10 @@ namespace gimo::detail::value_or_else template [[nodiscard]] - static constexpr auto on_value([[maybe_unused]] Action&& action, Nullable&& opt) + static constexpr auto on_value(Action&& /*action*/, Nullable&& opt) { + GIMO_ASSERT(detail::has_value(opt), "Nullable is empty while it's expected to contain a value."); + if constexpr (is_applicable_on) { return detail::forward_value(opt); @@ -60,6 +62,8 @@ namespace gimo::detail::value_or_else [[nodiscard]] static constexpr auto on_null(Action&& action, [[maybe_unused]] Nullable&& opt) { + GIMO_ASSERT(!detail::has_value(opt), "Nullable contains a value while it's expected to be empty."); + if constexpr (is_applicable_on) { return static_cast>( diff --git a/test/unit-tests/algorithm/AndForward.cpp b/test/unit-tests/algorithm/AndForward.cpp new file mode 100644 index 0000000..6afe1c3 --- /dev/null +++ b/test/unit-tests/algorithm/AndForward.cpp @@ -0,0 +1,134 @@ +// Copyright Dominic (DNKpp) Koepke 2026. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "gimo/algorithm/AndForward.hpp" +#include "gimo_ext/StdOptional.hpp" + +#include "TestCommons.hpp" + +using namespace gimo; + +TEMPLATE_LIST_TEST_CASE( + "and_forward algorithm invokes its action only when the input contains a value.", + "[algorithm]", + testing::with_qualification_list) +{ + using with_qualification = TestType; + + mimicpp::Mock< + void(int) &, + void(int) const&, + void(int) &&, + void(int) const&&> + action{}; + + using Algorithm = detail::and_forward_t; + STATIC_REQUIRE(gimo::applicable_to, typename with_qualification::template type>); + + SECTION("When input contains a value, the action is invoked.") + { + constexpr std::optional opt{1337}; + + SCOPED_EXP with_qualification::cast(action).expect_call(1337); + + Algorithm algorithm{std::move(action)}; + STATIC_REQUIRE(std::same_as); + + with_qualification::cast(algorithm)(opt); + } + + SECTION("When input contains no value, the pipeline silently terminates.") + { + constexpr std::optional opt{}; + Algorithm algorithm{std::move(action)}; + STATIC_REQUIRE(std::same_as); + + with_qualification::cast(algorithm)(opt); + } +} + +TEMPLATE_LIST_TEST_CASE( + "and_forward algorithm accepts nullables with any cv-ref qualification.", + "[algorithm]", + testing::with_qualification_list) +{ + using with_qualification = TestType; + + mimicpp::Mock< + void(int&) const, + void(int const&) const, + void(int&&) const, + void(int const&&) const> const action{}; + + using Algorithm = detail::and_forward_t; + STATIC_REQUIRE(gimo::applicable_to, typename with_qualification::template type>); + + Algorithm const algorithm{std::cref(action)}; + std::optional opt{42}; + + SCOPED_EXP action.expect_call(matches::type>) + and expect::arg<0>(matches::eq(42)); + algorithm(with_qualification::cast(opt)); +} + +TEMPLATE_LIST_TEST_CASE( + "and_forward algorithm supports expected_like types.", + "[algorithm]", + testing::with_qualification_list) +{ + using with_qualification = TestType; + + mimicpp::Mock< + void(int) &, + void(int) const&, + void(int) &&, + void(int) const&&> + action{}; + + using Algorithm = detail::and_forward_t; + STATIC_REQUIRE(gimo::applicable_to, typename with_qualification::template type>); + + SECTION("When input contains a value, the action is invoked.") + { + testing::ExpectedFake const expected{1337}; + + SCOPED_EXP with_qualification::cast(action).expect_call(1337); + + Algorithm algorithm{std::move(action)}; + STATIC_REQUIRE(std::same_as); + + with_qualification::cast(algorithm)(expected); + } + + SECTION("When input contains no value, the pipeline silently terminates.") + { + auto const expected = testing::ExpectedFake::from_error("An error."); + + Algorithm algorithm{std::move(action)}; + STATIC_REQUIRE(std::same_as); + + with_qualification::cast(algorithm)(expected); + } +} + +TEMPLATE_LIST_TEST_CASE( + "gimo::and_forward creates an appropriate pipeline.", + "[algorithm]", + testing::with_qualification_list) +{ + using with_qualification = TestType; + + mimicpp::Mock const inner{}; + auto action = [&](int const x) { return inner(x); }; + using DummyAction = decltype(action); + + decltype(auto) pipeline = and_forward(with_qualification::cast(action)); + STATIC_CHECK(std::same_as>, decltype(pipeline)>); + STATIC_CHECK(gimo::applicable_to, detail::and_forward_t>); + STATIC_CHECK(gimo::processable_by, decltype(pipeline)>); + + SCOPED_EXP inner.expect_call(42); + pipeline.apply(std::optional{42}); +} diff --git a/test/unit-tests/algorithm/CMakeLists.txt b/test/unit-tests/algorithm/CMakeLists.txt index d2c16b1..c719900 100644 --- a/test/unit-tests/algorithm/CMakeLists.txt +++ b/test/unit-tests/algorithm/CMakeLists.txt @@ -1,10 +1,11 @@ -# Copyright Dominic (DNKpp) Koepke 2025 - 2026. +# Copyright Dominic (DNKpp) Koepke 2025-2026. # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # https://www.boost.org/LICENSE_1_0.txt) target_sources(${TARGET_NAME} PRIVATE "BasicAlgorithm.cpp" + "AndForward.cpp" "AndThen.cpp" "OrElse.cpp" "Transform.cpp"