Отображение данных в формате json на структуру c++ и обратно (работа над ошибками)

от автора

Предыдущий вариант решения задачи отображения между структурой с++ и json получился как первый блин — комом. К счастью, разработка — процесс итерационный, и за первой версией всегда будет вторая. Комментарии (спасибо всем) и анализ дырок в первом блине позволили кое-что улучшить.

Что было плохо

  • невозможно использовать обычные структуры с++ (в том числе уже существующие). Все структуры необходимо определять с нуля специальным образом
  • Json объект можно отображать только на специальным образом определенную структуру
  • Json массив можно отображать только на специальный класс
  • невозможно использовать stl контейнеры
  • макросы просто необходимы (можно и без них, но регистрация методов установки полей жестко совмещена с инициализацией этих полей, поэтому без макросов определение структуры нечитаемо)
  • отображение никак не настраивается, т.е. нельзя задать, например, значения по умолчанию или пределы значений

Как стало теперь

Регистрация полей, которые будут участвовать в отображении, больше не привязана к структуре. Для регистрации используется функция

reg(V T::* ptr, std::string const & name, Options<U>&& ... options);

  • ptr — указатель на поле
  • name — имя поля
  • options — опции отображения

В качестве типов полей могут быть использованы:

  • bool
  • char, unsigned char, short, unsigned short, int unsigned int, long, long long
  • float, double
  • std::string
  • std::list
  • std::vector
  • std::map (ключем может быть только std::string)
  • std::unordered_map (ключем может быть только std::string)
  • std::multimap (ключем может быть только std::string)
  • std::unordered_multimap (ключем может быть только std::string)
  • структуры с++
  • перечисления

например

struct Friend {  std::string name;  std::list<int> counters; };  struct MiB {  std::list<Friend> friends;  std::vector<std::list<std::string>> groups;  std::map<std::string, std::vector<std::string>> books; };  struct_mapping::reg(&Friend::name, "name"); struct_mapping::reg(&Friend::counters, "counters");  struct_mapping::reg(&MiB::friends, "friends"); struct_mapping::reg(&MiB::groups, "groups"); struct_mapping::reg(&MiB::books, "books");

После регистрации, как и раньше, вызывается

map_json_to_struct(T & result_struct, std::basic_istream<char> & json_data);

  • result_struct — ссылка на результирующую структуру
  • json_data — ссылка на входной поток json данных

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

Небольшой полный пример выглядит так

#include <iostream> #include <sstream>  #include "struct_mapping/struct_mapping.h"  struct Planet {  bool giant;  long long surface_area;  double mass;  std::string satellite; };  int main() {  struct_mapping::reg(&Planet::giant, "giant");  struct_mapping::reg(&Planet::surface_area, "surface_area");  struct_mapping::reg(&Planet::mass, "mass");  struct_mapping::reg(&Planet::satellite, "satellite");   Planet earth;   std::istringstream json_data(R"json(   {    "giant": false,    "surface_area": 510072000000000,    "mass": 5.97237e24,    "satellite": "Moon"   }  )json");   struct_mapping::map_json_to_struct(earth, json_data);   std::cout << "earth" << std::endl;  std::cout << " giant        : " << std::boolalpha << earth.giant << std::endl;  std::cout << " surface_area : " << earth.surface_area << std::endl;  std::cout << " mass         : " << earth.mass << std::endl;  std::cout << " satellite    : " << earth.satellite << std::endl; }

что дает в результате

earth  giant        : false  surface_area : 510072000000000  mass         : 5.97237e+24  satellite    : Moon

Использование перечислений

Библиотека ожидает, что перечисления в json представляются в виде строк. Поэтому для использования перечислений требуется установить методы преобразования из строки в значение перечисления и наоборот, используя:

MemberString::set(From function_from_string_, To function_to_string_);

  • function_from_string_ — функция преобразования из строки в значение перечисления
  • function_to_string_ — функция преобразования из значения перечисления в строку

например

enum class Color {  red,  blue,  green, };  struct_mapping::MemberString<Color>::set(  [] (const std::string & value) {   if (value == "red") return Color::red;   if (value == "green") return Color::green;   if (value == "blue") return Color::blue;    throw struct_mapping::StructMappingException("bad convert '"+value+"' to Color");  },  [] (Color value) {   switch (value) {   case Color::red: return "red";   case Color::green: return "green";   default: return "blue";   }  });

в остальном использование перечислений аналогично остальным типам

#include <iostream> #include <list> #include <map> #include <sstream> #include <string>  #include "struct_mapping/struct_mapping.h"  namespace sm = struct_mapping;  enum class Color {  red,  blue,  green, };  Color color_from_string(const std::string & value) {  if (value == "red") return Color::red;  if (value == "blue") return Color::blue;   return Color::green; }  std::string color_to_string(Color color) {  switch (color) {  case Color::red: return "red";  case Color::green: return "green";  default: return "blue";  } }  struct Palette {  Color main_color;  Color background_color;  std::list<Color> special_colors;  std::map<std::string, Color> colors;   friend std::ostream & operator<<(std::ostream & os, const Palette & o) {   os << "main_color       : " << color_to_string(o.main_color) << std::endl;   os << "background_color : " << color_to_string(o.background_color) << std::endl;   os << "special_colors   : ";   for (auto color : o.special_colors)    os << color_to_string(color) << ", ";   os << std::endl << "colors           : ";   for (auto [name, color] : o.colors)    os << "[" << name << ", " << color_to_string(color) << "], ";   os << std::endl;    return os;  } };  int main() {  sm::MemberString<Color>::set(color_from_string, color_to_string);   sm::reg(&Palette::main_color, "main_color", sm::Required{});  sm::reg(&Palette::background_color, "background_color", sm::Default{Color::blue});  sm::reg(&Palette::special_colors, "special_colors");  sm::reg(&Palette::colors, "colors");   Palette palette;   std::istringstream json_data(R"json(  {   "main_color": "green",   "special_colors": ["green", "green", "red"],   "colors": {    "dark": "green",    "light": "red",    "neutral": "blue"   }  }  )json");   sm::map_json_to_struct(palette, json_data);   std::cout << palette << std::endl; }

результат

main_color       : green background_color : blue special_colors   : green, green, red,  colors           : [dark, green], [light, red], [neutral, blue],

Опции отображения

Появилась возможность задавать при регистрации поля опции, которые будут использоваться при отображении этого поля

  • Bounds
  • Default
  • NotEmpty
  • Required

Bounds

Устанавливает диапазон значений, в котором (включая границы диапазона) должно находится устанавливаемое значение. Применима для целочисленных типов и типов с плавающей точкой. Опция принимает два параметра — границы диапазона. Генерирует исключение при выходе устанавливаемого в процессе отображения значения за границы.

Bounds{нижняя граница, верхняя граница}

Пример задания опции:

reg(&Stage::engine_count, "engine_count", Bounds{1, 31});

Default

Устанавливает значение по умолчанию для поля. Применима для bool, целочисленных типов, типов с плавающей точкой, строк, контейнеров, структур с++ и перечислений. Опция принимает один параметр — значение по умолчанию.

Default{значение по умолчанию}

Пример задания опции:

reg(&Stage::engine_count, "engine_count", Default{3});

NotEmpty

Отмечает, что для поля не может быть установлено пустое значение. Применима для строк и контейнеров. Опция не принимает параметров. Генерирует исключение, если после завершения отображения значением поля является пустая строка или пустой контейнер.

Пример задания опции:

reg(&Spacecraft::name, "name", NotEmpty{}));

Required

