Skip to content

Instantly share code, notes, and snippets.

@redblobgames
Created May 17, 2018 22:06
Show Gist options
  • Save redblobgames/bbddffa3c83ea52337e6ccd51f0847d6 to your computer and use it in GitHub Desktop.
Save redblobgames/bbddffa3c83ea52337e6ccd51f0847d6 to your computer and use it in GitHub Desktop.
traverse variant+picojson using tags for variants instead of integer indices
// Copyright 2018 Red Blob Games <redblobgames@gmail.com>
// 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();
}
// Copyright 2018 Red Blob Games <redblobgames@gmail.com>
// 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