diff --git a/phlex/model/CMakeLists.txt b/phlex/model/CMakeLists.txt index ebf8bb7a4..5bf9f047c 100644 --- a/phlex/model/CMakeLists.txt +++ b/phlex/model/CMakeLists.txt @@ -14,6 +14,8 @@ cet_make_library( layer_path.cpp product_store.cpp products.cpp + data_product_concept.cpp + data_product_concept_registry.cpp product_specification.cpp LIBRARIES PUBLIC @@ -45,6 +47,8 @@ install( product_store.hpp products.hpp type_id.hpp + data_product_concept.hpp + data_product_concept_registry.hpp DESTINATION include/phlex/model ) diff --git a/phlex/model/data_product_concept.cpp b/phlex/model/data_product_concept.cpp new file mode 100644 index 000000000..b45c4ebd7 --- /dev/null +++ b/phlex/model/data_product_concept.cpp @@ -0,0 +1,39 @@ +#include "phlex/model/data_product_concept.hpp" + +#include +#include + +namespace phlex::experimental { + + data_product_concept::data_product_concept(std::string name) : name_(std::move(name)) + { + if (name_.empty()) { + throw std::invalid_argument("Data product concept name cannot be empty"); + } + } + + std::string const& data_product_concept::name() const noexcept { return name_; } + + std::unordered_set const& data_product_concept::concrete_types() + const noexcept + { + return concrete_types_; + } + + void data_product_concept::add_concrete_type(concrete_product_id type) + { + concrete_types_.insert(type); + } + + void data_product_concept::add_concrete_types( + std::unordered_set const& types) + { + concrete_types_.insert(types.begin(), types.end()); + } + + bool data_product_concept::has_concrete_type(concrete_product_id type) const + { + return concrete_types_.find(type) != concrete_types_.end(); + } + +} // namespace phlex::experimental diff --git a/phlex/model/data_product_concept.hpp b/phlex/model/data_product_concept.hpp new file mode 100644 index 000000000..6132390dd --- /dev/null +++ b/phlex/model/data_product_concept.hpp @@ -0,0 +1,50 @@ +#ifndef PHLEX_MODEL_DATA_PRODUCT_CONCEPT_HPP +#define PHLEX_MODEL_DATA_PRODUCT_CONCEPT_HPP + +#include "phlex/model/identifier.hpp" +#include "phlex/phlex_model_export.hpp" + +#include +#include +#include +#include +#include + +namespace phlex::experimental { + + // Type alias for std::type_index, representing a concrete data product type. + using concrete_product_id = std::type_index; + + // Represents a data product concept. + // + // A data product concept is a named category that groups related concrete + // data product types. For example, "hit" could be a concept that encompasses + // multiple hit types (HitV1, HitV2, etc.). + // + // Each data_product_concept has: + // - A unique name + // - A set of concrete types that model this concept + class PHLEX_MODEL_EXPORT data_product_concept { + public: + explicit data_product_concept(std::string name); + + std::string const& name() const noexcept; + + std::unordered_set const& concrete_types() const noexcept; + + void add_concrete_type(concrete_product_id type); + + void add_concrete_types(std::unordered_set const& types); + + bool has_concrete_type(concrete_product_id type) const; + + bool operator==(data_product_concept const&) const = default; + + private: + std::string name_; + std::unordered_set concrete_types_; + }; + +} // namespace phlex::experimental + +#endif // PHLEX_MODEL_DATA_PRODUCT_CONCEPT_HPP diff --git a/phlex/model/data_product_concept_registry.cpp b/phlex/model/data_product_concept_registry.cpp new file mode 100644 index 000000000..8ac9410ca --- /dev/null +++ b/phlex/model/data_product_concept_registry.cpp @@ -0,0 +1,99 @@ +#include "phlex/model/data_product_concept_registry.hpp" + +#include +#include +#include +#include + +namespace phlex::experimental { + + bool data_product_concept_registry::register_concept(std::unique_ptr con) + { + if (!con) { + throw std::invalid_argument{"Cannot register null concept"}; + } + auto name = con->name(); // Copy the name before moving from con. + + std::unique_lock lock{mutex_}; + if (auto* existing = find_concept_unlocked(name)) { + existing->add_concrete_types(con->concrete_types()); + return false; + } + concepts_.emplace(std::move(name), std::move(con)); + return true; + } + + bool data_product_concept_registry::add_concrete_type(std::string const& name, + concrete_product_id type) + { + std::unique_lock lock{mutex_}; + if (auto* existing = find_concept_unlocked(name)) { + existing->add_concrete_type(type); + return false; + } + + auto con = std::make_unique(name); + con->add_concrete_type(type); + concepts_.emplace(name, std::move(con)); + return true; + } + + data_product_concept* data_product_concept_registry::find_concept_unlocked( + std::string const& name) const + { + auto it = concepts_.find(name); + if (it != concepts_.end()) { + return it->second.get(); + } + return nullptr; + } + + data_product_concept const* data_product_concept_registry::find_concept( + std::string const& name) const + { + std::shared_lock lock{mutex_}; + return find_concept_unlocked(name); + } + + data_product_concept* data_product_concept_registry::find_concept(std::string const& name) + { + std::shared_lock lock{mutex_}; + return find_concept_unlocked(name); + } + + std::vector data_product_concept_registry::all_concept_names() const + { + std::shared_lock lock{mutex_}; + std::vector names; + names.reserve(concepts_.size()); + for (auto const& [name, _] : concepts_) { + names.push_back(name); + } + std::sort(names.begin(), names.end()); + return names; + } + + bool data_product_concept_registry::type_matches_concept(std::string const& concept_name, + concrete_product_id type) const + { + std::shared_lock lock{mutex_}; + auto const* con = find_concept_unlocked(concept_name); + if (!con) { + return false; + } + return con->has_concrete_type(type); + } + + data_product_concept const* data_product_concept_registry::get_concept_for_type( + concrete_product_id type) const + { + std::shared_lock lock{mutex_}; + for (auto const& [name, con] : concepts_) { + if (con->has_concrete_type(type)) { + return con.get(); + } + } + return nullptr; + } + +} // namespace phlex::experimental diff --git a/phlex/model/data_product_concept_registry.hpp b/phlex/model/data_product_concept_registry.hpp new file mode 100644 index 000000000..9e61e429a --- /dev/null +++ b/phlex/model/data_product_concept_registry.hpp @@ -0,0 +1,92 @@ +#ifndef PHLEX_MODEL_DATA_PRODUCT_CONCEPT_REGISTRY_HPP +#define PHLEX_MODEL_DATA_PRODUCT_CONCEPT_REGISTRY_HPP + +#include "phlex/model/data_product_concept.hpp" +#include "phlex/phlex_model_export.hpp" + +#include +#include +#include +#include +#include + +namespace phlex::experimental { + + // Registry for data product concepts. + // + // This registry stores data_product_concept instances and allows + // lookup by name. Each concept can be associated with one or more + // concrete data product types. + // + // All public operations are individually safe for concurrent invocation: + // a registry-level std::shared_mutex protects the map and every access to + // a stored concept's concrete-type set that is made through the registry's + // own methods. This guarantee does not extend past the return of a call. + // In particular, the non-const find_concept overload and + // get_concept_for_type return a pointer to a stored concept; the caller + // must not dereference or mutate through that pointer concurrently with a + // registry write, or concurrently with another caller's mutation via a + // similarly obtained pointer, since such access is not covered by the + // registry's lock. + class PHLEX_MODEL_EXPORT data_product_concept_registry { + public: + data_product_concept_registry() = default; + ~data_product_concept_registry() = default; + + // Register a data product concept. + // + // Returns true if a new concept was inserted. Returns false if a + // concept with the same name was already registered, in which case + // the incoming concept's concrete types are merged into the existing + // concept's set and the incoming concept is discarded. + // + // Throws std::invalid_argument if con is null. + bool register_concept(std::unique_ptr con); + + // Register a concrete data product type with the concept of the given name. + // If necessary, a new concept is added to the registry. + // Return true if a new concept was registered. + bool add_concrete_type(std::string const& name, concrete_product_id type); + + // Find a data product concept by name. + // + // The returned pointer is not safe to dereference (or, for the + // non-const overload, to mutate through) concurrently with a registry + // write after this method returns. + data_product_concept const* find_concept(std::string const& name) const; + data_product_concept* find_concept(std::string const& name); + + // Get all registered concept names. + std::vector all_concept_names() const; + + // Check if a concrete type is associated with a concept. + bool type_matches_concept(std::string const& concept_name, concrete_product_id type) const; + + // Get the concept associated with a concrete type. + // + // The returned pointer is not safe to dereference concurrently with a + // registry write after this method returns. + data_product_concept const* get_concept_for_type(concrete_product_id type) const; + + // data_product_concept_registry objects can be neither copied nor moved. + data_product_concept_registry(data_product_concept_registry const&) = delete; + data_product_concept_registry& operator=(data_product_concept_registry const&) = delete; + data_product_concept_registry(data_product_concept_registry&&) noexcept = delete; + data_product_concept_registry& operator=(data_product_concept_registry&&) noexcept = delete; + + private: + // Assumes the caller already holds mutex_ (shared or exclusive). + // Performs a plain, unlocked lookup. This overload is const yet returns + // a non-const pointer (legal, since the pointee is reached through + // std::unique_ptr const&::get()); a single helper serves both the + // const and non-const find_concept overloads, so the const overload's + // constness is nominal only as far as the pointee is concerned. + data_product_concept* find_concept_unlocked(std::string const& name) const; + + mutable std::shared_mutex mutex_; + std::unordered_map> concepts_; + }; + +} // namespace phlex::experimental + +#endif // PHLEX_MODEL_DATA_PRODUCT_CONCEPT_REGISTRY_HPP diff --git a/phlex/model/fwd.hpp b/phlex/model/fwd.hpp index ce525466c..fe69994b7 100644 --- a/phlex/model/fwd.hpp +++ b/phlex/model/fwd.hpp @@ -29,6 +29,8 @@ namespace phlex::detail { } namespace phlex::experimental { + class data_product_concept; + class data_product_concept_registry; class product_store; using product_store_const_ptr = std::shared_ptr; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c38165f23..dbadac99b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -168,6 +168,23 @@ cet_test( phlex::model_internal phlex::utilities_internal ) +cet_test( + data_product_concept + USE_CATCH2_MAIN + SOURCE + data_product_concept_test.cpp + LIBRARIES + phlex::model +) +cet_test( + data_product_concept_registry + USE_CATCH2_MAIN + SOURCE + data_product_concept_registry_test.cpp + LIBRARIES + phlex::model + TBB::tbb +) cet_test(data_cell_index USE_CATCH2_MAIN SOURCE data_cell_index_test.cpp LIBRARIES phlex::model_internal ) diff --git a/test/data_product_concept_registry_test.cpp b/test/data_product_concept_registry_test.cpp new file mode 100644 index 000000000..b539945c0 --- /dev/null +++ b/test/data_product_concept_registry_test.cpp @@ -0,0 +1,287 @@ +#include "catch2/catch_test_macros.hpp" +#include "phlex/model/data_product_concept_registry.hpp" + +#include +#include +#include +#include + +using namespace phlex::experimental; + +namespace { + struct test_type_1 { + int value; + auto operator<=>(test_type_1 const&) const = default; + }; + + struct test_type_2 { + std::string value; + auto operator<=>(test_type_2 const&) const = default; + }; +} + +TEST_CASE("newly constructed registry has no concept names", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + CHECK(registry.all_concept_names().empty()); +} + +TEST_CASE("register_concept registers a new concept", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + auto con = std::make_unique("test_concept"); + CHECK(registry.register_concept(std::move(con)) == true); + + auto const* found = registry.find_concept("test_concept"); + CHECK(found != nullptr); + CHECK(found->name() == "test_concept"); +} + +TEST_CASE("register_concept preserves the concrete types of a pre-populated concept", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + auto con = std::make_unique("populated_concept"); + con->add_concrete_type(typeid(test_type_1)); + con->add_concrete_type(typeid(test_type_2)); + + CHECK(registry.register_concept(std::move(con)) == true); + + auto const* found = registry.find_concept("populated_concept"); + CHECK(found != nullptr); + CHECK(found->has_concrete_type(typeid(test_type_1))); + CHECK(found->has_concrete_type(typeid(test_type_2))); +} + +TEST_CASE("register_concept(nullptr) throws std::invalid_argument", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + CHECK_THROWS_AS(registry.register_concept(nullptr), std::invalid_argument); +} + +TEST_CASE("register_concept merges concrete types on duplicate name", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + auto con1 = std::make_unique("duplicate"); + con1->add_concrete_type(typeid(test_type_1)); + + auto con2 = std::make_unique("duplicate"); + con2->add_concrete_type(typeid(test_type_1)); + con2->add_concrete_type(typeid(test_type_2)); + + CHECK(registry.register_concept(std::move(con1)) == true); + CHECK(registry.register_concept(std::move(con2)) == false); + + auto const names = registry.all_concept_names(); + CHECK(names.size() == 1ull); + CHECK(names[0] == "duplicate"); + + CHECK(registry.type_matches_concept("duplicate", typeid(test_type_1))); + CHECK(registry.type_matches_concept("duplicate", typeid(test_type_2))); + + auto const* found = registry.find_concept("duplicate"); + CHECK(found != nullptr); + CHECK(found->concrete_types().size() == 2ull); +} + +TEST_CASE("add_concrete_type adds a type to an empty registry", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + CHECK(registry.add_concrete_type("test_concept", typeid(test_type_1)) == true); + + auto const* found = registry.find_concept("test_concept"); + CHECK(found != nullptr); + CHECK(found->has_concrete_type(typeid(test_type_1))); +} + +TEST_CASE("add_concrete_type adds a type to an existing concept", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + CHECK(registry.add_concrete_type("test_concept", typeid(test_type_1)) == true); + CHECK(registry.add_concrete_type("test_concept", typeid(test_type_2)) == false); + + auto const* found = registry.find_concept("test_concept"); + CHECK(found != nullptr); + CHECK(found->has_concrete_type(typeid(test_type_1))); + CHECK(found->has_concrete_type(typeid(test_type_2))); +} + +TEST_CASE("add_concrete_type associates one concrete type with multiple concepts", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + registry.add_concrete_type("concept_a", typeid(test_type_1)); + registry.add_concrete_type("concept_b", typeid(test_type_1)); + + CHECK(registry.type_matches_concept("concept_a", typeid(test_type_1))); + CHECK(registry.type_matches_concept("concept_b", typeid(test_type_1))); +} + +TEST_CASE("add_concrete_type is idempotent for a duplicate type", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + CHECK(registry.add_concrete_type("test_concept", typeid(test_type_1)) == true); + CHECK(registry.add_concrete_type("test_concept", typeid(test_type_1)) == false); + + auto const* found = registry.find_concept("test_concept"); + CHECK(found != nullptr); + CHECK(found->concrete_types().size() == 1ull); +} + +TEST_CASE("add_concrete_type and register_concept produce consistent registry state", + "[data_product_concept_registry]") +{ + data_product_concept_registry via_add_concrete_type; + via_add_concrete_type.add_concrete_type("consistent_concept", typeid(test_type_1)); + via_add_concrete_type.add_concrete_type("consistent_concept", typeid(test_type_2)); + + data_product_concept_registry via_register_concept; + auto con = std::make_unique("consistent_concept"); + con->add_concrete_type(typeid(test_type_1)); + con->add_concrete_type(typeid(test_type_2)); + via_register_concept.register_concept(std::move(con)); + + for (auto const* registry : {&via_add_concrete_type, &via_register_concept}) { + auto const names = registry->all_concept_names(); + CHECK(names.size() == 1ull); + CHECK(names[0] == "consistent_concept"); + + CHECK(registry->type_matches_concept("consistent_concept", typeid(test_type_1))); + CHECK(registry->type_matches_concept("consistent_concept", typeid(test_type_2))); + CHECK_FALSE(registry->type_matches_concept("consistent_concept", typeid(int))); + } +} + +TEST_CASE("find_concept (non-const) finds a registered concept", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + registry.register_concept(std::make_unique("test_concept")); + + data_product_concept* found = registry.find_concept("test_concept"); + CHECK(found != nullptr); + CHECK(found->name() == "test_concept"); +} + +TEST_CASE("find_concept (const) finds a registered concept", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + registry.register_concept(std::make_unique("test_concept")); + + data_product_concept const* found = std::as_const(registry).find_concept("test_concept"); + CHECK(found != nullptr); + CHECK(found->name() == "test_concept"); +} + +TEST_CASE("find_concept (non-const) returns nullptr for an unknown name", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + CHECK(registry.find_concept("nonexistent") == nullptr); +} + +TEST_CASE("find_concept (const) returns nullptr for an unknown name", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + CHECK(std::as_const(registry).find_concept("nonexistent") == nullptr); +} + +TEST_CASE("all_concept_names returns every name exactly once in sorted order", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + registry.register_concept(std::make_unique("concept_c")); + registry.register_concept(std::make_unique("concept_a")); + registry.register_concept(std::make_unique("concept_b")); + + auto const names = registry.all_concept_names(); + CHECK(names.size() == 3ull); + CHECK(names[0] == "concept_a"); + CHECK(names[1] == "concept_b"); + CHECK(names[2] == "concept_c"); +} + +TEST_CASE("type_matches_concept is true for an associated type", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + auto con = std::make_unique("numeric"); + con->add_concrete_type(typeid(int)); + registry.register_concept(std::move(con)); + + CHECK(registry.type_matches_concept("numeric", typeid(int)) == true); +} + +TEST_CASE("type_matches_concept is false for an unassociated type", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + auto con = std::make_unique("numeric"); + con->add_concrete_type(typeid(int)); + registry.register_concept(std::move(con)); + + CHECK(registry.type_matches_concept("numeric", typeid(double)) == false); +} + +TEST_CASE("type_matches_concept is false for an unknown concept", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + CHECK(registry.type_matches_concept("nonexistent", typeid(int)) == false); +} + +TEST_CASE("type_matches_concept is true under both names for a shared type", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + registry.add_concrete_type("concept_a", typeid(test_type_1)); + registry.add_concrete_type("concept_b", typeid(test_type_1)); + + CHECK(registry.type_matches_concept("concept_a", typeid(test_type_1))); + CHECK(registry.type_matches_concept("concept_b", typeid(test_type_1))); +} + +TEST_CASE("get_concept_for_type returns the associated concept", "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + auto con = std::make_unique("type_a"); + con->add_concrete_type(typeid(test_type_1)); + registry.register_concept(std::move(con)); + + auto const* found = registry.get_concept_for_type(typeid(test_type_1)); + CHECK(found != nullptr); + CHECK(found->name() == "type_a"); +} + +TEST_CASE("get_concept_for_type returns nullptr for an unknown type", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + CHECK(registry.get_concept_for_type(typeid(int)) == nullptr); +} + +TEST_CASE("get_concept_for_type returns a non-null match when a type belongs to multiple " + "concepts", + "[data_product_concept_registry]") +{ + data_product_concept_registry registry; + + registry.add_concrete_type("concept_a", typeid(test_type_1)); + registry.add_concrete_type("concept_b", typeid(test_type_1)); + + auto const* found = registry.get_concept_for_type(typeid(test_type_1)); + CHECK(found != nullptr); + CHECK(found->has_concrete_type(typeid(test_type_1))); +} diff --git a/test/data_product_concept_test.cpp b/test/data_product_concept_test.cpp new file mode 100644 index 000000000..4ccc79d68 --- /dev/null +++ b/test/data_product_concept_test.cpp @@ -0,0 +1,102 @@ +#include "catch2/catch_test_macros.hpp" +#include "phlex/model/data_product_concept.hpp" + +#include +#include +#include + +using namespace phlex::experimental; + +namespace { + struct test_type_1 { + int value; + auto operator<=>(test_type_1 const&) const = default; + }; + + struct test_type_2 { + std::string value; + auto operator<=>(test_type_2 const&) const = default; + }; +} + +TEST_CASE("data_product_concept construction records the right name", "[data_product_concept]") +{ + data_product_concept con{"my_concept"}; + CHECK(con.name() == "my_concept"); +} + +TEST_CASE("data_product_concept starts with no concrete types", "[data_product_concept]") +{ + data_product_concept con{"empty_concept"}; + CHECK(con.concrete_types().empty()); +} + +TEST_CASE("data_product_concept add and check concrete types", "[data_product_concept]") +{ + data_product_concept con{"test_concept"}; + + auto const& type1 = typeid(test_type_1); + auto const& type2 = typeid(test_type_2); + + con.add_concrete_type(type1); + con.add_concrete_type(type2); + + CHECK(con.concrete_types().size() == 2ull); + CHECK(con.has_concrete_type(type1)); + CHECK(con.has_concrete_type(type2)); + CHECK_FALSE(con.has_concrete_type(typeid(int))); +} + +TEST_CASE("data_product_concept equality", "[data_product_concept]") +{ + data_product_concept concept1{"same_name"}; + data_product_concept concept2{"same_name"}; + data_product_concept concept3{"different_name"}; + + CHECK(concept1 == concept2); + CHECK(concept1 != concept3); +} + +TEST_CASE("data_product_concept with multiple primitive types", "[data_product_concept]") +{ + data_product_concept con{"shared_concept"}; + + auto const& type1 = typeid(int); + auto const& type2 = typeid(long); + + con.add_concrete_type(type1); + con.add_concrete_type(type2); + + CHECK(con.has_concrete_type(type1)); + CHECK(con.has_concrete_type(type2)); +} + +TEST_CASE("adding a concrete type is idempotent", "[data_product_concept]") +{ + data_product_concept con{"concept_A"}; + + auto const& type1 = typeid(test_type_1); + + con.add_concrete_type(type1); + con.add_concrete_type(type1); // Add again + + CHECK(con.concrete_types().size() == 1ull); + CHECK(con.has_concrete_type(type1)); +} + +TEST_CASE("data_product_concept construction with an empty name throws", "[data_product_concept]") +{ + CHECK_THROWS_AS(data_product_concept{""}, std::invalid_argument); +} + +TEST_CASE("add_concrete_types adds the union of an overlapping set", "[data_product_concept]") +{ + data_product_concept con{"concept_A"}; + con.add_concrete_type(typeid(test_type_1)); + + con.add_concrete_types({typeid(test_type_1), typeid(test_type_2)}); + + CHECK(con.concrete_types().size() == 2ull); + CHECK(con.has_concrete_type(typeid(test_type_1))); + CHECK(con.has_concrete_type(typeid(test_type_2))); +}