Отображение данных в формате 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

    Комментарии 5

      0
      Как же мне не хватает элементов рефлексии в крестах!
      Чтобы не описаться где-нибудь в этой простыне
      struct_mapping::reg(&Ocean::water_volume, "water_volume");
        0
        Полагаю это для самообразования, поэтому предлагаю копнуть какую-нибудь готовую либу, вроде nlohmann json, которая предоставляет очень интуитивный интерфейс, — там почти наверняка много шаблонной магии
          0
          Nlohmann json работает с JSON в DOM режиме. Представленный код делает JSON десериализацию в нативные структуры приложения с валидацией, конверсией и ремаппингом. Соврешенно разные задачи.
            0
            Крутая библиотека!
            Собираетесь ли ее развивать дальше? Было бы замечательно в нее добавить поддержку std::chrono::*.
            И еще хотелось бы видеть возможность создавать несколько маппингов в разных частях программы, чтобы была возможность использовать для разных json.
              0
              Было бы замечательно в нее добавить поддержку std::chrono::*

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


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

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


              map_json_to_struct(объект, json);
              ...
              map_json_to_struct(другой объект, json для другого объекта);
              ...
              map_json_to_struct(объект другого класса, json для объекта другого класса);
              ...

              если вопрос о возможности перерегистрировать поля с заданием разных опций, то такого нет

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое