1. Introdução
Aplicativos grandes usam configurações para transferir configurações. E geralmente acontece que os recursos de edição e exclusão levam a uma dessincronização entre o código do aplicativo e o que é armazenado nessas mesmas configurações. Simplesmente, nos últimos dados que você nunca usará novamente, liquidar. Idealmente, essas configurações gostaria de rastrear e marcar como obsoletas ou excluir completamente.
O problema
Aconteceu que a maioria das configurações dentro do nosso projeto foram escritas em uma mistura de json e yaml e analisadas usando a biblioteca libconfig . Não havia desejo de reescrever o código correspondente e o conteúdo das configurações, por exemplo, no yaml, especialmente quando há várias outras tarefas interessantes e mais complexas. E a parte da biblioteca escrita em C é boa por si só: é estável e rica em funcionalidades (no wrapper em C ++, tudo não é tão simples).
Um belo dia, tivemos o cuidado de descobrir a quantidade de lixo acumulado nos arquivos de configuração. Infelizmente, libconfig não tinha essa opção. Primeiro, tentamos bifurcar o projeto no github e fazer alterações na parte escrita em C ++ (todos os tipos de métodos de pesquisa e operador []), automatizando o processo de configuração do sinalizador visitado para o nó. Mas isso levaria a um patch muito grande, cuja adoção provavelmente seria adiada. E então a escolha caiu na direção de escrever seu próprio wrapper em C ++, sem afetar o núcleo do libconfig.
Do ponto de vista do uso da saída, temos o seguinte:
#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
No código de exemplo, passamos à configuração module.name. As configurações module.submodules.0.name e module.submodules.1.name não foram acessadas. É isso que nos é relatado no log.
Wrap
Como implementar isso se a bandeira visitada ou algo parecido não estiver dentro da libconfig? Os desenvolvedores da biblioteca pensaram com antecedência e adicionaram a capacidade de conectar um gancho ao nó config_setting_t, que é configurado usando a função config_setting_set_hook e lido usando config_setting_get_hook.
Defina este gancho como:
struct config_setting_hook { bool visited{false}; };
Existem duas estruturas principais dentro da libconfig: config_t e config_setting_t. O primeiro fornece acesso a toda a configuração como um todo e retorna um ponteiro para o nó raiz config_setting_t, o segundo - acesso aos nós pai e filho, além do valor dentro do nó atual.
Envolvemos ambas as estruturas nas classes correspondentes - alças.
Manipule em torno do config_t:
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; };
Observe que uma função é passada para o construtor de configuração que será chamado no destruidor no momento da travessia de todos os nós extremos. Como ele pode ser usado pode ser visto no exemplo acima.
Manipule em torno de 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; };
A principal mágica está nos métodos de pesquisa. Supõe-se que o sinalizador de nós visitados seja definido através do último argumento chamado visit, que é falso por padrão. Você tem o direito de indicar esse valor você mesmo. Mas como o acesso mais frequente aos nós ainda é via operador [], dentro dele o método de pesquisa é chamado com visita igual a true. Assim, os nós para os quais você chama o operador [] serão automaticamente marcados como visitados. Além disso, como visitado, toda a cadeia de nós, da corrente à raiz, será marcada.
Vamos para a implementação. Mostramos isso completamente para a classe 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<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)); }
E parcialmente para 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; }
Consideraremos auxiliares separadamente para trabalhar com um gancho:
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; }
E um auxiliar para ignorar nós extremos:
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); }
Conclusão
Acabou sendo um código bonito e mais flexível, em nossa opinião,. Mas não abandonamos a idéia de fazer alterações semelhantes à biblioteca libconfig original, ou melhor, sua interface escrita em C ++. Uma solicitação de recebimento está sendo preparada agora, mas já estamos trabalhando e estamos limpando nossas configurações de configurações não utilizadas.
App
Confira o código fonte aqui !