Есть ли способ указать более простую сериализацию JSON (де-) для std::map с использованием Cereal/C++?

Проект, над которым я работаю, представляет собой приложение C++, которое управляет большим количеством пользовательских аппаратных устройств. Приложение имеет интерфейс сокета/порта для клиента (например, графический интерфейс). Каждый тип устройства имеет свою собственную четко определенную схему JSON, и мы можем легко сериализовать их с помощью Cereal.

Но приложению также необходимо анализировать входящие запросы JSON от клиента. В одной части запроса указываются параметры фильтра устройства, что примерно аналогично предложению SQL WHERE, в котором все выражения объединяются по И. Например.:

"filter": { "type": "sensor", "status": "critical" }

Это будет означать, что клиент хочет выполнить операцию на каждом «сенсорном» устройстве с «критическим» статусом. На первый взгляд казалось, что реализация параметров фильтра на C++ будет представлять собой std::map. Но когда мы экспериментировали с использованием Cereal для десериализации объекта, это не удалось. И когда мы сериализуем жестко закодированную карту фильтров, она выглядит так:

"filter": [
   { "key": "type", "value": "sensor" },
   { "key": "status", "value": "critical" }
]

Теперь я понимаю, почему Cereal поддерживает такую ​​многословную сериализацию карты. В конце концов, ключ карты может быть не строкового типа. Но в данном случае ключ является строкой.

Я не очень заинтересован в том, чтобы переписывать нашу спецификацию интерфейса и заставлять наших клиентов генерировать явно неидиоматический JSON только для того, чтобы удовлетворить Cereal. Я новичок в Cereal, и мы застряли на этом этапе. Есть ли способ заставить Cereal анализировать этот фильтр как std::map? Или, может быть, я спрашиваю это неправильно. Есть ли какой-то другой контейнер stl, в который мы должны десериализоваться?


person Lee Jenkins    schedule 21.03.2014    source источник
comment
Два коротких вопроса: вы ожидаете, что ваш ввод всегда будет храниться в объекте JSON? Вы могли заметить, что хлопья используют массивы JSON для контейнеров переменного размера. Во-вторых, если у вас нет контроля над этим, есть ли у вас способ узнать количество пар имя-значение, которое ваш запрос вернет в объекте JSON?   -  person Azoth    schedule 22.03.2014
comment
@Azoth, да, протокол определен как JSON. И нет, заранее определенного количества пар нет — в этом-то и суть. На данный момент я подозреваю, что Cereal просто не будет работать, и мы выберем какой-нибудь другой способ десериализации фильтра.   -  person Lee Jenkins    schedule 23.03.2014


Ответы (1)


Позвольте мне сначала объяснить, почему хлопья выводят более подробный стиль, чем вам хотелось бы. хлопья написаны для работы с произвольными архивами сериализации и используют золотую середину для удовлетворения всех из них. Представьте себе, что тип ключа — это нечто гораздо более сложное, чем строковый или арифметический тип — как мы можем сериализовать его простым "key" : "value" способом?

Также обратите внимание, что злаки должны быть прародителями любых данных, которые они считывают.


При этом то, что вы хотите, вполне возможно с хлопьями, но есть несколько препятствий:

Самым большим препятствием, которое необходимо преодолеть, является тот факт, что желаемый ввод сериализует некоторое неизвестное количество пар имя-значение внутри объекта JSON, а не массива JSON. хлопья были разработаны для использования массивов JSON при работе с контейнерами, которые могут содержать переменное количество элементов, поскольку это имело наибольший смысл с учетом используемого базового синтаксического анализатора rapidjson.

Во-вторых, хлопья в настоящее время не ожидают, что имя в паре имя-значение будет фактически загружено в память — оно просто использует их как организационный инструмент.


Таким образом, вот полностью работающее решение (можно сделать более элегантным) вашей проблемы с минимальными изменениями в хлопьях (на самом деле это изменение, которое для хлопьев 1.1, текущая версия 1.0):

Добавьте эту функцию в JSONInputArchive:

//! Retrieves the current node name
/*! @return nullptr if no name exists */
const char * getNodeName() const
{
  return itsIteratorStack.back().name();
}

Затем вы можете написать специализацию сериализации для std::map (или неупорядоченной, в зависимости от того, что вы предпочитаете) для пары строк. Обязательно поместите это в пространство имен cereal, чтобы его мог найти компилятор. Этот код должен существовать где-то в ваших собственных файлах:

