Где-то пять лет назад мы сделали небольшую библиотеку-обертку над RapidJSON: json-dto. Целью было снизить количество писанины при работе RapidJSON. Вроде бы у нас получилось:
#include <json_dto/pub.hpp> #include <deque> #include <set> #include <map> struct my_message { std::deque<int> ids_; std::set<std::string> tags_; std::map<std::string, some_another_type> props_; ... template<typename Json_Io> void json_io(Json_Io & io) { io & json_dto::mandatory("ids", ids_) & json_dto::mandatory("tags", tags_) & json_dto::mandatory("properties", props_) ... ; } }; |
Библиотека открытая, но про нее мало кто знает. Тем удивительнее, ей пользуется еще кто-то кроме нас. По крайней до сих пор приходят запросы на добавление той или иной фичи в json-dto.
На прошлой неделе возник один такой запрос. Достаточно необычный. В очередной раз доказывающий, что если библиотека используется, то рано или поздно она начнет использоваться так, как ты даже и не подозревал.
Человек захотел, чтобы можно было описывать поля объекта, которые подлежат только сериализации. Т.е. они участвуют при записи в JSON, а вот при десериализации из JSON их значения должны игнорироваться. Автор этого запроса даже сделал PR с реализацией фичи, которую он хотел получить в json-dto. Однако, предложенный вариант мне не понравился.
Данный пост посвящен рассказу о том, почему же PR не был принят в json-dto, что захотелось получить мне и что в итоге удалось придумать.
Что хотелось иметь пользователю?
Нужно иметь возможность при перечислении подлежащих сериализации/десериализации полей указать, что определенное поле должно только сериализоваться, но не должно десериализоваться. Т.е., чтобы можно было указать что-то вроде:
struct demo { int version_; int payload_; ... template<typename Json_Io> void json_io(Json_Io & io) { io & json_dto::serialize_only("version", version_) & json_dto::mandatory("payload", payload_) ; } }; |
При сериализации значения объекта типа demo в результирующий JSON должны были попасть и поле demo::version_, и поле demo::payload_. А вот при десериализации из JSON должно было быть прочитано только значение поля demo::payload_, а значение "version" (если таковое присутствует в JSON) должно было быть проигнорировано.
Более того, в процессе общения выяснилось, что эти самые serialize-only-атрибуты представлены не обычными полями структуры/класса, а возвращаемыми значениями методов класса. Что-то вроде:
struct demo { int version() const { return 1; } int payload_; ... template<typename Json_Io> void json_io(Json_Io & io) { io & json_dto::serialize_only("version", version()) & json_dto::mandatory("payload", payload_) ; } }; |
Что пользователь предлагал сделать и почему мне это не понравилось?
Решение, которое в виде PR предложил сам пользователь, состояло в том, чтобы расширить json-dto еще парой функций для создания binder-ов для полей объекта (о binder-ах будет сказано ниже). Т.е., к уже имеющимся mandatory, optional, optional_null и optional_no_default добавился бы еще serialize_only.
Этот новый serialize_only работал бы по простой схеме: при сериализации преобразовывал значение поля в JSON-формат. При десериализации бы вообще ничего не делал. Т.е. даже не проверял бы наличие и формат поля в JSON-е.
Почему PR не прошел?
В предложенном варианте мне не понравилось, как минимум, две вещи.
Первое, и самое простое, -- это отсутствие симметрии. Т.е., если есть serialize_only, то логично было бы добавить еще и deserialize_only. Ведь вполне могут быть случаи, когда мы хотим прочитать значения из JSON-а, но не хотим их затем записывать. Например, это может потребоваться когда мы конвертируем какие-то данные из старого представления в новое. Скажем, был у нас JSON, в котором было поле version. А нужно сделать другой JSON, в котором поля version уже нет.
Следовательно, кроме новой функции serialize_only нужно ввести еще и deserialize_only. Что еще больше увеличивает набор функций для создания binder-ов: mandatory, optional, optional_null, optional_no_default, serialize_only и deserialize_only. Что не есть хорошо с учетом второй не понравившейся мне вещи.
Второй момент, который смущал гораздо сильнее, -- это разрушение привычной и понятной логики, которая ранее обеспечивалась функциями mandatory, optional и optional_no_default. А именно:
- mandatory означает, что атрибут всегда сериализуется, а при десериализации всегда требуется его присутствие;
- optional означает, что атрибут сериализуется только если его значение отличается от дефолтного, а при десериализации атрибут может и отсутствовать в JSON-е (в этом случае после десериализации он получает дефолтное значение). Если же атрибут при десериализации в JSON-е найден, то полностью проверяется его формат и, если нужно, его значение;
- optional_no_default означает, что атрибут сериализуется всегда. А при десериализации, если атрибут в JSON-е найден, то его формат и, если нужно, значение проверяются. Если при десериализации атрибута в JSON-е не найдено, то ничего не делается.
Теперь мы добавляем serialize_only, который должен вести себя как?
Должен ли он сериализоваться всегда или же только когда его значение отличается от какого-то дефолтного?
Должны ли мы при десериализации проверять наличие такого атрибута и его формат? Ведь одно дело, когда в JSON атрибута нет вообще. Ну нет и нет, что уж поделать. А вот если есть, но содержит какой-то невалидный мусор? Устраивает ли нас тот факт, что нам могут подсунуть какую-то откровенную ерунду, а мы оттуда возьмем лишь то, что нам интересно? А если не устраивает?
Т.е. если мы решаем какую-то конкретную задачу, типа взять отсюда вот такой JSON (который может быть только вот таким), прочитать из него лишь то-то и то-то, а затем создать новый JSON (который может быть только вот таким) и положить его вот сюда, то набор частных функций serialize_only/deserialize_only с жестко зашитой логикой поведения -- это нормальный и, возможно, единственно верный подход.
Но для универсальной библиотеки, которую разные люди используют в разных условиях и для разных задач... Мне показалось, что не следует двигаться по этому пути.
Что хотелось бы добавить в json-dto?
Во-первых, не хотелось добавлять в сам json-dto функциональность serialize_only и deserialize_only. Потому, что вряд ли это будет нужно большому количеству пользователей. А добавлять маргинальную фичу, на которую затем постоянно придется оглядываться, ну так себе идея.
Напомню простую вещь, которая не всем очевидна: сопровождение библиотеки -- это время и ресурсы. Которые нужно откуда-то брать. И трату которых никто не компенсирует. Поэтому чем меньше работа по сопровождению, тем дешевле это самое сопровождение обходится. Тем больше шансов, что это сопровождение вообще будет.
Так что вместо включения в состав json-dto новых дополнительных функций хотелось добавить какие-то средства, которые бы позволили пользователям, в случае острой необходимости, самостоятельно добавить и serialize_only, и deserialize_only, и еще что-нибудь эдакое.
Во-вторых, очень хотелось оставить набор штатных функций mandatory, optional и optional_no_default с их привычной логикой. И чтобы новая функциональность, вроде serialize_only/deserialize_only, могла быть вписана в уже имеющиеся mandatory/optional/optional_no_default.
Найденый способ
Пару слов про binder-ы для полей
Прежде чем рассказывать о том, какой способ был найден и реализован, нужно кратко пройтись по binder-ам для (де)сериализуемых полей объектов.
Когда пользователь вызывает функцию mandatory (или optional, или optional_no_default), то эта функция возвращает объект binder. Данный объект содержит имя (де)сериализуемого поля и ссылку на это самое поле. Плюс к тому, в объекте binder хранятся reader_writer (отвечает за форматирование значения) и validator (отвечает за проверку значения).
Так, когда пользователь записывает у себя:
template<typename Json_Io> void demo::json_io(Json_Io & io) { io & json_dto::mandatory("version", version_); } |
то за этой записью скрывается создание объекта binder-а и применение к этому объекту `operator&`.
С `operator&` связана совсем тривиальная логика.
Когда нужно сериализовать содержимое объекта посредством json_dto::to_json, то внутри to_json создается объект типа json_output_t, ссылка на который и передается в json_io. В результате чего компилятор находит определение `operator&` для json_output_t и binder-а. Этот оператор просто вызывает у binder-а метод write_to:
class json_output_t { public: ... template< typename Binder > json_output_t & operator & ( const Binder & b ) { b.write_to( m_object, m_allocator ); return *this; } private: rapidjson::Value & m_object; rapidjson::MemoryPoolAllocator<> & m_allocator; }; |
При десериализации посредством json_dto::from_json происходит практически тоже самое, только создается объект типа json_input_t, а `operator&` для него вызывает у binder-а метод read_from:
class json_input_t { public: ... template< typename Binder > json_input_t & operator & ( const Binder & b ) { b.read_from( m_object ); return *this; } private: const rapidjson::Value & m_object; }; |
Т.е. вся основная работа по (де)сериализации полей объекта выполняется объектами binder-ами. Объекты binder-ы создаются вспомогательными функциями mandatory, optional, optional_null, optional_no_default. При этом наиболее важными являются следующие вещи:
- объект binder хранит ссылку на поле объекта;
- (де)сериализацию выполняют нестатические методы read_from/write_to объекта binder-а;
- объект binder также хранит reader_writer и validator для работы со значением поля. Во многих случаях это будет дефолтные reader_writer/validator, но где-то это будут заданные пользователем экземпляры.
Основная идея
Идея состоит в том, чтобы:
- позволить передавать в binder не только ссылку на поле объекта. А какой-то прокси, внутри которого уже может быть что угодно. Хоть ссылка на поле объекта, хоть какой-то промежуточный объект...
- позволить пользователю делать специализацию для методов binder_t::write_to и binder_t::read_from.
Что это дает?
Например, пользователь может сделать свой прокси-тип serialize_only_proxy, который будет хранить ссылку на сериализуемое поле. И при вызове json_dto::mandatory пользователь будет отдавать в mandatory уже не ссылку на сериализуемое поле, а прокси-объект. Скажем, вот так:
template<typename F> struct serialize_only_proxy { const F & field_; }; template<typename F> auto serialize_only(const F & field) noexcept { return serialize_only_proxy{field}; } struct demo { int version() const { return 1; } ... template<typename Json_Io> void json_io(Json_Io & io) { io & json_dto::mandatory("version", serialize_only(version())) ...; } }; |
Далее пользователь делает специализации методов binder_t::read_from/write_to для своего serialize_only_proxy. Специализация read_from может вообще ничего не делать (т.е. вообще игнорировать наличие атрибута в JSON). А специализация write_to будет выполнять обычную сериализацию.
При этом очень хотелось бы, чтобы пользователь мог переиспользовать штатные реализации read_from/write_to. Поскольку там уже задействуются должным образом reader_writer и validator. И, если пользователю не нужна какая-то хитрая процедура (де)сериализации, то чтобы ему не нужно было самому вручную работать с reader_writer и validator.
Точки кастомизации
Тип binder_t в json-dto -- это шаблон класса:
template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > class binder_t { ... }; |
Параметр Field_Type в этом шаблоне означает тип (де)сериализуемого поля. Т.е., это будет, например, int или std::string, или std::vector<std::uint32_t>, или что-то еще.
Если разрешить передавать в mandatory/optional/прочие-функции не только ссылки на поля, а какие-то прокси объекты (вроде показанного выше serialize_only_proxy), то Field_Type будет обозначать тип прокси объекта. Т.е., если написать:
io & json_dto::mandatory("version", serialize_only(version())) |
То в этом месте будет создан объект binder_t<default_reader_writer_t, serialize_only_proxy<int>, mandatory_attr_t, empty_validator_t>.
В принципе, этого уже было бы достаточно, чтобы поддержать serialize_only/deserialize_only и пр. подобные штуки. Т.к. пользователь мог бы сделать частичную специализацию шаблона binder_t для своего serialize_only_proxy<F>:
template< typename Reader_Writer, typename Actual_Field_Type, typename Manopt_Policy, typename Validator > class binder_t< Reader_Writer, const serialize_only_proxy<Actual_Field_Type>, Manopt_Policy, Validator > { ... }; |
Плохо здесь то, что в каких-то случаях от пользователя может потребоваться слишком много телодвижений для поддержки своего прокси-типа.
Возьмем тривиальный случай, когда у нас есть простейший serialize_only_proxy, хранящий внутри константную ссылку. Здесь мы можем обойтись малой кровью:
template< typename Reader_Writer, typename Actual_Field_Type, typename Manopt_Policy, typename Validator > class binder_t< Reader_Writer, const serialize_only_proxy<Actual_Field_Type>, Manopt_Policy, Validator > : public binder_t< // Наследуем дефолтную реализацию. Reader_Writer, const Actual_Field_Type, Manopt_Policy, Validator > { using base_t = binder_t<Reader_Writer, const Actual_Field_Type, Manopt_Policy, Validator>; public: binder_t( Reader_Writer && reader_writer, serialize_only_proxy<Actual_Field_Type> proxy, Manopt_Policy && manopt_policy, Validator && validator) : base_t{ std::move(reader_writer), proxy.field_, // ВАЖНО: в базовый тип идет ссылка на исходное поле. std::move(manopt_policy), std::move(validator) } {} void read_from( const rapidjson::Value & object ) const { // Ничего не делаем. } }; |
Фокус здесь в том, что мы свою специализацию можем отнаследовать от дефолтной реализации. И в конструктор дефолтной реализации мы можем передать ссылку на поле объекта, как это и ожидается.
Но, допустим, нам нужно хранить именно прокси-объект. И из этого прокси-объекта мы ничего не можем отдать в дефолтную реализацию шаблона binder_t...
В этом случае пользователю ничего не останется, как полностью повторить у себя реализацию binder_t. Что не есть хорошо.
Поэтому я пошел несколько другим путем и добавил несколько точек кастомизации в штатную реализацию binder_t.
binder_data_holder_t
Первая точка кастомизации -- это шаблон класса binder_data_holder_t, который хранит все данные, необходимые для binder_t. Т.е. все то, что раньше хранилось непосредственно внутри binder_t -- reader_writer, field_name, field, validator -- все это теперь переехало в binder_data_holder_t. А binder_t теперь просто хранит у себя экземпляр binder_data_holder_t.
У шаблона binder_data_holder_t есть штатная реализация, которую можно переиспользовать для собственных целей.
binder_read_from_implementation_t и binder_write_to_implementation_t
Две другие добавленные точки кастомизации -- это шаблоны классов binder_read_from_implementation_t и binder_write_to_implementation_t.
Класс binder_read_from_implementation_t должен предоставить статический публичный метод read_from. А класс binder_write_to_implementation_t -- статический публичный метод write_to.
Класс binder_t теперь просто дергает эти статические методы при выполнении соответствующих операций.
У шаблонов binder_read_from_implementation_t и binder_write_to_implementation_t есть штатные реализации, которые можно переиспользовать для собственных целей.
Во что превратился binder_t?
Шаблон binder_t стал минималистичной оберткой, которую схематично можно представить следующим образом:
template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > class binder_t { using data_holder_t = binder_data_holder_t< Reader_Writer, Field_Type, Manopt_Policy, Validator >; public: binder_t( Reader_Writer && reader_writer, string_ref_t field_name, Field_Type & field, Manopt_Policy && manopt_policy, Validator && validator ) : m_data_holder{ std::move(reader_writer), field_name, field, std::move( manopt_policy ), std::move( validator ) } {} void read_from( const rapidjson::Value & object ) const { binder_read_from_implementation_t<data_holder_t>::read_from( m_data_holder, object ); } void write_to( rapidjson::Value & object, rapidjson::MemoryPoolAllocator<> & allocator ) const { binder_write_to_implementation_t<data_holder_t>::write_to( m_data_holder, object, allocator ); } private: data_holder_t m_data_holder; }; |
Как эти точки кастомизации использовать для своих нужд?
Итак, пользователь захотел сделать свой serialize_only. Что ему нужно сделать?
Во-первых, ему нужно описать свой тип прокси-объекта, который будет отдаваться в функции mandatory/optional/прочие-функции вместо ссылки на поле.
Во-вторых, ему нужно сделать частичную специализацию binder_data_holder_t для своего прокси-объекта.
В-третьих, ему нужно сделать частичную специализацию класса binder_read_from_implementation_t для случая использования прокси-объекта.
Вот, собственно, и все. Хотя, как можно увидеть ниже, выглядит это достаточно многословно. И многоэтажно... :)
Как это все выглядит в коде
Простой serialize_only
Вот пример простейшей реализации serialize_only для C++14. При десериализации наличие атрибута с таким именем и его формата вообще никак не проверяется.
namespace tutorial_20_1 { template< typename F > struct serialize_only_proxy_t { using field_type = const F; const F * m_field; }; template< typename F > serialize_only_proxy_t<F> serialize_only( const F & field ) noexcept { return { &field }; } } /* namespace tutorial_20_1 */ namespace json_dto { template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > class binder_data_holder_t< Reader_Writer, const tutorial_20_1::serialize_only_proxy_t<Field_Type>, Manopt_Policy, Validator > : public binder_data_holder_t< Reader_Writer, typename tutorial_20_1::serialize_only_proxy_t<Field_Type>::field_type, Manopt_Policy, Validator > { using serialize_only_proxy = tutorial_20_1::serialize_only_proxy_t<Field_Type>; using actual_field_type = typename serialize_only_proxy::field_type; using base_type = binder_data_holder_t< Reader_Writer, actual_field_type, Manopt_Policy, Validator >; public: binder_data_holder_t( Reader_Writer && reader_writer, string_ref_t field_name, const serialize_only_proxy & proxy, Manopt_Policy && manopt_policy, Validator && validator ) : base_type{ std::move(reader_writer), field_name, *(proxy.m_field), std::move(manopt_policy), std::move(validator) } {} }; template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > struct binder_read_from_implementation_t< binder_data_holder_t< Reader_Writer, const tutorial_20_1::serialize_only_proxy_t<Field_Type>, Manopt_Policy, Validator > > { using data_holder_t = binder_data_holder_t< Reader_Writer, const tutorial_20_1::serialize_only_proxy_t<Field_Type>, Manopt_Policy, Validator >; static void read_from( const data_holder_t & /*binder_data*/, const rapidjson::Value & /*object*/ ) { // Nothing to do. } }; } /* namespace json_dto */ |
Можно обратить внимание, что не нужно делать специализацию для binder_write_to_implementation_t. За счет того, что была сделана специализация для binder_data_holder_t за сериализацию отвечает штатная реализация binder_write_to_implementation_t.
Используется это вот так (можно обратить внимание на применение не только mandatory, но и optional):
struct example_data { std::vector< std::uint32_t > ids() const { return { 1u, 2u, 3u, 4u }; } std::uint32_t m_payload{}; std::uint32_t m_priority{}; int m_version_base{ 18 }; int version() const noexcept { return m_version_base + 2; } example_data() = default; example_data( std::uint32_t payload ) : m_payload{ payload } {} template < typename Json_Io > void json_io( Json_Io & io ) { io & json_dto::mandatory( "ids", tutorial_20_1::serialize_only( ids() ) ) & json_dto::mandatory( "payload", m_payload ) & json_dto::optional( "priority", tutorial_20_1::serialize_only( m_priority ), 0u ) & json_dto::optional( "version", tutorial_20_1::serialize_only( version() ), 18 ); } }; |
Простой deserialize_only
Простейшая реализация deserialize_only для C++14 выглядит вот так:
namespace tutorial_20_2 { template< typename F > struct deserialize_only_proxy_t { using field_type = F; F * m_field; }; template< typename F > deserialize_only_proxy_t<F> deserialize_only( F & field ) noexcept { static_assert( !std::is_const<F>::value, "deserialize_only can't be used with const objects" ); return { &field }; } } /* namespace tutorial_20_2 */ namespace json_dto { template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > class binder_data_holder_t< Reader_Writer, const tutorial_20_2::deserialize_only_proxy_t<Field_Type>, Manopt_Policy, Validator > : public binder_data_holder_t< Reader_Writer, typename tutorial_20_2::deserialize_only_proxy_t<Field_Type>::field_type, Manopt_Policy, Validator > { using deserialize_only_proxy = tutorial_20_2::deserialize_only_proxy_t<Field_Type>; using actual_field_type = typename deserialize_only_proxy::field_type; using base_type = binder_data_holder_t< Reader_Writer, actual_field_type, Manopt_Policy, Validator >; public: binder_data_holder_t( Reader_Writer && reader_writer, string_ref_t field_name, const deserialize_only_proxy & proxy, Manopt_Policy && manopt_policy, Validator && validator ) : base_type{ std::move(reader_writer), field_name, *(proxy.m_field), std::move(manopt_policy), std::move(validator) } {} }; template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > struct binder_write_to_implementation_t< binder_data_holder_t< Reader_Writer, const tutorial_20_2::deserialize_only_proxy_t<Field_Type>, Manopt_Policy, Validator > > { using data_holder_t = binder_data_holder_t< Reader_Writer, const tutorial_20_2::deserialize_only_proxy_t<Field_Type>, Manopt_Policy, Validator >; static void write_to( const data_holder_t & /*binder_data*/, rapidjson::Value & /*object*/, rapidjson::MemoryPoolAllocator<> & /*allocator*/ ) { // Nothing to do. } }; } /* namespace json_dto */ |
Здесь так же обращу внимание на то, что не пришлось делать специализацию для binder_read_from_implementation_t -- отлично подходит штатная версия.
Используется это вот так:
struct example_data { std::uint32_t m_a{}; std::uint32_t m_b{}; std::uint32_t m_c{}; template < typename Json_Io > void json_io( Json_Io & io ) { io & json_dto::mandatory( "a", tutorial_20_2::deserialize_only( m_a ) ) & json_dto::optional( "b", tutorial_20_2::deserialize_only( m_b ), 42u ) & json_dto::optional_no_default( "c", m_c ); } }; |
Более сложный ignore_after_deserialization
Два предыдущих примера были весьма тривиальными, т.к. мы просто запрещали выполнение read_from или write_to. А вот как можно сделать так, чтобы атрибут при десериализации полностью проверялся, но затем его прочитанное значение выбрасывалось бы. Но, при этом, атрибут сериализуется обычным образом:
namespace tutorial_20_3 { template< typename F > struct ignore_after_deserialization_proxy_t { using field_type = const F; const F * m_field; }; template< typename F > ignore_after_deserialization_proxy_t<F> ignore_after_deserialization( const F & field ) noexcept { return { &field }; } } /* namespace tutorial_20_3 */ namespace json_dto { template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > class binder_data_holder_t< Reader_Writer, const tutorial_20_3::ignore_after_deserialization_proxy_t<Field_Type>, Manopt_Policy, Validator > : public binder_data_holder_t< Reader_Writer, typename tutorial_20_3::ignore_after_deserialization_proxy_t<Field_Type>::field_type, Manopt_Policy, Validator > { using proxy_type = tutorial_20_3::ignore_after_deserialization_proxy_t<Field_Type>; using actual_field_type = typename proxy_type::field_type; using base_type = binder_data_holder_t< Reader_Writer, actual_field_type, Manopt_Policy, Validator >; public: binder_data_holder_t( Reader_Writer && reader_writer, string_ref_t field_name, const proxy_type & proxy, Manopt_Policy && manopt_policy, Validator && validator ) : base_type{ std::move(reader_writer), field_name, *(proxy.m_field), std::move(manopt_policy), std::move(validator) } {} }; template< typename Reader_Writer, typename Field_Type, typename Manopt_Policy, typename Validator > struct binder_read_from_implementation_t< binder_data_holder_t< Reader_Writer, const tutorial_20_3::ignore_after_deserialization_proxy_t<Field_Type>, Manopt_Policy, Validator > > { using proxy_type = tutorial_20_3::ignore_after_deserialization_proxy_t<Field_Type>; using data_holder_t = binder_data_holder_t< Reader_Writer, const proxy_type, Manopt_Policy, Validator >; static void read_from( const data_holder_t & binder_data, const rapidjson::Value & object ) { if( !object.IsObject() ) { throw ex_t{ "unable to extract field \"" + std::string{ binder_data.field_name().s } + "\": " "parent json type must be object" }; } // Temporary object for holding deserialized value. Field_Type tmp_object{}; const auto it = object.FindMember( binder_data.field_name() ); if( object.MemberEnd() != it ) { const auto & value = it->value; if( !value.IsNull() ) { binder_data.reader_writer().read( tmp_object, value ); } else { set_value_null_attr( tmp_object ); } } else { binder_data.manopt_policy().on_field_not_defined( tmp_object ); } binder_data.validator()( tmp_object ); // validate value. // NOTE: the value from tmp_object will be lost. } }; } /* namespace json_dto */ |
Применяться это может вот так (обращу внимание на использование ignore_after_deserialization совместно с optional):
struct example_data { std::vector< std::uint32_t > ids() const { return { 1u, 2u, 3u, 4u }; } std::uint32_t m_payload{}; std::uint32_t m_priority{}; int m_version_base{ 18 }; int version() const noexcept { return m_version_base + 2; } example_data() = default; example_data( std::uint32_t payload ) : m_payload{ payload } {} template < typename Json_Io > void json_io( Json_Io & io ) { io & json_dto::mandatory( "ids", tutorial_20_3::ignore_after_deserialization( ids() ) ) & json_dto::mandatory( "payload", m_payload ) & json_dto::mandatory( "priority", tutorial_20_3::ignore_after_deserialization( m_priority ), json_dto::min_max_constraint( std::uint32_t{0u}, std::uint32_t{9u} ) ) & json_dto::optional( "version", tutorial_20_3::ignore_after_deserialization( version() ), 18 ); } }; |
Вместо заключения
Спасибо всем, кто нашел в себе силы дочитать до этого места. Данный пост я писал в большей степени для себя самого. Чтобы в процессе формулирования текста еще раз понять, действительно ли найденное решение стоило того.
Пока что думается, что оно того стоило. Хотя я не могу сказать, что оно мне вот прям нравится. Просто это лучшее из придуманного. Оно решает поставленные задачи. И, не смотря на свою сложность и многословность, его смысл и принцип использования можно донести до пользователей (я на это надеюсь).
Так что все идет к тому, что данное решение завтра-послезавтра станет официальной частью json-dto. Если только кто-нибудь из читателей не укажет на какой-то из незамеченных мной фатальных недостатков.
В принципе, данный рассказ можно рассматривать и как заметку "из будней библиотекописателей".
Кроме того, лично я лишний раз убедился в том, что C++ не просто так настолько сложен. Шаблоны и их частичная специализация -- это же как раз то, что изрядно добавляет сложности языку. Но вот в случае, описанном в посте, именно шаблоны и их частичная специализация и позволяют мне получить нужное решение.
Комментариев нет:
Отправить комментарий