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

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

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

"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, в который мы должны десериализоваться?

7

Решение

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

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


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

Самым большим препятствием, которое нужно преодолеть, является тот факт, что желаемый входной поток сериализует некоторое неизвестное число пар имя-значение внутри объекта JSON, а не массива JSON. cereal был разработан для использования 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 на быстром языке).


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

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"}
}
6

Другие решения

Других решений пока нет …