Created
May 17, 2018 22:06
-
-
Save redblobgames/bbddffa3c83ea52337e6ccd51f0847d6 to your computer and use it in GitHub Desktop.
traverse variant+picojson using tags for variants instead of integer indices
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
// Copyright 2018 Red Blob Games <[email protected]> | |
// https://github.com/redblobgames/cpp-traverse | |
// License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html> | |
#include "traverse.h" | |
#include "traverse-picojson.h" | |
#include "traverse-variant.h" | |
#include "traverse-picojson-variant-tagged.h" | |
#include "variant-util.h" | |
#include "mapbox/variant.hpp" | |
template<typename ...T> using variant = mapbox::util::variant<T...>; | |
#include <type_traits> | |
#include <functional> | |
#include <sstream> | |
#include <vector> | |
#include "test.h" | |
struct Move { | |
int speed; | |
int turn; | |
}; | |
TRAVERSE_STRUCT(Move, FIELD(speed) FIELD(turn)) | |
struct Create { | |
int id; | |
int x, y; | |
}; | |
TRAVERSE_STRUCT(Create, FIELD(id) FIELD(x) FIELD(y)) | |
struct Quit { | |
int time; | |
}; | |
TRAVERSE_STRUCT(Quit, FIELD(time)) | |
MAKE_VARIANT(Message, Create, Move, Quit); | |
using MessageQueue = std::vector<Message>; | |
void test_serialization() { | |
const auto JSON_DATA = "[{\"data\":{\"speed\":1,\"turn\":2},\"tag\":\"Move\"},{\"data\":{\"id\":42,\"x\":-10,\"y\":-10},\"tag\":\"Create\"}]"; | |
Message m1{Move{1, 2}}; | |
Message m2{Create{42, -10, -10}}; | |
MessageQueue queue{m1, m2}; | |
// Test writing to json | |
{ | |
std::cout << "__ Serialize to PicoJSON __ " << std::endl; | |
picojson::value json1; | |
traverse::PicoJsonWriter jsonwriter{json1}; | |
visit(jsonwriter, queue); | |
TEST_EQ(json1.serialize(), JSON_DATA); | |
} | |
// Test reading from json | |
{ | |
picojson::value json2; | |
std::cout << "__ Deserialize from PicoJSON __ " << std::endl; | |
auto err = picojson::parse(json2, JSON_DATA); | |
TEST_EQ(err, ""); | |
std::cout << "__ Deserialize JSON to MessageQueue __ " << std::endl; | |
std::stringstream errors; | |
traverse::PicoJsonReader jsonreader{json2, errors}; | |
MessageQueue queue2; | |
visit(jsonreader, queue2); | |
std::stringstream before, after; | |
traverse::CoutWriter write_before(before), write_after(after); | |
visit(write_before, queue); | |
visit(write_after, queue2); | |
TEST_EQ(before.str(), after.str()); | |
} | |
// TODO: test handling of corrupted json | |
} | |
int main() { | |
test_serialization(); | |
} |
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
// Copyright 2018 Red Blob Games <[email protected]> | |
// https://github.com/redblobgames/cpp-traverse | |
// License: Apache v2.0 <http://www.apache.org/licenses/LICENSE-2.0.html> | |
/** | |
* traverse-variant.h makes mapbox::variant work with traverse's | |
* built in formats (cout, binary serialization). | |
* | |
* traverse-picojson.h makes traverse work with picojson, but only | |
* with built-in traverse data types (int, string, array, object). | |
* | |
* traverse-picojson-variant.h fills in the gap: picojson + variant | |
* by writing variants as an object {variant: ___, data: ___}. | |
* | |
* You must define your variants with the MAKE_VARIANT macro to | |
* register the names of the variants. The variant types should | |
* be simple words and not complex types. | |
*/ | |
#ifndef TRAVERSE_PICOJSON_VARIANT_H | |
#define TRAVERSE_PICOJSON_VARIANT_H | |
#include "traverse.h" | |
#include "traverse-picojson.h" | |
#include "traverse-variant.h" | |
namespace traverse { | |
// Using mapbox's variant here but if you use boost or another variant, | |
// change the includes above and these typedefs: | |
template<typename ...T> using variant = mapbox::util::variant<T...>; | |
using mapbox::util::apply_visitor; | |
template<typename T> struct RegisterNameMapping { | |
static std::vector<std::string> registry; | |
RegisterNameMapping(const char* descriptor) { | |
// NOTE: this expects the string to be "Foo, Bar, Baz" and not | |
// in any other format. It comes from the MAKE_VARIANT macro. | |
auto end = descriptor + strlen(descriptor); | |
while (descriptor != end) { | |
auto next = std::find(descriptor, end, ','); | |
registry.push_back(std::string(descriptor, next)); | |
descriptor = next; | |
while (*descriptor == ' ' || *descriptor == ',') { | |
++descriptor; | |
} | |
} | |
} | |
static int name_to_index(const std::string& name) { | |
auto it = std::find(registry.begin(), registry.end(), name); | |
if (it == registry.end()) { | |
return -1; | |
} else { | |
return it - registry.begin(); | |
} | |
} | |
static const std::string& index_to_name(int index) { | |
return registry[index]; | |
} | |
}; | |
template<typename T> std::vector<std::string> RegisterNameMapping<T>::registry; | |
struct PicoJsonWriterVariantHelper { | |
PicoJsonWriter& writer; | |
template<typename T> void operator()(const T& value) { | |
visit(writer, value); | |
} | |
}; | |
template<typename ...Variants> | |
void visit(PicoJsonWriter& writer, const variant<Variants...>& value) { | |
picojson::value::object output; | |
picojson::value output_tag; | |
picojson::value output_data; | |
PicoJsonWriter writer_tag = {output_tag}; | |
PicoJsonWriter writer_data = {output_data}; | |
unsigned which = value.which(); | |
std::string tag = RegisterNameMapping<variant<Variants...>>::index_to_name(which); | |
std::cout << tag << '\n'; | |
visit(writer_tag, tag); | |
apply_visitor(PicoJsonWriterVariantHelper{writer_data}, value); | |
output["tag"] = output_tag; | |
output["data"] = output_data; | |
writer.out = picojson::value(output); | |
} | |
template<typename VariantType> | |
void deserialize_variant_helper(PicoJsonReader& reader, | |
unsigned which, unsigned index, | |
VariantType&) { | |
reader.errors << "Error: tried to read variant " << which | |
<< " but there were only " << index << " types." | |
<< std::endl; | |
} | |
template<typename VariantType, typename First, typename ...Rest> | |
void deserialize_variant_helper(PicoJsonReader& reader, | |
unsigned which, unsigned index, | |
VariantType& value) { | |
if (which == index) { | |
value.template set<First>(); | |
visit(reader, value.template get<First>()); | |
} else { | |
deserialize_variant_helper<VariantType, Rest...>(reader, which, index+1, value); | |
} | |
} | |
template<typename ...Variants> | |
void visit(PicoJsonReader& reader, variant<Variants...>& value) { | |
auto input = reader.in.get<picojson::value::object>(); | |
auto input_tag = input.find("tag"); | |
if (input_tag == input.end()) { | |
reader.errors << "Error: JSON object missing field 'tag'\n"; | |
return; | |
} | |
auto input_data = input.find("data"); | |
if (input_data == input.end()) { | |
reader.errors << "Error: JSON object missing field 'data'\n"; | |
return; | |
} | |
// TODO: check that there are no other fields | |
PicoJsonReader reader_tag{input_tag->second, reader.errors}; | |
std::string tag; | |
visit(reader_tag, tag); | |
unsigned which = RegisterNameMapping<variant<Variants...>>::name_to_index(tag); | |
PicoJsonReader reader_data{input_data->second, reader.errors}; | |
deserialize_variant_helper<variant<Variants...>, Variants...> | |
(reader_data, which, 0, value); | |
} | |
} | |
#ifndef MAKE_VARIANT | |
#define MAKE_VARIANT(TYPE, ...) using TYPE = traverse::variant<__VA_ARGS__>; namespace traverse { RegisterNameMapping<TYPE> _register##TYPE(#__VA_ARGS__); } | |
#endif | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment