Introduce class AnyMap
This commit is contained in:
parent
2c3512c22a
commit
56022e8989
3 changed files with 457 additions and 0 deletions
274
include/cantera/base/AnyMap.h
Normal file
274
include/cantera/base/AnyMap.h
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
//! @file AnyMap.h
|
||||
|
||||
#ifndef CT_ANYMAP_H
|
||||
#define CT_ANYMAP_H
|
||||
|
||||
#include "cantera/base/ct_defs.h"
|
||||
#include "cantera/base/global.h"
|
||||
#include "cantera/base/ctexceptions.h"
|
||||
|
||||
#include <boost/any.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Cantera
|
||||
{
|
||||
|
||||
class AnyMap;
|
||||
|
||||
//! A wrapper for a variable whose type is determined at runtime
|
||||
/*!
|
||||
* Instances of AnyValue are used as values in an AnyMap. Values are converted
|
||||
* to a concrete type using the templated as() method or convenience methods
|
||||
* such as asString() and asDouble(). See AnyMap for usage examples.
|
||||
*
|
||||
* Elements are set using assignment, and the assignment operator has been
|
||||
* overloaded for specific types so that only those types are allowed to be
|
||||
* used in an AnyValue.
|
||||
*/
|
||||
class AnyValue
|
||||
{
|
||||
public:
|
||||
AnyValue& operator[](const std::string& key);
|
||||
|
||||
bool hasKey(const std::string& key) const;
|
||||
|
||||
// The value knows the name of its corresponding key in order to provide
|
||||
// comprehensible error messages.
|
||||
void setKey(const std::string& key) { m_key = key; };
|
||||
|
||||
template<class T>
|
||||
const T& as() const {
|
||||
try {
|
||||
return boost::any_cast<const T&>(m_value);
|
||||
} catch (boost::bad_any_cast&) {
|
||||
if (m_value.type() == typeid(void)) {
|
||||
// Values that have not been set are of type 'void'
|
||||
throw CanteraError("AnyValue::as", "Key '{}' not found", m_key);
|
||||
} else {
|
||||
throw CanteraError("AnyValue::as",
|
||||
"Key '{}' contains a '{}',\nnot a '{}'.",
|
||||
m_key, demangle(m_value.type()), demangle(typeid(T)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
T& as() {
|
||||
try {
|
||||
return boost::any_cast<T&>(m_value);
|
||||
} catch (boost::bad_any_cast&) {
|
||||
if (m_value.type() == typeid(void)) {
|
||||
// Values that have not been set are of type 'void'
|
||||
throw CanteraError("AnyValue::as", "Key '{}' not found", m_key);
|
||||
} else {
|
||||
throw CanteraError("AnyValue::as",
|
||||
"Key '{}' contains a '{}',\nnot a '{}'.",
|
||||
m_key, demangle(m_value.type()), demangle(typeid(T)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::type_info& type() {
|
||||
return m_value.type();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
bool is() const {
|
||||
return m_value.type() == typeid(T);
|
||||
}
|
||||
|
||||
AnyValue& operator=(const std::string& value) {
|
||||
m_value = value;
|
||||
return *this;
|
||||
}
|
||||
const std::string& asString() const {
|
||||
return as<std::string>();
|
||||
}
|
||||
|
||||
AnyValue& operator=(double value) {
|
||||
m_value = value;
|
||||
return *this;
|
||||
}
|
||||
double asDouble() const {
|
||||
return as<double>();
|
||||
}
|
||||
|
||||
template<class T>
|
||||
AnyValue& operator=(const std::vector<T>& value) {
|
||||
m_value = value;
|
||||
return *this;
|
||||
}
|
||||
template<class T>
|
||||
const std::vector<T>& asVector() const {
|
||||
return as<std::vector<T>>();
|
||||
}
|
||||
template<class T>
|
||||
std::vector<T>& asVector() {
|
||||
return as<std::vector<T>>();
|
||||
}
|
||||
|
||||
AnyValue& operator=(const AnyMap& value);
|
||||
AnyValue& operator=(AnyMap&& value);
|
||||
|
||||
template<class T>
|
||||
AnyValue& operator=(const std::unordered_map<std::string, T> items);
|
||||
|
||||
template<class T>
|
||||
AnyValue& operator=(const std::map<std::string, T> items);
|
||||
|
||||
template<class T>
|
||||
std::map<std::string, T> asMap();
|
||||
|
||||
private:
|
||||
std::string demangle(const std::type_info& type) const;
|
||||
|
||||
std::string m_key;
|
||||
boost::any m_value;
|
||||
static std::map<std::string, std::string> s_typenames;
|
||||
};
|
||||
|
||||
//! A map of string keys to values whose type can vary at runtime
|
||||
/*!
|
||||
* Values in an AnyMap are held by instances of AnyValue. Instances of AnyMap
|
||||
* can be nested to form a tree.
|
||||
*
|
||||
* ## Setting elements
|
||||
*
|
||||
* ```
|
||||
* AnyMap breakfast;
|
||||
* breakfast["spam"] = 123.4; // Creates a value of type 'double'
|
||||
* breakfast["eggs"] = "scrambled"; // Creates a value of type 'std::string'
|
||||
*
|
||||
* // Create a nested AnyMap named "beans" which has a key named "baked"
|
||||
* // whose value is a vector<double>
|
||||
* std::vector<double> v{3.14, 1.59, 2.65};
|
||||
* breakfast["beans/baked"] = v;
|
||||
* // Equivalently:
|
||||
* breakfast["beans"]["baked"] = v;
|
||||
*
|
||||
* // Create a nested AnyMap with values of the same type
|
||||
* std::map<std::string, double> breads{{"wheat", 4.0}, {"white", 2.5}};
|
||||
* breakfast["toast"] = breads;
|
||||
* // Equivalent to:
|
||||
* breakfast["toast"]["wheat"] = 4.0
|
||||
* breakfast["toast"]["white"] = 2.5
|
||||
* ```
|
||||
*
|
||||
* ## Accessing elements
|
||||
*
|
||||
* ```
|
||||
* double val1 = breakfast["spam"].asDouble();
|
||||
* std::string val2 = breakfast["eggs"].asString();
|
||||
* vector_fp val3 = breakfast["beans"]["baked"].asVector<double>();
|
||||
*
|
||||
* std::map<std::string, double> = breakfast["toast"].asMap<double>();
|
||||
* ```
|
||||
*
|
||||
* ## Checking for elements
|
||||
*
|
||||
* ```
|
||||
* try {
|
||||
* breakfast["waffle"].asDouble();
|
||||
* } except (std::exception& err) {
|
||||
* // Exception will be thrown.
|
||||
* // 'breakfast' will have an empty key named "waffle"
|
||||
* }
|
||||
*
|
||||
* try {
|
||||
* breakfast.at("grits").asDouble();
|
||||
* } except (std::exception& err) {
|
||||
* // Exception will be thrown and no new key will be added
|
||||
* }
|
||||
*
|
||||
* if (breakfast.hasKey("grits")) {
|
||||
* // do something with this entry
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Checking element types
|
||||
*
|
||||
* ```
|
||||
* if (breakfast["sausage"].is<vector<double>>()) {
|
||||
* // access using asVector<double>
|
||||
* } else if (breakfast["sausage"].type() == typeid(vector<std::string>)) {
|
||||
* // access using asVector<std::string>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class AnyMap
|
||||
{
|
||||
public:
|
||||
AnyMap() {};
|
||||
|
||||
AnyValue& operator[](const std::string& key);
|
||||
|
||||
AnyValue& at(const std::string& key);
|
||||
|
||||
bool hasKey(const std::string& key) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, AnyValue> m_data;
|
||||
friend class AnyValue;
|
||||
};
|
||||
|
||||
// Definitions for templated functions which require the full declaration of
|
||||
// class AnyMap.
|
||||
|
||||
template<class T>
|
||||
AnyValue& AnyValue::operator=(const std::unordered_map<std::string, T> items) {
|
||||
m_value = AnyMap();
|
||||
AnyMap& dest = as<AnyMap>();
|
||||
for (const auto& item : items) {
|
||||
dest[item.first] = item.second;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
AnyValue& AnyValue::operator=(const std::map<std::string, T> items) {
|
||||
m_value = AnyMap();
|
||||
AnyMap& dest = as<AnyMap>();
|
||||
for (const auto& item : items) {
|
||||
dest[item.first] = item.second;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline AnyMap& AnyValue::as<AnyMap>() {
|
||||
try {
|
||||
// This is where nested AnyMaps are created when the syntax
|
||||
// m[key1][key2] is used.
|
||||
if (m_value.type() == typeid(void)) {
|
||||
m_value = std::move(AnyMap());
|
||||
}
|
||||
return boost::any_cast<AnyMap&>(m_value);
|
||||
} catch (boost::bad_any_cast&) {
|
||||
throw CanteraError("AnyValue::as",
|
||||
"value of key '{}' is a '{}',\nnot an 'AnyMap'.",
|
||||
m_key, demangle(m_value.type()));
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
std::map<std::string, T> AnyValue::asMap()
|
||||
{
|
||||
std::map<std::string, T> dest;
|
||||
for (const auto& item : as<AnyMap>().m_data) {
|
||||
try {
|
||||
dest[item.first] = boost::any_cast<T>(item.second.m_value);
|
||||
} catch (boost::bad_any_cast&) {
|
||||
throw CanteraError("AnyValue::asMap",
|
||||
"Value of key '{}' is not a '{}'",
|
||||
item.first, demangle(typeid(T)));
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
114
src/base/AnyMap.cpp
Normal file
114
src/base/AnyMap.cpp
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
//! @file AnyMap.cpp
|
||||
|
||||
// This file is part of Cantera. See License.txt in the top-level directory or
|
||||
// at http://www.cantera.org/license.txt for license and copyright information.
|
||||
|
||||
#include "cantera/base/AnyMap.h"
|
||||
|
||||
namespace Cantera
|
||||
{
|
||||
|
||||
std::map<std::string, std::string> AnyValue::s_typenames = {
|
||||
{typeid(double).name(), "double"},
|
||||
{typeid(std::string).name(), "string"},
|
||||
{typeid(std::vector<double>).name(), "vector<double>"},
|
||||
{typeid(AnyMap).name(), "AnyMap"},
|
||||
};
|
||||
|
||||
// Methods of class AnyValue
|
||||
|
||||
AnyValue& AnyValue::operator[](const std::string& key)
|
||||
{
|
||||
return as<AnyMap>()[key];
|
||||
}
|
||||
|
||||
bool AnyValue::hasKey(const std::string& key) const {
|
||||
return (is<AnyMap>() && as<AnyMap>().hasKey(key));
|
||||
}
|
||||
|
||||
AnyValue& AnyValue::operator=(const AnyMap& value) {
|
||||
m_value = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
AnyValue& AnyValue::operator=(AnyMap&& value) {
|
||||
m_value = std::move(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::string AnyValue::demangle(const std::type_info& type) const
|
||||
{
|
||||
if (s_typenames.find(type.name()) != s_typenames.end()) {
|
||||
return s_typenames[type.name()];
|
||||
} else {
|
||||
return type.name();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Methods of class AnyMap
|
||||
|
||||
AnyValue& AnyMap::operator[](const std::string& key)
|
||||
{
|
||||
const auto& slash = boost::ifind_first(key, "/");
|
||||
if (!slash) {
|
||||
// Simple key
|
||||
const auto& iter = m_data.find(key);
|
||||
if (iter == m_data.end()) {
|
||||
// Create a new key return it
|
||||
// NOTE: 'insert' can be replaced with 'emplace' after support for
|
||||
// G++ 4.7 is dropped.
|
||||
AnyValue& value = m_data.insert({key, AnyValue()}).first->second;
|
||||
value.setKey(key);
|
||||
return value;
|
||||
} else {
|
||||
// Return an already-existing item
|
||||
return iter->second;
|
||||
}
|
||||
} else {
|
||||
// Split the first slash-delimited part of key and recurse
|
||||
std::string head(key.begin(), slash.begin());
|
||||
std::string tail(slash.end(), key.end());
|
||||
const auto& iter = m_data.find(head);
|
||||
if (iter == m_data.end()) {
|
||||
// Create a new key
|
||||
AnyValue& value = m_data.insert({head, AnyValue()}).first->second;
|
||||
value = std::move(AnyMap());
|
||||
value.setKey(head);
|
||||
return value.as<AnyMap>()[tail];
|
||||
} else {
|
||||
// Return an already existing key
|
||||
return iter->second.as<AnyMap>()[tail];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnyValue& AnyMap::at(const std::string& key)
|
||||
{
|
||||
const auto& slash = boost::ifind_first(key, "/");
|
||||
if (!slash) {
|
||||
return m_data.at(key);
|
||||
} else {
|
||||
std::string head(key.begin(), slash.begin());
|
||||
std::string tail(slash.end(), key.end());
|
||||
return m_data.at(head).as<AnyMap>().at(tail);
|
||||
}
|
||||
}
|
||||
|
||||
bool AnyMap::hasKey(const std::string& key) const
|
||||
{
|
||||
const auto& slash = boost::ifind_first(key, "/");
|
||||
if (!slash) {
|
||||
return (m_data.find(key) != m_data.end());
|
||||
} else {
|
||||
std::string head(key.begin(), slash.begin());
|
||||
std::string tail(slash.end(), key.end());
|
||||
if (m_data.find(head) == m_data.end() || !m_data.at(head).is<AnyMap>()) {
|
||||
return false;
|
||||
} else {
|
||||
return m_data.at(head).as<AnyMap>().hasKey(tail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
69
test/general/test_containers.cpp
Normal file
69
test/general/test_containers.cpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include "cantera/base/AnyMap.h"
|
||||
|
||||
using namespace Cantera;
|
||||
|
||||
TEST(AnyMap, paths) {
|
||||
AnyMap m;
|
||||
m["simple"] = "qux";
|
||||
m["compound/first"] = "bar";
|
||||
m["compound/second"] = "baz";
|
||||
m["several"]["layers"]["deep"] = "foo";
|
||||
|
||||
EXPECT_TRUE(m.hasKey("simple"));
|
||||
EXPECT_TRUE(m.hasKey("compound"));
|
||||
EXPECT_TRUE(m.hasKey("compound/first"));
|
||||
EXPECT_TRUE(m["compound"].hasKey("second"));
|
||||
|
||||
EXPECT_EQ(m["compound/first"].asString(), "bar");
|
||||
EXPECT_EQ(m["compound/second"].as<std::string>(), "baz");
|
||||
EXPECT_EQ(m["several/layers/deep"].asString(), "foo");
|
||||
EXPECT_THROW(m.at("missing"), std::exception);
|
||||
EXPECT_FALSE(m.hasKey("missing"));
|
||||
EXPECT_FALSE(m.hasKey("compound/missing"));
|
||||
EXPECT_THROW(m["missing"].asString(), std::exception);
|
||||
}
|
||||
|
||||
TEST(AnyMap, map_conversion) {
|
||||
AnyMap m;
|
||||
m["compound/first"] = "bar";
|
||||
m["compound/second"] = "baz";
|
||||
|
||||
auto x = m["compound"].asMap<std::string>();
|
||||
EXPECT_EQ(x.size(), (size_t) 2);
|
||||
EXPECT_EQ(x["first"], "bar");
|
||||
EXPECT_EQ(x["second"], "baz");
|
||||
|
||||
std::map<std::string, double> zz{{"a", 9.0}, {"b", 13.5}};
|
||||
m["foo"] = zz;
|
||||
EXPECT_TRUE(m.hasKey("foo/a"));
|
||||
EXPECT_DOUBLE_EQ(m["foo"]["b"].asDouble(), 13.5);
|
||||
|
||||
EXPECT_THROW(m["foo"]["a"].asString(), CanteraError);
|
||||
EXPECT_THROW(m["foo"]["b"].asVector<double>(), CanteraError);
|
||||
}
|
||||
|
||||
TEST(AnyMap, nested)
|
||||
{
|
||||
AnyMap m;
|
||||
AnyMap b;
|
||||
b["foo"] = "bar";
|
||||
b["qux"] = 0.5;
|
||||
m["baz"] = b;
|
||||
b["late"] = "nope"; // Should have added a copy
|
||||
EXPECT_EQ(m["baz/foo"].asString(), "bar");
|
||||
EXPECT_EQ(m["baz"]["qux"].asDouble(), 0.5);
|
||||
EXPECT_FALSE(m.hasKey("baz/late"));
|
||||
}
|
||||
|
||||
TEST(AnyMap, vector)
|
||||
{
|
||||
AnyMap m;
|
||||
vector_fp yy{9.6, 14.4, 28.8};
|
||||
m["nested"]["item"] = yy;
|
||||
vector_fp& yref = m["nested/item"].asVector<double>();
|
||||
yref.push_back(-1);
|
||||
EXPECT_EQ(yy.size(), (size_t) 3); // Should have added a copy
|
||||
// Should have modified the copy in the map
|
||||
EXPECT_EQ(m["nested/item"].asVector<double>().size(), (size_t) 4);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue