Motivation: Partially for fun / learning, but also so I can roll out a custom JSON-like file format for a fighting game I am writing. There are two caveats: you cannot have repeated keys (requires std::unordered_multimap
) and the keys are not in order (requires insertion order std::vector
on the side, or some external boost lib probably). I am mainly looking for critique on my level of comments and ability to read my code, but everything else I am of course welcome to hear.
#pragma once
#include <list>
#include <map>
#include <string>
#include <variant>
#include <vector>
#include <fstream>
#include <iostream>
#include <sstream>
namespace json {
class Value;
// six json data types
using null_t = std::nullptr_t;
using bool_t = bool;
using number_t = std::double_t;
using string_t = std::string;
using array_t = std::vector<Value>;
using object_t = std::map<string_t, Value>;
using aggregate_t = std::variant<
null_t, bool_t, number_t,
string_t, array_t, object_t>;
class Value : protected aggregate_t {
public:
using aggregate_t::variant;
// removes spurious E0291
Value() = default;
// converts int into double rather than bool
Value(int integer) : aggregate_t(static_cast<double>(integer)) {}
// converts c_string (pointer) into string rather than bool
Value(const char* c_string) : aggregate_t(std::string(c_string)) {}
public:
auto operator[](const string_t& key) -> Value& {
// transform into object if null
if (std::get_if<null_t>(this))
*this = object_t();
return std::get<object_t>(*this)[key];
}
auto operator[](std::size_t key) -> Value& {
// transform into array if null
if (std::get_if<null_t>(this))
*this = array_t();
if (key >= std::get<array_t>(*this).size())
std::get<array_t>(*this).resize(key + 1);
return std::get<array_t>(*this)[key];
}
auto save(std::ostream& stream, std::string prefix = "") -> std::ostream& {
static const std::string SPACING = " "; // "\t"; // " ";
// depending on the type, write to correct value with format to stream
std::visit([&stream, &prefix](auto&& value) {
using namespace std;
using T = decay_t<decltype(value)>;
if constexpr (is_same_v<T, nullptr_t>)
stream << "null";
if constexpr (is_same_v<T, bool_t>)
stream << (value ? "true" : "false");
else if constexpr (is_same_v<T, double_t>)
stream << value;
else if constexpr (is_same_v<T, string>)
stream << '"' << value << '"';
else if constexpr (is_same_v<T, array_t>) {
stream << "[\n";
auto [indent, remaining] = make_tuple(prefix + SPACING, value.size());
// for every json value, indent and print to stream
for (auto& json : value)
json.save(stream << indent, indent)
// if jsons remaining (not last), append comma
<< (--remaining ? ",\n" : "\n");
stream << prefix << "]";
}
else if constexpr (is_same_v<T, object_t>) {
stream << "{\n";
auto [indent, remaining] = make_tuple(prefix + SPACING, value.size());
// for every json value, indent with key and print to stream
for (auto& [key, json] : value)
json.save(stream << indent << '"' << key << "\" : ", indent)
// if jsons remaining (not last), append comma
<< (--remaining ? ",\n" : "\n");
stream << prefix << "}";
}
}, *static_cast<aggregate_t*>(this));
return stream;
}
auto load(std::istream& stream) -> std::istream& {
using namespace std;
switch ((stream >> ws).peek()) {
case '"': {
// get word surrounded by "
stringbuf buffer;
stream.ignore(1)
.get(buffer, '"')
.ignore(1);
*this = buffer.str();
} break;
case '[': {
array_t array;
for (stream.ignore(1); (stream >> ws).peek() != ']';)
// load child json and consume comma if available
if ((array.emplace_back().load(stream) >> ws).peek() == ',')
stream.ignore(1);
stream.ignore(1);
*this = move(array);
} break;
case '{': {
object_t object;
for (stream.ignore(1); (stream >> ws).peek() != '}';) {
// get word surrounded by "
stringbuf buffer;
stream.ignore(numeric_limits<streamsize>::max(), '"')
.get(buffer, '"')
.ignore(numeric_limits<streamsize>::max(), ':');
// load child json and consume comma if available
if ((object[buffer.str()].load(stream) >> ws).peek() == ',')
stream.ignore(1);
}
stream.ignore(1);
*this = move(object);
} break;
default: {
if (isdigit(stream.peek()) || stream.peek() == '.') {
double_t number;
stream >> number;
*this = number;
}
else if (isalpha(stream.peek())) {
// get alphabetic word
string word;
for (; isalpha(stream.peek()); stream.ignore())
word.push_back(stream.peek());
// set value to look-up table's value
static auto keyword_lut = map<string_view, Value>{
{"true", true}, {"false", false}, {"null", nullptr}};
*this = keyword_lut[word];
}
else
*this = nullptr;
} break;
}
return stream;
}
auto save_to_path(std::string_view file_path) -> void {
auto file = std::ofstream(std::string(file_path));
save(file);
}
auto load_from_path(std::string_view file_path) -> void {
auto file = std::ifstream(std::string(file_path));
load(file);
}
static void test() {
std::stringstream ss;
{
json::Value value;
auto& employee = value["employee"];
employee["name"] = "bob";
employee["age"] = 21;
employee["friends"][0] = "alice";
employee["friends"][1] = "billy";
employee["weight"] = 140.0;
value.save(ss);
}
std::cout << ss.str() << "\n\n";
{
auto example = std::stringstream(R"(
{
"firstName": "John",
"lastName": "Smith",
"isAlive": true,
"age": 27,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021-3100"
},
"phoneNumbers": [
{
"type": "home",
"number": "212 555-1234"
},
{
"type": "office",
"number": "646 555-4567"
},
{
"type": "mobile",
"number": "123 456-7890"
}
],
"children": [],
"spouse": null
})");
json::Value value;
value.load(example);
ss.clear();
value.save(ss);
}
std::cout << ss.str() << "\n\n";
}
};
}
int main() {
json::Value::test();
return getchar();
}