Запись данных в формате JSON

в 6:10, , рубрики: c++, json

Запись данных в формате 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("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-файл можно посмотреть в начале статьи. Все запятые проставлены, все скобочки закрыты когда нужно, в каждой строке нужное количество ведущих пробелов — красота!

Автор: Alarf

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js