diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h new file mode 100644 index 000000000..0976081c1 --- /dev/null +++ b/include/cantera/base/AnyMap.h @@ -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 +#include + +#include +#include +#include + +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 + const T& as() const { + try { + return boost::any_cast(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 + T& as() { + try { + return boost::any_cast(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 + 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(); + } + + AnyValue& operator=(double value) { + m_value = value; + return *this; + } + double asDouble() const { + return as(); + } + + template + AnyValue& operator=(const std::vector& value) { + m_value = value; + return *this; + } + template + const std::vector& asVector() const { + return as>(); + } + template + std::vector& asVector() { + return as>(); + } + + AnyValue& operator=(const AnyMap& value); + AnyValue& operator=(AnyMap&& value); + + template + AnyValue& operator=(const std::unordered_map items); + + template + AnyValue& operator=(const std::map items); + + template + std::map asMap(); + +private: + std::string demangle(const std::type_info& type) const; + + std::string m_key; + boost::any m_value; + static std::map 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 + * std::vector 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 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(); + * + * std::map = breakfast["toast"].asMap(); + * ``` + * + * ## 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>()) { + * // access using asVector + * } else if (breakfast["sausage"].type() == typeid(vector)) { + * // access using asVector + * } + * ``` + */ +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 m_data; + friend class AnyValue; +}; + +// Definitions for templated functions which require the full declaration of +// class AnyMap. + +template +AnyValue& AnyValue::operator=(const std::unordered_map items) { + m_value = AnyMap(); + AnyMap& dest = as(); + for (const auto& item : items) { + dest[item.first] = item.second; + } + return *this; +} + +template +AnyValue& AnyValue::operator=(const std::map items) { + m_value = AnyMap(); + AnyMap& dest = as(); + for (const auto& item : items) { + dest[item.first] = item.second; + } + return *this; +} + +template<> +inline AnyMap& AnyValue::as() { + 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(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 +std::map AnyValue::asMap() +{ + std::map dest; + for (const auto& item : as().m_data) { + try { + dest[item.first] = boost::any_cast(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 diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp new file mode 100644 index 000000000..4a9813eb8 --- /dev/null +++ b/src/base/AnyMap.cpp @@ -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 AnyValue::s_typenames = { + {typeid(double).name(), "double"}, + {typeid(std::string).name(), "string"}, + {typeid(std::vector).name(), "vector"}, + {typeid(AnyMap).name(), "AnyMap"}, +}; + +// Methods of class AnyValue + +AnyValue& AnyValue::operator[](const std::string& key) +{ + return as()[key]; +} + +bool AnyValue::hasKey(const std::string& key) const { + return (is() && as().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()[tail]; + } else { + // Return an already existing key + return iter->second.as()[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().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()) { + return false; + } else { + return m_data.at(head).as().hasKey(tail); + } + } +} + +} diff --git a/test/general/test_containers.cpp b/test/general/test_containers.cpp new file mode 100644 index 000000000..31d33d738 --- /dev/null +++ b/test/general/test_containers.cpp @@ -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(), "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(); + EXPECT_EQ(x.size(), (size_t) 2); + EXPECT_EQ(x["first"], "bar"); + EXPECT_EQ(x["second"], "baz"); + + std::map 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(), 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(); + 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().size(), (size_t) 4); +}