1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

JSON: Add new JSON::Formatter for configuring JSON output

This also fixes an issue where MIDI CI header data could contain spaces,
which is not allowed according to the spec.
This commit is contained in:
reuk 2023-12-05 14:35:09 +00:00
parent 06855ed05d
commit 224c4f706b
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
12 changed files with 203 additions and 96 deletions

View file

@ -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

View file

@ -94,37 +94,47 @@ std::unique_ptr<DynamicObject> 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 << '}';
}

View file

@ -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:
//==============================================================================

View file

@ -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<bool> (v) ? "true" : "false");
}
else if (v.isDouble())
{
auto d = static_cast<double> (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<var>& array,
int indentLevel, bool allOnOneLine, int maximumDecimalPlaces)
static void writeArray (OutputStream& out, const Array<var>& 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<bool> (v) ? "true" : "false");
}
else if (v.isDouble())
{
auto d = static_cast<double> (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);
}
}

View file

@ -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);

View file

@ -597,7 +597,10 @@ public:
private:
void expectDeepEqual (const std::optional<var>& a, const std::optional<var>& 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<var>& a, const std::optional<var>& b)

View file

@ -196,7 +196,10 @@ public:
void expectDeepEqual (const std::optional<var>& a, const std::optional<var>& 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<var>& a, const std::optional<var>& b)

View file

@ -835,7 +835,7 @@ struct JavascriptEngine::RootObject final : public DynamicObject
std::unique_ptr<DynamicObject> clone() const override { return std::make_unique<FunctionObject> (*this); }
void writeAsJSON (OutputStream& out, int /*indentLevel*/, bool /*allOnOneLine*/, int /*maximumDecimalPlaces*/) override
void writeAsJSON (OutputStream& out, const JSON::FormatOptions&) override
{
out << "function " << functionCode;
}

View file

@ -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"

View file

@ -1077,7 +1077,7 @@ struct PushNotifications::Pimpl
auto bundle = LocalRef<jobject> (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;
}

View file

@ -53,7 +53,7 @@ struct Encodings
/** Converts a JSON object to a list of bytes in 7-bit ASCII format. */
static std::vector<std::byte> 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.

View file

@ -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;