diff --git a/include/jsonv/serialization_builder.hpp b/include/jsonv/serialization_builder.hpp index 3fc9d53..516d36c 100644 --- a/include/jsonv/serialization_builder.hpp +++ b/include/jsonv/serialization_builder.hpp @@ -293,6 +293,27 @@ namespace jsonv * * \see enum_adapter * + * \paragraph serialization_builder_dls_ref_formats_level_polymorphic_type polymorphic_type + * + * - polymorphic_type<<TPointer>(std::string discrimination_key); + * + * Create an adapter for the \c TPointer type (usually \c std::shared_ptr or \c std::unique_ptr) that knows how to + * serialize and deserialize one or more types that can be polymorphically represented by \c TPointer, i.e. derived + * types. It uses a discrimination key to determine which concrete type should be instantiated when extracting values + * from json. + * + * \code + * .polymorphic_type>("type") + * .subtype("derived_1") + * .subtype("derived_2", keyed_subtype_action::check) + * .subtype("derived_3", keyed_subtype_action::insert); + * \endcode + * + * The \ref keyed_subtype_action can be used to configure the adapter to make sure that the discrimination key was + * correctly serialized (\ref keyed_subtype_action::check) or to insert the discrimination key for the underlying type + * so that the underlying type doesn't need to do that itself (\ref keyed_subtype_action::insert). The default is to do + * nothing (\ref keyed_subtype_action::none). + * * \paragraph serialization_builder_dsl_ref_formats_level_extend extend * * - extend(std::function<void (formats_builder&)> func) @@ -1128,18 +1149,23 @@ class polymorphic_adapter_builder : } template - polymorphic_adapter_builder& subtype(value discrimination_value) + polymorphic_adapter_builder& subtype(value discrimination_value, + keyed_subtype_action action = keyed_subtype_action::none) { if (_discrimination_key.empty()) throw std::logic_error("Cannot use single-argument subtype if no discrimination_key has been set"); - return subtype(_discrimination_key, std::move(discrimination_value)); + return subtype(_discrimination_key, std::move(discrimination_value), action); } template - polymorphic_adapter_builder& subtype(std::string discrimination_key, value discrimination_value) + polymorphic_adapter_builder& subtype(std::string discrimination_key, + value discrimination_value, + keyed_subtype_action action = keyed_subtype_action::none) { - _adapter->template add_subtype_keyed(std::move(discrimination_key), std::move(discrimination_value)); + _adapter->template add_subtype_keyed(std::move(discrimination_key), + std::move(discrimination_value), + action); reference_type(std::type_index(typeid(TSub)), std::type_index(typeid(TPointer))); return *this; } diff --git a/include/jsonv/serialization_util.hpp b/include/jsonv/serialization_util.hpp index 0b5180a..f8adaf2 100644 --- a/include/jsonv/serialization_util.hpp +++ b/include/jsonv/serialization_util.hpp @@ -14,6 +14,7 @@ #define __JSONV_SERIALIZATION_UTIL_HPP_INCLUDED__ #include +#include #include #include @@ -517,6 +518,22 @@ class enum_adapter : template > using enum_adapter_icase = enum_adapter; +/** + * What to do when serializing a keyed subtype of a \ref polymorphic_adapter. See \ref + * polymorphic_adapter::add_subtype_keyed. +**/ +enum class keyed_subtype_action : unsigned char +{ + /** Don't do any checking or insertion of the expected key/value pair. **/ + none, + /** Ensure the correct key/value pair was inserted by serialization. Throws \c std::runtime_error if it wasn't. **/ + check, + /** Insert the correct key/value pair as part of serialization. Throws \c std::runtime_error if the key is already + * present. + **/ + insert +}; + /** An adapter which can create polymorphic types. This allows you to parse JSON directly into a type heirarchy without * some middle layer. * @@ -570,8 +587,14 @@ class polymorphic_adapter : * \see add_subtype **/ template - void add_subtype_keyed(std::string key, value expected_value) + void add_subtype_keyed(std::string key, + value expected_value, + keyed_subtype_action action = keyed_subtype_action::none) { + std::type_index tidx = std::type_index(typeid(T)); + if (!_serialization_actions.emplace(tidx, std::make_tuple(key, expected_value, action)).second) + throw duplicate_type_error("polymorphic_adapter subtype", std::type_index(typeid(T))); + match_predicate op = [key, expected_value] (const extraction_context&, const value& value) { if (!value.is_object()) @@ -634,15 +657,57 @@ class polymorphic_adapter : { if (_check_null_output && !from) return null; - else - return context.to_json(typeid(*from), static_cast(&*from)); + + value serialized = context.to_json(typeid(*from), static_cast(&*from)); + + auto action_iter = _serialization_actions.find(std::type_index(typeid(*from))); + if (action_iter != _serialization_actions.end()) + { + auto errmsg = [&]() + { + return " polymorphic_adapter<" + demangle(typeid(TPointer).name()) + ">" + "subtype(" + demangle(typeid(*from).name()) + ")"; + }; + + const std::string& key = std::get<0>(action_iter->second); + const value& val = std::get<1>(action_iter->second); + const keyed_subtype_action& action = std::get<2>(action_iter->second); + + switch (action) + { + case keyed_subtype_action::none: + break; + case keyed_subtype_action::check: + if (!serialized.is_object()) + throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg()); + if (!serialized.count(key)) + throw std::runtime_error("Expected subtype key not found." + errmsg()); + if (serialized.at(key) != val) + throw std::runtime_error("Expected subtype key is not the expected value." + errmsg()); + break; + case keyed_subtype_action::insert: + if (!serialized.is_object()) + throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg()); + if (serialized.count(key)) + throw std::runtime_error("Subtype key already present when trying to insert." + errmsg()); + serialized[key] = val; + break; + default: + throw std::runtime_error("Unknown keyed_subtype_action."); + } + } + + return serialized; } private: using create_function = std::function; private: + using serialization_action = std::tuple; + std::vector> _subtype_ctors; + std::map _serialization_actions; bool _check_null_input = false; bool _check_null_output = false; }; diff --git a/src/jsonv-tests/serialization_builder_tests.cpp b/src/jsonv-tests/serialization_builder_tests.cpp index 8ee9024..c37e22e 100644 --- a/src/jsonv-tests/serialization_builder_tests.cpp +++ b/src/jsonv-tests/serialization_builder_tests.cpp @@ -604,36 +604,93 @@ struct b_derived : { builder.member("type", &b_derived::x); } - + + static void json_adapt_bad_value(adapter_builder& builder) + { + builder.member("type", &b_derived::bad); + } + + static void json_adapt_no_value(adapter_builder&) + { + } + std::string x = "b"; + std::string bad = "bad"; +}; + +struct c_derived : + base +{ + virtual std::string get() const override { return "c"; } + + static void json_adapt(adapter_builder&) + { + } + + static void json_adapt_some_value(adapter_builder& builder) + { + builder.member("type", &c_derived::x); + } + + std::string x = "c"; }; } TEST(serialization_builder_polymorphic_direct) { - formats fmts = - formats::compose - ({ - formats_builder() - .polymorphic_type>("type") - .subtype("a") - .subtype("b") - .type(a_derived::json_adapt) - .type(b_derived::json_adapt) - .register_container>>() - .check_references(formats::defaults()), - formats::defaults() - }); + auto make_fmts = [](std::function&)> b_adapter, + std::function&)> c_adapter) + { + return formats::compose + ({ + formats_builder() + .polymorphic_type>("type") + .subtype("a") + .subtype("b", keyed_subtype_action::check) + .subtype("c", keyed_subtype_action::insert) + .type(a_derived::json_adapt) + .type(b_adapter) + .type(c_adapter) + .register_container>>() + .check_references(formats::defaults()), + formats::defaults() + }); + }; + + auto make_bad_fmts = []() + { + formats_builder() + .polymorphic_type>("type") + .subtype("a") + .subtype("a"); + }; - value input = array({ object({{ "type", "a" }}), object({{ "type", "b" }}) }); + auto fmts = make_fmts(b_derived::json_adapt, c_derived::json_adapt); + value input = array({ object({{ "type", "a" }}), object({{ "type", "b" }}), object({{ "type", "c" }}) }); auto output = extract>>(input, fmts); ensure(output.at(0)->get() == "a"); ensure(output.at(1)->get() == "b"); + ensure(output.at(2)->get() == "c"); value encoded = to_json(output, fmts); ensure_eq(input, encoded); + + // If the b type serializes the wrong value for "type" we should get a runtime_error. + fmts = make_fmts(b_derived::json_adapt_bad_value, c_derived::json_adapt); + ensure_throws(std::runtime_error, to_json(output, fmts)); + + // If the b type does not add a "type" key we should get a runtime_error. + fmts = make_fmts(b_derived::json_adapt_no_value, c_derived::json_adapt); + ensure_throws(std::runtime_error, to_json(output, fmts)); + + // If the c type serializes "type" at all we should get an error, because we expected to insert it ourselves. + fmts = make_fmts(b_derived::json_adapt, c_derived::json_adapt_some_value); + ensure_throws(std::runtime_error, to_json(output, fmts)); + + // Attempting to register the same keyed subtype twice should result in a duplicate_type_error. + ensure_throws(duplicate_type_error, make_bad_fmts()); } TEST(serialization_builder_duplicate_type_actions)