Отмечает, что для поля обязательно должно быть установлено значение. Применима для bool, целочисленных типов, типов с плавающей точкой, строк, контейнеров, структур с++ и перечислений. Опция не принимает параметров. Генерирует исключение, если после завершения отображения значение для поля не было установлено.

Пример задания опции:

reg(&Spacecraft::name, "name", Required{}));

Пример использования опций

#include <iostream> #include <list> #include <map> #include <sstream> #include <string>  #include "struct_mapping/struct_mapping.h"  namespace sm = struct_mapping;  struct Stage {  unsigned short engine_count;  std::string fuel;  long length;   friend std::ostream & operator<<(std::ostream & os, const Stage & o) {   os << "  engine_count : " << o.engine_count << std::endl;   os << "  fuel         : " << o.fuel << std::endl;   os << "  length       : " << o.length << std::endl;    return os;  } };  struct Spacecraft {  bool in_development;  std::string name;  int mass;  std::map<std::string, Stage> stages;  std::list<std::string> crew;   friend std::ostream & operator<<(std::ostream & os, const Spacecraft & o) {   os << "in_development : " << std::boolalpha << o.in_development << std::endl;   os << "name           : " << o.name << std::endl;   os << "mass           : " << o.mass << std::endl;   os << "stages: " << std::endl;   for (auto& s : o.stages) os << " " << s.first << std::endl << s.second;   os << "crew: " << std::endl;   for (auto& p : o.crew) os << " " << p << std::endl;    return os;  } };  int main() {  sm::reg(&Stage::engine_count, "engine_count", sm::Default{6}, sm::Bounds{1, 31});  sm::reg(&Stage::fuel, "fuel", sm::Default{"subcooled"});  sm::reg(&Stage::length, "length", sm::Default{50});   sm::reg(&Spacecraft::in_development, "in_development", sm::Required{});  sm::reg(&Spacecraft::name, "name", sm::NotEmpty{});  sm::reg(&Spacecraft::mass, "mass",   sm::Default{5000000}, sm::Bounds{100000, 10000000});  sm::reg(&Spacecraft::stages, "stages", sm::NotEmpty{});  sm::reg(&Spacecraft::crew, "crew",   sm::Default{std::list<std::string>{"Arthur", "Ford", "Marvin"}});   Spacecraft starship;   std::istringstream json_data(R"json(  {   "in_development": false,   "name": "Vostok",   "stages": {    "first": {     "engine_count": 31,     "fuel": "compressed gas",     "length": 70    },    "second": {}   }  }  )json");   sm::map_json_to_struct(starship, json_data);   std::cout << starship << std::endl; }

результат

in_development : false name           : Vostok mass           : 5000000 stages:   first   engine_count : 31   fuel         : compressed gas   length       : 70  second   engine_count : 6   fuel         : subcooled   length       : 50 crew:   Arthur  Ford  Marvin

Обратное отображение структуры c++ на json

Для обратного отображения структуры на json необходимо предварительно зарегистрировать все поля всех структур, которые требуется отображать, используя для каждого поля

reg(V T::* ptr, std::string const & name, Options<U>&& ... options);

и вызвать непосредственно для обратного отображения функцию

map_struct_to_json(T & source_struct, std::basic_ostream<char> & json_data, std::string indent);

  • source_struct — ссылка на исходную структуру
  • json_data — ссылка на выходной поток json данных
  • indent — отступ (если задан, делает выходной формат лучше читаемым)

