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:
parent
06855ed05d
commit
224c4f706b
12 changed files with 203 additions and 96 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 << '}';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue