diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 662d54c435..4e2bc5d80c 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,30 @@ # JUCE breaking changes +# develop + +## Change + +The signature of DynamicObject::writeAsJSON() has been changed to accept a +more extensible JSON::FormatOptions argument. + +**Possible Issues** + +Code that overrides this function will fail to compile. + +**Workaround** + +Update the signatures of overriding functions. Use Formatter::getIndentLevel() +and Formatter::getMaxDecimalPlaces() as necessary. To find whether the output +should be multi-line, compare the result of Formatter::getSpacing() with +JSON::Spacing::multiLine. + +**Rationale** + +The previous signature made it impossible to add new formatting options. Now, +if we need to add further options in the future, these can be added to the +FormatOptions type, which will not be a breaking change. + + # Version 7.0.9 ## Change diff --git a/modules/juce_core/containers/juce_DynamicObject.cpp b/modules/juce_core/containers/juce_DynamicObject.cpp index 19aff6b29e..30cbbb85fa 100644 --- a/modules/juce_core/containers/juce_DynamicObject.cpp +++ b/modules/juce_core/containers/juce_DynamicObject.cpp @@ -94,37 +94,47 @@ std::unique_ptr DynamicObject::clone() const return result; } -void DynamicObject::writeAsJSON (OutputStream& out, const int indentLevel, const bool allOnOneLine, int maximumDecimalPlaces) +void DynamicObject::writeAsJSON (OutputStream& out, const JSON::FormatOptions& format) { out << '{'; - if (! allOnOneLine) + if (format.getSpacing() == JSON::Spacing::multiLine) out << newLine; const int numValues = properties.size(); for (int i = 0; i < numValues; ++i) { - if (! allOnOneLine) - JSONFormatter::writeSpaces (out, indentLevel + JSONFormatter::indentSize); + if (format.getSpacing() == JSON::Spacing::multiLine) + JSONFormatter::writeSpaces (out, format.getIndentLevel() + JSONFormatter::indentSize); out << '"'; JSONFormatter::writeString (out, properties.getName (i)); - out << "\": "; - JSONFormatter::write (out, properties.getValueAt (i), indentLevel + JSONFormatter::indentSize, allOnOneLine, maximumDecimalPlaces); + out << "\":"; + + if (format.getSpacing() != JSON::Spacing::none) + out << ' '; + + JSON::writeToStream (out, + properties.getValueAt (i), + format.withIndentLevel (format.getIndentLevel() + JSONFormatter::indentSize)); if (i < numValues - 1) { - if (allOnOneLine) - out << ", "; - else - out << ',' << newLine; + out << ","; + + switch (format.getSpacing()) + { + case JSON::Spacing::none: break; + case JSON::Spacing::singleLine: out << ' '; break; + case JSON::Spacing::multiLine: out << newLine; break; + } } - else if (! allOnOneLine) + else if (format.getSpacing() == JSON::Spacing::multiLine) out << newLine; } - if (! allOnOneLine) - JSONFormatter::writeSpaces (out, indentLevel); + if (format.getSpacing() == JSON::Spacing::multiLine) + JSONFormatter::writeSpaces (out, format.getIndentLevel()); out << '}'; } diff --git a/modules/juce_core/containers/juce_DynamicObject.h b/modules/juce_core/containers/juce_DynamicObject.h index dba6a84612..7faa377050 100644 --- a/modules/juce_core/containers/juce_DynamicObject.h +++ b/modules/juce_core/containers/juce_DynamicObject.h @@ -118,7 +118,7 @@ public: never need to call it directly, but it's virtual so that custom object types can stringify themselves appropriately. */ - virtual void writeAsJSON (OutputStream&, int indentLevel, bool allOnOneLine, int maximumDecimalPlaces); + virtual void writeAsJSON (OutputStream&, const JSON::FormatOptions&); private: //============================================================================== diff --git a/modules/juce_core/javascript/juce_JSON.cpp b/modules/juce_core/javascript/juce_JSON.cpp index aa1394dc48..9b15c02fd5 100644 --- a/modules/juce_core/javascript/juce_JSON.cpp +++ b/modules/juce_core/javascript/juce_JSON.cpp @@ -306,60 +306,6 @@ struct JSONParser //============================================================================== struct JSONFormatter { - static void write (OutputStream& out, const var& v, - int indentLevel, bool allOnOneLine, int maximumDecimalPlaces) - { - if (v.isString()) - { - out << '"'; - writeString (out, v.toString().getCharPointer()); - out << '"'; - } - else if (v.isVoid()) - { - out << "null"; - } - else if (v.isUndefined()) - { - out << "undefined"; - } - else if (v.isBool()) - { - out << (static_cast (v) ? "true" : "false"); - } - else if (v.isDouble()) - { - auto d = static_cast (v); - - if (juce_isfinite (d)) - { - out << serialiseDouble (d); - } - else - { - out << "null"; - } - } - else if (v.isArray()) - { - writeArray (out, *v.getArray(), indentLevel, allOnOneLine, maximumDecimalPlaces); - } - else if (v.isObject()) - { - if (auto* object = v.getDynamicObject()) - object->writeAsJSON (out, indentLevel, allOnOneLine, maximumDecimalPlaces); - else - jassertfalse; // Only DynamicObjects can be converted to JSON! - } - else - { - // Can't convert these other types of object to JSON! - jassert (! (v.isMethod() || v.isBinaryData())); - - out << v.toString(); - } - } - static void writeEscapedChar (OutputStream& out, const unsigned short value) { out << "\\u" << String::toHexString ((int) value).paddedLeft ('0', 4); @@ -416,36 +362,39 @@ struct JSONFormatter out.writeRepeatedByte (' ', (size_t) numSpaces); } - static void writeArray (OutputStream& out, const Array& array, - int indentLevel, bool allOnOneLine, int maximumDecimalPlaces) + static void writeArray (OutputStream& out, const Array& array, const JSON::FormatOptions& format) { out << '['; if (! array.isEmpty()) { - if (! allOnOneLine) + if (format.getSpacing() == JSON::Spacing::multiLine) out << newLine; for (int i = 0; i < array.size(); ++i) { - if (! allOnOneLine) - writeSpaces (out, indentLevel + indentSize); + if (format.getSpacing() == JSON::Spacing::multiLine) + writeSpaces (out, format.getIndentLevel() + indentSize); - write (out, array.getReference (i), indentLevel + indentSize, allOnOneLine, maximumDecimalPlaces); + JSON::writeToStream (out, array.getReference (i), format.withIndentLevel (format.getIndentLevel() + indentSize)); if (i < array.size() - 1) { - if (allOnOneLine) - out << ", "; - else - out << ',' << newLine; + out << ","; + + switch (format.getSpacing()) + { + case JSON::Spacing::none: break; + case JSON::Spacing::singleLine: out << ' '; break; + case JSON::Spacing::multiLine: out << newLine; break; + } } - else if (! allOnOneLine) + else if (format.getSpacing() == JSON::Spacing::multiLine) out << newLine; } - if (! allOnOneLine) - writeSpaces (out, indentLevel); + if (format.getSpacing() == JSON::Spacing::multiLine) + writeSpaces (out, format.getIndentLevel()); } out << ']'; @@ -454,6 +403,67 @@ struct JSONFormatter enum { indentSize = 2 }; }; + +void JSON::writeToStream (OutputStream& out, const var& v, const FormatOptions& opt) +{ + if (v.isString()) + { + out << '"'; + JSONFormatter::writeString (out, v.toString().getCharPointer()); + out << '"'; + } + else if (v.isVoid()) + { + out << "null"; + } + else if (v.isUndefined()) + { + out << "undefined"; + } + else if (v.isBool()) + { + out << (static_cast (v) ? "true" : "false"); + } + else if (v.isDouble()) + { + auto d = static_cast (v); + + if (juce_isfinite (d)) + { + out << serialiseDouble (d); + } + else + { + out << "null"; + } + } + else if (v.isArray()) + { + JSONFormatter::writeArray (out, *v.getArray(), opt); + } + else if (v.isObject()) + { + if (auto* object = v.getDynamicObject()) + object->writeAsJSON (out, opt); + else + jassertfalse; // Only DynamicObjects can be converted to JSON! + } + else + { + // Can't convert these other types of object to JSON! + jassert (! (v.isMethod() || v.isBinaryData())); + + out << v.toString(); + } +} + +String JSON::toString (const var& v, const FormatOptions& opt) +{ + MemoryOutputStream mo { 1024 }; + writeToStream (mo, v, opt); + return mo.toUTF8(); +} + //============================================================================== var JSON::parse (const String& text) { @@ -502,14 +512,14 @@ Result JSON::parse (const String& text, var& result) String JSON::toString (const var& data, const bool allOnOneLine, int maximumDecimalPlaces) { - MemoryOutputStream mo (1024); - JSONFormatter::write (mo, data, 0, allOnOneLine, maximumDecimalPlaces); - return mo.toUTF8(); + return toString (data, FormatOptions{}.withSpacing (allOnOneLine ? Spacing::singleLine : Spacing::multiLine) + .withMaxDecimalPlaces (maximumDecimalPlaces)); } void JSON::writeToStream (OutputStream& output, const var& data, const bool allOnOneLine, int maximumDecimalPlaces) { - JSONFormatter::write (output, data, 0, allOnOneLine, maximumDecimalPlaces); + writeToStream (output, data, FormatOptions{}.withSpacing (allOnOneLine ? Spacing::singleLine : Spacing::multiLine) + .withMaxDecimalPlaces (maximumDecimalPlaces)); } String JSON::escapeString (StringRef s) @@ -653,10 +663,10 @@ public: if (i > 0) v = createRandomVar (r, 0); - const bool oneLine = r.nextBool(); - String asString (JSON::toString (v, oneLine)); - var parsed = JSON::parse ("[" + asString + "]")[0]; - String parsedString (JSON::toString (parsed, oneLine)); + const auto oneLine = r.nextBool(); + const auto asString = JSON::toString (v, oneLine); + const auto parsed = JSON::parse ("[" + asString + "]")[0]; + const auto parsedString = JSON::toString (parsed, oneLine); expect (asString.isNotEmpty() && parsedString == asString); } } diff --git a/modules/juce_core/javascript/juce_JSON.h b/modules/juce_core/javascript/juce_JSON.h index 68538c9ccd..40a904aca7 100644 --- a/modules/juce_core/javascript/juce_JSON.h +++ b/modules/juce_core/javascript/juce_JSON.h @@ -88,6 +88,47 @@ public: */ static var parse (InputStream& input); + enum class Spacing + { + none, ///< All optional whitespace should be omitted + singleLine, ///< All output should be on a single line, but with some additional spacing, e.g. after commas and colons + multiLine, ///< Newlines and spaces will be included in the output, in order to make it easy to read for humans + }; + + /** + Allows formatting var objects as JSON with various configurable options. + */ + class [[nodiscard]] FormatOptions + { + public: + /** Returns a copy of this Formatter with the specified spacing. */ + FormatOptions withSpacing (Spacing x) const { return withMember (*this, &FormatOptions::spacing, x); } + + /** Returns a copy of this Formatter with the specified maximum number of decimal places. + This option determines the precision of floating point numbers in scientific notation. + */ + FormatOptions withMaxDecimalPlaces (int x) const { return withMember (*this, &FormatOptions::maxDecimalPlaces, x); } + + /** Returns a copy of this Formatter with the specified indent level. + This should only be necessary when serialising multiline nested types. + */ + FormatOptions withIndentLevel (int x) const { return withMember (*this, &FormatOptions::indent, x); } + + /** Returns the spacing used by this Formatter. */ + Spacing getSpacing() const { return spacing; } + + /** Returns the maximum number of decimal places used by this Formatter. */ + int getMaxDecimalPlaces() const { return maxDecimalPlaces; } + + /** Returns the indent level of this Formatter. */ + int getIndentLevel() const { return indent; } + + private: + Spacing spacing = Spacing::multiLine; + int maxDecimalPlaces = 15; + int indent = 0; + }; + //============================================================================== /** Returns a string which contains a JSON-formatted representation of the var object. If allOnOneLine is true, the result will be compacted into a single line of text @@ -100,6 +141,13 @@ public: bool allOnOneLine = false, int maximumDecimalPlaces = 15); + /** Returns a string which contains a JSON-formatted representation of the var object, using + formatting described by the FormatOptions parameter. + @see writeToStream + */ + static String toString (const var& objectToFormat, + const FormatOptions& formatOptions); + /** Parses a string that was created with the toString() method. This is slightly different to the parse() methods because they will reject primitive values and only accept array or object definitions, whereas this method will handle @@ -119,6 +167,14 @@ public: bool allOnOneLine = false, int maximumDecimalPlaces = 15); + /** Writes a JSON-formatted representation of the var object to the given stream, using + formatting described by the FormatOptions parameter. + @see toString + */ + static void writeToStream (OutputStream& output, + const var& objectToFormat, + const FormatOptions& formatOptions); + /** Returns a version of a string with any extended characters escaped. */ static String escapeString (StringRef); diff --git a/modules/juce_core/javascript/juce_JSONSerialisation_test.cpp b/modules/juce_core/javascript/juce_JSONSerialisation_test.cpp index 7d6708c88b..ecef13d93c 100644 --- a/modules/juce_core/javascript/juce_JSONSerialisation_test.cpp +++ b/modules/juce_core/javascript/juce_JSONSerialisation_test.cpp @@ -597,7 +597,10 @@ public: private: void expectDeepEqual (const std::optional& a, const std::optional& b) { - expect (deepEqual (a, b), a.has_value() && b.has_value() ? JSON::toString (*a) + " != " + JSON::toString (*b) : String()); + const auto text = a.has_value() && b.has_value() + ? JSON::toString (*a) + " != " + JSON::toString (*b) + : String(); + expect (deepEqual (a, b), text); } static bool deepEqual (const std::optional& a, const std::optional& b) diff --git a/modules/juce_core/javascript/juce_JSONUtils.cpp b/modules/juce_core/javascript/juce_JSONUtils.cpp index 88438e0ffa..e29d652da2 100644 --- a/modules/juce_core/javascript/juce_JSONUtils.cpp +++ b/modules/juce_core/javascript/juce_JSONUtils.cpp @@ -196,7 +196,10 @@ public: void expectDeepEqual (const std::optional& a, const std::optional& b) { - expect (deepEqual (a, b), a.has_value() && b.has_value() ? JSON::toString (*a) + " != " + JSON::toString (*b) : String()); + const auto text = a.has_value() && b.has_value() + ? JSON::toString (*a) + " != " + JSON::toString (*b) + : String(); + expect (deepEqual (a, b), text); } static bool deepEqual (const std::optional& a, const std::optional& b) diff --git a/modules/juce_core/javascript/juce_Javascript.cpp b/modules/juce_core/javascript/juce_Javascript.cpp index a8b1f68ee3..aae7b63070 100644 --- a/modules/juce_core/javascript/juce_Javascript.cpp +++ b/modules/juce_core/javascript/juce_Javascript.cpp @@ -835,7 +835,7 @@ struct JavascriptEngine::RootObject final : public DynamicObject std::unique_ptr clone() const override { return std::make_unique (*this); } - void writeAsJSON (OutputStream& out, int /*indentLevel*/, bool /*allOnOneLine*/, int /*maximumDecimalPlaces*/) override + void writeAsJSON (OutputStream& out, const JSON::FormatOptions&) override { out << "function " << functionCode; } diff --git a/modules/juce_core/juce_core.h b/modules/juce_core/juce_core.h index db381f6601..4ed2698dac 100644 --- a/modules/juce_core/juce_core.h +++ b/modules/juce_core/juce_core.h @@ -285,6 +285,7 @@ JUCE_END_IGNORE_WARNINGS_MSVC #include "misc/juce_ConsoleApplication.h" #include "containers/juce_Variant.h" #include "containers/juce_NamedValueSet.h" +#include "javascript/juce_JSON.h" #include "containers/juce_DynamicObject.h" #include "containers/juce_HashMap.h" #include "containers/juce_FixedSizeFunction.h" @@ -309,7 +310,6 @@ JUCE_END_IGNORE_WARNINGS_MSVC #include "files/juce_WildcardFileFilter.h" #include "streams/juce_FileInputSource.h" #include "logging/juce_FileLogger.h" -#include "javascript/juce_JSON.h" #include "javascript/juce_JSONUtils.h" #include "serialisation/juce_Serialisation.h" #include "javascript/juce_JSONSerialisation.h" diff --git a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp index 6c4c1382d2..d3af96aacb 100644 --- a/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp +++ b/modules/juce_gui_extra/native/juce_PushNotifications_android.cpp @@ -1077,7 +1077,7 @@ struct PushNotifications::Pimpl auto bundle = LocalRef (env->NewObject (AndroidBundle, AndroidBundle.constructor)); env->CallVoidMethod (bundle, AndroidBundle.putString, javaString ("properties").get(), - javaString (JSON::toString (varToParse, false)).get()); + javaString (JSON::toString (varToParse)).get()); return bundle; } diff --git a/modules/juce_midi_ci/ci/juce_CIEncodings.h b/modules/juce_midi_ci/ci/juce_CIEncodings.h index 0fe6b3ea48..dd9f712f6b 100644 --- a/modules/juce_midi_ci/ci/juce_CIEncodings.h +++ b/modules/juce_midi_ci/ci/juce_CIEncodings.h @@ -53,7 +53,7 @@ struct Encodings /** Converts a JSON object to a list of bytes in 7-bit ASCII format. */ static std::vector jsonTo7BitText (const var& v) { - return stringTo7BitText (JSON::toString (v, true)); + return stringTo7BitText (JSON::toString (v, JSON::FormatOptions{}.withSpacing (JSON::Spacing::none))); } /** Each group of seven stored bytes is transmitted as eight bytes. diff --git a/modules/juce_midi_ci/ci/juce_CIParser.cpp b/modules/juce_midi_ci/ci/juce_CIParser.cpp index 03b2eae9f3..5c8c5cb0ba 100644 --- a/modules/juce_midi_ci/ci/juce_CIParser.cpp +++ b/modules/juce_midi_ci/ci/juce_CIParser.cpp @@ -165,7 +165,7 @@ private: const auto json = ToVar::convert (body, opts); if (json.has_value()) - *result = String (getDescription (body)) + ": " + JSON::toString (*json, true); + *result = String (getDescription (body)) + ": " + JSON::toString (*json, JSON::FormatOptions{}.withSpacing (JSON::Spacing::none)); } const Message::Parsed* msg = nullptr;