Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions phlex/model/data_product_concept.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#include "phlex/model/data_product_concept.hpp"

#include "boost/core/demangle.hpp"
#include "fmt/format.h"

#include <any>
#include <stdexcept>
#include <string>
#include <unordered_set>
Expand Down Expand Up @@ -41,4 +45,63 @@ namespace phlex::experimental {
return concrete_types_.contains(type);
}

void data_product_concept::add_translator(std::string translator_name,
concrete_product_id source,
concrete_product_id target,
std::any function,
result_storage storage)
{
if (source == target) {
throw std::invalid_argument{
fmt::format("Cannot register translator '{}' with concept '{}': the source and target "
"types are both '{}'.",
translator_name,
name_,
boost::core::demangle(source.name()))};
}

for (auto type : {source, target}) {
if (!has_concrete_type(type)) {
throw std::invalid_argument{
fmt::format("Cannot register translator '{}' with concept '{}': type '{}' is not a "
"concrete type of that concept.",
translator_name,
name_,
boost::core::demangle(type.name()))};
}
}

translator_key const key{source, target};
if (auto it = translators_.find(key); it != translators_.end()) {
throw std::invalid_argument{
fmt::format("Cannot register translator '{}' with concept '{}': translator '{}' is "
"already registered for the conversion from '{}' to '{}'.",
translator_name,
name_,
it->second.name,
boost::core::demangle(source.name()),
boost::core::demangle(target.name()))};
}

translators_.emplace(key,
translator_registration{.name = std::move(translator_name),
.function = std::move(function),
.storage = storage});
}

translator_registration const* data_product_concept::find_translator(
concrete_product_id source, concrete_product_id target) const
{
auto it = translators_.find(translator_key{source, target});
if (it == translators_.end()) {
return nullptr;
}
return &it->second;
}

bool data_product_concept::operator==(data_product_concept const& other) const
{
return name_ == other.name_ && concrete_types_ == other.concrete_types_;
}

} // namespace phlex::experimental
60 changes: 59 additions & 1 deletion phlex/model/data_product_concept.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,46 @@
#include "phlex/model/identifier.hpp"
#include "phlex/phlex_model_export.hpp"

#include <any>
#include <cstdint>
#include <map>
#include <string>
#include <typeindex>
#include <typeinfo>
#include <unordered_set>
#include <utility>
#include <vector>

namespace phlex::experimental {

// Type alias for std::type_index, representing a concrete data product type.
using concrete_product_id = std::type_index;

// Declares the relationship between the result of a conversion function and
// its storage.
//
// A conversion is 'owned' when its result owns its storage, and 'borrowed'
// when its result refers to storage owned by the input product. A borrowed
// result dangles if its source is released, so the caller that uses a
// borrowed conversion must keep the source alive. This cannot be deduced and
// must be declared at registration. This declaration is recorded but not
// enforced by this class.
enum class result_storage : std::uint8_t { owned, borrowed };

// A conversion function registered with a data product concept, together
// with the information a translator node needs in order to use it.
struct translator_registration {
// Name of the translator algorithm, as supplied at registration.
std::string name;

// The conversion function, type-erased. The stored type is not checked
// against the registered source and target types.
std::any function;

// Whether the conversion's result owns or borrows its storage.
result_storage storage;
};

// Represents a data product concept.
//
// A data product concept is a named category that groups related concrete
Expand All @@ -24,6 +53,10 @@ namespace phlex::experimental {
// Each data_product_concept has:
// - A unique name, which can not be modified
// - A set of concrete types that model this concept
// - A set of conversion functions between pairs of those concrete types
//
// add_translator checks the concrete-type and ordered-pair invariants for
// registrations made through it. It does not check the stored function's type.
class PHLEX_MODEL_EXPORT data_product_concept {
public:
explicit data_product_concept(std::string name);
Expand All @@ -46,13 +79,38 @@ namespace phlex::experimental {

bool has_concrete_type(concrete_product_id type) const;

bool operator==(data_product_concept const&) const = default;
// Register a conversion function from the source to the target concrete
// type, under the given translator name.
//
// Throws std::invalid_argument if:
// - source and target are the same type,
// - either type is not already a concrete type of this concept, or
// - a translator is already registered for that ordered pair.
void add_translator(std::string translator_name,
concrete_product_id source,
concrete_product_id target,
std::any function,
result_storage storage);

// Get the translator registered for an ordered pair of concrete types, or
// nullptr if there is none. The returned pointer is invalidated by the
// destruction of this concept.
translator_registration const* find_translator(concrete_product_id source,
concrete_product_id target) const;

// Compares the name and the concrete types. Registered translators do not
// participate: a std::any holding a conversion function is not
// equality-comparable.
bool operator==(data_product_concept const& other) const;

private:
using translator_key = std::pair<concrete_product_id, concrete_product_id>;

// The name must remain consistent with the registry key.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members)
std::string const name_;
std::unordered_set<concrete_product_id> concrete_types_;
std::map<translator_key, translator_registration> translators_;
};

} // namespace phlex::experimental
Expand Down
28 changes: 24 additions & 4 deletions test/data_product_concept_registry_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,40 @@ TEST_CASE("get_concept_for_type returns the associated concept", "[data_product_
TEST_CASE("get_concept_for_type returns nullptr for an unknown type",
"[data_product_concept_registry]")
{
data_product_concept_registry const registry;
CHECK(registry.get_concept_for_type(typeid(int)) == nullptr);
SECTION("empty registry")
{
data_product_concept_registry const registry;
CHECK(registry.get_concept_for_type(typeid(int)) == nullptr);
}

SECTION("registry whose concepts all lack the type")
{
data_product_concept_registry registry;
registry.add_concrete_type("concept_a", typeid(test_type_1));
registry.add_concrete_type("concept_b", typeid(test_type_2));

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 "
TEST_CASE("get_concept_for_type returns a matching concept 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));
// "concept_other" is a distractor: it does not contain test_type_1, so it must never be
// returned for a test_type_1 query. Without it, every concept in the registry would match,
// and an implementation that returned an arbitrary concept without checking the type would
// still pass. Which of "concept_a" and "concept_b" is returned is unspecified (it depends
// on the registry's iteration order), so the checks below do not assume either one.
registry.add_concrete_type("concept_other", typeid(test_type_2));

auto const* found = registry.get_concept_for_type(typeid(test_type_1));
CHECK(found != nullptr);
REQUIRE(found != nullptr);
CHECK(found->has_concrete_type(typeid(test_type_1)));
CHECK_FALSE(found->has_concrete_type(typeid(test_type_2)));
CHECK(found->name() != "concept_other");
}
Loading
Loading