Skip to content

Instantly share code, notes, and snippets.

@aldanor
Last active May 6, 2020 15:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aldanor/c7c45ca4c02c1fe9fa209f88e94bebf4 to your computer and use it in GitHub Desktop.
Save aldanor/c7c45ca4c02c1fe9fa209f88e94bebf4 to your computer and use it in GitHub Desktop.
better-yaml
#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