From 7804938fc18ecf8d31d95bc616f870551f2ecda5 Mon Sep 17 00:00:00 2001 From: Sploder12 Date: Tue, 7 Jan 2025 23:46:09 -0500 Subject: [PATCH] converting to JSON --- src/include/sndx/data/data_tree.hpp | 127 ++++++++++++++++------ src/include/sndx/data/json.hpp | 158 ++++++++++++++++++++++++++++ src/tests/data/data_tree.cpp | 61 +++++++++++ src/tests/data/json.cpp | 46 ++++++++ src/tests/render/atlas.cpp | 2 +- 5 files changed, 360 insertions(+), 34 deletions(-) create mode 100644 src/include/sndx/data/json.hpp create mode 100644 src/tests/data/json.cpp diff --git a/src/include/sndx/data/data_tree.hpp b/src/include/sndx/data/data_tree.hpp index 9fcdd85..2a8dcaa 100644 --- a/src/include/sndx/data/data_tree.hpp +++ b/src/include/sndx/data/data_tree.hpp @@ -6,19 +6,20 @@ #include #include #include +#include #include #include #include #include #define SNDX_CREATE_GET(type, cnst, cnstxpr) \ - [[nodiscard]] cnstxpr cnst type##T* get##type##() cnst { \ + [[nodiscard]] cnstxpr cnst type##T* get##type() cnst { \ return get(); \ } #define SNDX_CREATE_GET_OR(type, cnstxpr) \ [[nodiscard]] cnstxpr type##T get##type##Or(const type##T& _alt) const { \ - if (auto _val = get##type##()) { \ + if (auto _val = get##type()) { \ return *_val; } \ return _alt; \ } @@ -42,23 +43,18 @@ namespace sndx::data { template using is_internal_t = std::bool_constant< - std::is_same_v || + std::is_same_v || std::is_same_v>; template static constexpr bool is_internal_t_v = is_internal_t(); friend class Value; + private: std::variant m_value; public: - constexpr Number(const Number&) noexcept = default; - constexpr Number(Number&&) noexcept = default; - constexpr Number& operator=(const Number&) noexcept = default; - constexpr Number& operator=(Number&&) noexcept = default; - constexpr ~Number() noexcept = default; - template constexpr Number(T&& value) noexcept : m_value(std::forward(value)) {} @@ -139,18 +135,20 @@ namespace sndx::data { } [[nodiscard]] - constexpr const FloatT getAsFloat() const noexcept { + constexpr FloatT getAsFloat() const noexcept { return getAs(); } }; - class Value { public: using NumberT = Number; using StringT = std::string; using BoolT = bool; + using IntT = NumberT::IntT; + using FloatT = NumberT::FloatT; + template using is_internal_t = std::bool_constant< std::is_same_v || @@ -160,19 +158,10 @@ namespace sndx::data { template static constexpr bool is_internal_t_v = is_internal_t(); - using IntT = NumberT::IntT; - using FloatT = NumberT::FloatT; - private: std::variant m_value; public: - Value(const Value&) = default; - Value(Value&&) noexcept = default; - Value& operator=(const Value&) = default; - Value& operator=(Value&&) noexcept = default; - ~Value() noexcept = default; - template Value(T&& value) noexcept : m_value(std::forward(value)) {} @@ -361,8 +350,6 @@ namespace sndx::data { return *this; } - ~DataArray() noexcept = default; - template bool operator==(const T& arr) const { if (size() != arr.size()) @@ -386,7 +373,11 @@ namespace sndx::data { template void emplace_back(Args&&... args) { - m_data.emplace_back(DataT(std::forward(args)...)); + m_data.emplace_back(std::forward(args)...); + } + + void resize(size_t size) { + m_data.resize(size); } void reserve(size_t size) { @@ -445,10 +436,13 @@ namespace sndx::data { std::unordered_map m_data{}; public: + DataDict() = default; DataDict(std::initializer_list> list) : - m_data{ list } { - } + m_data{ list } {} + + DataDict(const IdT& t, const DataT& d) : + m_data{ {t, d} } {} DataDict(const DataDict& other) { m_data.reserve(other.size()); @@ -475,8 +469,6 @@ namespace sndx::data { return *this; } - ~DataDict() noexcept = default; - bool operator==(const auto& dict) const { if (size() != dict.size()) return false; @@ -602,8 +594,8 @@ namespace sndx::data { Data(T&& value) noexcept : m_data(std::in_place_type_t{}, std::forward(value)) {} - explicit Data(const ArrayT& arr) : - m_data{ std::in_place_type_t{}, ArrayT(arr) } {} + Data(const ArrayT& arr) : + m_data{ std::in_place_type_t{}, arr } {} Data(const MapT& dict): m_data{ std::make_unique(dict)} {} @@ -644,11 +636,10 @@ namespace sndx::data { return *this; } - ~Data() noexcept = default; - template + requires std::constructible_from Data& operator=(T&& value) noexcept { - m_data = std::forward(value); + m_data.emplace(std::forward(value)); return *this; } @@ -658,7 +649,14 @@ namespace sndx::data { } Data& operator=(std::initializer_list arr_init) { - m_data.emplace(ArrayT(arr_init)); + ArrayT val{}; + val.reserve(arr_init.size()); + + for (auto&& d : arr_init) { + val.emplace_back(d); + } + + m_data.emplace(std::move(val)); return *this; } @@ -800,5 +798,68 @@ namespace sndx::data { SNDX_CREATE_GETS(Int, ); SNDX_CREATE_GETS(Float, ); + + Data& at(size_t idx) { + if (auto arr = getArray()) + return arr->at(idx); + + throw std::logic_error("Cannot index into non-array"); + } + + const Data& at(size_t idx) const { + if (auto arr = getArray()) + return arr->at(idx); + + throw std::logic_error("Cannot index into non-array"); + } + + Data& at(const StringT& id) { + if (auto map = getMap()) + return map->at(id); + + throw std::logic_error("Cannot index into non-map"); + } + + const Data& at(const StringT& id) const { + if (auto map = getMap()) + return map->at(id); + + throw std::logic_error("Cannot index into non-map"); + } + + Data& operator[](size_t idx) { + if (isNull()) + m_data.emplace(ArrayT{}); + + if (auto arr = getArray()) { + // this is not that great + if (idx >= arr->size()) + arr->resize(idx + 1); + + return arr->at(idx); + } + + throw std::logic_error("Cannot index into non-array"); + } + + Data& operator[](const StringT& id) { + if (isNull()) + m_data.emplace>(std::make_unique()); + + if (auto map = getMap()) { + return (*map)[id]; + } + + throw std::logic_error("Cannoit index into non-map"); + } + + void reserve(size_t size) { + if (auto arr = getArray()) { + arr->reserve(size); + } + else if (auto map = getMap()) { + map->reserve(size); + } + } }; } diff --git a/src/include/sndx/data/json.hpp b/src/include/sndx/data/json.hpp new file mode 100644 index 0000000..35c6ea2 --- /dev/null +++ b/src/include/sndx/data/json.hpp @@ -0,0 +1,158 @@ +#pragma once + +#include "./data_tree.hpp" + +namespace sndx::data { + namespace detail { + template + void toJSON(std::string&, const Data&, const Encoder&, int); + + template + void toJSON(std::string& out, const Data::NullT&, const Encoder& encoder, int depth) { + encoder.encodeNull(out, depth); + } + + template + void toJSON(std::string& out, const Data::ValueT& value , const Encoder& encoder, int depth) { + encoder.encodeValue(out, value, depth); + } + + template + void toJSON(std::string& out, const Data::ArrayT& array, const Encoder& encoder, int depth) { + encoder.encodeArrayStart(out, depth); + + for (size_t i = 0; i < array.size(); ++i) { + toJSON(out, array[i], encoder, depth + 1); + + if (i < array.size() - 1) { + encoder.encodeArraySeperator(out, depth + 1); + } + } + + encoder.encodeArrayEnd(out, depth); + } + + template + void toJSON(std::string& out, const Data::MapT& map, const Encoder& encoder, int depth) { + encoder.encodeMapStart(out, depth); + + size_t i = 0; + for (const auto& [id, value] : map) { + + encoder.encodeKey(out, id, depth + 1); + + toJSON(out, value, encoder, depth + 1); + + if (i++ < map.size() - 1) { + encoder.encodeMapSeperator(out, depth + 1); + } + } + + encoder.encodeMapEnd(out, depth); + } + + template + void toJSON(std::string& out, const Data& data, const Encoder& encoder, int depth) { + if (auto map = data.getMap()) { + toJSON(out, *map, encoder, depth); + } + else if (auto arr = data.getArray()) { + toJSON(out, *arr, encoder, depth); + } + else if (auto val = data.getValue()) { + toJSON(out, *val, encoder, depth); + } + else { + toJSON(out, nullptr, encoder, depth); + } + } + + inline std::string quote(const std::string& in) { + std::string out = "\""; + out.reserve(in.size() + 2); + + bool escaped = false; + for (auto c : in) { + switch (c) { + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + case '\n': + out += "\\n"; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + out += c; + break; + } + } + + out += '"'; + return out; + } + } + + struct packedJSONencoder { + void encodeArrayStart(std::string& out, int) const { + out += '['; + } + + void encodeArraySeperator(std::string& out, int) const { + out += ','; + } + + void encodeArrayEnd(std::string& out, int) const { + out += ']'; + } + + void encodeMapStart(std::string& out, int) const { + out += '{'; + } + + void encodeMapSeperator(std::string& out, int) const { + out += ','; + } + + void encodeMapEnd(std::string& out, int) const { + out += '}'; + } + + void encodeKey(std::string& out, const std::string& key, int) const { + out += detail::quote(key) + ':'; + } + + void encodeValue(std::string& out, const Data::ValueT& value, int) const { + if (auto str = value.getString()) { + out += detail::quote(*str); + } + else { + out += value.getAsString(); + } + } + + void encodeNull(std::string& out, int) const { + out += "null"; + } + }; + + template + std::string toJSON(const Data::MapT& data, const Encoder& encoder = packedJSONencoder{}) { + std::string out{}; + detail::toJSON(out, data, encoder, 0); + return out; + } +} \ No newline at end of file diff --git a/src/tests/data/data_tree.cpp b/src/tests/data/data_tree.cpp index 39a7089..bb1b19f 100644 --- a/src/tests/data/data_tree.cpp +++ b/src/tests/data/data_tree.cpp @@ -72,6 +72,7 @@ TEST_F(DataTreeTest, ValueCanBeNumber) { inumber = 4; EXPECT_EQ(inumber, 4); + EXPECT_EQ(inumber, fnumber); } TEST_F(DataTreeTest, ValueCanBeString) { @@ -222,3 +223,63 @@ TEST_F(DataTreeTest, DictCanBeListInitialized) { ADD_FAILURE() << "'sub' not found in dict!"; } } + +TEST_F(DataTreeTest, DataCanSquareBracket) { + data::Data dat{}; + + dat[0]["banana"][1] = 5; + + EXPECT_EQ(dat[0]["banana"][1], 5); + EXPECT_EQ(dat[0]["banana"][0], nullptr); + + dat[1]["banana"][0] = 5; + + EXPECT_EQ(dat[0]["banana"][1], 5); + EXPECT_EQ(dat[0]["banana"][0], nullptr); + EXPECT_EQ(dat[1]["banana"][0], 5); + EXPECT_EQ(dat[1]["banana"][1], nullptr); + + dat[0]["fish"] = true; + + EXPECT_TRUE(dat[0]["fish"]); +} + +TEST_F(DataTreeTest, DataThrowsOnBadIndexing) { + data::Data val = 5; + + EXPECT_THROW(val[0], std::logic_error); + EXPECT_THROW(val[":)"], std::logic_error); + + EXPECT_THROW(val.at(0), std::logic_error); + EXPECT_THROW(val.at(":)"), std::logic_error); + + data::Data arr = data::DataArray{ 5, 10 }; + + EXPECT_THROW(arr[":("], std::logic_error); + + EXPECT_THROW(arr.at(2), std::out_of_range); + EXPECT_THROW(arr.at(":("), std::logic_error); + + data::Data dict = data::DataDict{{":(", 10}}; + + EXPECT_THROW(dict[0], std::logic_error); + + EXPECT_THROW(dict.at(":)"), std::out_of_range); + EXPECT_THROW(dict.at(0), std::logic_error); +} + +TEST_F(DataTreeTest, DataCanBeAssigned) { + data::Data a = 5; + data::Data b = data::DataArray{ 5, 10 }; + data::Data c = data::DataDict{ {":(", 10} }; + + a = c; + c = b; + b = 5; + + b = { 10, 20 }; + + EXPECT_EQ(a, data::DataDict({":(", 10})); + EXPECT_EQ(b, data::DataArray(10, 20)); + EXPECT_EQ(c, data::DataArray(5, 10)); +} diff --git a/src/tests/data/json.cpp b/src/tests/data/json.cpp new file mode 100644 index 0000000..ce2cd11 --- /dev/null +++ b/src/tests/data/json.cpp @@ -0,0 +1,46 @@ +#include "data/json.hpp" + +#include "../common.hpp" + +#include + +class JsonTest : public ::testing::Test { + +}; + +using namespace sndx; + + +TEST_F(JsonTest, EmptyDictIsEmpty) { + data::DataDict dict{}; + + EXPECT_EQ(data::toJSON(dict), "{}"); +} + +TEST_F(JsonTest, NestedDictWorks) { + data::DataDict dict = { + { "pizza", 1 }, + { "sub", data::DataDict{ + {"b", "w"} + }} + }; + + auto json = data::toJSON(dict); + EXPECT_THAT(json, ::testing::AnyOf( + ::testing::Eq("{\"pizza\":1,\"sub\":{\"b\":\"w\"}}"), + ::testing::Eq("{\"sub\":{\"b\":\"w\"},\"pizza\":1}"))); +} + +TEST_F(JsonTest, ArrayWorks) { + data::DataDict dict = { + { "evil\"", nullptr }, + { "arr", data::DataArray{ + "b", "wa", true, false, 5, 0 + }} + }; + + auto json = data::toJSON(dict); + EXPECT_THAT(json, ::testing::AnyOf( + ::testing::Eq("{\"evil\\\"\":null,\"arr\":[\"b\",\"wa\",true,false,5,0]}"), + ::testing::Eq("{\"arr\":[\"b\",\"wa\",true,false,5,0],\"evil\\\"\":null}"))); +} \ No newline at end of file diff --git a/src/tests/render/atlas.cpp b/src/tests/render/atlas.cpp index 7ad20e0..41e6f5c 100644 --- a/src/tests/render/atlas.cpp +++ b/src/tests/render/atlas.cpp @@ -88,7 +88,7 @@ TEST_F(ImageAtlasTest, addsEntry) { auto atlas = builder.build>(10, 5); EXPECT_EQ(atlas.size(), 1); - EXPECT_NO_THROW(auto ign = atlas.getEntry(42)); + EXPECT_NO_THROW(std::ignore = atlas.getEntry(42)); } class IntenseImageAtlasTest : public ImageAtlasTest {