namespace cereal
{
  //! Saving for std::map<std::string, std::string>
  template <class Archive, class C, class A> inline
  void save( Archive & ar, std::map<std::string, std::string, C, A> const & map )
  {
    for( const auto & i : map )
      ar( cereal::make_nvp( i.first, i.second ) );
  }

  //! Loading for std::map<std::string, std::string>
  template <class Archive, class C, class A> inline
  void load( Archive & ar, std::map<std::string, std::string, C, A> & map )
  {
    map.clear();

    auto hint = map.begin();
    while( true )
    {
      const auto namePtr = ar.getNodeName();

      if( !namePtr )
        break;

      std::string key = namePtr;
      std::string value; ar( value );
      hint = map.emplace_hint( hint, std::move( key ), std::move( value ) );
    }
  }
} // namespace cereal

Это не самое элегантное решение, но оно работает хорошо. Я оставил все в общем шаблоне, но то, что я написал выше, будет работать только с архивами JSON с учетом внесенных изменений. Добавление аналогичного getNodeName() в XML-архив, вероятно, позволит ему работать и там, но очевидно, что это не имеет смысла для двоичных архивов.

Чтобы сделать это чистым, вы должны поставить enable_if вокруг этого для архивов, с которыми он работает. Вам также потребуется изменить архивы JSON в хлопьях для работы с объектами JSON переменного размера. Чтобы получить представление о том, как это сделать, посмотрите, как злаки устанавливают состояние в архиве, когда получают SizeTag для сериализации. По сути, вам нужно сделать так, чтобы архив не открывал массив, а вместо этого открывал объект, а затем создавал свою собственную версию loadSize(), которая бы видела, насколько велик объект (это будет Member на языке Rapidjson).


Чтобы увидеть вышеописанное в действии, запустите этот код:

int main()
{
  std::stringstream ss;
  {
    cereal::JSONOutputArchive ar(ss);
    std::map<std::string, std::string> filter = {{"type", "sensor"}, {"status", "critical"}};

    ar( CEREAL_NVP(filter) );
  }

  std::cout << ss.str() << std::endl;

  {
    cereal::JSONInputArchive ar(ss);
    cereal::JSONOutputArchive ar2(std::cout);

    std::map<std::string, std::string> filter;

    ar( CEREAL_NVP(filter) );
    ar2( CEREAL_NVP(filter) );
  }

  std::cout << std::endl;
  return 0;
}

и вы получите:

{
    "filter": {
        "status": "critical",
        "type": "sensor"
    }
}
{
    "filter": {
        "status": "critical",
        "type": "sensor"
    }
}
person Azoth    schedule 23.03.2014
comment
Я пытаюсь проверить это, но я столкнулся с парой загвоздок. Первая загвоздка заключалась в том, что в наших заголовках JSONInputArchive нет члена itsIteratorStack. Список элементов данных выглядит следующим образом: private: ReadStream itsReadStream; //!‹ Rapidjson поток записи std::vector‹Iterator› itsValueStack; //!‹ Стек значений rapidjson::Document itsDocument; //!‹ Rapidjson document Я перевернул кости и попытался заменить itsValueStack на itsIteratorStack, но тут компилятор жалуется на name: - person Lee Jenkins; 24.03.2014
comment
Тьфу, не привык к ограничениям комментариев StackOverflow. Ошибка компилятора после моей замены: /usr/local/include/cereal/archives/json.hpp:327:39: ошибка: ‘const value_type’ не имеет члена с именем ‘name’ return itsValueStack.back().name(); Я подозреваю, что либо (1) разница в версии (и я не могу определить, какая у нас версия хлопьев — в заголовках нет идентификатора), либо (2) может быть, отсутствует еще один фрагмент кода, например, метод name() для class Iterator, вложенных в JSONInputArchive. - person Lee Jenkins; 24.03.2014
comment
Вы используете последнюю версию хлопьев? Версия 1.0 была выпущена буквально день назад и содержит большое количество изменений: github.com/USCiLab/cereal. /релизы. - person Azoth; 24.03.2014
comment
Оно работает! Нам пришлось обновить наши зерновые жатки до версии 1.0, после чего все заработало. Большое спасибо за Вашу помощь! - person Lee Jenkins; 24.03.2014