From 5e2d2ddde532e98723b5c62dad352c0e26c0e567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20K=C3=BChl?= Date: Thu, 18 Dec 2025 16:13:36 +0100 Subject: [PATCH] added observer to repository and updated tests --- include/matador/object/basic_object_info.hpp | 3 + .../matador/object/foreign_node_completer.hpp | 2 +- include/matador/object/object_info.hpp | 32 +++++ include/matador/object/observer.hpp | 124 ++++++++++++++++++ include/matador/object/repository.hpp | 65 +++++---- include/matador/object/repository_node.hpp | 8 +- include/matador/query/schema.hpp | 12 +- source/core/CMakeLists.txt | 2 + source/core/object/observer.cpp | 10 ++ source/core/object/repository_node.cpp | 4 + test/core/object/RepositoryTest.cpp | 90 ++++++++----- 11 files changed, 286 insertions(+), 66 deletions(-) create mode 100644 include/matador/object/observer.hpp create mode 100644 source/core/object/observer.cpp diff --git a/include/matador/object/basic_object_info.hpp b/include/matador/object/basic_object_info.hpp index 02a4a0a..1e63077 100644 --- a/include/matador/object/basic_object_info.hpp +++ b/include/matador/object/basic_object_info.hpp @@ -36,6 +36,9 @@ public: void update_name(const std::string& name) const; + virtual void on_attach() const = 0; + virtual void on_detach() const = 0; + endpoint_iterator register_relation_endpoint(const std::type_index &type, const std::shared_ptr &endpoint); void unregister_relation_endpoint(const std::type_index &type); diff --git a/include/matador/object/foreign_node_completer.hpp b/include/matador/object/foreign_node_completer.hpp index 225852f..dbf5b4c 100644 --- a/include/matador/object/foreign_node_completer.hpp +++ b/include/matador/object/foreign_node_completer.hpp @@ -98,7 +98,7 @@ private: return; } - const auto node = repository_node::make_node(repo_.repo(), ""); + const auto node = repository_node::make_node(repo_.repo(), "", []{ return std::make_unique(); }); if (auto result = repo_.attach_node(node)) { foreign_node_completer::complete(result.value()); } diff --git a/include/matador/object/object_info.hpp b/include/matador/object/object_info.hpp index 1ff2eb0..03c4b63 100644 --- a/include/matador/object/object_info.hpp +++ b/include/matador/object/object_info.hpp @@ -2,12 +2,28 @@ #define OBJECT_INFO_HPP #include "matador/object/basic_object_info.hpp" +#include "matador/object/observer.hpp" #include namespace matador::object { class repository_node; +template +class observer_ptr { +public: + explicit observer_ptr(std::unique_ptr> &&observer) + : observer(std::move(observer)) {} + + operator bool() const { return observer != nullptr; } + + observer *get() const { return observer.get(); } + observer &operator*() const { return *observer; } + observer *operator->() const { return observer.get(); } + +private: + std::unique_ptr> observer; +}; template class object_info final : public basic_object_info { public: @@ -27,9 +43,25 @@ public: const Type &prototype() const { return prototype_; } std::unique_ptr create() const { return creator_(); } + void on_attach() const override { + for (auto &observer : observers_) { + observer->on_attach(*node_, prototype_); + } + } + + void on_detach() const override { + for (auto &observer : observers_) { + observer->on_detach(*node_, prototype_); + } + } + + void register_observer(observer_ptr observer) { + observers_.push_back(std::move(observer)); + } private: Type prototype_; create_func creator_{[]{ return std::make_unique(); }}; + std::vector> observers_; }; template diff --git a/include/matador/object/observer.hpp b/include/matador/object/observer.hpp new file mode 100644 index 0000000..8dd934b --- /dev/null +++ b/include/matador/object/observer.hpp @@ -0,0 +1,124 @@ +#ifndef MATADOR_OBSERVER_HPP +#define MATADOR_OBSERVER_HPP + +#include + +namespace matador::object { +class repository_node; + +class abstract_observer { +public: + virtual ~abstract_observer() = default; + +protected: + /** + * Constructs a basic_object_store_observer for the + * give std::type_index. + * + * @param ti type index of the observer + */ + explicit abstract_observer(const std::type_index &ti); + +public: + /** + * Return the type index of the observer. + * + * @return The type index of the observer. + */ + [[nodiscard]] const std::type_index& index() const; + +private: + std::type_index type_index_; +}; + +/** + * @class observer + * @tparam Type Type of the observer + * @brief Base class for typed observer classes + * + * When interested in observe + * - attach (prototype_node) + * - detach (prototype_node) + * - insert (an object) + * - update (an object) + * - delete (an object) + * actions an observer class instance must be + * registered within the repository. + * Use this class as a base class for all observer classes. + */ +template < class Type > +class observer : public abstract_observer { +public: + /** + * Default constructor + */ + observer() : abstract_observer(std::type_index(typeid(Type))) {} + + /** + * @brief Called on repository_node attachment + * + * When a prototype node is attached to the repository, + * this is called after the attaching succeeded. + * + * @param node The attached repository_node + * @param prototype The prototype object of the attached node + */ + virtual void on_attach(repository_node &node, const Type &prototype) = 0; + + /** + * @brief Called on repository_node detached + * + * When a prototype node is detached from the repository, + * this is called before the detaching succeeded. + * + * @param node The to be detached repository_node + * @param prototype The prototype object of the detached node + */ + virtual void on_detach(repository_node &node, const Type &prototype) = 0; + + /** + * @brief Called on object insertion. + * + * Called when an object is inserted + * into the repository. + * + * @param obj The proxy of the inserted object. + */ + virtual void on_insert(Type &obj) = 0; + + /** + * @brief Called on object update. + * + * Called when an object is updated + * in the repository. + * + * @param obj The proxy of the updated object. + */ + virtual void on_update(Type &obj) = 0; + + /** + * @brief Called on object deletion. + * + * Called when an object is deleted + * from the repository. + * + * @param obj The proxy of the deleted object. + */ + virtual void on_delete(Type &obj) = 0; +}; + +namespace internal { +template < class Type > +class null_observer : public observer { +public: + template < class OtherType > + explicit null_observer(const null_observer *) {} + void on_attach(repository_node &, Type &) override {} + void on_detach(repository_node &, Type &) override {} + void on_insert(Type &) override {} + void on_update(Type &) override {} + void on_delete(Type &) override {} +}; +} +} +#endif //MATADOR_OBSERVER_HPP \ No newline at end of file diff --git a/include/matador/object/repository.hpp b/include/matador/object/repository.hpp index a1f94ca..2c48cad 100644 --- a/include/matador/object/repository.hpp +++ b/include/matador/object/repository.hpp @@ -6,6 +6,7 @@ #include "matador/object/error_code.hpp" #include "matador/object/foreign_node_completer.hpp" #include "matador/object/object_info.hpp" +#include "matador/object/observer.hpp" #include "matador/object/relation_completer.hpp" #include "matador/object/repository_node.hpp" #include "matador/object/repository_node_iterator.hpp" @@ -37,43 +38,53 @@ public: */ explicit repository(std::string name = ""); - template - [[nodiscard]] utils::result attach(const std::string &name, const std::string &parent = "") { - if (const auto it = nodes_by_type_.find(typeid(Type)); it == nodes_by_type_.end() ) { - // if the type was not found - auto node = repository_node::make_node(*this, name); - if (auto result = attach_node(node, parent); !result) { - return utils::failure(result.err()); - } - foreign_node_completer::complete(node); - relation_completer::complete(node); - } else if (!has_node(name)) { - it->second->update_name(name); - nodes_by_name_[name] = it->second; - if (const auto i = nodes_by_name_.find(""); i != nodes_by_name_.end()) { - nodes_by_name_.erase(i); - } - relation_completer::complete(it->second); - log_.info("attach: update node name to '%s' (type: %s)", it->second->name().c_str(), it->second->type_index().name()); - } else { - // remove_node( ) - return utils::failure(make_error(error_code::NodeAlreadyExists, "Node '" + name + "' already exists")); - } - - return utils::ok(); + template + [[nodiscard]] utils::result attach(const std::string &name, Observers&&... observers) { + return attach_type(name, std::string{}, std::forward(observers)...); } - template - [[nodiscard]] utils::result attach(const std::string &name) { + template + [[nodiscard]] utils::result attach(const std::string &name, Observers&&... observers) { const auto ti = std::type_index(typeid(SuperType)); auto result = find_node(ti); if (!result) { return utils::failure(make_error(error_code::NodeNotFound, "Parent node '" + std::string(ti.name()) + "' not found")); } - return attach(name, (*result)->name()); + return attach_type(name, (*result)->name(), std::forward(observers)...); } + template + [[nodiscard]] utils::result attach(const std::string &name, const std::string &parent, Observers&&... observers) { + return attach_type(name, parent, std::forward(observers)...); + } + template + [[nodiscard]] utils::result attach_type(const std::string &name, const std::string &parent, Observers&&... observers) { + if (const auto it = nodes_by_type_.find(typeid(Type)); it == nodes_by_type_.end() ) { + // if the type was not found + auto node = repository_node::make_node(*this, name, []{ return std::make_unique(); }, std::forward(observers)...); + if (auto result = attach_node(node, parent); !result) { + return utils::failure(result.err()); + } + + node->on_attach(); + + foreign_node_completer::complete(node); + relation_completer::complete(node); + } else if (!has_node(name)) { + it->second->update_name(name); + nodes_by_name_[name] = it->second; + if (const auto i = nodes_by_name_.find(""); i != nodes_by_name_.end()) { + nodes_by_name_.erase(i); + } + relation_completer::complete(it->second); + log_.info("attach: update node name to '%s' (type: %s)", it->second->name().c_str(), it->second->type_index().name()); + } else { + return utils::failure(make_error(error_code::NodeAlreadyExists, "Node '" + name + "' already exists")); + } + + return utils::ok(); + } /** * Detaches a given node from the schema. If the diff --git a/include/matador/object/repository_node.hpp b/include/matador/object/repository_node.hpp index a2ecb3a..46b566d 100644 --- a/include/matador/object/repository_node.hpp +++ b/include/matador/object/repository_node.hpp @@ -20,8 +20,8 @@ public: template< typename Type> using creator_func = std::function()>; - template < typename Type > - static std::shared_ptr make_node(repository& repo, const std::string& name, creator_func creator = []{ return std::make_unique(); }) { + template < typename Type, typename... Observers > + static std::shared_ptr make_node(repository& repo, const std::string& name, creator_func creator, Observers&&... observers) { const std::type_index ti(typeid(Type)); auto node = std::shared_ptr(new repository_node(repo, name, ti)); @@ -31,6 +31,7 @@ public: std::move(obj), std::forward>(creator) ); + (info->register_observer(observer_ptr(std::unique_ptr(new Observers(std::forward(observers))))), ...); node->info_ = std::move(info); return node; @@ -63,6 +64,9 @@ public: [[nodiscard]] bool has_children() const; + void on_attach() const; + void on_detach() const; + private: explicit repository_node(repository& repo); repository_node(repository& repo, const std::type_index& ti); diff --git a/include/matador/query/schema.hpp b/include/matador/query/schema.hpp index ace8fff..117a5de 100644 --- a/include/matador/query/schema.hpp +++ b/include/matador/query/schema.hpp @@ -33,14 +33,14 @@ public: explicit schema(sql::connection_pool &pool); schema(sql::connection_pool &pool, const std::string &name); - template - [[nodiscard]] utils::result attach(const std::string &name, const std::string &parent = "") { - return repo_.attach(name, parent); + template class ObserverType> + [[nodiscard]] utils::result attach(const std::string &name, const std::string &parent = "", std::initializer_list*> observer = {}) { + return repo_.attach(name, parent, observer); } - template - [[nodiscard]] utils::result attach(const std::string &name) { - return repo_.attach(name); + template class ObserverType> + [[nodiscard]] utils::result attach(const std::string &name, std::initializer_list*> observer = {}) { + return repo_.attach(name, observer); } [[nodiscard]] utils::result create() const; diff --git a/source/core/CMakeLists.txt b/source/core/CMakeLists.txt index 386f137..34ff9bd 100644 --- a/source/core/CMakeLists.txt +++ b/source/core/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(matador-core STATIC ../../include/matador/object/object_info.hpp ../../include/matador/object/object_proxy.hpp ../../include/matador/object/object_ptr.hpp + ../../include/matador/object/observer.hpp ../../include/matador/object/primary_key_resolver.hpp ../../include/matador/object/relation_completer.hpp ../../include/matador/object/relation_endpoint.hpp @@ -83,6 +84,7 @@ add_library(matador-core STATIC object/internal/shadow_repository.cpp object/object.cpp object/object_generator.cpp + object/observer.cpp object/relation_endpoint.cpp object/repository.cpp object/repository_node.cpp diff --git a/source/core/object/observer.cpp b/source/core/object/observer.cpp new file mode 100644 index 0000000..b7c3a37 --- /dev/null +++ b/source/core/object/observer.cpp @@ -0,0 +1,10 @@ +#include "matador/object/observer.hpp" + +namespace matador::object { +abstract_observer::abstract_observer(const std::type_index& ti) +: type_index_(ti) {} + +const std::type_index& abstract_observer::index() const { + return type_index_; +} +} // namespace matador::object \ No newline at end of file diff --git a/source/core/object/repository_node.cpp b/source/core/object/repository_node.cpp index a9e9ff6..bf12725 100644 --- a/source/core/object/repository_node.cpp +++ b/source/core/object/repository_node.cpp @@ -55,7 +55,11 @@ const repository& repository_node::schema() const { bool repository_node::has_children() const { return first_child_->next_sibling_ != last_child_; } +void repository_node::on_attach() const { + info_->on_attach(); +} +void repository_node::on_detach() const {} repository_node::node_ptr repository_node::next() const { // if we have a child, child is the next iterator to return // (if we don't iterate over the siblings) diff --git a/test/core/object/RepositoryTest.cpp b/test/core/object/RepositoryTest.cpp index 9b9896d..1275930 100644 --- a/test/core/object/RepositoryTest.cpp +++ b/test/core/object/RepositoryTest.cpp @@ -4,6 +4,7 @@ #include "../../models/department.hpp" #include "../../models/recipe.hpp" +#include "../../models/person.hpp" struct node { }; @@ -105,14 +106,14 @@ TEST_CASE("Test one to many", "[relation][one-to-many]") { REQUIRE(repo.contains("departments")); REQUIRE(repo.contains("employees")); - auto info = repo.basic_info("departments")->get(); - REQUIRE(!info.endpoints_empty()); - REQUIRE(info.endpoints_size() == 1); - std::cout << *info.object(); - info = repo.basic_info("employees")->get(); - REQUIRE(!info.endpoints_empty()); - REQUIRE(info.endpoints_size() == 1); - std::cout << *info.object(); + auto result = repo.basic_info("departments"); + REQUIRE(!result->get().endpoints_empty()); + REQUIRE(result->get().endpoints_size() == 1); + std::cout << *result->get().object(); + result = repo.basic_info("employees"); + REQUIRE(!result->get().endpoints_empty()); + REQUIRE(result->get().endpoints_size() == 1); + std::cout << *result->get().object(); } TEST_CASE("Test one to many reverse", "[relation][one-to-many][reverse]") { @@ -127,14 +128,15 @@ TEST_CASE("Test one to many reverse", "[relation][one-to-many][reverse]") { REQUIRE(repo.contains("departments")); REQUIRE(repo.contains("employees")); - auto info = repo.basic_info("departments")->get(); - REQUIRE(!info.endpoints_empty()); - REQUIRE(info.endpoints_size() == 1); - std::cout << *info.object(); - info = repo.basic_info("employees")->get(); - REQUIRE(!info.endpoints_empty()); - REQUIRE(info.endpoints_size() == 1); - std::cout << *info.object(); + auto result = repo.basic_info("departments"); + REQUIRE(result.is_ok()); + REQUIRE(!result->get().endpoints_empty()); + REQUIRE(result->get().endpoints_size() == 1); + std::cout << *result->get().object(); + result = repo.basic_info("employees"); + REQUIRE(!result->get().endpoints_empty()); + REQUIRE(result->get().endpoints_size() == 1); + std::cout << *result->get().object(); } TEST_CASE("Test many to many relation", "[relation][many-to-many]") { @@ -142,9 +144,9 @@ TEST_CASE("Test many to many relation", "[relation][many-to-many]") { REQUIRE(repo.empty()); - auto result = repo.attach("recipes") + auto res = repo.attach("recipes") .and_then([&repo] { return repo.attach("ingredients"); }); - REQUIRE(result); + REQUIRE(res); REQUIRE(repo.size() == 3); REQUIRE(repo.contains("ingredients")); @@ -156,16 +158,44 @@ TEST_CASE("Test many to many relation", "[relation][many-to-many]") { std::cout << *repo.basic_info("recipe_ingredients")->get().object(); - auto info = repo.basic_info("ingredients")->get(); - REQUIRE(!info.endpoints_empty()); - REQUIRE(info.endpoints_size() == 1); - std::cout << *info.object(); - info = repo.basic_info("recipes")->get(); - REQUIRE(!info.endpoints_empty()); - REQUIRE(info.endpoints_size() == 1); - std::cout << *info.object(); - info = repo.basic_info("recipe_ingredients")->get(); - REQUIRE(!info.endpoints_empty()); - REQUIRE(info.endpoints_size() == 2); - std::cout << *info.object(); + auto result = repo.basic_info("ingredients"); + REQUIRE(!result->get().endpoints_empty()); + REQUIRE(result->get().endpoints_size() == 1); + std::cout << *result->get().object(); + result = repo.basic_info("recipes"); + REQUIRE(!result->get().endpoints_empty()); + REQUIRE(result->get().endpoints_size() == 1); + std::cout << *result->get().object(); + result = repo.basic_info("recipe_ingredients"); + REQUIRE(!result->get().endpoints_empty()); + REQUIRE(result->get().endpoints_size() == 2); + std::cout << *result->get().object(); } + +template +class test_observer : public matador::object::observer { +public: + void on_attach(object::repository_node& node, const Type& prototype) override { + int i = 0; + } + void on_detach(object::repository_node& node, const Type& prototype) override { + + } + void on_insert(Type& obj) override { + + } + void on_update(Type& obj) override { + + } + void on_delete(Type& obj) override { + + } +}; +TEST_CASE("Test repository observer", "[repository][observer]") { + object::repository repo; + + REQUIRE(repo.empty()); + + const auto result = repo.attach("person", test_observer()); + REQUIRE(result.is_ok()); +} \ No newline at end of file