Skip to content

Commit 8b6c161

Browse files
committed
Support converting hash-based containers even when transformation changes the hash
1 parent 68aad2c commit 8b6c161

File tree

3 files changed

+238
-2
lines changed

3 files changed

+238
-2
lines changed

immer/extra/archive/champ/champ.hpp

+29-2
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,19 @@ class hash_validation_failed_exception : public archive_exception
4545
}
4646
};
4747

48+
/**
49+
* incompatible_hash_mode:
50+
* When values are transformed in a way that changes how they are hashed, the
51+
* structure of the champ can't be preserved. The only solution is to recreate
52+
* the container from the values that it should contain.
53+
*
54+
* The mode can be enabled by returning incompatible_hash_wrapper from the
55+
* function that handles the target_container_type_request.
56+
*/
4857
template <class Container,
4958
typename Archive = container_archive_load<Container>,
50-
typename TransformF = boost::hana::id_t>
59+
typename TransformF = boost::hana::id_t,
60+
bool enable_incompatible_hash_mode = false>
5161
class container_loader
5262
{
5363
using champ_t = std::decay_t<decltype(std::declval<Container>().impl())>;
@@ -84,7 +94,23 @@ class container_loader
8494
throw invalid_node_id{root_id};
8595
}
8696

87-
auto [root, values] = nodes_.load_inner(root_id);
97+
auto [root, values] = nodes_.load_inner(root_id);
98+
99+
if constexpr (enable_incompatible_hash_mode) {
100+
if (auto* p = loaded_.find(root_id)) {
101+
return *p;
102+
}
103+
104+
auto result = Container{};
105+
for (const auto& items : values) {
106+
for (const auto& item : items) {
107+
result = std::move(result).insert(item);
108+
}
109+
}
110+
loaded_ = std::move(loaded_).set(root_id, result);
111+
return result;
112+
}
113+
88114
const auto items_count = [&values = values] {
89115
auto count = std::size_t{};
90116
for (const auto& items : values) {
@@ -129,6 +155,7 @@ class container_loader
129155
nodes_load,
130156
TransformF>
131157
nodes_;
158+
immer::map<node_id, Container> loaded_;
132159
};
133160

134161
template <class Container>

immer/extra/archive/champ/traits.hpp

+28
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,34 @@ auto transform_archive(const container_archive_load<Container>& ar, F&& func)
8282
.nodes = transform(ar.nodes, func),
8383
};
8484
}
85+
86+
/**
87+
* The wrapper is used to enable the incompatible_hash_mode, which is required
88+
* when the key of a hash-based container transformed in a way that changes its
89+
* hash.
90+
*/
91+
template <class Container>
92+
struct incompatible_hash_wrapper
93+
{};
8594
} // namespace champ
8695

96+
template <class Container>
97+
struct container_traits<champ::incompatible_hash_wrapper<Container>>
98+
: champ_traits<Container>
99+
{
100+
using base_t = champ_traits<Container>;
101+
102+
// Everything stays the same as for normal container, except that we tell
103+
// the loader to do something special.
104+
static constexpr bool enable_incompatible_hash_mode = true;
105+
106+
template <typename Archive = base_t::load_archive_t,
107+
typename TransformF = boost::hana::id_t>
108+
using loader_t =
109+
immer::archive::champ::container_loader<Container,
110+
Archive,
111+
TransformF,
112+
enable_incompatible_hash_mode>;
113+
};
114+
87115
} // namespace immer::archive

test/extra/archive/test_special_archive_auto.cpp

+181
Original file line numberDiff line numberDiff line change
@@ -819,3 +819,184 @@ TEST_CASE("Test table with a funny value no auto")
819819
json_str);
820820
REQUIRE(loaded == value);
821821
}
822+
823+
namespace {
824+
825+
struct int_key
826+
{
827+
BOOST_HANA_DEFINE_STRUCT(int_key, (int, id));
828+
};
829+
DEFINE_OPERATIONS(int_key);
830+
831+
struct string_key
832+
{
833+
BOOST_HANA_DEFINE_STRUCT(string_key, (std::string, id));
834+
};
835+
DEFINE_OPERATIONS(string_key);
836+
837+
struct test_champs
838+
{
839+
BOOST_HANA_DEFINE_STRUCT(test_champs,
840+
(immer::map<int, std::string>, map),
841+
(immer::table<int_key>, table),
842+
(immer::set<int>, set)
843+
844+
);
845+
};
846+
DEFINE_OPERATIONS(test_champs);
847+
848+
} // namespace
849+
850+
TEST_CASE("Structure breaks when hash is changed")
851+
{
852+
const auto value = test_champs{
853+
.map = {{123, "123"}, {456, "456"}},
854+
};
855+
856+
const auto names = immer::archive::get_archives_for_types(
857+
hana::tuple_t<test_champs>, hana::make_map());
858+
859+
const auto [json_str, ar] =
860+
immer::archive::to_json_with_auto_archive(value, names);
861+
// REQUIRE(json_str == "");
862+
863+
constexpr auto convert_pair = [](const std::pair<int, std::string>& old) {
864+
return std::make_pair(fmt::format("_{}_", old.first), old.second);
865+
};
866+
867+
const auto map = hana::make_map(hana::make_pair(
868+
hana::type_c<immer::map<int, std::string>>,
869+
hana::overload(convert_pair,
870+
[](immer::archive::target_container_type_request) {
871+
// We just return the desired new type, but the hash
872+
// of int is not compatible with the hash of string.
873+
return immer::map<std::string, std::string>{};
874+
}))
875+
876+
);
877+
878+
auto load_ar = immer::archive::transform_save_archive(ar, map);
879+
880+
REQUIRE_THROWS_AS(immer::archive::convert_container(ar, load_ar, value.map),
881+
immer::archive::champ::hash_validation_failed_exception);
882+
}
883+
884+
TEST_CASE("Converting between incompatible keys")
885+
{
886+
const auto value = test_champs{
887+
.map = {{123, "123"}, {456, "456"}},
888+
.table = {{901}, {902}},
889+
};
890+
891+
const auto names = immer::archive::get_archives_for_types(
892+
hana::tuple_t<test_champs>, hana::make_map());
893+
894+
const auto [json_str, ar] =
895+
immer::archive::to_json_with_auto_archive(value, names);
896+
// REQUIRE(json_str == "");
897+
898+
constexpr auto convert_pair = [](const std::pair<int, std::string>& old) {
899+
return std::make_pair(fmt::format("_{}_", old.first), old.second);
900+
};
901+
902+
constexpr auto convert_int_key = [](const int_key& old) {
903+
return string_key{fmt::format("x{}x", old.id)};
904+
};
905+
906+
/**
907+
* The problem is that the new key of the map has a completely different
908+
* hash from the old key, which makes the whole map structure unusable. We
909+
* need to have some special mode that essentially rebuilds the map. We will
910+
* lose all internal structural sharing but at least the same container_id
911+
* must return the same container (root node sharing).
912+
*/
913+
const auto map = hana::make_map(
914+
hana::make_pair(
915+
hana::type_c<immer::map<int, std::string>>,
916+
hana::overload(
917+
convert_pair,
918+
[](immer::archive::target_container_type_request) {
919+
return immer::archive::champ::incompatible_hash_wrapper<
920+
immer::map<std::string, std::string>>{};
921+
})),
922+
hana::make_pair(
923+
hana::type_c<immer::table<int_key>>,
924+
hana::overload(
925+
convert_int_key,
926+
[](immer::archive::target_container_type_request) {
927+
return immer::archive::champ::incompatible_hash_wrapper<
928+
immer::table<string_key>>{};
929+
})),
930+
hana::make_pair(
931+
hana::type_c<immer::set<int>>,
932+
hana::overload(
933+
[convert_int_key](int old) {
934+
return convert_int_key(int_key{old}).id;
935+
},
936+
[](immer::archive::target_container_type_request) {
937+
return immer::archive::champ::incompatible_hash_wrapper<
938+
immer::set<std::string>>{};
939+
}))
940+
941+
);
942+
943+
auto load_ar = immer::archive::transform_save_archive(ar, map);
944+
SECTION("maps")
945+
{
946+
constexpr auto convert_map = [convert_pair](const auto& map) {
947+
auto result = immer::map<std::string, std::string>{};
948+
for (const auto& item : map) {
949+
result = std::move(result).insert(convert_pair(item));
950+
}
951+
return result;
952+
};
953+
954+
const auto converted =
955+
immer::archive::convert_container(ar, load_ar, value.map);
956+
REQUIRE(converted == convert_map(value.map));
957+
958+
// Converting the same thing should return the same data
959+
const auto converted_2 =
960+
immer::archive::convert_container(ar, load_ar, value.map);
961+
REQUIRE(converted.identity() == converted_2.identity());
962+
}
963+
SECTION("tables")
964+
{
965+
constexpr auto convert_table = [convert_int_key](const auto& table) {
966+
auto result = immer::table<string_key>{};
967+
for (const auto& item : table) {
968+
result = std::move(result).insert(convert_int_key(item));
969+
}
970+
return result;
971+
};
972+
973+
const auto converted =
974+
immer::archive::convert_container(ar, load_ar, value.table);
975+
REQUIRE(converted == convert_table(value.table));
976+
977+
// Converting the same thing should return the same data
978+
const auto converted_2 =
979+
immer::archive::convert_container(ar, load_ar, value.table);
980+
REQUIRE(converted.impl().root == converted_2.impl().root);
981+
}
982+
SECTION("sets")
983+
{
984+
constexpr auto convert_set = [convert_int_key](const auto& set) {
985+
auto result = immer::set<std::string>{};
986+
for (const auto& item : set) {
987+
result =
988+
std::move(result).insert(convert_int_key(int_key{item}).id);
989+
}
990+
return result;
991+
};
992+
993+
const auto converted =
994+
immer::archive::convert_container(ar, load_ar, value.set);
995+
REQUIRE(converted == convert_set(value.set));
996+
997+
// Converting the same thing should return the same data
998+
const auto converted_2 =
999+
immer::archive::convert_container(ar, load_ar, value.set);
1000+
REQUIRE(converted.impl().root == converted_2.impl().root);
1001+
}
1002+
}

0 commit comments

Comments
 (0)