Конфигурационные файлы. Библиотека libconfig и определение неиспользуемых настроек

?v=1

Введение

Большие приложения используют конфиги для передачи настроек. И часто случаетя так, что редактирование и удаление фич приводит к рассинхронизации между кодом приложения и тем, что хранится в этих самых настройках. Попросту, в последних оседают данные, которыми вы больше никогда не воспользуетесь. Такие настройки в идеале хотелось бы отслеживать и помечать как deprecated, либо вовсе удалять.


Проблема

Так исторически сложилось, что большая часть конфигов внутри нашего проекта была написана на смеси json и yaml и парсилась с использованием библиотеки libconfig. Переписывать соответствующий код и содержимое конфигов, например на yaml, желания никакого не было, особенно когда есть куча других интересных и более сложных задач. Да и та часть библиотеки, что написана на C, сама по себе хороша: стабильна и богата функционалом (в обертке на C++ все не так однозначно).

В один прекрасный день мы озаботились тем, чтобы выяснить, сколько мусора у нас накопилось в конфигурационных файлах. И такой опции у libconfig, к сожалению, не оказалось. Сначала мы попытались форкнуть проект на github и внести правки в ту часть, что написана на C++ (всевозможные методы lookup и operator[]), автоматизировав процесс выставления флага visited для ноды. Но это привело бы к очень большому патчу, принятие которого наверняка затянулось бы. И тогда выбор пал в сторону написания своей собственной обертки на C++, не затрагивая ядро libconfig.

С точки зрения использования на выходе у нас получилось следующее:

#include 

#include 
#include 

int main(int argc, char* argv[])
{
  using namespace variti;
  using namespace variti::util;
  assert(argc = 2);
  config conf(
    [](const config_setting& st) {
      if (!st.visited())
        std::cerr << "config not visited: " << st.path() << "\n";
    });
  conf.load(argv[1]);
  auto root = conf.root();
  root["module"]["name"].to_string();
  return 0;
}
laptop :: work/configpp/example ‹master*› % cat config_example1.conf                                                                                                                                                                         
version = "1.0";
module: {
  name = "module1";
  submodules = (
    { name = "submodule1"; },
    { name = "submodule2"; }
  );
};
laptop :: work/configpp/example ‹master*› % ./config-example1 config_example1.conf                                                                                                                                                           
config not visited: root.module.submodules.0.name
config not visited: root.module.submodules.1.name

В коде примера мы обратились к настройке module.name. К настройкам module.submodules.0.name и module.submodules.1.name не обращались. Об этом нам и сообщается в логе.


Обертывание

Как это реализовать, если флага visited или чего-то подобного нет внутри libconfig? Разработчики библиотеки подумали заранее и добавили возможность прицепить к ноде config_setting_t хуку, которая проставляется с помощью функции config_setting_set_hook и читается с помощью config_setting_get_hook.

Определим эту хуку как:

struct config_setting_hook
{
  bool visited{false};
};

Внутри libconfig есть две основных структуры: config_t и config_setting_t. Первая предоставляет доступ ко всему конфигу в целом и возвращает указатель на рутовую ноду config_setting_t, вторая — доступ к родительской и дочерним нодам, а также значению внутри текущей ноды.

Обернем обе структуры в соответствующие классы — хэндлы.

Хэндл вокруг config_t:

using config_notify = std::function;

struct config
  : boost::noncopyable
{
  config(config_notify n = nullptr);
 ~config();

  void load(const std::string& filename);

  config_setting root() const;

  config_notify n;
  config_t* h;
};

Обратим внимание, что в конструктор config передается функция, которая будет вызвана в деструкторе в момент обхода всех крайних нод. Как она может быть использована — можно увидеть в примере выше.

Хэндл вокруг config_setting_t:

struct config_setting
  : boost::noncopyable
{
  config_setting(config_setting_t* h, bool visit = false);
 ~config_setting();

  bool to_bool() const;
  std::int32_t to_int32() const;
  std::int64_t to_int64() const;
  double to_double() const;
  std::string to_string() const;

  bool is_bool() const;
  bool is_int32() const;
  bool is_int64() const;
  bool is_double() const;
  bool is_string() const;
  bool is_group() const;
  bool is_array() const;
  bool is_list() const;
  bool is_scalar() const;
  bool is_root() const;

  std::string path() const;

  std::size_t size() const;

  bool exists(const std::string& name) const;

  config_setting parent() const;

  config_setting lookup(const std::string& name, bool visit = false) const;
  config_setting lookup(std::size_t indx, bool visit = false) const;

  config_setting operator[](const std::string& name) const;
  config_setting operator[](std::size_t indx) const;

  std::string filename() const;
  std::size_t fileline() const;

  bool visited() const;

  config_setting_t* h;
};

Основная магия кроется в методах lookup. Предполагается, что флаг visited ноды устанавливается через последний аргумент под названием visit, который по умолчанию равен false. Вы вправе сами указывать это значение. Но так как наиболее частый доступ к нодам все же происходит через operator[], то внутри них метод lookup вызывается с visit, равным true. Таким образом, ноды, для которых вы вызовете operator[], будут автоматом помечены как visited. Более того, как visited будет помечена вся цепочка нод от текущей и до рутовой.

Перейдем к реализации. Покажем полностью ее для класса config:

config::config(config_notify n)
  : n(n)
{
  h = (config_t*)malloc(sizeof(config_t));
  config_init(h);
  config_set_destructor(h,
    [](void* p) {
      delete reinterpret_cast(p);
    });
}

config::~config()
{
  if (n)
    for_each(root(), n);
  config_destroy(h);
  free(h);
}

void config::load(const std::string& filename)
{
  if (!config_read_file(h, filename.c_str()))
    throw std::runtime_error(std::string("config read file error: ") + filename);
}

config_setting config::root() const
{
  return config_setting(config_root_setting(h));
}

И частично для config_setting:

config_setting::config_setting(config_setting_t* h, bool visit)
  : h(h)
{
  assert(h);
  if (!config_setting_get_hook(h))
    hook(h, new config_setting_hook())
  if (visit)
    visit_up(h);
}

config_setting::~config_setting()
{
  h = nullptr;
}

std::size_t config_setting::size() const
{
  return config_setting_length(h);
}

config_setting config_setting::parent() const
{
  return config_setting(config_setting_parent(h));
}

bool config_setting::exists(const std::string& name) const
{
  if (!is_group())
    return false;
  return config_setting_get_member(h, name.c_str());
}

config_setting config_setting::lookup(const std::string& name, bool visit) const
{
  assert(is_group());
  auto p = config_setting_get_member(h, name.c_str());
  if (!p)
    throw_not_found(*this);
  return config_setting(p, visit);
}

config_setting config_setting::lookup(std::size_t indx, bool visit) const
{
  assert(is_group() || is_array() || is_list());
  auto p = config_setting_get_elem(h, indx);
  if (!p)
    throw_not_found(*this);
  return config_setting(p, visit);
}

config_setting config_setting::operator[](const std::string& name) const
{
  return lookup(name, true);
}

config_setting config_setting::operator[](std::size_t indx) const
{
  return lookup(indx, true);
}

bool config_setting::visited() const
{
  return boost::algorithm::starts_with(path(), "root") ||
         boost::algorithm::starts_with(path(), "root.version") ||
         hook(h)->visited;
}

Отдельно рассмотрим хэлперы для работы с хукой:

void hook(config_setting_t* h, config_setting_hook* k)
{
  config_setting_set_hook(h, k);
}

config_setting_hook* hook(config_setting_t* h)
{
  return reinterpret_cast(config_setting_get_hook(h));
}

void visit_up(config_setting_t* h)
{
  for (; !config_setting_is_root(h) && !hook(h)->visited; h = config_setting_parent(h))
    hook(h)->visited = true;
}

И хэлпер для обхода крайних нод:

template 
void for_each(const config_setting& st, F f)
{
  if (st.size())
    for (std::size_t i = 0; i < st.size(); ++i)
      for_each(st.lookup(i), f);
  else
    f(st);
}


Вывод

Получился красивый и более гибкий, на наш взгляд, код. Но мы не забросили мысль о том, чтобы внести аналогичные изменения в оригинальную библиотеку libconfig, а, точнее, в ее интерфейс, написанный C++. Сейчас готовится пулл-реквест, ну, а мы уже работаем и чистим наши конфиги от неиспользуемых настроек.


Приложение

Ознакомиться с исходным кодом можно тут!

© Habrahabr.ru