Konfigurationsdateien. Libconfig-Bibliothek und Definition nicht verwendeter Einstellungen

Einleitung


Große Anwendungen verwenden Configs, um Einstellungen zu übertragen. Und es kommt häufig vor, dass das Bearbeiten und Löschen von Features zu einer Desynchronisierung zwischen dem Anwendungscode und den in denselben Einstellungen gespeicherten Elementen führt. Einfach, in den letzten Daten, die Sie nie wieder verwenden werden, abrechnen. Solche Einstellungen möchte ich idealerweise nachverfolgen und als veraltet markieren oder komplett löschen.


Das problem


Es kam vor, dass die meisten Konfigurationen in unserem Projekt in einer Mischung aus json und yaml geschrieben und mit der libconfig- Bibliothek analysiert wurden. Es bestand kein Wunsch, den entsprechenden Code und den Inhalt der Konfigurationsdateien, beispielsweise in yaml, neu zu schreiben, insbesondere wenn es eine Reihe anderer interessanter und komplexerer Aufgaben gibt. Und der Teil der Bibliothek, der in C geschrieben ist, ist an sich gut: Es ist stabil und reich an Funktionalität (im Wrapper in C ++ ist alles nicht so einfach).


Eines schönen Tages haben wir darauf geachtet, herauszufinden, wie viel Müll wir in den Konfigurationsdateien angesammelt hatten. Leider hatte libconfig keine solche Option. Zuerst haben wir versucht, das Projekt auf github zu verteilen und Änderungen an dem Teil vorzunehmen, der in C ++ geschrieben wurde (alle Arten von Lookup- und Operator [] -Methoden), um den Prozess des Setzens des besuchten Flags für den Knoten zu automatisieren. Dies würde jedoch zu einem sehr großen Patch führen, dessen Einführung sich wahrscheinlich verzögern würde. Und dann fiel die Wahl darauf, einen eigenen Wrapper in C ++ zu schreiben, ohne den Kern von libconfig zu beeinträchtigen.


Unter dem Gesichtspunkt der Verwendung der Ausgabe haben wir Folgendes:


#include <variti/util/config.hpp> #include <iostream> #include <cassert> 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 

Im Beispielcode haben wir uns dem Setzen von module.name zugewandt. Auf die Einstellungen module.submodules.0.name und module.submodules.1.name wurde nicht zugegriffen. Dies wird uns im Protokoll mitgeteilt.


Wickeln


Wie kann man das implementieren, wenn das besuchte Flag oder so etwas nicht in libconfig enthalten ist? Die Entwickler der Bibliothek haben im Voraus überlegt und die Möglichkeit hinzugefügt, einen Hook an den Knoten config_setting_t anzuhängen, der mit der Funktion config_setting_set_hook festgelegt und mit config_setting_get_hook gelesen wird.


Definiere diesen Haken als:


 struct config_setting_hook { bool visited{false}; }; 

In libconfig gibt es zwei Hauptstrukturen: config_t und config_setting_t. Der erste bietet Zugriff auf die gesamte Konfiguration als Ganzes und gibt einen Zeiger auf den Stammknoten config_setting_t zurück, der zweite Zugriff auf die übergeordneten und untergeordneten Knoten sowie den Wert innerhalb des aktuellen Knotens.


Wir verpacken beide Strukturen in die entsprechenden Klassen - Handles.


Handle um config_t herum:


 using config_notify = std::function<void(const config_setting&)>; 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; }; 

Beachten Sie, dass eine Funktion an den config-Konstruktor übergeben wird, die beim Durchlaufen aller Extremknoten im Destruktor aufgerufen wird. Wie es verwendet werden kann, können Sie dem obigen Beispiel entnehmen.


Handle um config_setting_t herum:


 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; }; 

Die Hauptmagie liegt in Nachschlagemethoden. Es wird davon ausgegangen, dass das Flag für besuchte Knoten durch das letzte als visit bezeichnete Argument gesetzt wird, das standardmäßig falsch ist. Sie haben das Recht, diesen Wert selbst anzugeben. Da der häufigste Zugriff auf die Knoten jedoch immer noch über operator [] erfolgt, wird die Lookup-Methode mit visit gleich true aufgerufen. Daher werden die Knoten, für die Sie Operator [] aufrufen, automatisch als besucht markiert. Außerdem wird bei einem Besuch die gesamte Knotenkette von der aktuellen bis zur Wurzel markiert.


Fahren wir mit der Implementierung fort. Wir zeigen es komplett für die config Klasse:


 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<config_setting_hook*>(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)); } 

Und teilweise für 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; } 

Wir werden Helfer für die Arbeit mit einem Haken separat betrachten:


 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_hook*>(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; } 

Und ein Helfer, um extreme Knoten zu umgehen:


 template <typename F> 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); } 

Fazit


Unserer Meinung nach war es ein schöner und flexibler Code. Wir haben jedoch nicht auf die Idee verzichtet, ähnliche Änderungen an der ursprünglichen libconfig-Bibliothek oder vielmehr an der in C ++ geschriebenen Schnittstelle vorzunehmen. Eine Pull-Anfrage wird gerade vorbereitet, aber wir arbeiten bereits und bereinigen unsere Configs von nicht verwendeten Einstellungen.


App


Hier geht es zum Quellcode!

Source: https://habr.com/ru/post/de477454/


All Articles