A library meant to ease system configuration management supporting:
- xml & json file formats
- configurations distributed across multiple files
- configuration overrides
- configuration scoping
- expanding environment variables
- expanding internal value aliases
- referencing internal config trees
dconfig::Config config = dconfig::FileFactory({"config_cmp1.json", "config_cmp2.xml", "overrides.json"}).create();
dconfig::Config scoped = config.scope("Configuration.Component1");
asssert(config.get<std::string>("Configuration.Component1.name") == scoped.get<std::string>("name"));
- Library requires boost and C++11
- Testing requires gtest and gmock
Make sure BOOST_ROOT environment variable points to boost library directory
- ./configure release
- make
- bin/test
There are three supported ways of building a single dconfig::Config
object.
From text:
dconfig::Config config({file1Contets, file2Contents});
From files:
dconfig::Config config = dconfig::FileFactory({"pathToFile1.json"...}).create();
From main params:
${prog_name} --config pathToFile1.json pathToFile2.json pathToFile3.xml
int main(int argc, const char* argv[])
{
dconfig::Config config = dconfig::InitFactory(argc, argv).create();
...
Note | Entries of subsequent config files overwrite corresponding previous config file entries.
Config class provides two ways of accessing values of a field Config::get<T>(path)
and Config::getAll<T>(path)
. Template parameter allows Config to interpret the underlying value(s) as user selected types.
In case of single element value:
boost::optional<int32_t> value = config.get<int32_t>("dot.separated.path.to.element");
In case of multiple elements with the same name:
std::vector<int32_t> value = config.getAll<int32_t>("dot.separated.path.to.repeated.element");
Config scoping is a feature that allows to create a view (not an actual copy) of a config subset. All paths in the scoped config become relative to the scope. This functionality has been introduced to support configurations for reusable components.
dconfig::Config scoped = config.scope("Configuration.Component1");
assert(config.get<std::string>("Configuration.Component1.name") == scoped.get<std::string>("name"));
Note | Scoping is a cheap operation as the scoped config points to the same internal representation as the original one
Instead of necessarily providing all the information fixed in the config it is possible to use environment variables that will be filled in by the Config class during building phase. Environment variable syntax %env.{name of the variable}%
.
{
"Config":
{
"User" : "%env.USER%",
"Path" : "%env.PWD%"
}
}
In certain cases it is necessary to repeat same value inside the config in multiple places. To avoid duplication dconfig
supports value aliasing. Any previous (in top down order) tag value can be mentioned in any subsequents tags using a special syntax %config.{dot.separted.path.to.property}%
{
"Config":
{
"User" : "%env.USER%",
"Host" : "%env.HOSTNAME%"
},
"IOComponent":
{
"Identification" : "%config.Config.User%_%config.Config.Host%"
}
}
Becomes (given USER=username, HOST=hostname)
{
"Config":
{
"User" : "username",
"Host" : "hostname"
},
"IOComponent":
{
"Identification" : "username_hostname"
}
}
Note | This functionality is based on text replacement, thus there are no limitations on the number of aliases in one expression
While value aliasing is a useful tool to reference other configuration elements, it is limited to simple properties. When referencing configuration tree nodes is needed, dconfig provides node aliasing feature. In order to make use of it one needs to put %node.{dot.separted.path.to.node}%
as property value as demonstrated below.
{
"Config":
{
"Identification":
{
"User" : "username",
"Host" : "hostname"
}
},
"IOComponent":
{
"Identification" : "%node.Config.Identification%"
}
}
Becomes
{
"Config":
{
"Identification":
{
"User" : "username",
"Host" : "hostname"
}
},
"IOComponent":
{
"Identification" :
{
"User" : "username",
"Host" : "hostname"
}
}
}
Note | Aliased nodes are references and not copies, thus yielding better runtime performance and more compact representation in memory.
Paths to nodes and properties can be provided in two ways, absolute and relative. Aboslute paths require the user to specify complete set of dot separated node names that lead to the referenced property or node (see prior examples). Relative paths instead allow the user to specify referenced property or node in relation to reference position. The number of dots after function specifier (i.e. %param
or %node
) denote how many levels up in the configuration tree to go.
{
"Component" :
{
"Config":
{
"Identification":
{
"User" : "username",
"Host" : "hostname"
}
},
"IOComponent":
{
"Address" :
{
"node" : "127.0.0.1",
"port" : "27800"
},
"Destination" : "%config.Address.node%:%config.Address.port%",
"Identification" : "%node..Config.Identification%",
"Source" : "%config..Config.Identification.Host%"
}
}
}
Becomes
{
"Component" :
{
"Config":
{
"Identification":
{
"User" : "username",
"Host" : "hostname"
}
},
"IOComponent":
{
"Address" :
{
"node" : "127.0.0.1",
"port" : "27800"
},
"Destination" : "127.0.0.1:27800",
"Identification" :
{
"User" : "username",
"Host" : "hostname"
},
"Source" : "hostname"
}
}
}
Note | Relative paths take precedence in case of ambiguity.
The api is the same for XML and JSON files with one exception namely arrays. As there is no notion of an array in XML the syntax for array in XML is slightly different than for json and requires the usage of special tag character:
std::vector<int32_t> values = config.getAll<int32_t>("Config.Array.");
or
std::vector<int32_t> values = config.scope("Config.Array").getAll<int32_t>(".");
Corresponding json file:
{
"Config":
{
"Array": [ 10, 20 ]
}
}
Corresponding xml file:
<Config>
<Array>
<.>10</.>
<.>20</.>
</Array>
</Config>
Using getAll<...>("Config.Element")
is different from getAll<...>("Config.Element.")
as the former refers to repeated Element tag and the latter to an array under Element tag. Distinguishing the two is important to support repeated node arrays, like in the example below:
for (const auto& scope : config.scopes("Config.ManyArrays"))
auto values = scope.getAll<int>("");
Corresponding json file:
{
"Config":
{
"ManyArrays": [ 10, 20 ],
"ManyArrays": [ 30, 40 ]
}
}
Corresponding xml file:
<Config>
<ManyArrays>
<.>10</.>
<.>20</.>
</ManyArrays>
<ManyArrays>
<.>30</.>
<.>40</.>
</ManyArrays>
</Config>
In dconfig values are stored as vectors of strings (even single values are just one element vectors). It is possible to access those internal arrays of values directly from the API using getRef()
method and avoid copying data.
const std::vector<std::string>& values = config.getRef("Config.SingleValue");
assert(!values.empty());
const std::string& value = values[0];
or
const std::vector<std::string>& values = config.getRef("Config.ArrayOfValues.");
Corresponding json file:
{
"Config":
{
"SingleValue" : "value",
"ArrayOfValues": [ 10, 20 ]
}
}
Corresponding xml file:
<Config>
<SingleValue>value</SingleValue>
<ArrayOfValues>
<.>10</.>
<.>20</.>
</ArrayOfValues>
</Config>
Note | Accessing nodes with getRef() method is not supported and will return an empty array.
Copyright Adam Lach 2020. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt).