Skip to content
Snippets Groups Projects
Commit 4ac2050e authored by Felix Weiglhofer's avatar Felix Weiglhofer
Browse files

algo: Revisit config library.

- Small refactoring
- Add support for std::map
- Add some unittest coverage
- Add CBM_FORMAT to specify how structs should formatted in yaml
parent 1aa44641
No related branches found
No related tags found
1 merge request!1385algo: Revisit config library.
Pipeline #24705 passed
......@@ -13,7 +13,7 @@
namespace cbm::algo::config
{
using BaseTypes = std::tuple<bool, u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, std::string>;
using FundamentalTypes = std::tuple<bool, u8, i8, u16, i16, u32, i32, u64, i64, f32, f64, std::string>;
template<typename T, typename Tuple>
struct has_type;
......@@ -23,13 +23,16 @@ namespace cbm::algo::config
};
template<typename T>
constexpr bool IsBaseType = has_type<T, BaseTypes>::value;
constexpr bool IsFundamental = has_type<T, FundamentalTypes>::value;
template<typename T>
constexpr bool IsEnum = std::is_enum_v<T>;
template<typename T, typename = std::enable_if_t<IsBaseType<T>>>
constexpr std::string_view BaseTypeToStr()
template<typename T>
constexpr bool IsScalar = IsFundamental<T> || IsEnum<T>;
template<typename T, typename = std::enable_if_t<IsFundamental<T>>>
constexpr std::string_view Typename()
{
if constexpr (std::is_same_v<bool, T>) { return "bool"; }
else if constexpr (std::is_same_v<u8, T>) {
......@@ -50,8 +53,11 @@ namespace cbm::algo::config
else if constexpr (std::is_same_v<i32, T>) {
return "i32";
}
else if constexpr (std::is_same_v<float, T>) {
return "float";
else if constexpr (std::is_same_v<f32, T>) {
return "f32";
}
else if constexpr (std::is_same_v<f64, T>) {
return "f64";
}
else if constexpr (std::is_same_v<std::string, T>) {
return "string";
......@@ -83,6 +89,17 @@ namespace cbm::algo::config
template<typename T>
constexpr bool IsArray = is_std_array<T>::value;
template<typename>
struct is_std_map : std::false_type {
};
template<typename K, typename V, typename C, typename A>
struct is_std_map<std::map<K, V, C, A>> : std::true_type {
};
template<typename T>
constexpr bool IsMap = is_std_map<T>::value;
} // namespace cbm::algo::config
#endif // CBM_ALGO_BASE_CONFIG_BASETYPES_H
......@@ -63,4 +63,15 @@ namespace cbm::algo::config
} // namespace cbm::algo::config
#define CBM_PROPERTIES(...) \
public: \
static constexpr auto Properties = std::make_tuple(__VA_ARGS__)
/**
* @brief Optional tag to specify a formatting of the class (YAML::Flow vs YAML::Block)
*/
#define CBM_FORMAT(tag) \
public: \
static constexpr std::optional<YAML::EMITTER_MANIP> FormatAs = tag
#endif // CBM_ALGO_BASE_CONFIG_PROPERTY_H
......@@ -24,6 +24,16 @@ namespace cbm::algo::config
(func(std::integral_constant<T, Values> {}), ...);
}
template<typename T, typename = void>
struct GetFmtTag {
static constexpr std::optional<YAML::EMITTER_MANIP> value = {};
};
template<typename T>
struct GetFmtTag<T, std::void_t<decltype(T::FormatAs)>> {
static constexpr std::optional<YAML::EMITTER_MANIP> value = T::FormatAs;
};
template<typename T>
T Read(const YAML::Node& node)
{
......@@ -32,7 +42,7 @@ namespace cbm::algo::config
static_assert(!IsEnum<T> || detail::EnumHasDict_v<T>, "Enum must have a dictionary to be deserializable");
// TODO: error handling
if constexpr (IsBaseType<Type>) { return node.as<Type>(); }
if constexpr (IsFundamental<Type>) { return node.as<Type>(); }
else if constexpr (IsEnum<Type>) {
std::optional<T> maybet = FromString<T>(node.as<std::string>());
if (!maybet) { throw std::runtime_error(fmt::format("Invalid enum value: {}", node.as<std::string>())); }
......@@ -46,7 +56,7 @@ namespace cbm::algo::config
return vector;
}
else if constexpr (IsArray<Type>) {
Type array {};
Type array = {};
auto vector = Read<std::vector<typename Type::value_type>>(node);
if (vector.size() != array.size()) {
throw std::runtime_error(fmt::format("Array size mismatch: expected {}, got {}", array.size(), vector.size()));
......@@ -54,6 +64,20 @@ namespace cbm::algo::config
std::copy(vector.begin(), vector.end(), array.begin());
return array;
}
else if constexpr (IsMap<Type>) {
using Key_t = typename Type::key_type;
using Val_t = typename Type::mapped_type;
static_assert(IsScalar<Key_t>, "Map key must be a fundamental or enum type");
Type map;
for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) {
const auto& key = it->first;
const auto& value = it->second;
map[Read<Key_t>(key)] = Read<Val_t>(value);
}
return map;
}
else {
Type object;
constexpr auto nProperties = std::tuple_size<decltype(Type::Properties)>::value;
......@@ -74,11 +98,11 @@ namespace cbm::algo::config
std::stringstream docString;
if constexpr (IsBaseType<Type>) { docString << BaseTypeToStr<Type>(); }
if constexpr (IsFundamental<Type>) { docString << Typename<Type>(); }
else if constexpr (IsVector<Type> || IsArray<Type>) {
using ChildType = typename Type::value_type;
if constexpr (IsBaseType<ChildType>) {
docString << std::string(indent, ' ') << "list of " << BaseTypeToStr<ChildType>() << std::endl;
if constexpr (IsFundamental<ChildType>) {
docString << std::string(indent, ' ') << "list of " << Typename<ChildType>() << std::endl;
}
else {
docString << std::string(indent, ' ') << "list of" << std::endl;
......@@ -91,9 +115,9 @@ namespace cbm::algo::config
using ChildType = std::remove_cv_t<
std::remove_reference_t<decltype(std::get<index>(Type::Properties).Get(std::declval<Type>()))>>;
auto& property = std::get<index>(Type::Properties);
if constexpr (IsBaseType<ChildType>) {
if constexpr (IsFundamental<ChildType>) {
docString << std::string(indent, ' ') << property.Key() << ": " << property.Description() << " ["
<< BaseTypeToStr<ChildType>() << "]" << std::endl;
<< Typename<ChildType>() << "]" << std::endl;
}
else {
docString << std::string(indent, ' ') << property.Key() << ": " << property.Description() << std::endl;
......@@ -124,7 +148,12 @@ namespace cbm::algo::config
{
static_assert(!IsEnum<T> || detail::EnumHasDict_v<T>, "Enum must have a dictionary");
if constexpr (IsBaseType<T>) { ss << object; }
if constexpr (IsFundamental<T>) {
// Take care that i8 and u8 are printed as integers not as characters
if constexpr (std::is_same_v<T, i8> || std::is_same_v<T, u8>) ss << i32(object);
else
ss << object;
}
else if constexpr (IsEnum<T>) {
ss << ToString<T>(object);
}
......@@ -136,8 +165,19 @@ namespace cbm::algo::config
}
ss << YAML::EndSeq;
}
else if constexpr (IsMap<T>) {
ss << YAML::BeginMap;
for (const auto& [key, value] : object) {
if (formatEntries.has_value()) { ss << formatEntries.value(); }
ss << YAML::Key << key;
ss << YAML::Value;
DoDump(value, ss);
}
ss << YAML::EndMap;
}
else {
constexpr auto nProperties = std::tuple_size<decltype(T::Properties)>::value;
if (auto fmtTag = GetFmtTag<T>::value; fmtTag.has_value()) { ss << fmtTag.value(); }
ss << YAML::BeginMap;
ForEach(std::make_integer_sequence<std::size_t, nProperties> {}, [&](auto index) {
auto& property = std::get<index>(T::Properties);
......
......@@ -45,3 +45,9 @@ Set(DigiEventSelectorSources
CreateGTestExeAndAddTest(_GTestDigiEventSelector "${INCLUDE_DIRECTORIES}" "${LINK_DIRECTORIES}"
"${DigiEventSelectorSources}" "${PUB_DEPS}" "${PVT_DEPS}" "${INT_DEPS}" "")
Set(YamlConfigSources
_GTestYamlConfig.cxx
)
CreateGTestExeAndAddTest(_GTestYamlConfig "${INCLUDE_DIRECTORIES}" "${LINK_DIRECTORIES}"
"${YamlConfigSources}" "${PUB_DEPS}" "${PVT_DEPS}" "${INT_DEPS}" "")
/* Copyright (C) 2023 FIAS Frankfurt Institute for Advanced Studies, Frankfurt / Main
SPDX-License-Identifier: GPL-3.0-only
Authors: Felix Weiglhofer [committer] */
#include "config/Property.h"
#include "config/Yaml.h"
#include "gtest/gtest.h"
using namespace cbm::algo;
//
// TODO: These tests are nowhere near exhaustive. Extend after the DC.
//
TEST(Config, CanDeserializeStdMap)
{
using Map_t = std::map<i32, i32>;
std::stringstream ss;
ss << "1: 2\n";
ss << "3: 4\n";
ss << "5: 6\n";
YAML::Node node = YAML::Load(ss.str());
auto map = config::Read<Map_t>(node);
EXPECT_EQ(map.size(), 3);
EXPECT_EQ(map.at(1), 2);
EXPECT_EQ(map.at(3), 4);
EXPECT_EQ(map.at(5), 6);
}
TEST(Config, CanSerializeStdMap)
{
using Map_t = std::map<i32, i32>;
Map_t map;
map[1] = 2;
map[3] = 4;
map[5] = 6;
YAML::Node node = YAML::Load(config::Dump {}(map));
EXPECT_EQ(node.size(), 3);
EXPECT_EQ(node[1].as<i32>(), 2);
EXPECT_EQ(node[3].as<i32>(), 4);
EXPECT_EQ(node[5].as<i32>(), 6);
}
class Foo {
private:
i32 fBar = 42;
i32 fBaz = 43;
public:
i32 GetBar() const { return fBar; }
i32 GetBaz() const { return fBaz; }
CBM_PROPERTIES(config::Property(&Foo::fBar, "bar", ""), config::Property(&Foo::fBaz, "baz", ""));
};
TEST(Config, CanAccessPrivateMembers)
{
std::stringstream ss;
ss << "bar: 1\n";
ss << "baz: 2\n";
YAML::Node node = YAML::Load(ss.str());
auto foo = config::Read<Foo>(node);
EXPECT_EQ(foo.GetBar(), 1);
EXPECT_EQ(foo.GetBaz(), 2);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment