Запись данных в формате 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; i<Header->Count; 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 <stdio.h>
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("")
{
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("")
{
VALUE("Caption", (char *)&i);
VALUE("Value", i);
}
}
ARRAY("Slots")
for(int i=0; i<3; i++)
VALUE("", i);
}
}
Как выглядит сформированный этой программой JSON-файл можно посмотреть в начале статьи. Все запятые проставлены, все скобочки закрыты когда нужно, в каждой строке нужное количество ведущих пробелов — красота!