diff --git a/phlex/configuration.cpp b/phlex/configuration.cpp index 3be6f3906..a775f2759 100644 --- a/phlex/configuration.cpp +++ b/phlex/configuration.cpp @@ -1,5 +1,6 @@ #include "phlex/configuration.hpp" #include "phlex/core/product_query.hpp" +#include "phlex/model/identifier.hpp" #include "phlex/model/product_specification.hpp" #include @@ -7,8 +8,8 @@ #include #include -namespace { - [[maybe_unused]] std::optional value_if_exists( +namespace phlex::detail { + std::optional value_if_exists( boost::json::object const& obj, // will be used later for new product_query std::string_view parameter) { @@ -16,41 +17,7 @@ namespace { return std::nullopt; } auto const& val = obj.at(parameter); - if (!val.is_string()) { - std::string_view kind; - switch (val.kind()) { - case boost::json::kind::null: - // seems reasonable to interpret this as if the value were not provided - return std::nullopt; - break; - case boost::json::kind::bool_: - kind = "bool"; - break; - case boost::json::kind::int64: - kind = "std::int64_t"; - break; - case boost::json::kind::uint64: - kind = "std::uint64_t"; - break; - case boost::json::kind::double_: - kind = "double"; - break; - case boost::json::kind::array: - kind = "array"; - break; - case boost::json::kind::object: - kind = "object"; - break; - default: - // std::unreachable(); - break; - } - throw std::runtime_error( - fmt::format("Error retrieving parameter '{}'. Should be a string but is instead a {}", - parameter, - kind)); - } - return boost::json::value_to(val); + return boost::json::value_to(val); } } @@ -79,4 +46,10 @@ namespace phlex { auto layer = value_decorate_exception(query_object, "layer"); return product_query{experimental::product_specification::create(product), layer}; } + + experimental::identifier experimental::tag_invoke(boost::json::value_to_tag const&, + boost::json::value const& jv) + { + return identifier{std::string_view(jv.as_string())}; + } } diff --git a/phlex/configuration.hpp b/phlex/configuration.hpp index 82ef7aa34..7dcf3945d 100644 --- a/phlex/configuration.hpp +++ b/phlex/configuration.hpp @@ -3,6 +3,7 @@ #include "boost/json.hpp" #include "phlex/core/product_query.hpp" +#include "phlex/model/identifier.hpp" #include #include @@ -20,6 +21,10 @@ namespace phlex { throw std::runtime_error("Error retrieving parameter '" + key + "':\n" + e.what()); } + // Used later for product_query + std::optional value_if_exists(boost::json::object const& obj, + std::string_view parameter); + // helper for unpacking json array template std::array unpack_json_array(boost::json::array const& array, @@ -90,6 +95,11 @@ namespace phlex { product_query tag_invoke(boost::json::value_to_tag const&, boost::json::value const& jv); + namespace experimental { + identifier tag_invoke(boost::json::value_to_tag const&, + boost::json::value const& jv); + } + template std::array tag_invoke( boost::json::value_to_tag> const&, boost::json::value const& jv) diff --git a/phlex/model/CMakeLists.txt b/phlex/model/CMakeLists.txt index 0c4ae6798..256bae49f 100644 --- a/phlex/model/CMakeLists.txt +++ b/phlex/model/CMakeLists.txt @@ -7,6 +7,7 @@ cet_make_library( data_cell_counter.cpp data_layer_hierarchy.cpp data_cell_index.cpp + identifier.cpp product_matcher.cpp product_store.cpp products.cpp @@ -30,6 +31,7 @@ install( data_cell_counter.hpp data_layer_hierarchy.hpp data_cell_index.hpp + identifier.hpp product_matcher.hpp product_specification.hpp product_store.hpp diff --git a/phlex/model/identifier.cpp b/phlex/model/identifier.cpp new file mode 100644 index 000000000..1d1c2b2ff --- /dev/null +++ b/phlex/model/identifier.cpp @@ -0,0 +1,50 @@ +#include "identifier.hpp" + +#include +#include + +namespace phlex::experimental { + identifier_query literals::operator""_idq(char const* lit, std::size_t len) + { + return {identifier::hash_string(std::string_view(lit, len))}; + } + + std::uint64_t identifier::hash_string(std::string_view str) + { + // Hash quality is very important here, since comparisons are done using only the hash + using namespace boost::hash2; + xxhash_64 h; + hash_append(h, {}, str); + return h.result(); + } + + identifier::identifier(std::string_view str) : content_(str), hash_(hash_string(content_)) {} + + identifier::operator std::string_view() const noexcept { return std::string_view(content_); } + bool identifier::operator==(identifier const& rhs) const noexcept + { + if (hash_ == rhs.hash_) { + return content_ == rhs.content_; + } + return false; + } + std::strong_ordering identifier::operator<=>(identifier const& rhs) const noexcept + { + std::strong_ordering hash_cmp = hash_ <=> rhs.hash_; + if (hash_cmp == 0) { + return content_ <=> rhs.content_; + } + return hash_cmp; + } + + bool operator==(identifier const& lhs, identifier_query rhs) { return lhs.hash_ == rhs.hash; } + std::strong_ordering operator<=>(identifier const& lhs, identifier_query rhs) + { + return lhs.hash_ <=> rhs.hash; + } + + identifier literals::operator""_id(char const* lit, std::size_t len) + { + return identifier{std::string_view(lit, len)}; + } +} diff --git a/phlex/model/identifier.hpp b/phlex/model/identifier.hpp new file mode 100644 index 000000000..b9ca23d73 --- /dev/null +++ b/phlex/model/identifier.hpp @@ -0,0 +1,68 @@ +#ifndef PHLEX_MODEL_IDENTIFIER_H_ +#define PHLEX_MODEL_IDENTIFIER_H_ + +#include + +#include + +#include +#include +#include + +namespace phlex::experimental { + /// If you're comparing to an identifier you know at compile time, you're probably not going to need + /// to print it. + struct identifier_query { + std::uint64_t hash; + }; + + /// Carries around the string itself (as a shared_ptr to string to make copies lighter) + /// along with a precomputed hash used for all comparisons + class identifier { + public: + static std::uint64_t hash_string(std::string_view str); + identifier(identifier const& other) = default; + identifier(identifier&& other) noexcept = default; + + explicit identifier(std::string_view str); + + identifier& operator=(identifier const& rhs) = default; + identifier& operator=(identifier&& rhs) noexcept = default; + + ~identifier() = default; + + // Conversion to std::string_view + explicit operator std::string_view() const noexcept; + + bool operator==(identifier const& rhs) const noexcept; + std::strong_ordering operator<=>(identifier const& rhs) const noexcept; + + // Comparison operators with _id queries + friend bool operator==(identifier const& lhs, identifier_query rhs); + friend std::strong_ordering operator<=>(identifier const& lhs, identifier_query rhs); + friend std::hash; + + private: + std::string content_; + std::uint64_t hash_; + }; + + // Identifier UDL + namespace literals { + identifier operator""_id(char const* lit, std::size_t len); + identifier_query operator""_idq(char const* lit, std::size_t len); + } + + // Really trying to avoid the extra function call here + inline std::string_view format_as(identifier const& id) { return std::string_view(id); } +} + +template <> +struct std::hash { + std::size_t operator()(phlex::experimental::identifier const& id) const noexcept + { + return id.hash_; + } +}; + +#endif // PHLEX_MODEL_IDENTIFIER_H_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ab51b55f9..40f016dff 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,6 +23,7 @@ cet_test(type_deduction SOURCE type_deduction.cpp LIBRARIES phlex::metaprogramming ) cet_test(type_id SOURCE type_id.cpp LIBRARIES phlex::model fmt::fmt) +cet_test(identifier SOURCE identifier.cpp LIBRARIES phlex::model phlex::configuration fmt::fmt) cet_test( yielding_driver SOURCE diff --git a/test/identifier.cpp b/test/identifier.cpp new file mode 100644 index 000000000..c61375601 --- /dev/null +++ b/test/identifier.cpp @@ -0,0 +1,69 @@ +#include "phlex/model/identifier.hpp" +#include + +#include +#include +#include +#include + +#include +#include + +int main() +{ + using namespace phlex::experimental; + using namespace phlex::experimental::literals; + using namespace std::string_view_literals; + identifier a = "a"_id; + identifier a_copy = a; + identifier a2 = "a"_id; + identifier b = "b"_id; + + boost::json::object parsed_json = boost::json::parse(R"( {"identifier": "b" } )").as_object(); + auto b_from_json = phlex::detail::value_if_exists(parsed_json, "identifier"); + assert(b_from_json); + + fmt::print("a ({}) == \"a\"_idq: {}\n", a, a == "a"_idq); + fmt::print("a == a_copy ({}): {}\n", a_copy, a == a_copy); + fmt::print("a == a2 ({}): {}\n", a2, a == a2); + fmt::print("a != b ({}): {}\n", b, a != b); + fmt::print("b == *b_from_json ({}): {}\n", *b_from_json, b == *b_from_json); + + assert(a == "a"_idq); + assert(a == a_copy); + assert(a == a2); + assert(a != b); + assert(b == *b_from_json); + + // reassigning + a = "new a"_id; + a_copy = a; + + std::array strings{ + "a"sv, "b"sv, "c"sv, "d"sv, "e"sv, "long-id-1"sv, "long-id-2"sv, "test"sv, "other_test"sv}; + std::array identifiers{"a"_id, + "b"_id, + "c"_id, + "d"_id, + "e"_id, + "long-id-1"_id, + "long-id-2"_id, + "test"_id, + "other_test"_id}; + std::ranges::sort(identifiers); + std::ranges::sort(strings); + fmt::print("Sorted identifiers (should not be in alphabetical order): \n"); + for (auto const& id : identifiers) { + fmt::print("- {}\n", id); + } + + bool ok = false; + for (std::size_t i = 0; i < identifiers.size(); ++i) { + if (strings.at(i) != std::string_view(identifiers.at(i))) { + ok = true; + break; + } + } + assert(ok); + return 0; +}