mirror of
https://github.com/RetroDECK/Supermodel.git
synced 2025-02-16 17:35:39 +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
|
||||
* -----------
|
||||
* ===========
|
||||
*
|
||||
* Accessing Hierarchical Nodes
|
||||
* ----------------------------
|
||||
*
|
||||
* A hierarchical data structure supporting arbitrary nesting. Each node
|
||||
* (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).
|
||||
*
|
||||
* All values are simply strings for now. They may have defaults, which are
|
||||
* actually supplied by the caller and returned when a queried node cannot be
|
||||
* found.
|
||||
* Nesting is denoted with the '/' separator. For example:
|
||||
*
|
||||
* 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
|
||||
* linked list. Parents also maintain pointers to the first and last of their
|
||||
|
@ -22,15 +48,51 @@
|
|||
*
|
||||
* Example:
|
||||
*
|
||||
* <game name="scud">
|
||||
* <roms>
|
||||
* <crom name="foo.bin" offset=0/>
|
||||
* <crom name="bar.bin" offset=2/>
|
||||
* </roms>
|
||||
* <game name="scud">
|
||||
* <roms>
|
||||
* <crom name="foo.bin" offset=0/>
|
||||
* <crom name="bar.bin" offset=2/>
|
||||
* </roms>
|
||||
*
|
||||
* config["game/roms"].Value() <-- should return nothing
|
||||
* config["game/roms/crom/offset"].Value() <-- "2", the second <crom> tag
|
||||
* config["game/roms"].begin() <-- iterate over two <crom> tags
|
||||
* 1. config["game/roms"].ValueAs<std::string>()
|
||||
* 2. config["game/roms/crom/offset"].ValueAs<int>()
|
||||
* 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
|
||||
* -------------
|
||||
|
@ -51,64 +113,48 @@
|
|||
* INI files contain zero or more sections followed by zero or more settings
|
||||
* each.
|
||||
*
|
||||
* Setting0 = 100
|
||||
* [ Global ]
|
||||
* Setting1 = 200
|
||||
* [ Section1 ]
|
||||
* SettingA = 1 ; this is a comment
|
||||
* SettingB = foo
|
||||
* [ Section2 ]
|
||||
* SettingX = bar
|
||||
* SettingZ = "baz" ; quotes are optional and are stripped off during parsing
|
||||
* Setting0 = 100
|
||||
* [ Global ]
|
||||
* Setting1 = 200
|
||||
* [ Section1 ]
|
||||
* SettingA = 1 ; this is a comment
|
||||
* SettingB = foo
|
||||
* [ Section2 ]
|
||||
* SettingX = bar
|
||||
* SettingZ = "baz" ; quotes are optional and are stripped off during parsing
|
||||
*
|
||||
* 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".
|
||||
*
|
||||
* TODO
|
||||
* ----
|
||||
* - Define our own exceptions?
|
||||
*/
|
||||
|
||||
#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
|
||||
{
|
||||
Node Node::s_empty_node;
|
||||
|
||||
bool Node::ValueAsBool() const
|
||||
void Node::CheckEmptyOrMissing() const
|
||||
{
|
||||
const char *value = m_value.c_str();
|
||||
if (!value || !stricmp(value, "false") || !stricmp(value, "off") || !stricmp(value, "no"))
|
||||
return false;
|
||||
else if (!stricmp(value, "true") || !stricmp(value, "on") || !stricmp(value, "yes"))
|
||||
return true;
|
||||
return bool(ValueAsUnsigned());
|
||||
if (m_missing)
|
||||
throw std::range_error(Util::Format() << "Node \"" << m_key << "\" does not exist");
|
||||
if (Empty() && !m_missing)
|
||||
throw std::logic_error(Util::Format() << "Node \"" << m_key << "\" has no value" );
|
||||
}
|
||||
|
||||
bool Node::ValueAsBoolWithDefault(bool default_value) const
|
||||
const Node &Node::MissingNode(const std::string &key) const
|
||||
{
|
||||
if (this == &s_empty_node)
|
||||
return default_value;
|
||||
return ValueAsBool();
|
||||
}
|
||||
|
||||
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();
|
||||
auto it = m_missing_nodes.find(key);
|
||||
if (it == m_missing_nodes.end())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
const Node &Node::operator[](const std::string &path) const
|
||||
|
@ -119,7 +165,7 @@ namespace Util
|
|||
{
|
||||
auto it = e->m_children.find(key);
|
||||
if (it == e->m_children.end())
|
||||
return s_empty_node;
|
||||
return e->MissingNode(key);
|
||||
e = it->second.get();
|
||||
}
|
||||
return *e;
|
||||
|
@ -127,68 +173,72 @@ namespace Util
|
|||
|
||||
Node &Node::Get(const std::string &path)
|
||||
{
|
||||
// This is probably dangerous and we should just have a non-const []
|
||||
return const_cast<Node &>(operator[](path));
|
||||
Node *node = TryGet(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
|
||||
{
|
||||
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::ostringstream os;
|
||||
std::fill_n(std::ostream_iterator<char>(os), 2 * 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);
|
||||
Serialize(&os, indent_level);
|
||||
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
|
||||
// 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)
|
||||
{
|
||||
|
@ -209,10 +259,11 @@ namespace Util
|
|||
return;
|
||||
Destroy();
|
||||
*const_cast<std::string *>(&m_key) = that.m_key;
|
||||
m_value = that.m_value;
|
||||
for (Ptr_t child = that.m_first_child; child; child = child->m_next_sibling)
|
||||
if (that.m_value)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -240,26 +291,27 @@ namespace Util
|
|||
}
|
||||
|
||||
Node::Node()
|
||||
: m_key("config"), // a default key value
|
||||
m_value(nullptr) // this node is empty
|
||||
{
|
||||
//std::cout << "<<< Created " << "<null>" << " (" << this << ")" << std::endl;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Node::Node(const std::string &key, const std::string &value)
|
||||
: m_key(key),
|
||||
m_value(value)
|
||||
m_value(std::make_shared<ValueInstance<std::string>>(value))
|
||||
{
|
||||
//std::cout << "<<< Created " << key << '=' << value << " (" << this << ")" << std::endl;
|
||||
}
|
||||
|
||||
Node::Node(const Node &that)
|
||||
: m_key(that.m_key),
|
||||
m_value(that.m_value)
|
||||
{
|
||||
DeepCopy(that);
|
||||
}
|
||||
|
@ -273,244 +325,5 @@ namespace Util
|
|||
{
|
||||
//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
|
||||
} // Util
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#ifndef INCLUDED_UTIL_CONFIG_H
|
||||
#define INCLUDED_UTIL_CONFIG_H
|
||||
|
||||
#include "Util/Format.h"
|
||||
#include "Util/GenericValue.h"
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <iterator>
|
||||
#include <exception>
|
||||
|
||||
namespace Util
|
||||
{
|
||||
|
@ -12,33 +13,33 @@ namespace Util
|
|||
{
|
||||
class Node
|
||||
{
|
||||
public:
|
||||
typedef std::shared_ptr<Node> Ptr_t;
|
||||
typedef std::shared_ptr<const Node> ConstPtr_t;
|
||||
|
||||
private:
|
||||
Ptr_t m_next_sibling;
|
||||
Ptr_t m_first_child;
|
||||
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)
|
||||
std::string m_value;
|
||||
static Node s_empty_node; // key, value, and children must always be empty
|
||||
typedef std::shared_ptr<Node> ptr_t;
|
||||
typedef std::shared_ptr<const Node> const_ptr_t;
|
||||
const std::string m_key; // this cannot be changed (maps depend on it)
|
||||
std::shared_ptr<GenericValue> m_value; // null pointer marks this node as an invalid, empty node
|
||||
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()
|
||||
{
|
||||
m_value.reset();
|
||||
m_next_sibling.reset();
|
||||
m_first_child.reset();
|
||||
m_last_child.reset();
|
||||
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 Swap(Node &rhs);
|
||||
Node(); // prohibit accidental/unintentional creation of blank nodes
|
||||
friend Ptr_t CreateEmpty();
|
||||
Node(); // prohibit accidental/unintentional creation of empty nodes
|
||||
|
||||
public:
|
||||
class const_iterator;
|
||||
|
@ -46,10 +47,10 @@ namespace Util
|
|||
class iterator: public std::iterator<std::forward_iterator_tag, Node>
|
||||
{
|
||||
private:
|
||||
Ptr_t m_node;
|
||||
ptr_t m_node;
|
||||
friend class const_iterator;
|
||||
public:
|
||||
inline iterator(Ptr_t node = Ptr_t())
|
||||
inline iterator(ptr_t node = ptr_t())
|
||||
: m_node(node)
|
||||
{}
|
||||
inline iterator(const iterator &it)
|
||||
|
@ -72,7 +73,7 @@ namespace Util
|
|||
{
|
||||
return *m_node;
|
||||
}
|
||||
inline Ptr_t operator->() const
|
||||
inline ptr_t operator->() const
|
||||
{
|
||||
return m_node;
|
||||
}
|
||||
|
@ -89,12 +90,12 @@ namespace Util
|
|||
class const_iterator: public std::iterator<std::forward_iterator_tag, const Node>
|
||||
{
|
||||
private:
|
||||
ConstPtr_t m_node;
|
||||
const_ptr_t m_node;
|
||||
public:
|
||||
inline const_iterator(ConstPtr_t node = ConstPtr_t())
|
||||
inline const_iterator(const_ptr_t node = const_ptr_t())
|
||||
: 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))
|
||||
{}
|
||||
inline const_iterator(const const_iterator &it)
|
||||
|
@ -120,7 +121,7 @@ namespace Util
|
|||
{
|
||||
return *m_node;
|
||||
}
|
||||
inline ConstPtr_t operator->() const
|
||||
inline const_ptr_t operator->() const
|
||||
{
|
||||
return m_node;
|
||||
}
|
||||
|
@ -159,57 +160,135 @@ namespace Util
|
|||
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 m_value;
|
||||
}
|
||||
|
||||
inline void SetValue(const std::shared_ptr<GenericValue> &value)
|
||||
{
|
||||
m_value = value;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T &Value() const
|
||||
{
|
||||
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;
|
||||
return m_value->ValueAs<T>();
|
||||
}
|
||||
|
||||
const inline std::string &Value() const
|
||||
// Set value of this node
|
||||
template <typename T>
|
||||
inline void SetValue(const T &value)
|
||||
{
|
||||
//TODO: if empty node, throw exception? Use ValueWithDefault() otherwise.
|
||||
return m_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");
|
||||
}
|
||||
|
||||
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)
|
||||
// char* is a troublesome case because we want to convert it to std::string
|
||||
inline void SetValue(const char *value)
|
||||
{
|
||||
if (this != &s_empty_node) // not allowed to modify empty node
|
||||
m_value = value;
|
||||
SetValue<std::string>(value);
|
||||
}
|
||||
|
||||
inline bool Empty() const
|
||||
// Add a child node
|
||||
template <typename T>
|
||||
Node &Add(const std::string &path, const T &value)
|
||||
{
|
||||
return m_key.empty();
|
||||
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
|
||||
{
|
||||
return !Empty();
|
||||
}
|
||||
|
||||
// True if no keys under this node
|
||||
inline bool IsLeaf() const
|
||||
{
|
||||
return m_children.empty();
|
||||
}
|
||||
|
||||
// True if this node has keys
|
||||
inline bool HasChildren() const
|
||||
{
|
||||
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;
|
||||
|
||||
// These throw if the node is missing
|
||||
Node &Get(const std::string &path);
|
||||
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;
|
||||
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=(Node &&rhs);
|
||||
Node(const std::string &key);
|
||||
|
@ -218,15 +297,6 @@ namespace Util
|
|||
Node(Node &&that);
|
||||
~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
|
||||
} // Util
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "Util/NewConfig.h"
|
||||
#include "Util/ConfigBuilders.h"
|
||||
#include <iostream>
|
||||
|
||||
static void PrintTestResults(std::vector<std::pair<std::string, bool>> results)
|
||||
|
@ -30,58 +31,61 @@ int main()
|
|||
* </roms>
|
||||
* </game>
|
||||
*/
|
||||
Util::Config::Node::Ptr_t root = std::make_shared<Util::Config::Node>("global");
|
||||
auto &game = root->Add("game");
|
||||
game.Add("name", "scud");
|
||||
Util::Config::Node root("global");
|
||||
auto &game = root.Add("game");
|
||||
game.Add<std::string>("name", "scud");
|
||||
auto &roms = game.Add("roms");
|
||||
auto &crom1 = roms.Add("crom");
|
||||
crom1.Add("name", "foo.bin");
|
||||
crom1.Add("offset", "0");
|
||||
crom1.Add<std::string>("name", "foo.bin");
|
||||
crom1.Add<std::string>("offset", "0");
|
||||
auto &crom2 = roms.Add("crom");
|
||||
crom2.Add("name", "bar.bin");
|
||||
crom2.Add("offset", "2");
|
||||
crom2.Add<std::string>("name", "bar.bin");
|
||||
crom2.Add<std::string>("offset", "2");
|
||||
|
||||
const char *expected_output =
|
||||
"global children={ game }\n"
|
||||
" game children={ name roms }\n"
|
||||
" name=scud children={ }\n"
|
||||
" roms children={ crom }\n"
|
||||
" crom children={ name offset }\n"
|
||||
" name=foo.bin children={ }\n"
|
||||
" offset=0 children={ }\n"
|
||||
" crom children={ name offset }\n"
|
||||
" name=bar.bin children={ }\n"
|
||||
" offset=2 children={ }\n";
|
||||
"global=\"\" children={ game }\n"
|
||||
" game=\"\" children={ name roms }\n"
|
||||
" name=\"scud\" children={ }\n"
|
||||
" roms=\"\" children={ crom }\n"
|
||||
" crom=\"\" children={ name offset }\n"
|
||||
" name=\"foo.bin\" children={ }\n"
|
||||
" offset=\"0\" children={ }\n"
|
||||
" crom=\"\" children={ name offset }\n"
|
||||
" name=\"bar.bin\" children={ }\n"
|
||||
" offset=\"2\" children={ }\n";
|
||||
std::cout << "Expected output:" << std::endl << std::endl << expected_output << std::endl;
|
||||
std::cout << "Actual config tree:" << std::endl << std::endl;
|
||||
root->Print();
|
||||
root.Serialize(&std::cout);
|
||||
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
|
||||
auto &global = *root;
|
||||
std::cout << "game/roms/crom/name=" << global["game/roms/crom/name"].Value() << " (expected: bar.bin)" << std::endl << std::endl;
|
||||
test_results.push_back({ "Lookup", global["game/roms/crom/name"].Value() == "bar.bin" });
|
||||
std::cout << "game/roms/crom/name=" << root["game/roms/crom/name"].Value<std::string>() << " (expected: bar.bin)" << std::endl << std::endl;
|
||||
test_results.push_back({ "Lookup", root["game/roms/crom/name"].Value<std::string>() == "bar.bin" });
|
||||
|
||||
// Make a copy using copy constructor
|
||||
std::cout << "Copy constructed:" << std::endl << std::endl;
|
||||
Util::Config::Node::Ptr_t copy = std::make_shared<Util::Config::Node>(*root);
|
||||
copy->Print();
|
||||
Util::Config::Node copy(root);
|
||||
copy.Serialize(&std::cout);
|
||||
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;
|
||||
Util::Config::Node copy2("foo", "bar");
|
||||
copy2.Add("x/y/z", "test");
|
||||
copy2.Add("dummy", "value");
|
||||
copy2.Add<std::string>("x/y/z", "test");
|
||||
copy2.Add<std::string>("dummy", "value");
|
||||
std::cout << "Initially set to:" << std::endl;
|
||||
copy2.Print();
|
||||
copy2 = *root;
|
||||
copy2.Serialize(&std::cout);
|
||||
copy2 = root;
|
||||
std::cout << "Copy of \"global\":" << std::endl;
|
||||
copy2.Print();
|
||||
copy2.Serialize(&std::cout);
|
||||
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
|
||||
const char *xml =
|
||||
|
@ -102,77 +106,78 @@ int main()
|
|||
" </roms> \n"
|
||||
"</game> \n";
|
||||
const char *expected_xml_config_tree =
|
||||
"xml children={ game }\n"
|
||||
" game children={ name roms }\n"
|
||||
" name=scud children={ }\n"
|
||||
" roms children={ region }\n"
|
||||
" region children={ byte_swap file name stride }\n"
|
||||
" name=crom children={ }\n"
|
||||
" stride=8 children={ }\n"
|
||||
" byte_swap=true children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=0 children={ }\n"
|
||||
" name=epr-19734.20 children={ }\n"
|
||||
" crc32=0xBE897336 children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=2 children={ }\n"
|
||||
" name=epr-19733.19 children={ }\n"
|
||||
" crc32=0x6565E29A children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=4 children={ }\n"
|
||||
" name=epr-19732.18 children={ }\n"
|
||||
" crc32=0x23E864BB children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=6 children={ }\n"
|
||||
" name=epr-19731.17 children={ }\n"
|
||||
" crc32=0x3EE6447E children={ }\n"
|
||||
" region children={ byte_swap file name stride }\n"
|
||||
" name=banked_crom children={ }\n"
|
||||
" stride=8 children={ }\n"
|
||||
" byte_swap=true children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=0x0000000 children={ }\n"
|
||||
" name=mpr-20364.4 children={ }\n"
|
||||
" crc32=0xA2A68EF2 children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=0x0000002 children={ }\n"
|
||||
" name=mpr-20363.3 children={ }\n"
|
||||
" crc32=0x3E3CC6FF children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=0x0000004 children={ }\n"
|
||||
" name=mpr-20362.2 children={ }\n"
|
||||
" crc32=0xF7E60DFD children={ }\n"
|
||||
" file children={ crc32 name offset }\n"
|
||||
" offset=0x0000006 children={ }\n"
|
||||
" name=mpr-20361.1 children={ }\n"
|
||||
" crc32=0xDDB66C2F children={ }\n";
|
||||
"xml=\"\" children={ game }\n"
|
||||
" game=\"\" children={ name roms }\n"
|
||||
" name=\"scud\" children={ }\n"
|
||||
" roms=\"\" children={ region }\n"
|
||||
" region=\"\" children={ byte_swap file name stride }\n"
|
||||
" name=\"crom\" children={ }\n"
|
||||
" stride=\"8\" children={ }\n"
|
||||
" byte_swap=\"true\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"0\" children={ }\n"
|
||||
" name=\"epr-19734.20\" children={ }\n"
|
||||
" crc32=\"0xBE897336\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"2\" children={ }\n"
|
||||
" name=\"epr-19733.19\" children={ }\n"
|
||||
" crc32=\"0x6565E29A\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"4\" children={ }\n"
|
||||
" name=\"epr-19732.18\" children={ }\n"
|
||||
" crc32=\"0x23E864BB\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"6\" children={ }\n"
|
||||
" name=\"epr-19731.17\" children={ }\n"
|
||||
" crc32=\"0x3EE6447E\" children={ }\n"
|
||||
" region=\"\" children={ byte_swap file name stride }\n"
|
||||
" name=\"banked_crom\" children={ }\n"
|
||||
" stride=\"8\" children={ }\n"
|
||||
" byte_swap=\"true\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"0x0000000\" children={ }\n"
|
||||
" name=\"mpr-20364.4\" children={ }\n"
|
||||
" crc32=\"0xA2A68EF2\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"0x0000002\" children={ }\n"
|
||||
" name=\"mpr-20363.3\" children={ }\n"
|
||||
" crc32=\"0x3E3CC6FF\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"0x0000004\" children={ }\n"
|
||||
" name=\"mpr-20362.2\" children={ }\n"
|
||||
" crc32=\"0xF7E60DFD\" children={ }\n"
|
||||
" file=\"\" children={ crc32 name offset }\n"
|
||||
" offset=\"0x0000006\" children={ }\n"
|
||||
" name=\"mpr-20361.1\" children={ }\n"
|
||||
" crc32=\"0xDDB66C2F\" children={ }\n";
|
||||
std::cout << "Expected output:" << std::endl << std::endl << expected_xml_config_tree << std::endl;
|
||||
std::cout << "Actual config tree:" << std::endl << std::endl;
|
||||
Util::Config::Node::Ptr_t xml_config = Util::Config::FromXML(xml);
|
||||
xml_config->Print();
|
||||
Util::Config::Node xml_config("xml");
|
||||
Util::Config::FromXML(&xml_config, xml);
|
||||
xml_config.Serialize(&std::cout);
|
||||
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
|
||||
{
|
||||
Util::Config::Node::Ptr_t config = Util::Config::CreateEmpty();
|
||||
config->Add("foo/bar/baz", "bart");
|
||||
config->Print();
|
||||
Util::Config::Node config("global");
|
||||
config.Add<std::string>("foo/bar/baz", "bart");
|
||||
config.Serialize(&std::cout);
|
||||
std::cout << std::endl;
|
||||
test_results.push_back({ "Nested key 1", config->Get("foo/bar/baz").Value() == "bart" });
|
||||
config->Get("foo/bar/baz").Set("x", "xx");
|
||||
config->Get("foo/bar/baz").Set("y/z", "zz");
|
||||
config->Print();
|
||||
test_results.push_back({ "Nested key 1", config["foo/bar/baz"].Value<std::string>() == "bart" });
|
||||
config.Get("foo/bar/baz").Set<std::string>("x", "xx");
|
||||
config.Get("foo/bar/baz").Set<std::string>("y/z", "zz");
|
||||
config.Serialize(&std::cout);
|
||||
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 3", config->Get("foo/bar/baz/y/z").Value() == "zz" });
|
||||
config->Add("a/b/c");
|
||||
config->Get("a/b/c").SetValue("d");
|
||||
config->Print();
|
||||
test_results.push_back({ "Nested key 2", config["foo/bar/baz/x"].Value<std::string>() == "xx" });
|
||||
test_results.push_back({ "Nested key 3", config["foo/bar/baz/y/z"].Value<std::string>() == "zz" });
|
||||
config.Add("a/b/c");
|
||||
config.Get("a/b/c").SetValue<std::string>("d");
|
||||
config.Serialize(&std::cout);
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue