diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/core/StarFormattedJson.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/core/StarFormattedJson.cpp')
-rw-r--r-- | source/core/StarFormattedJson.cpp | 674 |
1 files changed, 674 insertions, 0 deletions
diff --git a/source/core/StarFormattedJson.cpp b/source/core/StarFormattedJson.cpp new file mode 100644 index 0000000..f31f3ba --- /dev/null +++ b/source/core/StarFormattedJson.cpp @@ -0,0 +1,674 @@ +#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<String> formatting = {}); + + Maybe<FormattedJson> m_root; + List<FormattedJson> m_stack; +}; + +template <> +class JsonStreamer<FormattedJson> { +public: + static void toJsonStream(FormattedJson const& val, JsonStream& stream, bool sort); +}; + +ValueElement::ValueElement(FormattedJson const& json) : value(make_shared<FormattedJson>(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::const_iterator, FormattedJsonBuilderStream, FormattedJson>( + string.begin(), string.end(), true); +} + +FormattedJson FormattedJson::parseJson(String const& string) { + return inputUtf32Json<String::const_iterator, FormattedJsonBuilderStream, FormattedJson>( + string.begin(), string.end(), false); +} + +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<Json>::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 %s, must be Object type", typeName()); + + Maybe<pair<ElementLocation, ElementLocation>> entry = m_objectEntryLocations.maybe(key); + if (entry.isNothing()) + throw JsonException::format("No such key in FormattedJson::get(\"%s\")", 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 %s, must be Array type", typeName()); + + if (index >= m_arrayElementLocations.size()) + throw JsonException::format("FormattedJson::get(%s) out of range", index); + + ElementLocation loc = m_arrayElementLocations.at(index); + return getFormattedJson(loc); +} + +struct WhitespaceStyle { + String beforeKey; + String beforeColon; + String beforeValue; + String beforeComma; +}; + +template <class ElementType> +FormattedJson::ElementLocation indexOf(FormattedJson::ElementList const& elements, FormattedJson::ElementLocation pos) { + for (; pos < elements.size(); ++pos) { + if (elements[pos].is<ElementType>()) + return pos; + } + return NPos; +} + +template <class ElementType> +FormattedJson::ElementLocation lastIndexOf( + FormattedJson::ElementList const& elements, FormattedJson::ElementLocation pos) { + while (pos > 0) { + --pos; + if (elements[pos].is<ElementType>()) + 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<WhitespaceElement>()) + whitespace += elem.get<WhitespaceElement>().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<ValueElement>(elements, insertLoc); + if (valueLoc == NPos) + valueLoc = indexOf<ValueElement>(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<CommaElement>(elements, valueLoc); + if (commaLoc != NPos) { + style.beforeComma = concatWhitespace(elements, valueLoc + 1, commaLoc); + } + + FormattedJson::ElementLocation colonLoc = lastIndexOf<ColonElement>(elements, valueLoc); + starAssert((colonLoc == NPos) == array); + if (colonLoc != NPos) { + style.beforeValue = concatWhitespace(elements, colonLoc + 1, valueLoc); + + FormattedJson::ElementLocation keyLoc = lastIndexOf<ObjectKeyElement>(elements, colonLoc); + starAssert(keyLoc != NPos); + style.beforeColon = concatWhitespace(elements, keyLoc + 1, colonLoc); + + FormattedJson::ElementLocation prevValueLoc = lastIndexOf<ValueElement>(elements, keyLoc); + if (prevValueLoc == NPos) + prevValueLoc = 0; + style.beforeKey = concatWhitespace(elements, prevValueLoc, keyLoc); + + } else { + FormattedJson::ElementLocation prevValueLoc = lastIndexOf<ValueElement>(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<ValueElement>()) + insertWhitespace(destination, at, style.beforeValue); + if (element.is<ObjectKeyElement>()) + insertWhitespace(destination, at, style.beforeKey); + if (element.is<ColonElement>()) + insertWhitespace(destination, at, style.beforeColon); + if (element.is<CommaElement>()) + 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<ValueElement>(destination, at); + if (at == NPos) + at = 0; + else + at += 1; + bool empty = lastIndexOf<ValueElement>(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 \"%s\", 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 \"%s\", 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<JsonElement>& 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<WhitespaceElement>()) + 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 %s, must be Object type", typeName()); + + Maybe<pair<ElementLocation, ElementLocation>> 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 %s, must be Array type", typeName()); + + if (index > m_arrayElementLocations.size()) + throw JsonException::format("FormattedJson::insert(%s) 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 %s, 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 %s, must be Array type", typeName()); + + if (index >= m_arrayElementLocations.size()) + throw JsonException::format("FormattedJson::set(%s) 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 %s, must be Array type", typeName()); + + if (index >= m_arrayElementLocations.size()) + throw JsonException::format("FormattedJson::eraseIndex(%s) 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 %s, 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 %s, 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<std::back_insert_iterator<String>, 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<ValueElement>().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<ColonElement>() || elem.is<ObjectKeyElement>()) + 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 %s, must be Object type", typeName()); + + Maybe<pair<ElementLocation, ElementLocation>> 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<ObjectKeyElement>()) { + starAssert(isType(Json::Type::Object)); + m_lastKey = loc; + + } else if (elem.is<ValueElement>()) { + m_lastValue = loc; + + if (m_lastKey.isValid()) { + starAssert(isType(Json::Type::Object)); + String key = m_elements[*m_lastKey].get<ObjectKeyElement>().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<ValueElement>().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<double>(formatted); + putValue(d, formatted); +} + +void FormattedJsonBuilderStream::putInteger(String::Char const* s, size_t len) { + String formatted(s, len); + long long d = lexicalCast<long long>(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<String> 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<FormattedJson>::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<Json>::toJsonStream(val.toJson(), stream, sort); + return; + } + + for (JsonElement elem : val.elements()) { + if (elem.is<ObjectKeyElement>()) { + String::WideString key = elem.get<ObjectKeyElement>().key.wideString(); + stream.objectKey(key.c_str(), key.length()); + } else if (elem.is<WhitespaceElement>()) { + String::WideString white = elem.get<WhitespaceElement>().whitespace.wideString(); + stream.putWhitespace(white.c_str(), white.length()); + } else if (elem.is<ColonElement>()) { + stream.putColon(); + } else if (elem.is<CommaElement>()) { + stream.putComma(); + } else { + toJsonStream(*elem.get<ValueElement>().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<ValueElement>()) + return os << "ValueElement{" << elem.get<ValueElement>().value << "}"; + if (elem.is<ObjectKeyElement>()) + return os << "ObjectKeyElement{" << elem.get<ObjectKeyElement>().key << "}"; + if (elem.is<WhitespaceElement>()) + return os << "WhitespaceElement{" << elem.get<WhitespaceElement>().whitespace << "}"; + if (elem.is<ColonElement>()) + return os << "ColonElement{}"; + if (elem.is<CommaElement>()) + return os << "CommaElement{}"; + starAssert(false); + return os; +} + +std::ostream& operator<<(std::ostream& os, FormattedJson const& json) { + return os << json.repr(); +} + +} |