Skip to content

Commit

Permalink
Merge pull request #115 from vexingcodes/issue/NF-74/poly
Browse files Browse the repository at this point in the history
Add ability to insert discrimination key for polymorphic adapter.
  • Loading branch information
tgockel authored Jun 8, 2018
2 parents 1176108 + 8d886aa commit cbdb48b
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 22 deletions.
34 changes: 30 additions & 4 deletions include/jsonv/serialization_builder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,27 @@ namespace jsonv
*
* \see enum_adapter
*
* \paragraph serialization_builder_dls_ref_formats_level_polymorphic_type polymorphic_type
*
* - <tt>polymorphic_type<&lt;TPointer&gt;(std::string discrimination_key);</tt>
*
* 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<std::unique_ptr<base>>("type")
* .subtype<derived_1>("derived_1")
* .subtype<derived_2>("derived_2", keyed_subtype_action::check)
* .subtype<derived_3>("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
*
* - <tt>extend(std::function&lt;void (formats_builder&amp;)&gt; func)</tt>
Expand Down Expand Up @@ -1128,18 +1149,23 @@ class polymorphic_adapter_builder :
}

template <typename TSub>
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<TSub>(_discrimination_key, std::move(discrimination_value));
return subtype<TSub>(_discrimination_key, std::move(discrimination_value), action);
}

template <typename TSub>
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<TSub>(std::move(discrimination_key), std::move(discrimination_value));
_adapter->template add_subtype_keyed<TSub>(std::move(discrimination_key),
std::move(discrimination_value),
action);
reference_type(std::type_index(typeid(TSub)), std::type_index(typeid(TPointer)));
return *this;
}
Expand Down
71 changes: 68 additions & 3 deletions include/jsonv/serialization_util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#define __JSONV_SERIALIZATION_UTIL_HPP_INCLUDED__

#include <jsonv/config.hpp>
#include <jsonv/demangle.hpp>
#include <jsonv/functional.hpp>
#include <jsonv/serialization.hpp>

Expand Down Expand Up @@ -517,6 +518,22 @@ class enum_adapter :
template <typename TEnum, typename FEnumComp = std::less<TEnum>>
using enum_adapter_icase = enum_adapter<TEnum, FEnumComp, value_less_icase>;

/**
* 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.
*
Expand Down Expand Up @@ -570,8 +587,14 @@ class polymorphic_adapter :
* \see add_subtype
**/
template <typename T>
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())
Expand Down Expand Up @@ -634,15 +657,57 @@ class polymorphic_adapter :
{
if (_check_null_output && !from)
return null;
else
return context.to_json(typeid(*from), static_cast<const void*>(&*from));

value serialized = context.to_json(typeid(*from), static_cast<const void*>(&*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<TPointer (const extraction_context&, const value&)>;

private:
using serialization_action = std::tuple<std::string, value, keyed_subtype_action>;

std::vector<std::pair<match_predicate, create_function>> _subtype_ctors;
std::map<std::type_index, serialization_action> _serialization_actions;
bool _check_null_input = false;
bool _check_null_output = false;
};
Expand Down
87 changes: 72 additions & 15 deletions src/jsonv-tests/serialization_builder_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -604,36 +604,93 @@ struct b_derived :
{
builder.member("type", &b_derived::x);
}


static void json_adapt_bad_value(adapter_builder<b_derived>& builder)
{
builder.member("type", &b_derived::bad);
}

static void json_adapt_no_value(adapter_builder<b_derived>&)
{
}

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<c_derived>&)
{
}

static void json_adapt_some_value(adapter_builder<c_derived>& builder)
{
builder.member("type", &c_derived::x);
}

std::string x = "c";
};

}

TEST(serialization_builder_polymorphic_direct)
{
formats fmts =
formats::compose
({
formats_builder()
.polymorphic_type<std::unique_ptr<base>>("type")
.subtype<a_derived>("a")
.subtype<b_derived>("b")
.type<a_derived>(a_derived::json_adapt)
.type<b_derived>(b_derived::json_adapt)
.register_container<std::vector<std::unique_ptr<base>>>()
.check_references(formats::defaults()),
formats::defaults()
});
auto make_fmts = [](std::function<void(adapter_builder<b_derived>&)> b_adapter,
std::function<void(adapter_builder<c_derived>&)> c_adapter)
{
return formats::compose
({
formats_builder()
.polymorphic_type<std::unique_ptr<base>>("type")
.subtype<a_derived>("a")
.subtype<b_derived>("b", keyed_subtype_action::check)
.subtype<c_derived>("c", keyed_subtype_action::insert)
.type<a_derived>(a_derived::json_adapt)
.type<b_derived>(b_adapter)
.type<c_derived>(c_adapter)
.register_container<std::vector<std::unique_ptr<base>>>()
.check_references(formats::defaults()),
formats::defaults()
});
};

auto make_bad_fmts = []()
{
formats_builder()
.polymorphic_type<std::unique_ptr<base>>("type")
.subtype<a_derived>("a")
.subtype<a_derived>("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<std::vector<std::unique_ptr<base>>>(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)
Expand Down

0 comments on commit cbdb48b

Please sign in to comment.