mirror of
https://github.com/RetroDECK/Supermodel.git
synced 2025-04-10 19:15:14 +00:00
Added support for multiple value types in config nodes.
Removed Ptr_t and ConstPtr_t from public interface, moved config tree builders into their own file, and made them pass by reference and value. Exceptions can be thrown now on lookup failures. Removed s_empty_node -- failed lookups in [] operator create a permanent "hidden" child. Updated comment in NewConfig.cpp.
This commit is contained in:
parent
01a6ad1596
commit
2efe18b525
235
Src/Util/ConfigBuilders.cpp
Normal file
235
Src/Util/ConfigBuilders.cpp
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
#include "Util/NewConfig.h"
|
||||||
|
#include "OSD/Logger.h"
|
||||||
|
#include "Pkgs/tinyxml2.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <queue>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace Util
|
||||||
|
{
|
||||||
|
namespace Config
|
||||||
|
{
|
||||||
|
static void PopulateFromXML(Util::Config::Node *config, const tinyxml2::XMLDocument &xml)
|
||||||
|
{
|
||||||
|
using namespace tinyxml2;
|
||||||
|
std::queue<std::pair<const XMLElement *, Util::Config::Node *>> q;
|
||||||
|
|
||||||
|
// Push the top level of the XML tree
|
||||||
|
for (const XMLElement *e = xml.RootElement(); e != 0; e = e->NextSiblingElement())
|
||||||
|
q.push( { e, config } );
|
||||||
|
|
||||||
|
// Process the elements in order, pushing subsequent levels along with the
|
||||||
|
// config nodes to add them to
|
||||||
|
while (!q.empty())
|
||||||
|
{
|
||||||
|
const XMLElement *element = q.front().first;
|
||||||
|
Util::Config::Node *parent_node = q.front().second;
|
||||||
|
q.pop();
|
||||||
|
|
||||||
|
// Create a config entry for this XML element
|
||||||
|
Util::Config::Node *node = &parent_node->Add(element->Name(), element->GetText() ? std::string(element->GetText()) : std::string());
|
||||||
|
|
||||||
|
// Create entries for each attribute
|
||||||
|
for (const XMLAttribute *a = element->FirstAttribute(); a != 0; a = a->Next())
|
||||||
|
node->Add(a->Name(), a->Value());
|
||||||
|
|
||||||
|
// Push all child elements
|
||||||
|
for (const XMLElement *e = element->FirstChildElement(); e != 0; e = e->NextSiblingElement())
|
||||||
|
q.push( { e, node } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FromXML(Node *config, const std::string &text)
|
||||||
|
{
|
||||||
|
*config = Node("xml");
|
||||||
|
using namespace tinyxml2;
|
||||||
|
XMLDocument xml;
|
||||||
|
auto ret = xml.Parse(text.c_str());
|
||||||
|
if (ret != XML_SUCCESS)
|
||||||
|
{
|
||||||
|
ErrorLog("Failed to parse XML (%s).", xml.ErrorName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
PopulateFromXML(config, xml);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FromXMLFile(Node *config, const std::string &filename)
|
||||||
|
{
|
||||||
|
*config = Node("xml");
|
||||||
|
using namespace tinyxml2;
|
||||||
|
XMLDocument xml;
|
||||||
|
auto ret = xml.LoadFile(filename.c_str());
|
||||||
|
if (ret != XML_SUCCESS)
|
||||||
|
{
|
||||||
|
ErrorLog("Failed to parse %s (%s).", filename.c_str(), xml.ErrorName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
PopulateFromXML(config, xml);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string StripComment(const std::string &line)
|
||||||
|
{
|
||||||
|
// Find first semicolon not enclosed in ""
|
||||||
|
bool inside_quotes = false;
|
||||||
|
for (auto it = line.begin(); it != line.end(); ++it)
|
||||||
|
{
|
||||||
|
inside_quotes ^= (*it == '\"');
|
||||||
|
if (!inside_quotes && *it == ';')
|
||||||
|
return std::string(line.begin(), it);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ParseAssignment(Node *current_section, const std::string &filename, size_t line_num, const std::string &line)
|
||||||
|
{
|
||||||
|
size_t idx_equals = line.find_first_of('=');
|
||||||
|
if (idx_equals == std::string::npos)
|
||||||
|
{
|
||||||
|
ErrorLog("%s:%d: Syntax error. No '=' found.", filename.c_str(), line_num);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string lvalue(TrimWhiteSpace(std::string(line.begin(), line.begin() + idx_equals)));
|
||||||
|
if (lvalue.empty())
|
||||||
|
{
|
||||||
|
ErrorLog("%s:%d: Syntax error. Setting name missing before '='.", filename.c_str(), line_num);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get rvalue, which is allowed to be empty and strip off quotes if they
|
||||||
|
// exist. Only a single pair of quotes encasing the rvalue are permitted.
|
||||||
|
std::string rvalue(TrimWhiteSpace(std::string(line.begin() + idx_equals + 1, line.end())));
|
||||||
|
size_t idx_first_quote = rvalue.find_first_of('\"');
|
||||||
|
size_t idx_last_quote = rvalue.find_last_of('\"');
|
||||||
|
if (idx_first_quote == 0 && idx_last_quote == (rvalue.length() - 1) && idx_first_quote != idx_last_quote)
|
||||||
|
rvalue = std::string(rvalue.begin() + idx_first_quote + 1, rvalue.begin() + idx_last_quote);
|
||||||
|
if (std::count(rvalue.begin(), rvalue.end(), '\"') != 0)
|
||||||
|
{
|
||||||
|
ErrorLog("%s:%d: Warning: Extraneous quotes present on line.", filename.c_str(), line_num);
|
||||||
|
}
|
||||||
|
// In INI files, we do not allow multiple settings with the same key. If
|
||||||
|
// a setting is specified multiple times, previous ones are overwritten.
|
||||||
|
current_section->Set(lvalue, rvalue);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FromINIFile(Node *config, const std::string &filename)
|
||||||
|
{
|
||||||
|
std::ifstream file;
|
||||||
|
file.open(filename);
|
||||||
|
if (file.fail())
|
||||||
|
{
|
||||||
|
ErrorLog("Unable to open '%s'. Configuration will not be loaded.", filename.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
*config = Node("Global"); // the root is also the "Global" section
|
||||||
|
Node &global = *config;
|
||||||
|
Node *current_section = &global;
|
||||||
|
|
||||||
|
size_t line_num = 1;
|
||||||
|
while (!file.eof())
|
||||||
|
{
|
||||||
|
if (file.fail())
|
||||||
|
{
|
||||||
|
ErrorLog("%s:%d: File read error. Configuration will be incomplete.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
std::getline(file, line);
|
||||||
|
line = StripComment(line);
|
||||||
|
line = TrimWhiteSpace(line);
|
||||||
|
|
||||||
|
if (!line.empty())
|
||||||
|
{
|
||||||
|
if (*line.begin() == '[' && *line.rbegin() == ']')
|
||||||
|
{
|
||||||
|
// Check if section exists else create a new one
|
||||||
|
std::string section(TrimWhiteSpace(std::string(line.begin() + 1, line.begin() + line.length() - 1)));
|
||||||
|
if (section.empty() || section == "Global") // empty section names (e.g., "[]") assigned to "[ Global ]"
|
||||||
|
current_section = &global;
|
||||||
|
else if (!global.TryGet(section))
|
||||||
|
{
|
||||||
|
Node &new_section = global.Add(section);
|
||||||
|
current_section = &new_section;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
current_section = global.TryGet(section);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ParseAssignment(current_section, filename, line_num, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
line_num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Produces a new INI section by merging two existing sections.
|
||||||
|
*
|
||||||
|
* - Nodes from x and y with children are ignored as per INI semantics.
|
||||||
|
* The presence of children indicates a section, not a setting.
|
||||||
|
* - Settings from y override settings in x if already present.
|
||||||
|
* - If multiple settings with the same key are present in either of the
|
||||||
|
* source configs (which technically violates INI semantics), the last
|
||||||
|
* ends up being used.
|
||||||
|
* - x's key is retained.
|
||||||
|
*/
|
||||||
|
void MergeINISections(Node *merged, const Node &x, const Node &y)
|
||||||
|
{
|
||||||
|
*merged = Node(x.Key()); // result has same key as x
|
||||||
|
// First copy settings from section x
|
||||||
|
for (auto it = x.begin(); it != x.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->IsLeaf())
|
||||||
|
{
|
||||||
|
// INI semantics: take care to only create a single setting per key
|
||||||
|
merged->Set(it->Key(), it->GetValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Merge in settings from section y
|
||||||
|
for (auto it = y.begin(); it != y.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->IsLeaf() && it->Exists())
|
||||||
|
merged->Set(it->Key(), it->GetValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WriteSection(std::ofstream &file, const Node §ion)
|
||||||
|
{
|
||||||
|
file << "[ " << section.Key() << " ]" << std::endl;
|
||||||
|
file << std::endl;
|
||||||
|
for (auto it = section.begin(); it != section.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->IsLeaf()) //TODO: check if value exists?
|
||||||
|
file << it->Key() << " = \"" << it->ValueAs<std::string>() << "\"" << std::endl;
|
||||||
|
}
|
||||||
|
file << std::endl << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment)
|
||||||
|
{
|
||||||
|
std::ofstream file;
|
||||||
|
file.open(filename);
|
||||||
|
if (file.fail())
|
||||||
|
{
|
||||||
|
ErrorLog("Unable to write to '%s'. Configuration will not be saved.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!header_comment.empty())
|
||||||
|
file << header_comment << std::endl << std::endl;
|
||||||
|
WriteSection(file, config);
|
||||||
|
for (auto it = config.begin(); it != config.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->HasChildren())
|
||||||
|
WriteSection(file, *it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
Src/Util/ConfigBuilders.h
Normal file
20
Src/Util/ConfigBuilders.h
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#ifndef INCLUDED_UTIL_CONFIGBUILDERS_H
|
||||||
|
#define INCLUDED_UTIL_CONFIGBUILDERS_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Util
|
||||||
|
{
|
||||||
|
namespace Config
|
||||||
|
{
|
||||||
|
class Node;
|
||||||
|
|
||||||
|
bool FromXML(Node *config, const std::string &text);
|
||||||
|
bool FromXMLFile(Node *config, const std::string &filename);
|
||||||
|
bool FromINIFile(Node *config, const std::string &filename);
|
||||||
|
void MergeINISections(Node *merged, const Node &x, const Node &y);
|
||||||
|
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // INCLUDED_UTIL_CONFIGBUILDERS_H
|
124
Src/Util/GenericValue.h
Normal file
124
Src/Util/GenericValue.h
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
#ifndef INCLUDED_UTIL_GENERICVALUE_H
|
||||||
|
#define INCLUDED_UTIL_GENERICVALUE_H
|
||||||
|
|
||||||
|
#include "Util/Format.h"
|
||||||
|
#include <typeinfo>
|
||||||
|
#include <typeindex>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace Util
|
||||||
|
{
|
||||||
|
class GenericValue
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::type_index m_type;
|
||||||
|
|
||||||
|
virtual void *GetData() = 0;
|
||||||
|
virtual const void *GetData() const = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
template <typename T>
|
||||||
|
inline bool Is() const
|
||||||
|
{
|
||||||
|
return m_type == std::type_index(typeid(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
const T &Value() const
|
||||||
|
{
|
||||||
|
if (!Is<T>())
|
||||||
|
throw std::logic_error(Util::Format() << "GenericValue::Value(): cannot get value as " << std::type_index(typeid(T)).name() <<" because it is stored as " << m_type.name());
|
||||||
|
return *reinterpret_cast<const T *>(GetData());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T ValueAs() const
|
||||||
|
{
|
||||||
|
if (m_type == std::type_index(typeid(T)))
|
||||||
|
return *reinterpret_cast<const T *>(GetData());
|
||||||
|
std::stringstream ss;
|
||||||
|
Serialize(&ss);
|
||||||
|
T tmp;
|
||||||
|
ss >> tmp;
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void Set(const T &value)
|
||||||
|
{
|
||||||
|
if (!Is<T>())
|
||||||
|
throw std::logic_error(Util::Format() << "GenericValue::Set(): cannot set value as " << std::type_index(typeid(T)).name() <<" because it is stored as " << m_type.name());
|
||||||
|
*reinterpret_cast<T *>(GetData()) = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Set(const char *value)
|
||||||
|
{
|
||||||
|
Set<std::string>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Serialize(std::ostream *os) const = 0;
|
||||||
|
virtual std::shared_ptr<GenericValue> MakeCopy() const = 0;
|
||||||
|
|
||||||
|
GenericValue(std::type_index type)
|
||||||
|
: m_type(type)
|
||||||
|
{}
|
||||||
|
|
||||||
|
virtual ~GenericValue()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct ValueInstance: public GenericValue
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
T m_data;
|
||||||
|
|
||||||
|
void *GetData()
|
||||||
|
{
|
||||||
|
return reinterpret_cast<void *>(&m_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const void *GetData() const
|
||||||
|
{
|
||||||
|
return reinterpret_cast<const void *>(&m_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
void Serialize(std::ostream *os) const
|
||||||
|
{
|
||||||
|
*os << m_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<GenericValue> MakeCopy() const
|
||||||
|
{
|
||||||
|
return std::make_shared<ValueInstance<T>>(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueInstance(const ValueInstance<T> &that)
|
||||||
|
: GenericValue(std::type_index(typeid(T))),
|
||||||
|
m_data(that.m_data)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueInstance(const T &data)
|
||||||
|
: GenericValue(std::type_index(typeid(T))),
|
||||||
|
m_data(data)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueInstance()
|
||||||
|
: GenericValue(std::type_index(typeid(T)))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~ValueInstance()
|
||||||
|
{
|
||||||
|
//std::cout << "ValueInstance destroyed" << std::endl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // Util
|
||||||
|
|
||||||
|
#endif // INCLUDED_UTIL_GENERICVALUE_H
|
|
@ -1,15 +1,41 @@
|
||||||
/*
|
/*
|
||||||
* Config Tree
|
* Config Tree
|
||||||
* -----------
|
* ===========
|
||||||
|
*
|
||||||
|
* Accessing Hierarchical Nodes
|
||||||
|
* ----------------------------
|
||||||
*
|
*
|
||||||
* A hierarchical data structure supporting arbitrary nesting. Each node
|
* A hierarchical data structure supporting arbitrary nesting. Each node
|
||||||
* (Config::Node) has a key and either a value or children (in fact, it may
|
* (Config::Node) has a key and either a value or children (in fact, it may
|
||||||
* have both, but this does not make semantic sense and so the config tree
|
* have both, but this rarely makes semantic sense and so the config tree
|
||||||
* builders take care not to allow it).
|
* builders take care not to allow it).
|
||||||
*
|
*
|
||||||
* All values are simply strings for now. They may have defaults, which are
|
* Nesting is denoted with the '/' separator. For example:
|
||||||
* actually supplied by the caller and returned when a queried node cannot be
|
*
|
||||||
* found.
|
* 1. node.ValueAs<int>()
|
||||||
|
* 2. node["foo"]
|
||||||
|
* 3. node["foo"].ValueAs<int>()
|
||||||
|
* 4. node["foo/bar"]
|
||||||
|
*
|
||||||
|
* [1] accesses the value of the node (more on this below). [2] accesses the
|
||||||
|
* a child node with key "foo". [3] accesses the value of the child node "foo".
|
||||||
|
* [4] accesses child "bar" of child "foo", and so forth.
|
||||||
|
*
|
||||||
|
* Similar to map semantics, the operator [] never fails. If the node does not
|
||||||
|
* exist, it creates a dummy "missing" node that is retained as a hidden child.
|
||||||
|
* This node will have an empty value, which cannot be accessed, except using
|
||||||
|
* the ValueAsDefault<> method. This scheme exists to simplify lookup code for
|
||||||
|
* keys known at compile time, the logic being that any "missing" key should
|
||||||
|
* could just as well have been there in the first place, thus making the added
|
||||||
|
* memory usage negligible. For example, it easily allows values with defaults
|
||||||
|
* to be expressed as:
|
||||||
|
*
|
||||||
|
* config["foo/bar"].ValueAsDefault<std::string>("default_value")
|
||||||
|
*
|
||||||
|
* If the lookups are not known at compile time (e.g., are driven by the
|
||||||
|
* program user or external data), it is safer to use Get() and TryGet(), which
|
||||||
|
* can throw and return nullptr, respectively, and avoid wasting memory with
|
||||||
|
* dummy nodes.
|
||||||
*
|
*
|
||||||
* Nodes at the same nesting level (siblings) are strung together in a
|
* Nodes at the same nesting level (siblings) are strung together in a
|
||||||
* linked list. Parents also maintain pointers to the first and last of their
|
* linked list. Parents also maintain pointers to the first and last of their
|
||||||
|
@ -28,9 +54,45 @@
|
||||||
* <crom name="bar.bin" offset=2/>
|
* <crom name="bar.bin" offset=2/>
|
||||||
* </roms>
|
* </roms>
|
||||||
*
|
*
|
||||||
* config["game/roms"].Value() <-- should return nothing
|
* 1. config["game/roms"].ValueAs<std::string>()
|
||||||
* config["game/roms/crom/offset"].Value() <-- "2", the second <crom> tag
|
* 2. config["game/roms/crom/offset"].ValueAs<int>()
|
||||||
* config["game/roms"].begin() <-- iterate over two <crom> tags
|
* 3. config["game/roms"].begin()
|
||||||
|
*
|
||||||
|
* [1] will either throw an exception or return an empty string (depending on
|
||||||
|
* how the XML was translated to a config tree), [2] will return 2 (the second
|
||||||
|
* "crom" tag), and [3] can be used to iterate over both "crom" tags in order.
|
||||||
|
*
|
||||||
|
* Values
|
||||||
|
* ------
|
||||||
|
*
|
||||||
|
* To check whether or not a value is defined, use the Exists() and Empty()
|
||||||
|
* methods.
|
||||||
|
*
|
||||||
|
* Values are stored in a special container and can be virtually anything.
|
||||||
|
* However, it is recommended that only PODs and std::string be used. Arrays
|
||||||
|
* and pointers will probably not behave as expected and should be avoided.
|
||||||
|
* Trees build from files have all values loaded as std::strings. The types can
|
||||||
|
* be changed by subsequent re-assignments or by manually building a tree.
|
||||||
|
*
|
||||||
|
* To access a value of a given known type, T, use:
|
||||||
|
*
|
||||||
|
* node.Value<T>()
|
||||||
|
*
|
||||||
|
* Conversions as supported but are implemented via serialization and de-
|
||||||
|
* serialization using sstream. Most "sane" conversions will work as expected.
|
||||||
|
* When a conversion to T is desired, or if the stored value type is not
|
||||||
|
* precisely known, use:
|
||||||
|
*
|
||||||
|
* node.ValueAs<T>()
|
||||||
|
*
|
||||||
|
* These functions will throw an exception if the value is not defined at all
|
||||||
|
* or if the node does not exist (i.e., a "missing" node from a failed lookup).
|
||||||
|
* An alternative that is guaranteed to succeed is:
|
||||||
|
*
|
||||||
|
* node.ValueAsDefault<T>(default_value)
|
||||||
|
*
|
||||||
|
* If the value or node does not exist, default_value is returned, otherwise
|
||||||
|
* the stored value is returned with conversion (if needed) to type T.
|
||||||
*
|
*
|
||||||
* INI Semantics
|
* INI Semantics
|
||||||
* -------------
|
* -------------
|
||||||
|
@ -63,52 +125,36 @@
|
||||||
*
|
*
|
||||||
* Any setting not explicitly part of a section is assigned to the "Global"
|
* Any setting not explicitly part of a section is assigned to the "Global"
|
||||||
* section. For example, Setting0 and Setting1 above are both part of "Global".
|
* section. For example, Setting0 and Setting1 above are both part of "Global".
|
||||||
|
*
|
||||||
|
* TODO
|
||||||
|
* ----
|
||||||
|
* - Define our own exceptions?
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "Util/NewConfig.h"
|
#include "Util/NewConfig.h"
|
||||||
#include "OSD/Logger.h"
|
|
||||||
#include "Pkgs/tinyxml2.h"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <fstream>
|
|
||||||
#include <queue>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace Util
|
namespace Util
|
||||||
{
|
{
|
||||||
namespace Config
|
namespace Config
|
||||||
{
|
{
|
||||||
Node Node::s_empty_node;
|
void Node::CheckEmptyOrMissing() const
|
||||||
|
|
||||||
bool Node::ValueAsBool() const
|
|
||||||
{
|
{
|
||||||
const char *value = m_value.c_str();
|
if (m_missing)
|
||||||
if (!value || !stricmp(value, "false") || !stricmp(value, "off") || !stricmp(value, "no"))
|
throw std::range_error(Util::Format() << "Node \"" << m_key << "\" does not exist");
|
||||||
return false;
|
if (Empty() && !m_missing)
|
||||||
else if (!stricmp(value, "true") || !stricmp(value, "on") || !stricmp(value, "yes"))
|
throw std::logic_error(Util::Format() << "Node \"" << m_key << "\" has no value" );
|
||||||
return true;
|
|
||||||
return bool(ValueAsUnsigned());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Node::ValueAsBoolWithDefault(bool default_value) const
|
const Node &Node::MissingNode(const std::string &key) const
|
||||||
{
|
{
|
||||||
if (this == &s_empty_node)
|
auto it = m_missing_nodes.find(key);
|
||||||
return default_value;
|
if (it == m_missing_nodes.end())
|
||||||
return ValueAsBool();
|
{
|
||||||
|
auto result = m_missing_nodes.emplace(key, key);
|
||||||
|
result.first->second.m_missing = true; // mark this node as missing
|
||||||
|
return result.first->second;
|
||||||
}
|
}
|
||||||
|
return it->second;
|
||||||
uint64_t Node::ValueAsUnsigned() const
|
|
||||||
{
|
|
||||||
if (m_value.find("0x") == 0 || m_value.find("0X") == 0)
|
|
||||||
return strtoull(m_value.c_str() + 2, 0, 16);
|
|
||||||
return strtoull(m_value.c_str(), 0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t Node::ValueAsUnsignedWithDefault(uint64_t default_value) const
|
|
||||||
{
|
|
||||||
if (this == &s_empty_node)
|
|
||||||
return default_value;
|
|
||||||
return ValueAsUnsigned();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Node &Node::operator[](const std::string &path) const
|
const Node &Node::operator[](const std::string &path) const
|
||||||
|
@ -119,7 +165,7 @@ namespace Util
|
||||||
{
|
{
|
||||||
auto it = e->m_children.find(key);
|
auto it = e->m_children.find(key);
|
||||||
if (it == e->m_children.end())
|
if (it == e->m_children.end())
|
||||||
return s_empty_node;
|
return e->MissingNode(key);
|
||||||
e = it->second.get();
|
e = it->second.get();
|
||||||
}
|
}
|
||||||
return *e;
|
return *e;
|
||||||
|
@ -127,68 +173,72 @@ namespace Util
|
||||||
|
|
||||||
Node &Node::Get(const std::string &path)
|
Node &Node::Get(const std::string &path)
|
||||||
{
|
{
|
||||||
// This is probably dangerous and we should just have a non-const []
|
Node *node = TryGet(path);
|
||||||
return const_cast<Node &>(operator[](path));
|
if (!node)
|
||||||
|
throw std::range_error(Util::Format() << "Node \"" << path << "\" does not exist");
|
||||||
|
return *node;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Node &Node::Get(const std::string &path) const
|
const Node &Node::Get(const std::string &path) const
|
||||||
{
|
{
|
||||||
return operator[](path);
|
const Node *node = TryGet(path);
|
||||||
|
if (!node)
|
||||||
|
throw std::range_error(Util::Format() << "Node \"" << path << "\" does not exist");
|
||||||
|
return *node;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node *Node::TryGet(const std::string &path)
|
||||||
|
{
|
||||||
|
Node *e = this;
|
||||||
|
std::vector<std::string> keys = Util::Format(path).Split('/');
|
||||||
|
for (auto &key: keys)
|
||||||
|
{
|
||||||
|
auto it = e->m_children.find(key);
|
||||||
|
if (it == e->m_children.end())
|
||||||
|
return nullptr;
|
||||||
|
e = it->second.get();
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node *Node::TryGet(const std::string &path) const
|
||||||
|
{
|
||||||
|
const Node *e = this;
|
||||||
|
std::vector<std::string> keys = Util::Format(path).Split('/');
|
||||||
|
for (auto &key: keys)
|
||||||
|
{
|
||||||
|
auto it = e->m_children.find(key);
|
||||||
|
if (it == e->m_children.end())
|
||||||
|
return nullptr;
|
||||||
|
e = it->second.get();
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Node::Serialize(std::ostream *os, size_t indent_level) const
|
||||||
|
{
|
||||||
|
std::fill_n(std::ostream_iterator<char>(*os), 2 * indent_level, ' ');
|
||||||
|
*os << m_key << "=\"";
|
||||||
|
if (Exists())
|
||||||
|
m_value->Serialize(os);
|
||||||
|
*os << "\" children={";
|
||||||
|
for (auto v: m_children)
|
||||||
|
*os << ' ' << v.first;
|
||||||
|
*os << " }" << std::endl;
|
||||||
|
for (ptr_t child = m_first_child; child; child = child->m_next_sibling)
|
||||||
|
child->Serialize(os, indent_level + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Node::ToString(size_t indent_level) const
|
std::string Node::ToString(size_t indent_level) const
|
||||||
{
|
{
|
||||||
std::ostringstream os;
|
std::ostringstream os;
|
||||||
std::fill_n(std::ostream_iterator<char>(os), 2 * indent_level, ' ');
|
Serialize(&os, indent_level);
|
||||||
os << m_key;
|
|
||||||
if (m_value.length())
|
|
||||||
os << '=' << m_value;
|
|
||||||
os << " children={";
|
|
||||||
for (auto v: m_children)
|
|
||||||
os << ' ' << v.first;
|
|
||||||
os << " }" << std::endl;
|
|
||||||
for (Ptr_t child = m_first_child; child; child = child->m_next_sibling)
|
|
||||||
os << child->ToString(indent_level + 1);
|
|
||||||
return os.str();
|
return os.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Node::Print(size_t indent_level) const
|
|
||||||
{
|
|
||||||
std::cout << ToString(indent_level);
|
|
||||||
}
|
|
||||||
|
|
||||||
Node &Node::Add(const std::string &path, const std::string &value)
|
|
||||||
{
|
|
||||||
std::vector<std::string> keys = Util::Format(path).Split('/');
|
|
||||||
Node *parent = this;
|
|
||||||
Ptr_t node;
|
|
||||||
for (size_t i = 0; i < keys.size(); i++)
|
|
||||||
{
|
|
||||||
// Create node at this level
|
|
||||||
node = std::make_shared<Node>(keys[i]);
|
|
||||||
// The leaf node gets the value
|
|
||||||
if (i == keys.size() - 1)
|
|
||||||
node->SetValue(value);
|
|
||||||
// Attach node to parent and move down to next nesting level: last
|
|
||||||
// created node is new parent
|
|
||||||
AddChild(*parent, node);
|
|
||||||
parent = node.get();
|
|
||||||
}
|
|
||||||
return *node;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Node::Set(const std::string &key, const std::string &value)
|
|
||||||
{
|
|
||||||
Node &node = Get(key);
|
|
||||||
if (node.Empty())
|
|
||||||
Add(key, value);
|
|
||||||
else
|
|
||||||
node.SetValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a newly-created node (which, among other things, implies no
|
// Adds a newly-created node (which, among other things, implies no
|
||||||
// children) as a child
|
// children) as a child
|
||||||
void Node::AddChild(Node &parent, Ptr_t &node)
|
void Node::AddChild(Node &parent, ptr_t &node)
|
||||||
{
|
{
|
||||||
if (!parent.m_last_child)
|
if (!parent.m_last_child)
|
||||||
{
|
{
|
||||||
|
@ -209,10 +259,11 @@ namespace Util
|
||||||
return;
|
return;
|
||||||
Destroy();
|
Destroy();
|
||||||
*const_cast<std::string *>(&m_key) = that.m_key;
|
*const_cast<std::string *>(&m_key) = that.m_key;
|
||||||
m_value = that.m_value;
|
if (that.m_value)
|
||||||
for (Ptr_t child = that.m_first_child; child; child = child->m_next_sibling)
|
m_value = that.m_value->MakeCopy();
|
||||||
|
for (ptr_t child = that.m_first_child; child; child = child->m_next_sibling)
|
||||||
{
|
{
|
||||||
Ptr_t copied_child = std::make_shared<Node>(*child);
|
ptr_t copied_child = std::make_shared<Node>(*child);
|
||||||
AddChild(*this, copied_child);
|
AddChild(*this, copied_child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,26 +291,27 @@ namespace Util
|
||||||
}
|
}
|
||||||
|
|
||||||
Node::Node()
|
Node::Node()
|
||||||
|
: m_key("config"), // a default key value
|
||||||
|
m_value(nullptr) // this node is empty
|
||||||
{
|
{
|
||||||
//std::cout << "<<< Created " << "<null>" << " (" << this << ")" << std::endl;
|
//std::cout << "<<< Created " << "<null>" << " (" << this << ")" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Node::Node(const std::string &key)
|
Node::Node(const std::string &key)
|
||||||
: m_key(key)
|
: m_key(key),
|
||||||
|
m_value(nullptr) // this node is empty
|
||||||
{
|
{
|
||||||
//std::cout << "<<< Created " << key << " (" << this << ")" << std::endl;
|
//std::cout << "<<< Created " << key << " (" << this << ")" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Node::Node(const std::string &key, const std::string &value)
|
Node::Node(const std::string &key, const std::string &value)
|
||||||
: m_key(key),
|
: m_key(key),
|
||||||
m_value(value)
|
m_value(std::make_shared<ValueInstance<std::string>>(value))
|
||||||
{
|
{
|
||||||
//std::cout << "<<< Created " << key << '=' << value << " (" << this << ")" << std::endl;
|
//std::cout << "<<< Created " << key << '=' << value << " (" << this << ")" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Node::Node(const Node &that)
|
Node::Node(const Node &that)
|
||||||
: m_key(that.m_key),
|
|
||||||
m_value(that.m_value)
|
|
||||||
{
|
{
|
||||||
DeepCopy(that);
|
DeepCopy(that);
|
||||||
}
|
}
|
||||||
|
@ -273,244 +325,5 @@ namespace Util
|
||||||
{
|
{
|
||||||
//std::cout << ">>> Destroyed " << m_key << " (" << this << ")" << std::endl;
|
//std::cout << ">>> Destroyed " << m_key << " (" << this << ")" << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// An explicit function so that users don't accidentally create empty nodes
|
|
||||||
Node::Ptr_t CreateEmpty()
|
|
||||||
{
|
|
||||||
return std::shared_ptr<Node>(new Node());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void PopulateFromXML(Util::Config::Node::Ptr_t &config, const tinyxml2::XMLDocument &xml)
|
|
||||||
{
|
|
||||||
using namespace tinyxml2;
|
|
||||||
std::queue<std::pair<const XMLElement *, Util::Config::Node *>> q;
|
|
||||||
|
|
||||||
// Push the top level of the XML tree
|
|
||||||
for (const XMLElement *e = xml.RootElement(); e != 0; e = e->NextSiblingElement())
|
|
||||||
q.push( { e, config.get() } );
|
|
||||||
|
|
||||||
// Process the elements in order, pushing subsequent levels along with the
|
|
||||||
// config nodes to add them to
|
|
||||||
while (!q.empty())
|
|
||||||
{
|
|
||||||
const XMLElement *element = q.front().first;
|
|
||||||
Util::Config::Node *parent_node = q.front().second;
|
|
||||||
q.pop();
|
|
||||||
|
|
||||||
// Create a config entry for this XML element
|
|
||||||
Util::Config::Node *node = &parent_node->Add(element->Name(), element->GetText() ? std::string(element->GetText()) : std::string());
|
|
||||||
|
|
||||||
// Create entries for each attribute
|
|
||||||
for (const XMLAttribute *a = element->FirstAttribute(); a != 0; a = a->Next())
|
|
||||||
node->Add(a->Name(), a->Value());
|
|
||||||
|
|
||||||
// Push all child elements
|
|
||||||
for (const XMLElement *e = element->FirstChildElement(); e != 0; e = e->NextSiblingElement())
|
|
||||||
q.push( { e, node } );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Node::Ptr_t FromXML(const std::string &text)
|
|
||||||
{
|
|
||||||
Node::Ptr_t config = std::make_shared<Util::Config::Node>("xml");
|
|
||||||
using namespace tinyxml2;
|
|
||||||
XMLDocument xml;
|
|
||||||
auto ret = xml.Parse(text.c_str());
|
|
||||||
if (ret != XML_SUCCESS)
|
|
||||||
{
|
|
||||||
ErrorLog("Failed to parse XML (%s).", xml.ErrorName());
|
|
||||||
return CreateEmpty();
|
|
||||||
}
|
|
||||||
PopulateFromXML(config, xml);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
Node::Ptr_t FromXMLFile(const std::string &filename)
|
|
||||||
{
|
|
||||||
Node::Ptr_t config = std::make_shared<Util::Config::Node>("xml");
|
|
||||||
using namespace tinyxml2;
|
|
||||||
XMLDocument xml;
|
|
||||||
auto ret = xml.LoadFile(filename.c_str());
|
|
||||||
if (ret != XML_SUCCESS)
|
|
||||||
{
|
|
||||||
ErrorLog("Failed to parse %s (%s).", filename.c_str(), xml.ErrorName());
|
|
||||||
return CreateEmpty();
|
|
||||||
}
|
|
||||||
PopulateFromXML(config, xml);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string StripComment(const std::string &line)
|
|
||||||
{
|
|
||||||
// Find first semicolon not enclosed in ""
|
|
||||||
bool inside_quotes = false;
|
|
||||||
for (auto it = line.begin(); it != line.end(); ++it)
|
|
||||||
{
|
|
||||||
inside_quotes ^= (*it == '\"');
|
|
||||||
if (!inside_quotes && *it == ';')
|
|
||||||
return std::string(line.begin(), it);
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ParseAssignment(Node ¤t_section, const std::string &filename, size_t line_num, const std::string &line)
|
|
||||||
{
|
|
||||||
size_t idx_equals = line.find_first_of('=');
|
|
||||||
if (idx_equals == std::string::npos)
|
|
||||||
{
|
|
||||||
ErrorLog("%s:%d: Syntax error. No '=' found.", filename.c_str(), line_num);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std::string lvalue(TrimWhiteSpace(std::string(line.begin(), line.begin() + idx_equals)));
|
|
||||||
if (lvalue.empty())
|
|
||||||
{
|
|
||||||
ErrorLog("%s:%d: Syntax error. Setting name missing before '='.", filename.c_str(), line_num);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Get rvalue, which is allowed to be empty and strip off quotes if they
|
|
||||||
// exist. Only a single pair of quotes encasing the rvalue are permitted.
|
|
||||||
std::string rvalue(TrimWhiteSpace(std::string(line.begin() + idx_equals + 1, line.end())));
|
|
||||||
size_t idx_first_quote = rvalue.find_first_of('\"');
|
|
||||||
size_t idx_last_quote = rvalue.find_last_of('\"');
|
|
||||||
if (idx_first_quote == 0 && idx_last_quote == (rvalue.length() - 1) && idx_first_quote != idx_last_quote)
|
|
||||||
rvalue = std::string(rvalue.begin() + idx_first_quote + 1, rvalue.begin() + idx_last_quote);
|
|
||||||
if (std::count(rvalue.begin(), rvalue.end(), '\"') != 0)
|
|
||||||
{
|
|
||||||
ErrorLog("%s:%d: Warning: Extraneous quotes present on line.", filename.c_str(), line_num);
|
|
||||||
}
|
|
||||||
// In INI files, we do not allow multiple settings with the same key. If
|
|
||||||
// a setting is specified multiple times, previous ones are overwritten.
|
|
||||||
if (current_section[lvalue].Empty())
|
|
||||||
current_section.Add(lvalue, rvalue);
|
|
||||||
else
|
|
||||||
current_section.Get(lvalue).SetValue(rvalue);
|
|
||||||
}
|
|
||||||
|
|
||||||
Node::Ptr_t FromINIFile(const std::string &filename)
|
|
||||||
{
|
|
||||||
std::ifstream file;
|
|
||||||
file.open(filename);
|
|
||||||
if (file.fail())
|
|
||||||
{
|
|
||||||
ErrorLog("Unable to open '%s'. Configuration will not be loaded.", filename.c_str());
|
|
||||||
return CreateEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Node::Ptr_t global_ptr = std::make_shared<Node>("Global"); // the root is also the "Global" section
|
|
||||||
Node &global = *global_ptr;
|
|
||||||
Node *current_section = &global;
|
|
||||||
|
|
||||||
size_t line_num = 1;
|
|
||||||
while (!file.eof())
|
|
||||||
{
|
|
||||||
if (file.fail())
|
|
||||||
{
|
|
||||||
ErrorLog("%s:%d: File read error. Configuration will be incomplete.");
|
|
||||||
return CreateEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string line;
|
|
||||||
std::getline(file, line);
|
|
||||||
line = StripComment(line);
|
|
||||||
line = TrimWhiteSpace(line);
|
|
||||||
|
|
||||||
if (!line.empty())
|
|
||||||
{
|
|
||||||
if (*line.begin() == '[' && *line.rbegin() == ']')
|
|
||||||
{
|
|
||||||
// Check if section exists else create a new one
|
|
||||||
std::string section(TrimWhiteSpace(std::string(line.begin() + 1, line.begin() + line.length() - 1)));
|
|
||||||
if (section.empty() || section == "Global") // empty section names (e.g., "[]") assigned to "[ Global ]"
|
|
||||||
current_section = &global;
|
|
||||||
else if (global[section].Empty())
|
|
||||||
{
|
|
||||||
Node &new_section = global.Add(section);
|
|
||||||
current_section = &new_section;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
current_section = &global.Get(section);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ParseAssignment(*current_section, filename, line_num, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
line_num++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return global_ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Produces a new INI section by merging two existing sections.
|
|
||||||
*
|
|
||||||
* - Nodes from x and y with children are ignored as per INI semantics.
|
|
||||||
* The presence of children indicates a section, not a setting.
|
|
||||||
* - Settings from y override settings in x if already present.
|
|
||||||
* - If multiple settings with the same key are present in either of the
|
|
||||||
* source configs (which technically violates INI semantics), the last
|
|
||||||
* ends up being used.
|
|
||||||
* - x's key is retained.
|
|
||||||
*/
|
|
||||||
Node::Ptr_t MergeINISections(const Node &x, const Node &y)
|
|
||||||
{
|
|
||||||
Node::Ptr_t merged_ptr = std::make_shared<Node>(x.Key()); // result has same key as x
|
|
||||||
Node &merged = *merged_ptr;
|
|
||||||
// First copy settings from section x
|
|
||||||
for (auto it = x.begin(); it != x.end(); ++it)
|
|
||||||
{
|
|
||||||
auto &key = it->Key();
|
|
||||||
auto &value = it->Value();
|
|
||||||
if (it->IsLeaf())
|
|
||||||
{
|
|
||||||
// INI semantics: take care to only create a single setting per key
|
|
||||||
merged.Set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Merge in settings from section y
|
|
||||||
for (auto it = y.begin(); it != y.end(); ++it)
|
|
||||||
{
|
|
||||||
auto &key = it->Key();
|
|
||||||
auto &value = it->Value();
|
|
||||||
if (it->IsLeaf())
|
|
||||||
{
|
|
||||||
merged.Set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged_ptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void WriteSection(std::ofstream &file, const Node §ion)
|
|
||||||
{
|
|
||||||
file << "[ " << section.Key() << " ]" << std::endl;
|
|
||||||
file << std::endl;
|
|
||||||
for (auto it = section.begin(); it != section.end(); ++it)
|
|
||||||
{
|
|
||||||
if (it->IsLeaf())
|
|
||||||
file << it->Key() << " = \"" << it->Value() << "\"" << std::endl;
|
|
||||||
}
|
|
||||||
file << std::endl << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment)
|
|
||||||
{
|
|
||||||
std::ofstream file;
|
|
||||||
file.open(filename);
|
|
||||||
if (file.fail())
|
|
||||||
{
|
|
||||||
ErrorLog("Unable to write to '%s'. Configuration will not be saved.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!header_comment.empty())
|
|
||||||
file << header_comment << std::endl << std::endl;
|
|
||||||
WriteSection(file, config);
|
|
||||||
for (auto it = config.begin(); it != config.end(); ++it)
|
|
||||||
{
|
|
||||||
if (it->HasChildren())
|
|
||||||
WriteSection(file, *it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // Config
|
} // Config
|
||||||
} // Util
|
} // Util
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
#ifndef INCLUDED_UTIL_CONFIG_H
|
#ifndef INCLUDED_UTIL_CONFIG_H
|
||||||
#define INCLUDED_UTIL_CONFIG_H
|
#define INCLUDED_UTIL_CONFIG_H
|
||||||
|
|
||||||
#include "Util/Format.h"
|
#include "Util/GenericValue.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
namespace Util
|
namespace Util
|
||||||
{
|
{
|
||||||
|
@ -12,33 +13,33 @@ namespace Util
|
||||||
{
|
{
|
||||||
class Node
|
class Node
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
typedef std::shared_ptr<Node> Ptr_t;
|
|
||||||
typedef std::shared_ptr<const Node> ConstPtr_t;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ptr_t m_next_sibling;
|
typedef std::shared_ptr<Node> ptr_t;
|
||||||
Ptr_t m_first_child;
|
typedef std::shared_ptr<const Node> const_ptr_t;
|
||||||
Ptr_t m_last_child;
|
|
||||||
std::map<std::string, Ptr_t> m_children;
|
|
||||||
const std::string m_key; // this cannot be changed (maps depend on it)
|
const std::string m_key; // this cannot be changed (maps depend on it)
|
||||||
std::string m_value;
|
std::shared_ptr<GenericValue> m_value; // null pointer marks this node as an invalid, empty node
|
||||||
static Node s_empty_node; // key, value, and children must always be empty
|
ptr_t m_next_sibling;
|
||||||
|
ptr_t m_first_child;
|
||||||
|
ptr_t m_last_child;
|
||||||
|
std::map<std::string, ptr_t> m_children;
|
||||||
|
mutable std::map<std::string, Node> m_missing_nodes; // missing nodes from failed queries (must also be empty)
|
||||||
|
bool m_missing = false;
|
||||||
|
|
||||||
void Destroy()
|
void Destroy()
|
||||||
{
|
{
|
||||||
|
m_value.reset();
|
||||||
m_next_sibling.reset();
|
m_next_sibling.reset();
|
||||||
m_first_child.reset();
|
m_first_child.reset();
|
||||||
m_last_child.reset();
|
m_last_child.reset();
|
||||||
m_children.clear();
|
m_children.clear();
|
||||||
m_value.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddChild(Node &parent, Ptr_t &node);
|
void CheckEmptyOrMissing() const;
|
||||||
|
const Node &MissingNode(const std::string &key) const;
|
||||||
|
void AddChild(Node &parent, ptr_t &node);
|
||||||
void DeepCopy(const Node &that);
|
void DeepCopy(const Node &that);
|
||||||
void Swap(Node &rhs);
|
void Swap(Node &rhs);
|
||||||
Node(); // prohibit accidental/unintentional creation of blank nodes
|
Node(); // prohibit accidental/unintentional creation of empty nodes
|
||||||
friend Ptr_t CreateEmpty();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
class const_iterator;
|
class const_iterator;
|
||||||
|
@ -46,10 +47,10 @@ namespace Util
|
||||||
class iterator: public std::iterator<std::forward_iterator_tag, Node>
|
class iterator: public std::iterator<std::forward_iterator_tag, Node>
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
Ptr_t m_node;
|
ptr_t m_node;
|
||||||
friend class const_iterator;
|
friend class const_iterator;
|
||||||
public:
|
public:
|
||||||
inline iterator(Ptr_t node = Ptr_t())
|
inline iterator(ptr_t node = ptr_t())
|
||||||
: m_node(node)
|
: m_node(node)
|
||||||
{}
|
{}
|
||||||
inline iterator(const iterator &it)
|
inline iterator(const iterator &it)
|
||||||
|
@ -72,7 +73,7 @@ namespace Util
|
||||||
{
|
{
|
||||||
return *m_node;
|
return *m_node;
|
||||||
}
|
}
|
||||||
inline Ptr_t operator->() const
|
inline ptr_t operator->() const
|
||||||
{
|
{
|
||||||
return m_node;
|
return m_node;
|
||||||
}
|
}
|
||||||
|
@ -89,12 +90,12 @@ namespace Util
|
||||||
class const_iterator: public std::iterator<std::forward_iterator_tag, const Node>
|
class const_iterator: public std::iterator<std::forward_iterator_tag, const Node>
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
ConstPtr_t m_node;
|
const_ptr_t m_node;
|
||||||
public:
|
public:
|
||||||
inline const_iterator(ConstPtr_t node = ConstPtr_t())
|
inline const_iterator(const_ptr_t node = const_ptr_t())
|
||||||
: m_node(node)
|
: m_node(node)
|
||||||
{}
|
{}
|
||||||
inline const_iterator(Ptr_t node = Ptr_t())
|
inline const_iterator(ptr_t node = ptr_t())
|
||||||
: m_node(std::const_pointer_cast<const Node>(node))
|
: m_node(std::const_pointer_cast<const Node>(node))
|
||||||
{}
|
{}
|
||||||
inline const_iterator(const const_iterator &it)
|
inline const_iterator(const const_iterator &it)
|
||||||
|
@ -120,7 +121,7 @@ namespace Util
|
||||||
{
|
{
|
||||||
return *m_node;
|
return *m_node;
|
||||||
}
|
}
|
||||||
inline ConstPtr_t operator->() const
|
inline const_ptr_t operator->() const
|
||||||
{
|
{
|
||||||
return m_node;
|
return m_node;
|
||||||
}
|
}
|
||||||
|
@ -159,57 +160,135 @@ namespace Util
|
||||||
return m_key;
|
return m_key;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inline std::string &ValueWithDefault(const std::string &default_value) const
|
inline std::shared_ptr<GenericValue> GetValue() const
|
||||||
{
|
{
|
||||||
if (this == &s_empty_node)
|
|
||||||
return default_value;
|
|
||||||
return m_value;
|
return m_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inline std::string &Value() const
|
inline void SetValue(const std::shared_ptr<GenericValue> &value)
|
||||||
{
|
{
|
||||||
//TODO: if empty node, throw exception? Use ValueWithDefault() otherwise.
|
|
||||||
return m_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ValueAsBool() const;
|
|
||||||
bool ValueAsBoolWithDefault(bool default_value) const;
|
|
||||||
uint64_t ValueAsUnsigned() const;
|
|
||||||
uint64_t ValueAsUnsignedWithDefault(uint64_t default_value) const;
|
|
||||||
|
|
||||||
inline void SetValue(const std::string &value)
|
|
||||||
{
|
|
||||||
if (this != &s_empty_node) // not allowed to modify empty node
|
|
||||||
m_value = value;
|
m_value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool Empty() const
|
template <typename T>
|
||||||
|
const T &Value() const
|
||||||
{
|
{
|
||||||
return m_key.empty();
|
CheckEmptyOrMissing();
|
||||||
|
return m_value->Value<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T ValueAs() const
|
||||||
|
{
|
||||||
|
CheckEmptyOrMissing();
|
||||||
|
return m_value->ValueAs<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T ValueAsDefault(const T &default_value) const
|
||||||
|
{
|
||||||
|
if (Empty())
|
||||||
|
return default_value;
|
||||||
|
return m_value->ValueAs<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set value of this node
|
||||||
|
template <typename T>
|
||||||
|
inline void SetValue(const T &value)
|
||||||
|
{
|
||||||
|
if (!m_missing)
|
||||||
|
{
|
||||||
|
if (!Empty() && m_value->Is<T>())
|
||||||
|
m_value->Set(value);
|
||||||
|
else
|
||||||
|
m_value = std::make_shared<ValueInstance<T>>(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw std::range_error(Util::Format() << "Node \"" << m_key << "\" does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
// char* is a troublesome case because we want to convert it to std::string
|
||||||
|
inline void SetValue(const char *value)
|
||||||
|
{
|
||||||
|
SetValue<std::string>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a child node
|
||||||
|
template <typename T>
|
||||||
|
Node &Add(const std::string &path, const T &value)
|
||||||
|
{
|
||||||
|
std::vector<std::string> keys = Util::Format(path).Split('/');
|
||||||
|
Node *parent = this;
|
||||||
|
ptr_t node;
|
||||||
|
for (size_t i = 0; i < keys.size(); i++)
|
||||||
|
{
|
||||||
|
// Create node at this level
|
||||||
|
node = std::make_shared<Node>(keys[i]);
|
||||||
|
// The leaf node gets the value
|
||||||
|
if (i == keys.size() - 1)
|
||||||
|
node->SetValue(value);
|
||||||
|
// Attach node to parent and move down to next nesting level: last
|
||||||
|
// created node is new parent
|
||||||
|
AddChild(*parent, node);
|
||||||
|
parent = node.get();
|
||||||
|
}
|
||||||
|
return *node;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node &Add(const std::string &path)
|
||||||
|
{
|
||||||
|
return Add(path, std::string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the value of the matching child node if it exists, else add new
|
||||||
|
template <typename T>
|
||||||
|
void Set(const std::string &key, const T &value)
|
||||||
|
{
|
||||||
|
Node *node = TryGet(key);
|
||||||
|
if (node)
|
||||||
|
node->SetValue(value);
|
||||||
|
else
|
||||||
|
Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if value is empty (does not exist)
|
||||||
|
inline bool Empty() const
|
||||||
|
{
|
||||||
|
return !m_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if value exists (is not empty)
|
||||||
inline bool Exists() const
|
inline bool Exists() const
|
||||||
{
|
{
|
||||||
return !Empty();
|
return !Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True if no keys under this node
|
||||||
inline bool IsLeaf() const
|
inline bool IsLeaf() const
|
||||||
{
|
{
|
||||||
return m_children.empty();
|
return m_children.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True if this node has keys
|
||||||
inline bool HasChildren() const
|
inline bool HasChildren() const
|
||||||
{
|
{
|
||||||
return !IsLeaf();
|
return !IsLeaf();
|
||||||
}
|
}
|
||||||
|
|
||||||
Node &Get(const std::string &path);
|
// Always succeeds -- failed lookups permanently create an empty node.
|
||||||
|
// Use with caution. Intended for hard-coded lookups.
|
||||||
const Node &operator[](const std::string &path) const;
|
const Node &operator[](const std::string &path) const;
|
||||||
|
|
||||||
|
// These throw if the node is missing
|
||||||
|
Node &Get(const std::string &path);
|
||||||
const Node &Get(const std::string &path) const;
|
const Node &Get(const std::string &path) const;
|
||||||
void Print(size_t indent_level = 0) const;
|
|
||||||
|
// This returns nullptr if node is missing
|
||||||
|
Node *TryGet(const std::string &path);
|
||||||
|
const Node *TryGet(const std::string &path) const;
|
||||||
|
|
||||||
|
void Serialize(std::ostream *os, size_t indent_level = 0) const;
|
||||||
std::string ToString(size_t indent_level = 0) const;
|
std::string ToString(size_t indent_level = 0) const;
|
||||||
Node &Add(const std::string &key, const std::string &value = "");
|
|
||||||
void Set(const std::string &key, const std::string &value);
|
|
||||||
Node &operator=(const Node &rhs);
|
Node &operator=(const Node &rhs);
|
||||||
Node &operator=(Node &&rhs);
|
Node &operator=(Node &&rhs);
|
||||||
Node(const std::string &key);
|
Node(const std::string &key);
|
||||||
|
@ -218,15 +297,6 @@ namespace Util
|
||||||
Node(Node &&that);
|
Node(Node &&that);
|
||||||
~Node();
|
~Node();
|
||||||
};
|
};
|
||||||
|
|
||||||
//TODO: CreateEmpty() should take a key that defaults to blank
|
|
||||||
//TODO: define deep-copy assignment operator and get rid of Ptr_t and ConstPtr_t
|
|
||||||
Node::Ptr_t CreateEmpty();
|
|
||||||
Node::Ptr_t FromXML(const std::string &text);
|
|
||||||
Node::Ptr_t FromXMLFile(const std::string &filename);
|
|
||||||
Node::Ptr_t FromINIFile(const std::string &filename);
|
|
||||||
Node::Ptr_t MergeINISections(const Node &x, const Node &y);
|
|
||||||
void WriteINIFile(const std::string &filename, const Node &config, const std::string &header_comment);
|
|
||||||
} // Config
|
} // Config
|
||||||
} // Util
|
} // Util
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#include "Util/NewConfig.h"
|
#include "Util/NewConfig.h"
|
||||||
|
#include "Util/ConfigBuilders.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
static void PrintTestResults(std::vector<std::pair<std::string, bool>> results)
|
static void PrintTestResults(std::vector<std::pair<std::string, bool>> results)
|
||||||
|
@ -30,58 +31,61 @@ int main()
|
||||||
* </roms>
|
* </roms>
|
||||||
* </game>
|
* </game>
|
||||||
*/
|
*/
|
||||||
Util::Config::Node::Ptr_t root = std::make_shared<Util::Config::Node>("global");
|
Util::Config::Node root("global");
|
||||||
auto &game = root->Add("game");
|
auto &game = root.Add("game");
|
||||||
game.Add("name", "scud");
|
game.Add<std::string>("name", "scud");
|
||||||
auto &roms = game.Add("roms");
|
auto &roms = game.Add("roms");
|
||||||
auto &crom1 = roms.Add("crom");
|
auto &crom1 = roms.Add("crom");
|
||||||
crom1.Add("name", "foo.bin");
|
crom1.Add<std::string>("name", "foo.bin");
|
||||||
crom1.Add("offset", "0");
|
crom1.Add<std::string>("offset", "0");
|
||||||
auto &crom2 = roms.Add("crom");
|
auto &crom2 = roms.Add("crom");
|
||||||
crom2.Add("name", "bar.bin");
|
crom2.Add<std::string>("name", "bar.bin");
|
||||||
crom2.Add("offset", "2");
|
crom2.Add<std::string>("offset", "2");
|
||||||
|
|
||||||
const char *expected_output =
|
const char *expected_output =
|
||||||
"global children={ game }\n"
|
"global=\"\" children={ game }\n"
|
||||||
" game children={ name roms }\n"
|
" game=\"\" children={ name roms }\n"
|
||||||
" name=scud children={ }\n"
|
" name=\"scud\" children={ }\n"
|
||||||
" roms children={ crom }\n"
|
" roms=\"\" children={ crom }\n"
|
||||||
" crom children={ name offset }\n"
|
" crom=\"\" children={ name offset }\n"
|
||||||
" name=foo.bin children={ }\n"
|
" name=\"foo.bin\" children={ }\n"
|
||||||
" offset=0 children={ }\n"
|
" offset=\"0\" children={ }\n"
|
||||||
" crom children={ name offset }\n"
|
" crom=\"\" children={ name offset }\n"
|
||||||
" name=bar.bin children={ }\n"
|
" name=\"bar.bin\" children={ }\n"
|
||||||
" offset=2 children={ }\n";
|
" offset=\"2\" children={ }\n";
|
||||||
std::cout << "Expected output:" << std::endl << std::endl << expected_output << std::endl;
|
std::cout << "Expected output:" << std::endl << std::endl << expected_output << std::endl;
|
||||||
std::cout << "Actual config tree:" << std::endl << std::endl;
|
std::cout << "Actual config tree:" << std::endl << std::endl;
|
||||||
root->Print();
|
root.Serialize(&std::cout);
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
test_results.push_back({ "Manual", root->ToString() == expected_output });
|
test_results.push_back({ "Manual", root.ToString() == expected_output });
|
||||||
|
|
||||||
// Expect second crom: bar.bin
|
// Expect second crom: bar.bin
|
||||||
auto &global = *root;
|
std::cout << "game/roms/crom/name=" << root["game/roms/crom/name"].Value<std::string>() << " (expected: bar.bin)" << std::endl << std::endl;
|
||||||
std::cout << "game/roms/crom/name=" << global["game/roms/crom/name"].Value() << " (expected: bar.bin)" << std::endl << std::endl;
|
test_results.push_back({ "Lookup", root["game/roms/crom/name"].Value<std::string>() == "bar.bin" });
|
||||||
test_results.push_back({ "Lookup", global["game/roms/crom/name"].Value() == "bar.bin" });
|
|
||||||
|
|
||||||
// Make a copy using copy constructor
|
// Make a copy using copy constructor
|
||||||
std::cout << "Copy constructed:" << std::endl << std::endl;
|
std::cout << "Copy constructed:" << std::endl << std::endl;
|
||||||
Util::Config::Node::Ptr_t copy = std::make_shared<Util::Config::Node>(*root);
|
Util::Config::Node copy(root);
|
||||||
copy->Print();
|
copy.Serialize(&std::cout);
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
test_results.push_back({ "Copy constructed", copy->ToString() == root->ToString() });
|
test_results.push_back({ "Copy constructed", copy.ToString() == root.ToString() });
|
||||||
|
|
||||||
// Make a deep copy using assignment operator
|
// Make a deep copy using assignment operator, check equality, then modify a
|
||||||
|
// value and ensure it does not change in the copy
|
||||||
std::cout << "Copy by assignment:" << std::endl << std::endl;
|
std::cout << "Copy by assignment:" << std::endl << std::endl;
|
||||||
Util::Config::Node copy2("foo", "bar");
|
Util::Config::Node copy2("foo", "bar");
|
||||||
copy2.Add("x/y/z", "test");
|
copy2.Add<std::string>("x/y/z", "test");
|
||||||
copy2.Add("dummy", "value");
|
copy2.Add<std::string>("dummy", "value");
|
||||||
std::cout << "Initially set to:" << std::endl;
|
std::cout << "Initially set to:" << std::endl;
|
||||||
copy2.Print();
|
copy2.Serialize(&std::cout);
|
||||||
copy2 = *root;
|
copy2 = root;
|
||||||
std::cout << "Copy of \"global\":" << std::endl;
|
std::cout << "Copy of \"global\":" << std::endl;
|
||||||
copy2.Print();
|
copy2.Serialize(&std::cout);
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
test_results.push_back({ "Copy by assignment", copy2.ToString() == root->ToString() });
|
bool copies_are_equivalent = copy2.ToString() == root.ToString();
|
||||||
|
copy2.Get("game/name").SetValue("vf3");
|
||||||
|
bool copies_are_unique = copy2["game/name"].Value<std::string>() == "vf3" && root["game/name"].Value<std::string>() == "scud";
|
||||||
|
test_results.push_back({ "Copy by assignment", copies_are_equivalent && copies_are_unique });
|
||||||
|
|
||||||
// Parse from XML
|
// Parse from XML
|
||||||
const char *xml =
|
const char *xml =
|
||||||
|
@ -102,75 +106,76 @@ int main()
|
||||||
" </roms> \n"
|
" </roms> \n"
|
||||||
"</game> \n";
|
"</game> \n";
|
||||||
const char *expected_xml_config_tree =
|
const char *expected_xml_config_tree =
|
||||||
"xml children={ game }\n"
|
"xml=\"\" children={ game }\n"
|
||||||
" game children={ name roms }\n"
|
" game=\"\" children={ name roms }\n"
|
||||||
" name=scud children={ }\n"
|
" name=\"scud\" children={ }\n"
|
||||||
" roms children={ region }\n"
|
" roms=\"\" children={ region }\n"
|
||||||
" region children={ byte_swap file name stride }\n"
|
" region=\"\" children={ byte_swap file name stride }\n"
|
||||||
" name=crom children={ }\n"
|
" name=\"crom\" children={ }\n"
|
||||||
" stride=8 children={ }\n"
|
" stride=\"8\" children={ }\n"
|
||||||
" byte_swap=true children={ }\n"
|
" byte_swap=\"true\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=0 children={ }\n"
|
" offset=\"0\" children={ }\n"
|
||||||
" name=epr-19734.20 children={ }\n"
|
" name=\"epr-19734.20\" children={ }\n"
|
||||||
" crc32=0xBE897336 children={ }\n"
|
" crc32=\"0xBE897336\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=2 children={ }\n"
|
" offset=\"2\" children={ }\n"
|
||||||
" name=epr-19733.19 children={ }\n"
|
" name=\"epr-19733.19\" children={ }\n"
|
||||||
" crc32=0x6565E29A children={ }\n"
|
" crc32=\"0x6565E29A\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=4 children={ }\n"
|
" offset=\"4\" children={ }\n"
|
||||||
" name=epr-19732.18 children={ }\n"
|
" name=\"epr-19732.18\" children={ }\n"
|
||||||
" crc32=0x23E864BB children={ }\n"
|
" crc32=\"0x23E864BB\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=6 children={ }\n"
|
" offset=\"6\" children={ }\n"
|
||||||
" name=epr-19731.17 children={ }\n"
|
" name=\"epr-19731.17\" children={ }\n"
|
||||||
" crc32=0x3EE6447E children={ }\n"
|
" crc32=\"0x3EE6447E\" children={ }\n"
|
||||||
" region children={ byte_swap file name stride }\n"
|
" region=\"\" children={ byte_swap file name stride }\n"
|
||||||
" name=banked_crom children={ }\n"
|
" name=\"banked_crom\" children={ }\n"
|
||||||
" stride=8 children={ }\n"
|
" stride=\"8\" children={ }\n"
|
||||||
" byte_swap=true children={ }\n"
|
" byte_swap=\"true\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=0x0000000 children={ }\n"
|
" offset=\"0x0000000\" children={ }\n"
|
||||||
" name=mpr-20364.4 children={ }\n"
|
" name=\"mpr-20364.4\" children={ }\n"
|
||||||
" crc32=0xA2A68EF2 children={ }\n"
|
" crc32=\"0xA2A68EF2\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=0x0000002 children={ }\n"
|
" offset=\"0x0000002\" children={ }\n"
|
||||||
" name=mpr-20363.3 children={ }\n"
|
" name=\"mpr-20363.3\" children={ }\n"
|
||||||
" crc32=0x3E3CC6FF children={ }\n"
|
" crc32=\"0x3E3CC6FF\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=0x0000004 children={ }\n"
|
" offset=\"0x0000004\" children={ }\n"
|
||||||
" name=mpr-20362.2 children={ }\n"
|
" name=\"mpr-20362.2\" children={ }\n"
|
||||||
" crc32=0xF7E60DFD children={ }\n"
|
" crc32=\"0xF7E60DFD\" children={ }\n"
|
||||||
" file children={ crc32 name offset }\n"
|
" file=\"\" children={ crc32 name offset }\n"
|
||||||
" offset=0x0000006 children={ }\n"
|
" offset=\"0x0000006\" children={ }\n"
|
||||||
" name=mpr-20361.1 children={ }\n"
|
" name=\"mpr-20361.1\" children={ }\n"
|
||||||
" crc32=0xDDB66C2F children={ }\n";
|
" crc32=\"0xDDB66C2F\" children={ }\n";
|
||||||
std::cout << "Expected output:" << std::endl << std::endl << expected_xml_config_tree << std::endl;
|
std::cout << "Expected output:" << std::endl << std::endl << expected_xml_config_tree << std::endl;
|
||||||
std::cout << "Actual config tree:" << std::endl << std::endl;
|
std::cout << "Actual config tree:" << std::endl << std::endl;
|
||||||
Util::Config::Node::Ptr_t xml_config = Util::Config::FromXML(xml);
|
Util::Config::Node xml_config("xml");
|
||||||
xml_config->Print();
|
Util::Config::FromXML(&xml_config, xml);
|
||||||
|
xml_config.Serialize(&std::cout);
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
test_results.push_back({ "XML", xml_config->ToString() == expected_xml_config_tree });
|
test_results.push_back({ "XML", xml_config.ToString() == expected_xml_config_tree });
|
||||||
|
|
||||||
// Create a nested key
|
// Create a nested key
|
||||||
{
|
{
|
||||||
Util::Config::Node::Ptr_t config = Util::Config::CreateEmpty();
|
Util::Config::Node config("global");
|
||||||
config->Add("foo/bar/baz", "bart");
|
config.Add<std::string>("foo/bar/baz", "bart");
|
||||||
config->Print();
|
config.Serialize(&std::cout);
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
test_results.push_back({ "Nested key 1", config->Get("foo/bar/baz").Value() == "bart" });
|
test_results.push_back({ "Nested key 1", config["foo/bar/baz"].Value<std::string>() == "bart" });
|
||||||
config->Get("foo/bar/baz").Set("x", "xx");
|
config.Get("foo/bar/baz").Set<std::string>("x", "xx");
|
||||||
config->Get("foo/bar/baz").Set("y/z", "zz");
|
config.Get("foo/bar/baz").Set<std::string>("y/z", "zz");
|
||||||
config->Print();
|
config.Serialize(&std::cout);
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
test_results.push_back({ "Nested key 2", config->Get("foo/bar/baz/x").Value() == "xx" });
|
test_results.push_back({ "Nested key 2", config["foo/bar/baz/x"].Value<std::string>() == "xx" });
|
||||||
test_results.push_back({ "Nested key 3", config->Get("foo/bar/baz/y/z").Value() == "zz" });
|
test_results.push_back({ "Nested key 3", config["foo/bar/baz/y/z"].Value<std::string>() == "zz" });
|
||||||
config->Add("a/b/c");
|
config.Add("a/b/c");
|
||||||
config->Get("a/b/c").SetValue("d");
|
config.Get("a/b/c").SetValue<std::string>("d");
|
||||||
config->Print();
|
config.Serialize(&std::cout);
|
||||||
std::cout << std::endl;
|
std::cout << std::endl;
|
||||||
test_results.push_back({ "Nested key 4", config->Get("a/b/c").Value() == "d" });
|
test_results.push_back({ "Nested key 4", config["a/b/c"].Value<std::string>() == "d" });
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintTestResults(test_results);
|
PrintTestResults(test_results);
|
||||||
|
|
Loading…
Reference in a new issue