[Из песочницы] Отображение данных в формате json на структуру C++
В идеале хотелось бы определить структуру С++
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