[Из песочницы] Запись данных в формате JSON

В одной из моих программ понадобилась запись данных в формате JSON. Вкратце — XML-подобный формат, вполне подходит на замену Windows INI-файлам или тому же XML. Удобен тем, что поддерживает массивы и вложенность собственных структур, но при этом не замусоривает файл данных своими тегами до полной нечитабельности человеком. Вот пример файла данных: { «Comment»: «My comment», «Count»:10, «DiskParam»: { «DB»:10.000000, «DBAngle»:1.234000 }, «Range»: true, «Blades»: [ { «Caption»: «A», «Value»:65 }, { «Caption»: «B», «Value»:66 }, { «Caption»: «C», «Value»:67 } ], «Slots»: [ 0,1,2 ] } Формат довольно простой, вполне можно работать с ним без всяких библиотек. Поэтому первоначально за запись отвечал примерно такой участок кода: fprintf (pOut,»{\n»); fprintf (pOut,» \«Comment\»:\»%s\», Header→Comment); fprintf (pOut,»,\n \«NumSt\»:%d», Header→NumSt); //Пропущено немного кода fprintf (pOut,»,\n \«DBMax\»:%lf», Header→DBMax); fprintf (pOut,»,\n \«Range\»:%s», Header→Range? «true»: «false»); fprintf (pOut,»,\n \«Blades\»:\n [»); for (int i=0; iCount; i++) { TElement &e=Element[i]; fprintf (pOut, i?»,\n {»:»\n {»); fprintf (pOut,»\«Caption\»:\»%s\», e.Caption); fprintf (pOut,»,\«Value\»:%lf», e.BaseChar); fprintf (pOut,»}»); } fprintf (pOut,»\n ]»); //Пропущено много кода fprintf (pOut,»\n}»); Корявенько, хотя вполне работоспособно. Но программа активно дорабатывалась, формат данных менялся по 5 раз на дню и остро встала проблема отслеживания всех изменений. Несмотря на некоторое форматирование исходника было тяжело не забыть закрыть какой-нибудь тег или правильно напечатать нужное число пробелов для форматирования уже собственно файла данных. Даже в приведенном фрагменте перед публикацией обнаружилась ошибка, не ставилась запятая между элементами массива.Решил я этот техпроцесс слегка механизировать и создать микробиблиотеку для работы с JSON.Что я хотел? Чтобы в своей программе я писал что-то на псевдоязыке:

Key («Ключ1»); Value («Значение1»); Key («Ключ2»); Value («Значение2»); Object («Объект1»); Key («Ключ3»); Value («Значение3»); //Ключ3, Ключ4 являются элементами Объект1 Key («Ключ4»); Value («Значение4»); Array («Массив1»); Key («Ключ5»); Value («Значение5»); //Ключ5…КлючN являются элементами Массив1 Key («Ключ6»); Value («Значение6»); … Key («КлючN»); Value («ЗначениеN»); А компилятор/программа пусть сами учтут отступы, которые определяют структуру файла данных. В нужный момент подставят открывающий и, главное, закрывающий тег. Дело осложнялось тем, что внутри этого скрипта хотелось использовать конструкции C++, например циклы внутри массивов.После нескольких дней непрерывной мозговой осады этой проблемы нашлось довольно изящное решение. Для контроля за вложением друг в друга JSON-сущностей и своевременного закрытия тегов используется область видимости переменных. Все очень просто, создается экземпляр одного из классов TJson*** — записывается ключ и открывающий тег и все следующие созданные объекты считаются его вложениями. Уничтожается экземпляр — ставится закрывающий тег.

#define TCF_USED 1

class TTagCloser { public: TTagCloser *Owner; static TTagCloser *Current; static int Depth; int Flags; int Count; int operator ()(){Flags^=TCF_USED; return Flags&TCF_USED;} TTagCloser (){Count=Flags=0; Owner=Current; Current=this; Depth++;} ~TTagCloser (){Depth--; Current=Owner;} }; TTagCloser *TTagCloser: Current=NULL; int TTagCloser: Depth=-1; Простой класс, все назначение которого — временно связать порожденные объекты в некое подобие дерева. Для чего нужен перегруженный operator () будет понятно чуть позже.У этого класса есть наследник, в котором заложен базовый функционал записи в JSON-формате. Программист должен только переопределить функции Write***.

#define TCF_OBJECT 4 #define TCF_ARRAY 2

class TJsonTagCloser: public TTagCloser { public: void WriteTab (); void WriteInt (int); void WriteDouble (double); void WriteStr (char *);

TJsonTagCloser (char *Key); }; //---------------------------------------------------------------------------- TJsonTagCloser: TJsonTagCloser (char *Key): TTagCloser () { if (Owner) { if (Owner→Count) WriteStr (»,»); if (Owner→Flags&TCF_ARRAY) { if (! Owner→Count) WriteTab (); } else { WriteTab (); WriteStr (»\»); if (Key) WriteStr (Key); WriteStr (»\»:»); } Owner→Count++; } } Функция WriteTab () введена в программу удобства гиков, любящих лазить в файлы данных «Блокнотом». Она должна записать в файл данных перевод строки и число пробелов, соответствующее глубине вложения (TTagCloser: Depth). Если бы форматирование не было нужно, то функция выродилась бы в WriteTab (){;}.У меня в тестовом примере функции Write*** определены так:

#include void TJsonTagCloser: WriteTab (){printf (»\n%*s», Depth*2,»);} void TJsonTagCloser: WriteInt (int Value){printf (»%d», Value);} void TJsonTagCloser: WriteDouble (double Value){printf (»%lf», Value);} void TJsonTagCloser: WriteStr (char *Value){printf (»%s», Value);} JSON-формат предполагает наличие в потоке данных Объектов (смахивают на СИшные структуры), Массивов (они и в Африке массивы) и просто пар «Ключ: Значение». Все это многообразие может быть перемешано и вложено друг в дружку, например в паре «Ключ: Значение» Значением может быть Массив Объектов. Для работы с этими сущностями созданы следующие классы: class TJsonArray: public TJsonTagCloser { public: TJsonArray (char *Key); ~TJsonArray (); }; class TJsonObject: public TJsonTagCloser { public: TJsonObject (char *Key); ~TJsonObject (); }; class TJsonValue: public TJsonTagCloser { public: TJsonValue (char *Key, int Value): TJsonTagCloser (Key){WriteInt (Value);} TJsonValue (char *Key, double Value): TJsonTagCloser (Key){WriteDouble (Value);} TJsonValue (char *Key, bool Value): TJsonTagCloser (Key){WriteStr ((char *)(Value? «true»: «false»));} TJsonValue (char *Key, char *Value); }; TJsonArray: TJsonArray (char *Key): TJsonTagCloser (Key) { Flags|=TCF_ARRAY; if (Owner && (!(Owner→Flags&TCF_ARRAY) || Owner→Count>1)) WriteTab (); WriteStr (»[»); } TJsonArray::~TJsonArray () { WriteTab (); WriteStr (»]»); } //---------------------------------------------------------------------------- TJsonObject: TJsonObject (char *Key): TJsonTagCloser (Key) { Flags|=TCF_OBJECT; if (Owner && (!(Owner→Flags&TCF_ARRAY) || Owner→Count>1)) WriteTab (); WriteStr (»{»); } TJsonObject::~TJsonObject () { WriteTab (); WriteStr (»}»); } TJsonValue: TJsonValue (char *Key, char *Value): TJsonTagCloser (Key) { if (Value) { WriteStr (»\»); WriteStr (Value); WriteStr (»\»); } else WriteStr («null»); } Для удобства использования библиотеки в своей программе определены макросы: #define ARRAY (k) for (TJsonArray array (k); array ();) #define OBJECT (k) for (TJsonObject object (k); object ();) #define VALUE (k, v) {TJsonValue value (k, v);} Вот и добрались до перегруженного operator (). Он нужен для однократного выполнения тела цикла for, то есть в первый вызов он возвращает true, а в последующие — false.А вот так в теле программы выглядит скрипт, на котором пишется заполнение файла данных:

void main () { OBJECT («TrashKey») //Ключ корневого объекта игнорируется, даже если ошибочно задан { VALUE («Comment», «My comment»); VALUE («Count», 10); OBJECT («DiskParam») { VALUE («DB», 10.0); VALUE («DBAngle», 1.234); } VALUE («Range», true); ARRAY («Blades») { for (int i='A'; i<'A'+3; i++) OBJECT("TrashKey") //Внутри массива ключ игнорируется, даже если ошибочно задан { VALUE("Caption", (char *)&i); //!!процессорно-зависимый код VALUE("Value", i); } } ARRAY("Slots") for(int i=0; i<3; i++) VALUE("", i); } } Как выглядит сформированный этой программой JSON-файл можно посмотреть в начале статьи. Все запятые проставлены, все скобочки закрыты когда нужно, в каждой строке нужное количество ведущих пробелов — красота!

© Habrahabr.ru