#include <iostream> #include <sstream>  #include "struct_mapping/struct_mapping.h"  struct OceanPart {  std::string name;  double average_depth;  std::vector<int> temperature; };  struct OceanColor {  std::string name; };  struct Ocean {  double water_volume;  long long surface_area;  bool liquid;  std::string name;   OceanColor color;   std::vector<OceanPart> parts; };  struct Planet {  bool giant;  long long surface_area;  double mass;  double volume;  long long orbital_period;  std::string name;  bool terrestrial;  std::string shape;   Ocean ocean; };  int main() {  struct_mapping::reg(&OceanPart::name, "name");  struct_mapping::reg(&OceanPart::average_depth, "average_depth");  struct_mapping::reg(&OceanPart::temperature, "temperature");   struct_mapping::reg(&OceanColor::name, "name");   struct_mapping::reg(&Ocean::water_volume, "water_volume");  struct_mapping::reg(&Ocean::surface_area, "surface_area");  struct_mapping::reg(&Ocean::liquid, "liquid");  struct_mapping::reg(&Ocean::name, "name");  struct_mapping::reg(&Ocean::color, "color");  struct_mapping::reg(&Ocean::parts, "parts");   struct_mapping::reg(&Planet::giant, "giant");  struct_mapping::reg(&Planet::surface_area, "surface_area");  struct_mapping::reg(&Planet::mass, "mass");  struct_mapping::reg(&Planet::volume, "volume");  struct_mapping::reg(&Planet::orbital_period, "orbital_period");  struct_mapping::reg(&Planet::name, "name");  struct_mapping::reg(&Planet::terrestrial, "terrestrial");  struct_mapping::reg(&Planet::shape, "shape");  struct_mapping::reg(&Planet::ocean, "ocean");   Planet earth;   earth.giant = false;  earth.terrestrial = true;  earth.surface_area = 510072000;  earth.orbital_period = 365 * 24 * 3600;  earth.mass = 5.97237e24;  earth.name = "Terra";  earth.volume = 1.08321e12;  earth.shape = "nearly spherical";   earth.ocean.water_volume = 1332000000;  earth.ocean.surface_area = 361132000;  earth.ocean.liquid = true;  earth.ocean.name = "World Ocean";  earth.ocean.color.name = "blue";   OceanPart pacific;  pacific.name = "Pacific Ocean";  pacific.average_depth = 4.280111;  pacific.temperature = std::vector<int>{-3, 5, 12};   OceanPart atlantic;  atlantic.name = "Atlantic Ocean";  atlantic.average_depth = 3.646;  atlantic.temperature = std::vector<int>{-3, 0};   earth.ocean.parts.push_back(pacific);  earth.ocean.parts.push_back(atlantic);   std::ostringstream json_data;  struct_mapping::map_struct_to_json(earth, json_data, "  ");   std::cout << json_data.str() << std::endl; }

результат

{   "giant": false,   "surface_area": 510072000,   "mass": 5.97237e+24,   "volume": 1.08321e+12,   "orbital_period": 31536000,   "name": "Terra",   "terrestrial": true,   "shape": "nearly spherical",   "ocean": {     "water_volume": 1.332e+09,     "surface_area": 361132000,     "liquid": true,     "name": "World Ocean",     "color": {       "name": "blue"     },     "parts": [       {         "name": "Pacific Ocean",         "average_depth": 4.28011,         "temperature": [           -3,           5,           12         ]       },       {         "name": "Atlantic Ocean",         "average_depth": 3.646,         "temperature": [           -3,           0         ]       }     ]   } }

В итоге

  • использовать обычные структуры с++ можно (и нужно)
  • Json объекты можно отображать как на специальным образом определенную структуру (такая возможность осталась), так и на обычные структуры
  • Json массивы можно отображать на std::vector и std::list. Общие требования к контейнерам на которые можно отображать массивы пока не полностью сформированы.
  • json объекты можно отображать на ассоциативные контейнеры, с ограничением на то, что ключ должен быть строкой. Общие требования к контейнерам, как и с массивами, пока не полностью сформированы.
  • макросы не нужны и уж точно не необходимы. Возможность их использования осталась (как вариант) при регистрации, совмещенной с инициализацией полей. Но скорее всего, будет выпилена.
  • отображение можно настраивать с помощью опций

Библиотека доступна на GitHub

ссылка на оригинал статьи https://habr.com/ru/post/510438/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *