Introduce class AnyMap

This commit is contained in:
Ray Speth 2017-02-26 23:19:57 -05:00
parent 2c3512c22a
commit 56022e8989
3 changed files with 457 additions and 0 deletions

View 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
View 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);
}
}
}
}

View 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);
}