Last active
May 6, 2020 15:08
-
-
Save aldanor/c7c45ca4c02c1fe9fa209f88e94bebf4 to your computer and use it in GitHub Desktop.
better-yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define CATCH_CONFIG_MAIN | |
#include <catch.hpp> | |
#include <array> | |
#include <cstdint> | |
#include <limits> | |
#include <map> | |
#include <optional> | |
#include <sstream> | |
#include <stdexcept> | |
#include <tuple> | |
#include <type_traits> | |
#include <unordered_map> | |
#include <utility> | |
#include <variant> | |
#include <yaml-cpp/yaml.h> | |
template<typename T> | |
struct yaml_decode_fn; | |
namespace detail { | |
inline std::string at_pos(const YAML::Node& node) { | |
auto mark = node.Mark(); | |
return " at " + std::to_string(mark.line + 1) + ":" + std::to_string(mark.column); | |
} | |
template<typename T> | |
std::string int_name() { | |
return (std::is_signed_v<T> ? "int" : "uint") + std::to_string(sizeof(T) * 8); | |
} | |
inline void check_sequence( | |
const YAML::Node& node, | |
const std::string& tag, | |
std::optional<std::size_t> size | |
) { | |
if (!node.IsSequence()) { | |
throw std::domain_error( | |
"failed to parse " + tag + detail::at_pos(node) + ": not a sequence" | |
); | |
} else if (size && node.size() != *size) { | |
throw std::domain_error( | |
"failed to parse " + tag + detail::at_pos(node) + ": expected length " | |
+ std::to_string(*size) + ", got " + std::to_string(node.size()) | |
); | |
} | |
} | |
template<typename C> | |
void decode_map(const YAML::Node& node, C& container, const std::string& tag) { | |
if (!node.IsMap()) { | |
throw std::domain_error( | |
"failed to parse " + tag + detail::at_pos(node) + ": not a map" | |
); | |
} | |
using K = std::decay_t<typename C::key_type>; | |
using V = std::decay_t<typename C::mapped_type>; | |
for (auto&& kv : node) { | |
container[yaml_decode_fn<K>{}(kv.first)] = yaml_decode_fn<V>{}(kv.second); | |
} | |
} | |
} // namespace detail | |
template<typename T, typename = void> | |
struct yaml_decoder { | |
static T decode(const YAML::Node& node) { | |
T value; | |
if (!YAML::convert<T>::decode(node, value)) { | |
throw std::domain_error("failed to parse value" + detail::at_pos(node)); | |
} | |
return value; | |
} | |
}; | |
template<typename T> | |
struct yaml_decoder< | |
T, | |
std::enable_if_t<std::is_integral_v<T> && !std::is_same_v<T, bool>> | |
> { | |
static T decode(const YAML::Node& node) { | |
constexpr bool is_signed = std::is_signed_v<T>; | |
constexpr auto v_min = static_cast<std::int64_t>(std::numeric_limits<T>::min()); | |
constexpr auto v_max = static_cast<std::uint64_t>(std::numeric_limits<T>::max()); | |
std::int64_t v_i64 = 0; | |
std::uint64_t v_u64 = 0; | |
bool ok_i64 = YAML::convert<std::int64_t>::decode(node, v_i64); | |
bool ok_u64 = YAML::convert<std::uint64_t>::decode(node, v_u64); | |
std::string v_err; | |
if (v_i64 < v_min) { | |
v_err = std::to_string(v_i64); | |
} else if (ok_u64 && v_i64 >= 0 && v_u64 > v_max) { | |
v_err = std::to_string(v_u64); | |
} else if (is_signed && ok_i64) { | |
return static_cast<T>(v_i64); | |
} else if (!is_signed && ok_u64) { | |
return static_cast<T>(v_u64); | |
} | |
if (!v_err.empty()) { | |
throw std::domain_error( | |
"value" + detail::at_pos(node) + " out of bounds for " | |
+ detail::int_name<T>() + ": " + v_err | |
); | |
} | |
throw std::domain_error( | |
"failed to parse " + detail::int_name<T>() + detail::at_pos(node) | |
); | |
} | |
}; | |
template<typename T, std::size_t N> | |
struct yaml_decoder<std::array<T, N>> { | |
static auto decode(const YAML::Node& node) { | |
detail::check_sequence(node, "std::array", {N}); | |
std::array<T, N> out {}; | |
for (std::size_t i = 0; i < N; ++i) { | |
out[i] = std::move(yaml_decode_fn<T>{}(node[i])); | |
} | |
return out; | |
} | |
}; | |
template<typename T> | |
struct yaml_decoder<std::vector<T>> { | |
static auto decode(const YAML::Node& node) { | |
detail::check_sequence(node, "std::vector", {}); | |
std::vector<T> out; | |
for (auto&& child : node) { | |
out.emplace_back(yaml_decode_fn<T>{}(child)); | |
} | |
return out; | |
} | |
}; | |
template<typename... Ts> | |
struct yaml_decoder<std::tuple<Ts...>> { | |
using T = std::tuple<Ts...>; | |
constexpr static auto N = std::tuple_size_v<T>; | |
template<std::size_t I> | |
static void decode_element(const YAML::Node& node, T& out) { | |
std::get<I>(out) = yaml_decode_fn<std::tuple_element_t<I, T>>{}(node, I); | |
} | |
template<std::size_t... I> | |
static auto decode_tuple(const YAML::Node& node, T& out, std::index_sequence<I...>) { | |
(decode_element<I>(node, out), ...); | |
} | |
static auto decode(const YAML::Node& node) { | |
T out {}; | |
detail::check_sequence(node, "std::tuple", {N}); | |
decode_tuple(node, out, std::make_index_sequence<N>()); | |
return out; | |
} | |
}; | |
template<typename K, typename V> | |
struct yaml_decoder<std::map<K, V>> { | |
static auto decode(const YAML::Node& node) { | |
std::map<K, V> out {}; | |
detail::decode_map(node, out, "std::map"); | |
return out; | |
} | |
}; | |
template<typename K, typename V> | |
struct yaml_decoder<std::unordered_map<K, V>> { | |
static auto decode(const YAML::Node& node) { | |
std::unordered_map<K, V> out {}; | |
detail::decode_map(node, out, "std::unordered_map"); | |
return out; | |
} | |
}; | |
template<typename... Ts> | |
struct yaml_decoder<std::variant<Ts...>> { | |
using T = std::variant<Ts...>; | |
template<typename V> | |
static void try_decode(const YAML::Node& node, T& out, bool& done) { | |
if (!done) { | |
try { | |
out.template emplace<V>(yaml_decode_fn<V>{}(node)); | |
done = true; | |
} catch (...) {} | |
} | |
} | |
static auto decode(const YAML::Node& node) { | |
T out {}; | |
bool done = false; | |
(try_decode<Ts>(node, out, done), ...); | |
if (!done) { | |
throw std::domain_error("failed to parse std::variant" + detail::at_pos(node)); | |
} | |
return out; | |
} | |
}; | |
template<typename T> | |
struct yaml_decode_fn { | |
T operator()(const YAML::Node& node) const { | |
if (!node) { | |
throw std::domain_error("failed to parse value: invalid node"); | |
} | |
return yaml_decoder<T>::decode(node); | |
} | |
template<typename K> | |
T operator()(const YAML::Node& node, K&& key) const { | |
if (!node) { | |
throw std::domain_error("failed to parse value: invalid node" ); | |
} | |
auto child = node[key]; | |
if (!node[key]) { | |
std::stringstream err; | |
err << "failed to parse value" + detail::at_pos(node) + ": invalid index: "; | |
using U = std::decay_t<K>; | |
if constexpr (std::is_same_v<U, const char*> || std::is_same_v<U, std::string>) { | |
err << "'" << key << "'"; | |
} else { | |
err << key; | |
} | |
throw std::domain_error(err.str()); | |
} | |
return yaml_decode_fn<T>{}(child); | |
} | |
}; | |
template<typename T> | |
struct yaml_decode_maybe_fn { | |
std::optional<T> operator()(const YAML::Node& node) const { | |
if (!node) { | |
return std::nullopt; | |
} | |
return yaml_decode_fn<T>{}(node); | |
} | |
template<typename K> | |
std::optional<T> operator()(const YAML::Node& node, K&& key) const { | |
return yaml_decode_maybe_fn<T>{}(node[key]); | |
} | |
}; | |
template<typename T> | |
inline constexpr auto yaml_decode = yaml_decode_fn<T>{}; | |
template<typename T> | |
inline constexpr auto yaml_decode_maybe = yaml_decode_maybe_fn<T>{}; | |
TEST_CASE("yaml_decode [general]") { | |
auto s = YAML::Load("foo:\n bar: 42\n baz: [10, 20]"); | |
REQUIRE(yaml_decode<std::int8_t>(s["foo"]["bar"]) == 42); | |
REQUIRE(yaml_decode<std::int8_t>(s["foo"], "bar") == 42); | |
REQUIRE(yaml_decode<std::int8_t>(s["foo"]["baz"][1]) == 20); | |
REQUIRE(yaml_decode<std::int8_t>(s["foo"]["baz"], 1) == 20); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<std::int8_t>(s), | |
"failed to parse int8 at 1:0" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<std::int8_t>(s["foo"]), | |
"failed to parse int8 at 2:2" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<std::int8_t>(s["x"]), | |
"failed to parse value: invalid node" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<std::int8_t>(s["x"], "y"), | |
"failed to parse value: invalid node" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<std::int8_t>(s, "x"), | |
"failed to parse value at 1:0: invalid index: 'x'" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<std::int8_t>(s, std::string("x")), | |
"failed to parse value at 1:0: invalid index: 'x'" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<std::int8_t>(s["foo"]["baz"], 10), | |
"failed to parse value at 3:7: invalid index: 10" | |
); | |
} | |
TEST_CASE("yaml_decode_maybe") { | |
auto s = YAML::Load("foo:\n bar: 42\n baz: [10, 20]"); | |
REQUIRE(!yaml_decode_maybe<std::int8_t>(s["foo"]["x"])); | |
REQUIRE(*yaml_decode_maybe<std::int8_t>(s["foo"]["bar"]) == 42); | |
REQUIRE(*yaml_decode_maybe<std::int8_t>(s["foo"], "bar") == 42); | |
REQUIRE_THROWS_WITH( | |
yaml_decode_maybe<std::int8_t>(s["foo"]["baz"]), | |
"failed to parse int8 at 3:7" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode_maybe<std::int8_t>(s["foo"], "baz"), | |
"failed to parse int8 at 3:7" | |
); | |
} | |
template<typename T, typename S, bool min> | |
void test_yaml_decode_int_min_max() { | |
constexpr auto t_min = static_cast<std::int64_t>(std::numeric_limits<T>::min()); | |
constexpr auto t_max = static_cast<std::uint64_t>(std::numeric_limits<T>::max()); | |
constexpr auto v = min ? | |
std::numeric_limits<S>::min() : std::numeric_limits<S>::max(); | |
constexpr auto ok = min ? | |
static_cast<std::int64_t>(v) >= t_min : static_cast<std::uint64_t>(v) <= t_max; | |
auto s = std::to_string(v); | |
if (ok) { | |
REQUIRE(yaml_decode<T>(YAML::Load(s)) == static_cast<T>(v)); | |
} else { | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load(s)), | |
"value at 1:0 out of bounds for " + detail::int_name<T>() + ": " + s | |
); | |
} | |
} | |
template<typename T> | |
void test_yaml_decode_int() { | |
DYNAMIC_SECTION("yaml_decode<" + detail::int_name<T>() + ">") { | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("foo")), | |
"failed to parse " + detail::int_name<T>() + " at 1:0" | |
); | |
REQUIRE(yaml_decode<T>(YAML::Load("0")) == 0); | |
REQUIRE(yaml_decode<T>(YAML::Load("1")) == 1); | |
if constexpr (std::is_signed_v<T>) { | |
REQUIRE(yaml_decode<T>(YAML::Load("-1")) == -1); | |
} else { | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("-1")), | |
"value at 1:0 out of bounds for " + detail::int_name<T>() + ": -1" | |
); | |
} | |
test_yaml_decode_int_min_max<T, std::int8_t, true>(); | |
test_yaml_decode_int_min_max<T, std::int16_t, true>(); | |
test_yaml_decode_int_min_max<T, std::int32_t, true>(); | |
test_yaml_decode_int_min_max<T, std::int64_t, true>(); | |
test_yaml_decode_int_min_max<T, std::int8_t, false>(); | |
test_yaml_decode_int_min_max<T, std::int16_t, false>(); | |
test_yaml_decode_int_min_max<T, std::int32_t, false>(); | |
test_yaml_decode_int_min_max<T, std::int64_t, false>(); | |
test_yaml_decode_int_min_max<T, std::uint8_t, false>(); | |
test_yaml_decode_int_min_max<T, std::uint16_t, false>(); | |
test_yaml_decode_int_min_max<T, std::uint32_t, false>(); | |
test_yaml_decode_int_min_max<T, std::uint64_t, false>(); | |
} | |
} | |
TEST_CASE("yaml_decode [integers]") { | |
test_yaml_decode_int<std::int8_t>(); | |
test_yaml_decode_int<std::int16_t>(); | |
test_yaml_decode_int<std::int32_t>(); | |
test_yaml_decode_int<std::int64_t>(); | |
test_yaml_decode_int<std::uint8_t>(); | |
test_yaml_decode_int<std::uint16_t>(); | |
test_yaml_decode_int<std::uint32_t>(); | |
test_yaml_decode_int<std::uint64_t>(); | |
} | |
TEST_CASE("yaml_decode [boolean") { | |
REQUIRE(yaml_decode<bool>(YAML::Load("true")) == true); | |
REQUIRE(yaml_decode<bool>(YAML::Load("false")) == false); | |
REQUIRE_THROWS_WITH(yaml_decode<bool>(YAML::Load("0")), "failed to parse value at 1:0"); | |
REQUIRE_THROWS_WITH(yaml_decode<bool>(YAML::Load("1")), "failed to parse value at 1:0"); | |
REQUIRE_THROWS_WITH(yaml_decode<bool>(YAML::Load("foo")), "failed to parse value at 1:0"); | |
} | |
struct Brick { | |
int x; | |
Brick() = delete; | |
Brick(const Brick&) = delete; | |
Brick& operator=(const Brick&) = delete; | |
Brick(Brick&&) = default; | |
}; | |
template<> | |
struct yaml_decoder<Brick> { | |
static Brick decode(const YAML::Node& node) { | |
return {yaml_decode<int>(node, "x")}; | |
} | |
}; | |
TEST_CASE("yaml_decode [array]") { | |
using T = std::array<std::int8_t, 2>; | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("1")), | |
"failed to parse std::array at 1:0: not a sequence" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("[1,2,3]")), | |
"failed to parse std::array at 1:0: expected length 2, got 3" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("[100,300]")), | |
"value at 1:5 out of bounds for int8: 300" | |
); | |
REQUIRE(yaml_decode<T>(YAML::Load("[100,-50]")) == T{100, -50}); | |
} | |
TEST_CASE("yaml_decode [vector]") { | |
using T = std::vector<std::int8_t>; | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("1")), | |
"failed to parse std::vector at 1:0: not a sequence" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("[100,300]")), | |
"value at 1:5 out of bounds for int8: 300" | |
); | |
REQUIRE(yaml_decode<T>(YAML::Load("[]")).empty()); | |
REQUIRE(yaml_decode<T>(YAML::Load("[100,-50]")) == T{100, -50}); | |
REQUIRE(yaml_decode<std::vector<Brick>>(YAML::Load("[{x: 42}]"))[0].x == 42); | |
} | |
TEST_CASE("yaml_decode [tuple]") { | |
using T = std::tuple<std::int8_t, std::string>; | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("1")), | |
"failed to parse std::tuple at 1:0: not a sequence" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("[1,2,3]")), | |
"failed to parse std::tuple at 1:0: expected length 2, got 3" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("[1000,'foo']")), | |
"value at 1:1 out of bounds for int8: 1000" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("[100,[1]]")), | |
"failed to parse value at 1:5" | |
); | |
REQUIRE(yaml_decode<T>(YAML::Load("[100,foo]")) == T(100, "foo")); | |
} | |
template<typename T> | |
void test_map(const std::string& tag) { | |
static_assert(std::is_same_v<typename T::key_type, std::string>); | |
static_assert(std::is_same_v<typename T::mapped_type, std::int8_t>); | |
DYNAMIC_SECTION("yaml_decode<" + tag + ">") { | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("1")), | |
"failed to parse " + tag + " at 1:0: not a map" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("{null: 2}")), | |
"failed to parse value at 1:1" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("{foo: 300}")), | |
"value at 1:6 out of bounds for int8: 300" | |
); | |
REQUIRE(yaml_decode<T>(YAML::Load("{}")).empty()); | |
REQUIRE(yaml_decode<T>(YAML::Load("{a: 1, b: -1}")) == T{{"a", 1}, {"b", -1}}); | |
} | |
} | |
TEST_CASE("yaml_decode [mapping]") { | |
test_map<std::map<std::string, std::int8_t>>("std::map"); | |
test_map<std::unordered_map<std::string, std::int8_t>>("std::unordered_map"); | |
} | |
TEST_CASE("yaml_decode [variant]") { | |
using T = std::variant<std::int8_t, bool>; | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("foo")), | |
"failed to parse std::variant at 1:0" | |
); | |
REQUIRE_THROWS_WITH( | |
yaml_decode<T>(YAML::Load("300")), | |
"failed to parse std::variant at 1:0" | |
); | |
REQUIRE(yaml_decode<T>(YAML::Load("true")) == T{true}); | |
REQUIRE(yaml_decode<T>(YAML::Load("false")) == T{false}); | |
REQUIRE(yaml_decode<T>(YAML::Load("10")) == T{std::int8_t(10)}); | |
REQUIRE(yaml_decode<T>(YAML::Load("-10")) == T{std::int8_t(-10)}); | |
} | |
TEST_CASE("yaml_decode [nested]") { | |
using T = std::map<std::string, std::vector<std::variant<bool, int>>>; | |
auto s = "foo: [true, -10]\nbar: []"; | |
T v = {{"foo", {{true}, {-10}}}, {"bar", {}}}; | |
REQUIRE(yaml_decode<T>(YAML::Load(s)) == v); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment