#include "StarFormattedJson.hpp" #include "StarJsonBuilder.hpp" #include "StarLexicalCast.hpp" namespace Star { class FormattedJsonBuilderStream : public JsonStream { public: virtual void beginObject(); virtual void objectKey(String::Char const* s, size_t len); virtual void endObject(); virtual void beginArray(); virtual void endArray(); virtual void putString(String::Char const* s, size_t len); virtual void putDouble(String::Char const* s, size_t len); virtual void putInteger(String::Char const* s, size_t len); virtual void putBoolean(bool b); virtual void putNull(); virtual void putWhitespace(String::Char const* s, size_t len); virtual void putComma(); virtual void putColon(); FormattedJson takeTop(); private: void push(FormattedJson const& v); FormattedJson pop(); FormattedJson& current(); void putValue(Json const& value, Maybe formatting = {}); Maybe m_root; List m_stack; }; template <> class JsonStreamer { public: static void toJsonStream(FormattedJson const& val, JsonStream& stream, bool sort); }; ValueElement::ValueElement(FormattedJson const& json) : value(make_shared(json)) {} bool ValueElement::operator==(ValueElement const& v) const { return *value == *v.value; } bool ObjectKeyElement::operator==(ObjectKeyElement const& v) const { return key == v.key; } bool WhitespaceElement::operator==(WhitespaceElement const& v) const { return whitespace == v.whitespace; } bool ColonElement::operator==(ColonElement const&) const { return true; } bool CommaElement::operator==(CommaElement const&) const { return true; } FormattedJson FormattedJson::parse(String const& string) { return inputUtf32Json( string.begin(), string.end(), JsonParseType::Value); } FormattedJson FormattedJson::parseJson(String const& string) { return inputUtf32Json( string.begin(), string.end(), JsonParseType::Top); } FormattedJson FormattedJson::ofType(Json::Type type) { FormattedJson json; json.m_jsonValue = Json::ofType(type); return json; } FormattedJson::FormattedJson() : FormattedJson(Json()) {} FormattedJson::FormattedJson(Json const& json) : m_jsonValue(Json::ofType(json.type())), m_elements(), m_formatting(), m_lastKey(), m_objectEntryLocations(), m_arrayElementLocations() { if (json.type() == Json::Type::Object || json.type() == Json::Type::Array) { FormattedJsonBuilderStream stream; JsonStreamer::toJsonStream(json, stream, false); FormattedJson parsed = stream.takeTop(); for (JsonElement const& elem : parsed.elements()) { appendElement(elem); } } m_jsonValue = json; } Json const& FormattedJson::toJson() const { return m_jsonValue; } FormattedJson FormattedJson::get(String const& key) const { if (type() != Json::Type::Object) throw JsonException::format("Cannot call get with key on FormattedJson type {}, must be Object type", typeName()); Maybe> entry = m_objectEntryLocations.maybe(key); if (entry.isNothing()) throw JsonException::format("No such key in FormattedJson::get(\"{}\")", key); return getFormattedJson(entry->second); } FormattedJson FormattedJson::get(size_t index) const { if (type() != Json::Type::Array) throw JsonException::format("Cannot call get with index on FormattedJson type {}, must be Array type", typeName()); if (index >= m_arrayElementLocations.size()) throw JsonException::format("FormattedJson::get({}) out of range", index); ElementLocation loc = m_arrayElementLocations.at(index); return getFormattedJson(loc); } struct WhitespaceStyle { String beforeKey; String beforeColon; String beforeValue; String beforeComma; }; template FormattedJson::ElementLocation indexOf(FormattedJson::ElementList const& elements, FormattedJson::ElementLocation pos) { for (; pos < elements.size(); ++pos) { if (elements[pos].is()) return pos; } return NPos; } template FormattedJson::ElementLocation lastIndexOf( FormattedJson::ElementList const& elements, FormattedJson::ElementLocation pos) { while (pos > 0) { --pos; if (elements[pos].is()) return pos; } return NPos; } String concatWhitespace(FormattedJson::ElementList const& elements, FormattedJson::ElementLocation from, FormattedJson::ElementLocation to) { String whitespace; for (JsonElement const& elem : elements.slice(from, to)) { if (elem.is()) whitespace += elem.get().whitespace; } return whitespace; } WhitespaceStyle detectWhitespace(FormattedJson::ElementList const& elements, FormattedJson::ElementLocation insertLoc, bool array) { WhitespaceStyle style; // Find a nearby value as a reference location to learn whitespace from. FormattedJson::ElementLocation valueLoc = lastIndexOf(elements, insertLoc); if (valueLoc == NPos) valueLoc = indexOf(elements, insertLoc); if (valueLoc == NPos) { // This object/array is empty. Pre-key/value whitespace will be the total of // the whitespace already present, plus some guessed indentation if it // contained a newline. String beforeValue = concatWhitespace(elements, 0, elements.size()); if (beforeValue.find('\n') != NPos) beforeValue += " "; if (array) return WhitespaceStyle{"", "", beforeValue, ""}; return WhitespaceStyle{beforeValue, "", "", ""}; } FormattedJson::ElementLocation commaLoc = indexOf(elements, valueLoc); if (commaLoc != NPos) { style.beforeComma = concatWhitespace(elements, valueLoc + 1, commaLoc); } FormattedJson::ElementLocation colonLoc = lastIndexOf(elements, valueLoc); starAssert((colonLoc == NPos) == array); if (colonLoc != NPos) { style.beforeValue = concatWhitespace(elements, colonLoc + 1, valueLoc); FormattedJson::ElementLocation keyLoc = lastIndexOf(elements, colonLoc); starAssert(keyLoc != NPos); style.beforeColon = concatWhitespace(elements, keyLoc + 1, colonLoc); FormattedJson::ElementLocation prevValueLoc = lastIndexOf(elements, keyLoc); if (prevValueLoc == NPos) prevValueLoc = 0; style.beforeKey = concatWhitespace(elements, prevValueLoc, keyLoc); } else { FormattedJson::ElementLocation prevValueLoc = lastIndexOf(elements, valueLoc); if (prevValueLoc == NPos) prevValueLoc = 0; style.beforeValue = concatWhitespace(elements, prevValueLoc, valueLoc); } return style; } void insertWhitespace(FormattedJson::ElementList& destination, FormattedJson::ElementLocation& at, String const& whitespace) { if (whitespace == "") return; destination.insertAt(at++, WhitespaceElement{whitespace}); } void insertWithWhitespace(FormattedJson::ElementList& destination, WhitespaceStyle const& style, FormattedJson::ElementLocation& at, JsonElement const& element) { if (element.is()) insertWhitespace(destination, at, style.beforeValue); if (element.is()) insertWhitespace(destination, at, style.beforeKey); if (element.is()) insertWhitespace(destination, at, style.beforeColon); if (element.is()) insertWhitespace(destination, at, style.beforeComma); destination.insertAt(at++, element); } void insertWithCommaAndFormatting(FormattedJson::ElementList& destination, FormattedJson::ElementLocation at, bool array, FormattedJson::ElementList const& elements) { // Find the previous value we're inserting after, if any. at = lastIndexOf(destination, at); if (at == NPos) at = 0; else at += 1; bool empty = lastIndexOf(destination, destination.size()) == NPos; bool appendComma = at == 0 && !empty; bool prependComma = !appendComma && !empty; WhitespaceStyle style = detectWhitespace(destination, at, array); if (prependComma) { // Inserting into a non-empty object/array. Prepend a comma insertWithWhitespace(destination, style, at, CommaElement{}); } for (JsonElement const& elem : elements) { insertWithWhitespace(destination, style, at, elem); } if (appendComma) { insertWithWhitespace(destination, style, at, CommaElement{}); } } FormattedJson FormattedJson::prepend(String const& key, FormattedJson const& value) const { return objectInsert(key, value, 0); } FormattedJson FormattedJson::insertBefore(String const& key, FormattedJson const& value, String const& beforeKey) const { if (!m_objectEntryLocations.contains(beforeKey)) throw JsonException::format("Cannot insert before key \"{}\", which does not exist", beforeKey); ElementLocation loc = m_objectEntryLocations.get(beforeKey).first; return objectInsert(key, value, loc); } FormattedJson FormattedJson::insertAfter(String const& key, FormattedJson const& value, String const& afterKey) const { if (!m_objectEntryLocations.contains(afterKey)) throw JsonException::format("Cannot insert after key \"{}\", which does not exist", afterKey); ElementLocation loc = m_objectEntryLocations.get(afterKey).second; return objectInsert(key, value, loc + 1); } FormattedJson FormattedJson::append(String const& key, FormattedJson const& value) const { return objectInsert(key, value, m_elements.size()); } FormattedJson FormattedJson::set(String const& key, FormattedJson const& value) const { return objectInsert(key, value, m_elements.size()); } void removeValueFromArray(List& elements, size_t loc) { // Remove the value itself, the comma following and the whitespace up to the // next value. // If it's the last value, it removes the value, and the preceding whitespace // and comma. size_t commaLoc = elements.indexOf(CommaElement{}, loc); if (commaLoc != NPos) { elements.eraseAt(loc, commaLoc + 1); while (loc < elements.size() && elements.at(loc).is()) elements.eraseAt(loc); } else { commaLoc = elements.lastIndexOf(CommaElement{}, loc); if (commaLoc == NPos) commaLoc = 0; elements.eraseAt(commaLoc, loc + 1); } } FormattedJson FormattedJson::eraseKey(String const& key) const { if (type() != Json::Type::Object) throw JsonException::format("Cannot call erase with key on FormattedJson type {}, must be Object type", typeName()); Maybe> maybeEntry = m_objectEntryLocations.maybe(key); if (maybeEntry.isNothing()) return *this; ElementLocation loc = maybeEntry->first; ElementList elements = m_elements; elements.eraseAt(loc, maybeEntry->second); // Remove key, colon and whitespace up to the value removeValueFromArray(elements, loc); return object(elements); } FormattedJson FormattedJson::insert(size_t index, FormattedJson const& value) const { if (type() != Json::Type::Array) throw JsonException::format( "Cannot call insert with index on FormattedJson type {}, must be Array type", typeName()); if (index > m_arrayElementLocations.size()) throw JsonException::format("FormattedJson::insert({}) out of range", index); ElementList elements = m_elements; ElementLocation insertPosition = elements.size(); if (index < m_arrayElementLocations.size()) insertPosition = m_arrayElementLocations.at(index); insertWithCommaAndFormatting(elements, insertPosition, true, {ValueElement{value}}); return array(elements); } FormattedJson FormattedJson::append(FormattedJson const& value) const { if (type() != Json::Type::Array) throw JsonException::format("Cannot call append on FormattedJson type {}, must be Array type", typeName()); ElementList elements = m_elements; insertWithCommaAndFormatting(elements, elements.size(), true, {ValueElement{value}}); return array(elements); } FormattedJson FormattedJson::set(size_t index, FormattedJson const& value) const { if (type() != Json::Type::Array) throw JsonException::format("Cannot call set with index on FormattedJson type {}, must be Array type", typeName()); if (index >= m_arrayElementLocations.size()) throw JsonException::format("FormattedJson::set({}) out of range", index); ElementLocation loc = m_arrayElementLocations.at(index); ElementList elements = m_elements; elements.at(loc) = ValueElement{value}; return array(elements); } FormattedJson FormattedJson::eraseIndex(size_t index) const { if (type() != Json::Type::Array) throw JsonException::format("Cannot call set with index on FormattedJson type {}, must be Array type", typeName()); if (index >= m_arrayElementLocations.size()) throw JsonException::format("FormattedJson::eraseIndex({}) out of range", index); ElementLocation loc = m_arrayElementLocations.at(index); ElementList elements = m_elements; removeValueFromArray(elements, loc); return array(elements); } size_t FormattedJson::size() const { return m_jsonValue.size(); } bool FormattedJson::contains(String const& key) const { return m_jsonValue.contains(key); } Json::Type FormattedJson::type() const { return m_jsonValue.type(); } bool FormattedJson::isType(Json::Type type) const { return m_jsonValue.isType(type); } String FormattedJson::typeName() const { return m_jsonValue.typeName(); } String FormattedJson::toFormattedDouble() const { if (!isType(Json::Type::Float)) throw JsonException::format("Cannot call toFormattedDouble on Json type {}, must be Float", typeName()); if (m_formatting.isValid()) return *m_formatting; return toJson().repr(); } String FormattedJson::toFormattedInt() const { if (!isType(Json::Type::Int)) throw JsonException::format("Cannot call toFormattedInt on Json type {}, must be Int", typeName()); if (m_formatting.isValid()) return *m_formatting; return toJson().repr(); } String FormattedJson::repr() const { if (m_formatting.isValid()) return *m_formatting; String result; outputUtf32Json, FormattedJson>(*this, std::back_inserter(result), 0, false); return result; } String FormattedJson::printJson() const { if (type() != Json::Type::Object && type() != Json::Type::Array) throw JsonException("printJson called on non-top-level JSON type"); return repr(); } Json elemToJson(JsonElement const& elem) { return elem.get().value->toJson(); } FormattedJson::ElementList const& FormattedJson::elements() const { return m_elements; } bool FormattedJson::operator==(FormattedJson const& v) const { return m_jsonValue == v.m_jsonValue; } bool FormattedJson::operator!=(FormattedJson const& v) const { return !(*this == v); } FormattedJson FormattedJson::object(ElementList const& elements) { FormattedJson json = ofType(Json::Type::Object); for (JsonElement const& elem : elements) { json.appendElement(elem); } return json; } FormattedJson FormattedJson::array(ElementList const& elements) { FormattedJson json = ofType(Json::Type::Array); for (JsonElement const& elem : elements) { if (elem.is() || elem.is()) throw JsonException("Invalid FormattedJson element in Json array"); json.appendElement(elem); } return json; } FormattedJson FormattedJson::objectInsert(String const& key, FormattedJson const& value, ElementLocation loc) const { if (type() != Json::Type::Object) throw JsonException::format("Cannot call set with key on FormattedJson type {}, must be Object type", typeName()); Maybe> maybeEntry = m_objectEntryLocations.maybe(key); if (maybeEntry.isValid()) { ElementList elements = m_elements; elements.at(maybeEntry->second) = ValueElement{value}; return object(elements); } ElementList elements = m_elements; insertWithCommaAndFormatting(elements, loc, false, {ObjectKeyElement{key}, ColonElement{}, ValueElement{value}}); return object(elements); } void FormattedJson::appendElement(JsonElement const& elem) { ElementLocation loc = m_elements.size(); m_elements.append(elem); if (elem.is()) { starAssert(isType(Json::Type::Object)); m_lastKey = loc; } else if (elem.is()) { m_lastValue = loc; if (m_lastKey.isValid()) { starAssert(isType(Json::Type::Object)); String key = m_elements[*m_lastKey].get().key; m_objectEntryLocations[key] = make_pair(*m_lastKey, loc); m_jsonValue = m_jsonValue.set(key, elemToJson(elem)); m_lastKey = {}; } else { starAssert(isType(Json::Type::Array)); m_arrayElementLocations.append(loc); m_jsonValue = m_jsonValue.append(elemToJson(elem)); } } } FormattedJson const& FormattedJson::getFormattedJson(ElementLocation loc) const { return *m_elements[loc].get().value; } FormattedJson FormattedJson::formattedAs(String const& formatting) const { starAssert(Json::parse(formatting) == toJson()); FormattedJson json = *this; json.m_formatting = formatting; return json; } void FormattedJsonBuilderStream::beginObject() { FormattedJson value = FormattedJson::ofType(Json::Type::Object); push(value); } void FormattedJsonBuilderStream::objectKey(String::Char const* s, size_t len) { current().appendElement(ObjectKeyElement{String(s, len)}); } void FormattedJsonBuilderStream::endObject() { FormattedJson value = pop(); if (m_stack.size() > 0) current().appendElement(ValueElement{value}); else m_root = value; } void FormattedJsonBuilderStream::beginArray() { FormattedJson value = FormattedJson::ofType(Json::Type::Array); push(value); } void FormattedJsonBuilderStream::endArray() { FormattedJson value = pop(); if (m_stack.size() > 0) current().appendElement(ValueElement{value}); else m_root = value; } void FormattedJsonBuilderStream::putString(String::Char const* s, size_t len) { putValue(String(s, len)); } void FormattedJsonBuilderStream::putDouble(String::Char const* s, size_t len) { String formatted(s, len); double d = lexicalCast(formatted); putValue(d, formatted); } void FormattedJsonBuilderStream::putInteger(String::Char const* s, size_t len) { String formatted(s, len); long long d = lexicalCast(formatted); putValue(d, formatted); } void FormattedJsonBuilderStream::putBoolean(bool b) { putValue(b); } void FormattedJsonBuilderStream::putNull() { putValue(Json::ofType(Json::Type::Null)); } void FormattedJsonBuilderStream::putWhitespace(String::Char const* s, size_t len) { if (m_stack.size() > 0) current().appendElement(WhitespaceElement{String(s, len)}); } void FormattedJsonBuilderStream::putColon() { current().appendElement(ColonElement{}); } void FormattedJsonBuilderStream::putComma() { current().appendElement(CommaElement{}); } FormattedJson FormattedJsonBuilderStream::takeTop() { return m_root.take(); } void FormattedJsonBuilderStream::push(FormattedJson const& v) { m_stack.push_back(v); } FormattedJson FormattedJsonBuilderStream::pop() { FormattedJson result = m_stack.back(); m_stack.pop_back(); return result; } FormattedJson& FormattedJsonBuilderStream::current() { return m_stack.back(); } void FormattedJsonBuilderStream::putValue(Json const& value, Maybe formatting) { FormattedJson formattedValue = value; if (formatting.isValid()) formattedValue = formattedValue.formattedAs(*formatting); if (m_stack.size() > 0) current().appendElement(ValueElement{formattedValue}); else { m_root = formattedValue; } } void JsonStreamer::toJsonStream(FormattedJson const& val, JsonStream& stream, bool sort) { if (val.isType(Json::Type::Object)) stream.beginObject(); else if (val.isType(Json::Type::Array)) stream.beginArray(); else if (val.isType(Json::Type::Float)) { // Float and Int are to be formatted the same way they were parsed to // preserve, e.g. negative zeroes and trailing 0 digits on decimals. auto ws = val.toFormattedDouble().wideString(); stream.putDouble(ws.c_str(), ws.length()); return; } else if (val.isType(Json::Type::Int)) { auto ws = val.toFormattedInt().wideString(); stream.putInteger(ws.c_str(), ws.length()); return; } else { // If val is not an object, array or number, it has no formatting and no // elements. Stream the wrapped Json value the usual way. JsonStreamer::toJsonStream(val.toJson(), stream, sort); return; } for (JsonElement elem : val.elements()) { if (elem.is()) { String::WideString key = elem.get().key.wideString(); stream.objectKey(key.c_str(), key.length()); } else if (elem.is()) { String::WideString white = elem.get().whitespace.wideString(); stream.putWhitespace(white.c_str(), white.length()); } else if (elem.is()) { stream.putColon(); } else if (elem.is()) { stream.putComma(); } else { toJsonStream(*elem.get().value, stream, sort); } } if (val.isType(Json::Type::Object)) stream.endObject(); if (val.isType(Json::Type::Array)) stream.endArray(); } std::ostream& operator<<(std::ostream& os, JsonElement const& elem) { if (elem.is()) return os << "ValueElement{" << elem.get().value << "}"; if (elem.is()) return os << "ObjectKeyElement{" << elem.get().key << "}"; if (elem.is()) return os << "WhitespaceElement{" << elem.get().whitespace << "}"; if (elem.is()) return os << "ColonElement{}"; if (elem.is()) return os << "CommaElement{}"; starAssert(false); return os; } std::ostream& operator<<(std::ostream& os, FormattedJson const& json) { return os << json.repr(); } }