пятница, 8 сентября 2017 г.

[prog.c++] json_dto-0.2

Мы сегодня обновили свою небольшую библиотеку json_dto, которая служит хоть и тонкой, но очень полезной оберткой над такой замечательной штукой, как rapidjson. Мы сделали json_dto где-то года полтора назад, взяв идеи из Boost.Serialization, для уменьшения объема писанины при работе с JSON в C++. Например, json_dto позволяет писать вот так:

class User {
public :
   /* ... */

   template<typename JSON_IO>
   void json_io(JSON_IO & io) {
      io & json_dto::mandatory("id", _id)
         & json_dto::mandatory("name", _name)
         & json_dto::mandatory("birthday", _birthday)
         & json_dto::mandatory("phone", _phone);
   }
};

вместо того, чтобы писать вот такие простыни (взято отсюда как пример несложного кода):

class User {
public:

/* ... */

    rapidjson::Document toJSON() {
        rapidjson::Value json_val;
        rapidjson::Document doc;
        auto& allocator = doc.GetAllocator();

        doc.SetObject();

        json_val.SetUint64(_id);
        doc.AddMember("id", json_val, allocator);

        json_val.SetString(_name.c_str(), allocator);
        doc.AddMember("name", json_val, allocator);

        // see http://rapidjson.org/md_doc_tutorial.html#DeepCopyValue
        json_val.CopyFrom(_birthday.toJSON(), allocator);
        doc.AddMember("birthday", json_val, allocator);

        json_val.SetUint64(_phone);
        doc.AddMember("phone", json_val, allocator);

        return doc;
    }

    static User fromJSON(const rapidjson::Value& doc) {
        if(!doc.IsObject())
            throw std::runtime_error("document should be an object");

        static const char* members[] = { "id""name""phone",
                                         "birthday" };
        for(size_t i = 0; i < sizeof(members)/sizeof(members[0]); i++)
            if(!doc.HasMember(members[i]))
                throw std::runtime_error("missing fields");

        uint64_t id = doc["id"].GetUint64();
        std::string name = doc["name"].GetString();
        uint64_t phone = doc["phone"].GetUint64();
        Date birthday = Date::fromJSON(doc["birthday"]);

        User result(id, name, phone, birthday);
        return result;
    }

/* ... */

};

Версия 0.2 нарушает совместимость с версий 0.1, хотя с этим могут столкнуться только те, кому приходилось писать собственные специализации шаблонной функции json_dto::read_json_value.

Основное изменение в json_dto-0.2 -- это добавление еще одного способа поддержки ввода-вывода пользовательских типов. В версии 0.1 сериализовать/десериализовать собственный тип можно было только одним способом: сделать собственную специализацию шаблонной функции read_json_value из пространства имен json_dto. К сожалению, это не работало для случаев, когда нужно было сделать свою сериализацию для своего шаблонного типа.

В json_dto-0.2 теперь можно определить свои собственные версии read_json_value и write_json_value в том же самом пространстве имен, в котором определен и сериализуемый тип. Нужные read_json_value/write_json_value будут найдены компилятором автоматически за счет ADL (argument dependent lookup).

Вот пример того, как теперь определяется кастомная сериализация/десериализация для собственного шаблонного типа:

// Наше собственное пространство имен, в котором определяется наш шаблонный тип.
namespace bounded_types
{

// Шаблонный тип.
// Экземпляры объектов этого типа нужно будет уметь сериализовать/десериализовать.
templatetypename T, T min, T max, T default_value = T{} >
class bounded_value_t
{
public :
   using value_type = T;

   bounded_value_t()
      {
         ensure_valid( m_value );
      }
   bounded_value_t( value_type value ) : m_value{ ensure_valid( value ) }
      {}

   value_type get() const { return m_value; }
   void set( value_type v ) { m_value = ensure_valid( v ); }

private :
   value_type m_value{ default_value };

   static value_type
   ensure_valid( value_type v )
      {
         if( v < min || v > max )
            throw std::invalid_argument{ "value is out of range!" };
         return v;
      }

};

// Определяем функцию десериализации шаблона bounded_value_t.
templatetypename T, T min, T max, T default_value >
void
read_json_value(
   bounded_value_t< T, min, max, default_value > & value,
   const rapidjson::Value & from )
{
   using json_dto::read_json_value;

   T v{ default_value };
   read_json_value( v, from );

   value.set( v );
}

// Определяем функцию сериализации шаблона bounded_value_t.
templatetypename T, T min, T max, T default_value >
void
write_json_value(
   const bounded_value_t< T, min, max, default_value > & value,
   rapidjson::Value & object,
   rapidjson::MemoryPoolAllocator<> & allocator )
{
   using json_dto::write_json_value;
   write_json_value( value.get(), object, allocator );
}

/* namespace bounded_types */

Ну а далее все используется уже привычным образом:

struct message_t
{
   // Вот в таком виде наш шаблон будет использоваться.
   using priority_t = bounded_types::bounded_value_t< int095 >;

   message_t() = default;

   message_t(
      std::string from,
      std::tm when,
      std::string text,
      priority_t::value_type priority,
      importance_levels::level_t importance ) {...}

   std::string m_from;
   std::tm m_when;
   std::string m_text;
   priority_t m_priority;
   importance_levels::level_t m_importance;

   template < typename JSON_IO >
   void
   json_io( JSON_IO & io )
   {
      io
         & json_dto::mandatory( "from", m_from )
         & json_dto::mandatory( "when", m_when )
         & json_dto::mandatory( "text", m_text )
         & json_dto::mandatory( "priority", m_priority )
         & json_dto::optional( "importance", m_importance,
               importance_levels::normal );
   }
};

Старый способ с собственной специализацией read_json_value/write_json_value в пространстве имен json_dto продолжает работать. Но нужно обратить внимание, что теперь у read_json_value другой формат:

template < typename DTO >
void
read_json_value(
   DTO & v,
   const rapidjson::Value & object );

Т.е. первым параметром теперь является ссылка на объект, подлежащий десериализации.

Формат write_json_value остался прежним.

Еще в json_dto-0.2 появилась поддержка std::experimental::optional и std::optional. Так что если ваш компилятор поддерживает какой-то из этих типов, то можно использовать их совместно с json_dto без каких-либо дополнительных усилий.

В общем, нам json_dto экономит много сил и времени при работе с JSON-ом. Приглашаем взять и попробовать. Библиотека распространяется под 3-х пунктной BSD-лицензией, т.е. безвозмездно, т.е. даром ;) Если появятся какие-то соображения или предложения по развитию и расширению json_dto, то мы с удовольствием их выслушаем.

Комментариев нет: