[Из песочницы] Отображение данных в формате json на структуру C++

?v=1

В идеале хотелось бы определить структуру С++

struct Person {
 std::string name;
 int age;
 bool student;
} person;

передать экземпляр person в метод отображения вместе с данными json_data

map_json_to_struct(person, json_data)

после чего просто пользоваться заполненной структурой

std::cout << person.name << " : " << person.age;

StructMapping пытается решить эту задачу.

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

#define MANAGED_STRUCT_NAME Person // определяем макрос, который задает имя
                                   // структуры
BEGIN_MANAGED_STRUCT               // определяем начало структуры

MANAGED_FIELD(std::string, name)   // определяем поле с типом 'std::string'и именем
                                   // 'name'
MANAGED_FIELD(int, age)            // определяем поле с типом 'int' и именем 'age'
MANAGED_FIELD(bool, student)       // определяем поле с типом 'bool' и именем
                                   // 'student'

END_MANAGED_STRUCT                 // определяем конец структуры
#undef MANAGED_STRUCT_NAME         // убираем макрос, который задавал имя структуры,
                                   // чтобы не было варнингов о переопределении
                                   // макроса в дальнейшем

создаем экземпляр

Person person;

задаем json данные

std::istringstream json_data(R"json(
{
 "name": "Jeebs",
 "age": 42,
 "student": true
}
)json");

передаем экземпляр person в метод отображения вместе с данными json

struct_mapping::mapper::map_json_to_struct(person, json_data);

пользуемся

std::cout << person.name << " : " << person.age;


Полностью код выглядит так
#include 
#include 

#include "struct_mapping/struct_mapping.h"

#define MANAGED_STRUCT_NAME Person
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)
MANAGED_FIELD(int, age)
MANAGED_FIELD(bool, student)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

int main() {
 Person person;

 std::istringstream json_data(R"json(
  {
   "name": "Jeebs",
   "age": 42,
   "student": true
  }
 )json");

 struct_mapping::mapper::map_json_to_struct(person, json_data);

 std::cout <<
  person.name << " : " <<
  person.age << " : " <<
  std::boolalpha << person.student <<
  std::endl;
}

Кроме простых типов (логического типа, целочисленные, с плавающей точкой и строки) поле структуры может быть так же структурой

MANAGED_FIELD_STRUCT(тип поля, имя поля)


Например так
#define MANAGED_STRUCT_NAME President <-- определяем структуру President
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)
MANAGED_FIELD(double, mass)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

#define MANAGED_STRUCT_NAME Earth
BEGIN_MANAGED_STRUCT

MANAGED_FIELD_STRUCT(President, president) <-- определяем поле с типом President

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

или массивом

MANAGED_FIELD_ARRAY(тип элемента массива, имя поля)

размерность массивов можно увеличивать

MANAGED_FIELD_ARRAY(MANAGED_ARRAY(MANAGED_ARRAY(std::string)), planet_groups)


Пример определения структуры с массивами
#define MANAGED_STRUCT_NAME MiB
BEGIN_MANAGED_STRUCT

MANAGED_FIELD_ARRAY(std::string, friends)
MANAGED_FIELD_ARRAY(MANAGED_ARRAY(std::string), alien_groups)
MANAGED_FIELD_ARRAY(MANAGED_ARRAY(MANAGED_ARRAY(std::string)), planet_groups)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

Основная задача — отобразить данные в формате json на структуру с++. Структура с++ — это набор полей. Каждое поле имеет имя и содержит значение определенного типа. Поэтому на структуру с++ отображаются json объекты. В структуре с++ типы полей соответствуют типам json значений и могут быть следующими:


  • bool — хранит json значение true или false
  • integral или floating point — хранит json число
  • std: string — хранит json строку
  • управляемая структура — хранит json объект
  • управляемый массив — хранит json массив

Для решения задачи используются парсер json и управляемые структуры.


Парсер json

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

Например для json данных

 {
  "price": 273,
  "author": {
   "name": "bk192077"
  },
  "chapters": [
   "launch",
   "new horizons"
  ]
 }

будут выполнена следующая последовательность вызовов

start_struct("")
set_integral("price", 273)
start_struct("author")
set_string("name", "bk192077")
end_struct()
start_array("chapters")
set_string("", "launch")
set_string("", "new horizons")
end_array()
end_struct()


Управляемые структуры

Общими словами можно сказать, что каждая управляемя структура имеет переменную (use_name), хранящую имя используемого поля. Если use_name не пустая, то она хранит имя поля, которому будут транслироваться события от парсера. Изначально use_name пустая (используемых полей нет).


  • если при парсинге встречается начало json объекта или json массива, то:
    • если use_name пустая, то в нее помещается имя поля, парсинг которого был начат
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name
  • если при парсинге встречается конец json объекта или json массива, то:
    • если use_name пустая, то ничего не происходит
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name. Если после трансляции события получен признак завершения цепочки использования, то use_name очищается
  • если при парсинге встречается установка значения, то:
    • если use_name пустая, то значение устанавливается для поля текущего экземпляра структуры по имени этого поля (а в случае массива значение добавляется в массив)
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name

Для этого управляемые структуры содержат несколько служебных функций

void set(std::string const &, bool) {...}
void set(std::string const &, long long) {...}
void set(std::string const &, double) {...}
void set(std::string const &, std::string const &) {...}
void use(std::string const &) {...}
bool release() {...}

Фактически эти функции вызываются парсером в следующем соответствии

Например, для Person

функции set (почти одинаковы по реализации и перегружены по типу устанавливаемого значения)

void set(std::string const & field_name, bool value) {
 if (use_name.empty()) {
  // установка значения поля непосредственно у данного экземпляра структуры
  Fs_set_field>::fs[field_name](*this, value);
 } else {
  // трансляция вызова полю, которое отмечено как используемое
  Fs_set>
   ::fs[use_name](*this, field_name, value);
 }
}

функция use (после этого все вызовы будут транслироваться полю field_name)

void use(std::string const & field_name) {
 if (use_name.empty()) {
  // поле field_name становится используемым
  use_name = field_name;
 } else {
  // вызов транслируется полю use_name
  Fs_use>
   ::fs[use_name](*this, field_name);
 }
}

функция release (из цепочки использования удаляется последний элемент)

bool release() {
 // это была последняя структура в цепочке использования
 if (use_name.empty()) return true;

 if (Fs_release>::fs[use_name](*this)) {
  use_name.clear();
 }

 // это была не последняя структура в цепочке использования
 return false;
}

Поля управляемой структуры инициализируются значением по умолчания, которое возвращает функция инициализации. Перед возвратом значения эта функция выполняет дополнительные действия, зависящие от типа поля.


для простых типов

регистрируется функция, которая выполняет установку значения поля по имени этого поля. Например, для структуры Person и поля age типа int

(такой код будет после подстановки макроса)

int age = [] {
 using value_type =
  std::conditional_t, bool,
   std::conditional_t, std::string const &,
    std::conditional_t, double, long long>>>;
     Fs_set_field>::add(
  "age",
  [] (Person & o, value_type value) {
   o.age = static_cast(value);
  });

 using USING_bool = bool;
 return USING_bool{};
}();

выражение

o.age = static_cast(value);

выполняет непосредственно установку значения для конкретного экземпляра структуры. В таком виде оно используется в gcc, для clang используется

bool Person::*p= &Person::age;
o.*p = static_cast(value);     

(различные варианты присутствуют, потому что clang не поддерживает первый вариант, а gcc падает по ICE на втором варианте)


для структур и массивов

регистрируются шесть функций, каждая из которых просто вызывает такую же функцию у структуры, в которой определяется поле. Например, для структуры Earth и поля president типа President

President president = [] {
 Fs_set>::add(
  "president",
  [] (Earth & o, std::string const & field_name, bool value) {
   o.president.set(field_name, value);
  });

 Fs_set>::add(
  "president",
  [] (Earth & o, std::string const & field_name, double value) {
   o.president.set(field_name, value);
 });

 Fs_set>::add(
  "president",
  [] (Earth & o, std::string const & field_name, long long value) {
   o.president.set(field_name, value);
  });

 Fs_set>::add(
  "president",
  [] (Earth & o, std::string const & field_name, std::string const & value) {
   o.president.set(field_name, value);
  });

 Fs_use>::add(
  "president",
  [] (Earth & o, std::string const & name) {
   o.president.use(name);
  });

 Fs_release>::add(
  "president",
  [] (Earth & o) {
   return o.president.release();
  });

 using USING_President = President;
 return USING_President{};
}();

для clang, опять же, вместо

o.president.set(field_name, value);

используется

President Earth::*p = &Earth::president;
auto& pp = o.*p;
pp.set(field_name, value);


как все будет работать на примере следующего кода

#include 

#include "struct_mapping/struct_mapping.h"

#define MANAGED_STRUCT_NAME Author
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

#define MANAGED_STRUCT_NAME Book
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(int, price)
MANAGED_FIELD_STRUCT(Author, author)
MANAGED_FIELD_ARRAY(std::string, chapters)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

int main() {
 Book white_space;

 std::istringstream json_data(R"json(
  {
   "price": 273,
   "author": {
    "name": "bk192077"
   },
   "chapters": [
    "launch",
    "new horizons"
   ]
  }
 )json");

 struct_mapping::mapper::map_json_to_struct(white_space, json_data);
}


при компиляции

код

#define MANAGED_STRUCT_NAME Author
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

определяет структуру

struct Author {
 void set(std::string const &, bool) {...}
 void set(std::string const &, std::string &) {...}
 void set(std::string const &, long long) {...}
 void set(std::string const &, double) {...}
 void use(std::string const &) {...}
 bool release() {...}

 std::string name;
};

поле name будет инициализировано пустой строкой. До этой инициализации в экземпляр класса Fs_set_field будет добавлена функция установки значения для данного поля

Fs_set_field>::add(
 "name",
 [] (Author & o, std::string cont & value) {
  o.name = value;
 });

код

#define MANAGED_STRUCT_NAME Book
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(int, price)
MANAGED_FIELD_STRUCT(Author, author)
MANAGED_FIELD_ARRAY(std::string, chapters)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

определяет структуру

struct Book {
 void set(std::string const &, bool) {...}
 void set(std::string const &, std::string &) {...}
 void set(std::string const &, long long) {...}
 void set(std::string const &, double) {...}
 void use(std::string const &) {...}
 bool release() {...}

 int price;
 Author author;
 ManagedArray chapters;
};

поле price будет инициализировано нулем. До этой инициализации в экземпляр класса Fs_set_field будет добавлена функция установки значения для данного поля

Fs_set_field>::add(
 "price",
 [] (Book & o, long long value) {
  o.price = static_cast(value);
 });

поле author будет инициализировано значением по умолчанию Author. До этой инициализации в экземпляры классов Fs_ будут добавлены функции:

Fs_set>::add(
 "author",
 [] (Book & o, std::string const & field_name, bool value) {
  o.author.set(field_name, value);
 });

Fs_set>::add(
 "author",
 [] (Book & o, std::string const & field_name, double value) {
  o.author.set(field_name, value);
 });

Fs_set>::add(
 "author",
 [] (Book & o, std::string const & field_name, long long value) {
  o.author.set(field_name, value);
 });

Fs_set>::add(
 "author",
 [] (Book & o, std::string const & field_name, std::string cont & value) {
  o.author.set(field_name, value);
 });

Fs_use>::add(
 "author",
 [] (Book & o, std::string const & name) {
  o.author.use(name);
 });

Fs_release>::add(
 "author",
 [] (Book & o) {
  return o.author.release();
 });

действия с полем chapters будут аналогичны действиям с полем author


при выполнении

вызов map_json_to_struct создает экземпляр парсера и запускает процедуру парсинга, в процессе чего выполняются следующие действия (map_json_to_struct фактически просто транслирует вызовы управляемой структуре, поэтому ее действия не рассматриваются):


  • желаемая простота использования. В основном все сводится к определению структуры с применением набора макросов (хотя можно и вручную все прописать)
  • работает медленно, но для основного применения в качестве загрузки конфигурации и начального состояния приложения подходит (скорость на этом этапе не важна)
  • требуется компиляция с -std=c++17 В основном для:
    • if constexpr
    • static inline

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

© Habrahabr.ru