Skip to content

Commit

Permalink
add notify
Browse files Browse the repository at this point in the history
  • Loading branch information
SachiSakurane committed Feb 20, 2025
1 parent 55e0fef commit 7830268
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 0 deletions.
41 changes: 41 additions & 0 deletions include/riw/notify/behavior.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include <algorithm>
#include <concepts>
#include <type_traits>

#include <riw/notify/observer.hpp>

namespace riw {
template <std::copy_constructible Type>
class behavior : public riw::observer<Type> {
public:
using value_type = riw::observer<Type>::value_type;

constexpr behavior(value_type initial_value) : current_value{initial_value} {}

template <class... Args>
constexpr behavior(Args &&...args) : current_value{std::forward<Args>(args)...} {}

behavior(const behavior &) = default;
behavior(behavior &&) = default;

behavior &operator=(const value_type &value) {
this->current_value = value;
this->notify(observe());
return *this;
}
behavior &operator=(value_type &&value) {
this->current_value = std::move(value);
this->notify(observe());
return *this;
}

operator value_type() const { return current_value; }
value_type observe() const override { return current_value; }

private:
value_type current_value;
};

} // namespace riw
14 changes: 14 additions & 0 deletions include/riw/notify/concepts/observable.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include <concepts>

namespace riw {
template <class ObservableType>
concept observable =
std::copy_constructible<typename ObservableType::value_type> &&
requires(ObservableType o, typename ObservableType::subscription_type &subscription) {
{ o.observe() } -> std::same_as<typename ObservableType::value_type>;
o.add_subscription(subscription);
o.remove_subscription(subscription);
};
} // namespace riw
9 changes: 9 additions & 0 deletions include/riw/notify/concepts/subscribable.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <concepts>

namespace riw {
template <class S>
concept subscribable = std::copy_constructible<typename S::value_type> &&
requires(S s) { s.receive(std::declval<typename S::value_type>); };
} // namespace riw
142 changes: 142 additions & 0 deletions include/riw/notify/connection.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#pragma once

#include <type_traits>

#include <riw/notify/concepts/observable.hpp>
#include <riw/notify/concepts/subscribable.hpp>

namespace riw {

namespace detail {
template <class F, class O>
concept connectable_observable_to_function =
observable<O> && std::is_invocable_v<F, typename O::value_type> &&
!std::is_same_v<std::invoke_result_t<F, typename O::value_type>, void>;

template <observable O, connectable_observable_to_function<O> F>
struct connection_observable_traits {
using value_type = typename O::value_type;
using invoke_result_type = std::invoke_result_t<F, value_type>;

using observer_type = observer<invoke_result_type>;
using subscription_type = subscription<value_type>;
};

template <observable O, connectable_observable_to_function<O> F>
class observable_connection : public connection_observable_traits<O, F>::observer_type,
public connection_observable_traits<O, F>::subscription_type {
public:
using value_type = connection_observable_traits<O, F>::invoke_result_type;

constexpr observable_connection(O &o, F &&f) : obs{o}, func{std::forward<F>(f)} {
obs.add_subscription(*this);
}

~observable_connection() { obs.remove_subscription(*this); }

operator value_type() const { return func(obs.observe()); }
value_type observe() const override { return func(obs.observe()); }

void receive(typename connection_observable_traits<O, F>::value_type v) override {
connection_observable_traits<O, F>::observer_type::notify(func(v));
}

private:
O &obs;
const F func;
};

template <observable O, connectable_observable_to_function<O> F>
class observable_connection_r : public connection_observable_traits<O, F>::observer_type,
public connection_observable_traits<O, F>::subscription_type {
public:
using value_type = connection_observable_traits<O, F>::invoke_result_type;

constexpr observable_connection_r(O &&o, F &&f)
: obs{std::forward<O>(o)}, func{std::forward<F>(f)} {
obs.add_subscription(*this);
}

~observable_connection_r() { obs.remove_subscription(*this); }

operator value_type() const { return func(obs.observe()); }
value_type observe() const override { return func(obs.observe()); }

void receive(typename connection_observable_traits<O, F>::value_type v) override {
connection_observable_traits<O, F>::observer_type::notify(func(v));
}

private:
O obs;
const F func;
};

template <class F, class O>
concept connectable_observable_to_void_function =
observable<O> && std::is_invocable_v<F, typename O::value_type> &&
std::is_same_v<std::invoke_result_t<F, typename O::value_type>, void>;

template <observable O, connectable_observable_to_void_function<O> F>
struct connection_subscription_traits {
using value_type = typename O::value_type;

using subscription_type = subscription<value_type>;
};

template <observable O, connectable_observable_to_void_function<O> F>
class subscription_connection : public connection_subscription_traits<O, F>::subscription_type {
public:
constexpr subscription_connection(O &o, F &&f) : obs{o}, func{std::forward<F>(f)} {
obs.add_subscription(*this);
}

~subscription_connection() { obs.remove_subscription(*this); }

void receive(typename connection_subscription_traits<O, F>::value_type v) override { func(v); }

private:
O &obs;
const F func;
};

template <observable O, connectable_observable_to_void_function<O> F>
class subscription_connection_r : public connection_subscription_traits<O, F>::subscription_type {
public:
constexpr subscription_connection_r(O &o, F &&f)
: obs{std::forward<O>(o)}, func{std::forward<F>(f)} {
obs.add_subscription(*this);
}

~subscription_connection_r() { obs.remove_subscription(*this); }

void receive(typename connection_subscription_traits<O, F>::value_type v) override { func(v); }

private:
O obs;
const F func;
};
} // namespace detail

// to observable
template <observable O, detail::connectable_observable_to_function<O> F>
[[nodiscard]] inline auto operator|(O &o, F &&f) {
return detail::observable_connection<O, F>{o, std::forward<F>(f)};
}

template <observable O, detail::connectable_observable_to_function<O> F>
[[nodiscard]] inline auto operator|(O &&o, F &&f) {
return detail::observable_connection_r<O, F>{std::forward<O>(o), std::forward<F>(f)};
}

// to subscription
template <observable O, detail::connectable_observable_to_void_function<O> F>
[[nodiscard]] inline auto operator|(O &o, F &&f) {
return detail::subscription_connection<O, F>{o, std::forward<F>(f)};
}

template <observable O, detail::connectable_observable_to_void_function<O> F>
[[nodiscard]] inline auto operator|(O &&o, F &&f) {
return detail::subscription_connection_r<O, F>{std::forward<O>(o), std::forward<F>(f)};
}

} // namespace riw
41 changes: 41 additions & 0 deletions include/riw/notify/observer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include <vector>

#include <riw/notify/concepts/observable.hpp>
#include <riw/notify/subsctiption.hpp>

namespace riw {
template <std::copy_constructible Type>
class observer {
public:
using value_type = Type;
using subscription_type = riw::subscription<Type>;

void add_subscription(subscription_type &s) {
subscriptions.emplace_back(s);
s.receive(observe());
}

void remove_subscription(subscription_type &s) {
if (auto it = std::remove_if(std::begin(subscriptions), std::end(subscriptions),
[&s](const auto &ss) { return &(ss.get()) == &s; });
it != std::end(subscriptions)) {
subscriptions.erase(it);
}
}

virtual value_type observe() const = 0;

virtual void notify(value_type v) {
for (auto &sub : subscriptions) {
sub.get().receive(v);
}
}

std::size_t subscriptions_count() const { return subscriptions.size(); };

private:
std::vector<std::reference_wrapper<subscription_type>> subscriptions;
};
} // namespace riw
13 changes: 13 additions & 0 deletions include/riw/notify/subsctiption.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#pragma once

#include <concepts>

namespace riw {
template <class Type>
class subscription {
public:
using value_type = Type;

virtual void receive(value_type v) = 0;
};
} // namespace riw
11 changes: 11 additions & 0 deletions test/notify/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cc_test(
name = "notify_test",
size = "small",
srcs = glob(["*.cpp"]),
deps = [
"//:riw_test_library",
"@googletest//:gtest",
"@googletest//:gtest_main",
],
visibility = ["//visibility:public"],
)
44 changes: 44 additions & 0 deletions test/notify/behavior.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include <iostream>
#include <string>

#include <gtest/gtest.h>
#include <riw/notify/behavior.hpp>
#include <riw/notify/connection.hpp>

TEST(Notify_BehaviorTest, Construction) {
class ClassA {};

riw::behavior<int> i{10};
riw::behavior<std::string> s{"string"};
riw::behavior<ClassA> c{};
}

TEST(Notify_BehaviorTest, Cast) {
riw::behavior<int> i{10};
ASSERT_EQ(i, 10);

riw::behavior<std::string> s{"string"};
ASSERT_STREQ(static_cast<std::string>(s).c_str(), "string");
}

TEST(Notify_BehaviorTest, SubstitutionOperator) {
{
riw::behavior<int> i{10};
i = 42;
ASSERT_EQ(i, 42);
}
{
riw::behavior<std::string> s{"string"};
s = std::string{"namasute"};

ASSERT_STREQ(static_cast<std::string>(s).c_str(), "namasute");
}
{
struct ClassA {
int x;
};
riw::behavior<ClassA> c{ClassA{.x = 42}};
c = ClassA{.x = 4242};
ASSERT_EQ(static_cast<ClassA>(c).x, 4242);
}
}
66 changes: 66 additions & 0 deletions test/notify/connection.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#include <iostream>
#include <string>

#include <gtest/gtest.h>
#include <riw/notify/behavior.hpp>
#include <riw/notify/connection.hpp>

using namespace std::literals::string_literals;

TEST(Notify_ConnectionTest, Pipeline) {
riw::behavior<int> i{10};

ASSERT_EQ(i, 10);

auto o = i | [](auto i) { return i * 2; };
ASSERT_EQ(o, 20);

auto oo = i | [](auto i) { return i * 2; } | [](auto i) { return i * 2; };
ASSERT_EQ(oo, 40);

auto ooo = oo | [](auto i) { return i * 2; };
ASSERT_EQ(ooo, 80);
}

TEST(Notify_ConnectionTest, Transform) {
riw::behavior<int> i{10};

ASSERT_EQ(i, 10);

auto o = i | [](auto i) { return std::to_string(i); } | [](auto s) { return s + "nyan"s; };

ASSERT_EQ(static_cast<std::string>(o), "10nyan"s);
}

TEST(Notify_ConnectionTest, Effect) {
riw::behavior<int> i{10};

ASSERT_EQ(i, 10);

auto o = i | [](auto i) {
std::cout << "value: " << i;
return i * 2;
};

testing::internal::CaptureStdout();
i = 42;
std::string log = testing::internal::GetCapturedStdout();

ASSERT_EQ(i, 42);
ASSERT_EQ(static_cast<std::string>(log), "value: 42"s);
}

TEST(Notify_ConnectionTest, Notify) {
riw::behavior<int> i{42};

ASSERT_EQ(i, 42);
auto o = i | [](auto i) { return std::to_string(i); };

testing::internal::CaptureStdout();

auto s = o | [](auto s) { std::cout << "string: " << s << std::endl; };

std::string log = testing::internal::GetCapturedStdout();

ASSERT_EQ(static_cast<std::string>(log), "string: 42\n"s);
}

0 comments on commit 7830268

Please sign in to comment.