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

Javascript: Move javascript implementation into a separate module

This commit is contained in:
Anthony Nicholls 2024-11-05 13:50:57 +00:00
parent 637226addc
commit df6f3f8e28
69 changed files with 941 additions and 1351 deletions

View file

@ -0,0 +1,868 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
struct JSONParser
{
JSONParser (String::CharPointerType text) : startLocation (text), currentLocation (text) {}
String::CharPointerType startLocation, currentLocation;
struct ErrorException
{
String message;
int line = 1, column = 1;
String getDescription() const { return String (line) + ":" + String (column) + ": error: " + message; }
Result getResult() const { return Result::fail (getDescription()); }
};
[[noreturn]] void throwError (juce::String message, String::CharPointerType location)
{
ErrorException e;
e.message = std::move (message);
for (auto i = startLocation; i < location && ! i.isEmpty(); ++i)
{
++e.column;
if (*i == '\n') { e.column = 1; e.line++; }
}
throw e;
}
void skipWhitespace() { currentLocation = currentLocation.findEndOfWhitespace(); }
juce_wchar readChar() { return currentLocation.getAndAdvance(); }
juce_wchar peekChar() const { return *currentLocation; }
bool matchIf (char c) { if (peekChar() == (juce_wchar) c) { ++currentLocation; return true; } return false; }
bool isEOF() const { return peekChar() == 0; }
bool matchString (const char* t)
{
while (*t != 0)
if (! matchIf (*t++))
return false;
return true;
}
var parseObjectOrArray()
{
skipWhitespace();
if (matchIf ('{')) return parseObject();
if (matchIf ('[')) return parseArray();
if (! isEOF())
throwError ("Expected '{' or '['", currentLocation);
return {};
}
int parseHexDigit()
{
const auto digitValue = CharacterFunctions::getHexDigitValue (readChar());
if (digitValue < 0)
throwError ("Invalid hex character", currentLocation - 1);
return digitValue;
}
CharPointer_UTF16::CharType parseCodeUnit()
{
return (CharPointer_UTF16::CharType) ( parseHexDigit() << 12
| (parseHexDigit() << 8)
| (parseHexDigit() << 4)
| (parseHexDigit()));
}
static constexpr juce_wchar asCodePoint (CharPointer_UTF16::CharType codeUnit)
{
return (juce_wchar) (uint32) (uint16) codeUnit;
}
CharPointer_UTF16::CharType parseLowSurrogateCodeUnit()
{
const auto errorLocation = currentLocation;
const auto throwLowSurrogateError = [&]()
{
throwError ("Expected UTF-16 low surrogate", errorLocation);
};
if (readChar() != '\\' || readChar() != 'u')
throwLowSurrogateError();
const auto lowSurrogate = parseCodeUnit();
if (! CharacterFunctions::isLowSurrogate (asCodePoint (lowSurrogate)))
throwLowSurrogateError();
return lowSurrogate;
}
juce_wchar parseEscapeSequence()
{
const auto errorLocation = currentLocation - 2;
const auto codeUnits = [&]() -> std::array<CharPointer_UTF16::CharType, 2>
{
const auto firstCodeUnit = parseCodeUnit();
if (CharacterFunctions::isNonSurrogateCodePoint (asCodePoint (firstCodeUnit)))
return { firstCodeUnit, 0 };
if (! CharacterFunctions::isHighSurrogate (asCodePoint (firstCodeUnit)))
throwError ("Invalid UTF-16 escape sequence", errorLocation);
return { firstCodeUnit, parseLowSurrogateCodeUnit() };
}();
return CharPointer_UTF16 (codeUnits.data()).getAndAdvance();
}
String parseString (const juce_wchar quoteChar)
{
MemoryOutputStream buffer (256);
for (;;)
{
auto c = readChar();
if (c == quoteChar)
break;
if (c == '\\')
{
c = readChar();
switch (c)
{
case '"':
case '\'':
case '\\':
case '/': break;
case 'a': c = '\a'; break;
case 'b': c = '\b'; break;
case 'f': c = '\f'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'u': c = parseEscapeSequence(); break;
default: break;
}
}
if (c == 0)
throwError ("Unexpected EOF in string constant", currentLocation);
buffer.appendUTF8Char (c);
}
return buffer.toUTF8();
}
var parseAny()
{
skipWhitespace();
auto originalLocation = currentLocation;
switch (readChar())
{
case '{': return parseObject();
case '[': return parseArray();
case '"': return parseString ('"');
case '\'': return parseString ('\'');
case '-':
skipWhitespace();
return parseNumber (true);
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
currentLocation = originalLocation;
return parseNumber (false);
case 't': // "true"
if (matchString ("rue"))
return var (true);
break;
case 'f': // "false"
if (matchString ("alse"))
return var (false);
break;
case 'n': // "null"
if (matchString ("ull"))
return {};
break;
default:
break;
}
throwError ("Syntax error", originalLocation);
}
var parseNumber (bool isNegative)
{
auto originalPos = currentLocation;
int64 intValue = readChar() - '0';
jassert (intValue >= 0 && intValue < 10);
for (;;)
{
auto lastPos = currentLocation;
auto c = readChar();
auto digit = ((int) c) - '0';
if (isPositiveAndBelow (digit, 10))
{
intValue = intValue * 10 + digit;
continue;
}
if (c == 'e' || c == 'E' || c == '.')
{
currentLocation = originalPos;
auto asDouble = CharacterFunctions::readDoubleValue (currentLocation);
return var (isNegative ? -asDouble : asDouble);
}
if (CharacterFunctions::isWhitespace (c)
|| c == ',' || c == '}' || c == ']' || c == 0)
{
currentLocation = lastPos;
break;
}
throwError ("Syntax error in number", lastPos);
}
auto correctedValue = isNegative ? -intValue : intValue;
return (intValue >> 31) != 0 ? var (correctedValue)
: var ((int) correctedValue);
}
var parseObject()
{
auto resultObject = new DynamicObject();
var result (resultObject);
auto& resultProperties = resultObject->getProperties();
auto startOfObjectDecl = currentLocation;
for (;;)
{
skipWhitespace();
auto errorLocation = currentLocation;
auto c = readChar();
if (c == '}')
break;
if (c == 0)
throwError ("Unexpected EOF in object declaration", startOfObjectDecl);
if (c != '"')
throwError ("Expected a property name in double-quotes", errorLocation);
errorLocation = currentLocation;
Identifier propertyName (parseString ('"'));
if (! propertyName.isValid())
throwError ("Invalid property name", errorLocation);
skipWhitespace();
errorLocation = currentLocation;
if (readChar() != ':')
throwError ("Expected ':'", errorLocation);
resultProperties.set (propertyName, parseAny());
skipWhitespace();
if (matchIf (',')) continue;
if (matchIf ('}')) break;
throwError ("Expected ',' or '}'", currentLocation);
}
return result;
}
var parseArray()
{
auto result = var (Array<var>());
auto destArray = result.getArray();
auto startOfArrayDecl = currentLocation;
for (;;)
{
skipWhitespace();
if (matchIf (']'))
break;
if (isEOF())
throwError ("Unexpected EOF in array declaration", startOfArrayDecl);
destArray->add (parseAny());
skipWhitespace();
if (matchIf (',')) continue;
if (matchIf (']')) break;
throwError ("Expected ',' or ']'", currentLocation);
}
return result;
}
};
//==============================================================================
struct JSONFormatter
{
static void writeEscapedChar (OutputStream& out, const unsigned short value)
{
out << "\\u" << String::toHexString ((int) value).paddedLeft ('0', 4);
}
static void writeString (OutputStream& out, String::CharPointerType t, JSON::Encoding encoding)
{
for (;;)
{
const auto c = t.getAndAdvance();
switch (c)
{
case 0: return;
case '\"': out << "\\\""; break;
case '\\': out << "\\\\"; break;
case '\b': out << "\\b"; break;
case '\f': out << "\\f"; break;
case '\t': out << "\\t"; break;
case '\r': out << "\\r"; break;
case '\n': out << "\\n"; break;
default:
if (CharacterFunctions::isAsciiControlCharacter (c))
{
writeEscapedChar (out, (unsigned short) c);
}
else
{
switch (encoding)
{
case JSON::Encoding::utf8:
out << String::charToString (c);
break;
case JSON::Encoding::ascii:
if (CharacterFunctions::isAscii (c))
{
out << String::charToString (c);
}
else if (CharacterFunctions::isPartOfBasicMultilingualPlane (c))
{
if (CharacterFunctions::isNonSurrogateCodePoint (c))
writeEscapedChar (out, (unsigned short) c);
else
jassertfalse; // Illegal unicode character
}
else
{
CharPointer_UTF16::CharType codeUnits[2] = {};
CharPointer_UTF16 utf16 (codeUnits);
utf16.write (c);
for (auto& codeUnit : codeUnits)
writeEscapedChar (out, (unsigned short) codeUnit);
}
break;
}
}
break;
}
}
}
static void writeSpaces (OutputStream& out, int numSpaces)
{
out.writeRepeatedByte (' ', (size_t) numSpaces);
}
static void writeArray (OutputStream& out, const Array<var>& array, const JSON::FormatOptions& format)
{
out << '[';
if (! array.isEmpty())
{
if (format.getSpacing() == JSON::Spacing::multiLine)
out << newLine;
for (int i = 0; i < array.size(); ++i)
{
if (format.getSpacing() == JSON::Spacing::multiLine)
writeSpaces (out, format.getIndentLevel() + indentSize);
JSON::writeToStream (out, array.getReference (i), format.withIndentLevel (format.getIndentLevel() + indentSize));
if (i < array.size() - 1)
{
out << ",";
switch (format.getSpacing())
{
case JSON::Spacing::none: break;
case JSON::Spacing::singleLine: out << ' '; break;
case JSON::Spacing::multiLine: out << newLine; break;
}
}
else if (format.getSpacing() == JSON::Spacing::multiLine)
out << newLine;
}
if (format.getSpacing() == JSON::Spacing::multiLine)
writeSpaces (out, format.getIndentLevel());
}
out << ']';
}
enum { indentSize = 2 };
};
void JSON::writeToStream (OutputStream& out, const var& v, const FormatOptions& opt)
{
if (v.isString())
{
out << '"';
JSONFormatter::writeString (out, v.toString().getCharPointer(), opt.getEncoding());
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, opt.getMaxDecimalPlaces());
}
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)
{
var result;
if (parse (text, result))
return result;
return {};
}
var JSON::fromString (StringRef text)
{
try
{
return JSONParser (text.text).parseAny();
}
catch (const JSONParser::ErrorException&) {}
return {};
}
var JSON::parse (InputStream& input)
{
return parse (input.readEntireStreamAsString());
}
var JSON::parse (const File& file)
{
return parse (file.loadFileAsString());
}
Result JSON::parse (const String& text, var& result)
{
try
{
result = JSONParser (text.getCharPointer()).parseObjectOrArray();
}
catch (const JSONParser::ErrorException& error)
{
return error.getResult();
}
return Result::ok();
}
String JSON::toString (const var& data, const bool allOnOneLine, int maximumDecimalPlaces)
{
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)
{
writeToStream (output, data, FormatOptions{}.withSpacing (allOnOneLine ? Spacing::singleLine : Spacing::multiLine)
.withMaxDecimalPlaces (maximumDecimalPlaces));
}
String JSON::escapeString (StringRef s)
{
MemoryOutputStream mo;
JSONFormatter::writeString (mo, s.text, Encoding::ascii);
return mo.toString();
}
Result JSON::parseQuotedString (String::CharPointerType& t, var& result)
{
try
{
JSONParser parser (t);
auto quote = parser.readChar();
if (quote != '"' && quote != '\'')
return Result::fail ("Not a quoted string!");
result = parser.parseString (quote);
t = parser.currentLocation;
}
catch (const JSONParser::ErrorException& error)
{
return error.getResult();
}
return Result::ok();
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class JSONTests final : public UnitTest
{
public:
JSONTests()
: UnitTest ("JSON", UnitTestCategories::json)
{}
static String createRandomWideCharString (Random& r)
{
juce_wchar buffer[40] = { 0 };
for (int i = 0; i < numElementsInArray (buffer) - 1; ++i)
{
if (r.nextBool())
{
do
{
buffer[i] = (juce_wchar) (1 + r.nextInt (0x10ffff - 1));
}
while (! CharPointer_UTF16::canRepresent (buffer[i]));
}
else
buffer[i] = (juce_wchar) (1 + r.nextInt (0xff));
}
return CharPointer_UTF32 (buffer);
}
static String createRandomIdentifier (Random& r)
{
char buffer[30] = { 0 };
for (int i = 0; i < numElementsInArray (buffer) - 1; ++i)
{
static const char chars[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-:";
buffer[i] = chars [r.nextInt (sizeof (chars) - 1)];
}
return CharPointer_ASCII (buffer);
}
// Creates a random double that can be easily stringified, to avoid
// false failures when decimal places are rounded or truncated slightly
static var createRandomDouble (Random& r)
{
return var ((r.nextDouble() * 1000.0) + 0.1);
}
static var createRandomVar (Random& r, int depth)
{
switch (r.nextInt (depth > 3 ? 6 : 8))
{
case 0: return {};
case 1: return r.nextInt();
case 2: return r.nextInt64();
case 3: return r.nextBool();
case 4: return createRandomDouble (r);
case 5: return createRandomWideCharString (r);
case 6:
{
var v (createRandomVar (r, depth + 1));
for (int i = 1 + r.nextInt (30); --i >= 0;)
v.append (createRandomVar (r, depth + 1));
return v;
}
case 7:
{
auto o = new DynamicObject();
for (int i = r.nextInt (30); --i >= 0;)
o->setProperty (createRandomIdentifier (r), createRandomVar (r, depth + 1));
return o;
}
default:
return {};
}
}
void expectCharacterEncoding (juce_wchar character, const String& expectedOutput, JSON::Encoding encoding)
{
const auto input = String::charToString (character);
const auto quotedOutput = '"' + expectedOutput + '"';
expectEquals (JSON::toString (input, JSON::FormatOptions{}.withEncoding (encoding)), quotedOutput);
expectEquals (JSON::fromString (quotedOutput).toString(), input);
}
void expectNoEscapeSequence (juce_wchar input)
{
const auto inputString = String::charToString (input);
expectCharacterEncoding (input, inputString, JSON::Encoding::ascii);
expectCharacterEncoding (input, inputString, JSON::Encoding::utf8);
}
void expectEscapeSequenceForAllEncodings (juce_wchar input, const String& escapeSequence)
{
expectCharacterEncoding (input, escapeSequence, JSON::Encoding::ascii);
expectCharacterEncoding (input, escapeSequence, JSON::Encoding::utf8);
}
void expectEscapeSequenceForAsciiEncodingOnly (juce_wchar input, const String& escapeSequence)
{
expectCharacterEncoding (input, escapeSequence, JSON::Encoding::ascii);
expectCharacterEncoding (input, String::charToString (input), JSON::Encoding::utf8);
}
void runTest() override
{
beginTest ("Float formatting");
{
std::map<double, String> tests;
tests[1] = "1.0";
tests[1.1] = "1.1";
tests[1.01] = "1.01";
tests[0.76378] = "0.76378";
tests[-10] = "-10.0";
tests[10.01] = "10.01";
tests[0.0123] = "0.0123";
tests[-3.7e-27] = "-3.7e-27";
tests[1e+40] = "1.0e40";
tests[-12345678901234567.0] = "-1.234567890123457e16";
tests[192000] = "192000.0";
tests[1234567] = "1.234567e6";
tests[0.00006] = "0.00006";
tests[0.000006] = "6.0e-6";
for (auto& test : tests)
expectEquals (JSON::toString (test.first), test.second);
}
beginTest ("ASCII control characters are always escaped");
{
expectEscapeSequenceForAllEncodings ('\x01', "\\u0001");
expectEscapeSequenceForAllEncodings ('\x02', "\\u0002");
expectEscapeSequenceForAllEncodings ('\x03', "\\u0003");
expectEscapeSequenceForAllEncodings ('\x04', "\\u0004");
expectEscapeSequenceForAllEncodings ('\x05', "\\u0005");
expectEscapeSequenceForAllEncodings ('\x06', "\\u0006");
expectEscapeSequenceForAllEncodings ('\x07', "\\u0007");
expectEscapeSequenceForAllEncodings ('\x08', "\\b");
expectEscapeSequenceForAllEncodings ('\x09', "\\t");
expectEscapeSequenceForAllEncodings ('\x0a', "\\n");
expectEscapeSequenceForAllEncodings ('\x0b', "\\u000b");
expectEscapeSequenceForAllEncodings ('\x0c', "\\f");
expectEscapeSequenceForAllEncodings ('\x0d', "\\r");
expectEscapeSequenceForAllEncodings ('\x0e', "\\u000e");
expectEscapeSequenceForAllEncodings ('\x0f', "\\u000f");
expectEscapeSequenceForAllEncodings ('\x10', "\\u0010");
expectEscapeSequenceForAllEncodings ('\x11', "\\u0011");
expectEscapeSequenceForAllEncodings ('\x12', "\\u0012");
expectEscapeSequenceForAllEncodings ('\x13', "\\u0013");
expectEscapeSequenceForAllEncodings ('\x14', "\\u0014");
expectEscapeSequenceForAllEncodings ('\x15', "\\u0015");
expectEscapeSequenceForAllEncodings ('\x16', "\\u0016");
expectEscapeSequenceForAllEncodings ('\x17', "\\u0017");
expectEscapeSequenceForAllEncodings ('\x18', "\\u0018");
expectEscapeSequenceForAllEncodings ('\x19', "\\u0019");
expectEscapeSequenceForAllEncodings ('\x1a', "\\u001a");
expectEscapeSequenceForAllEncodings ('\x1b', "\\u001b");
expectEscapeSequenceForAllEncodings ('\x1c', "\\u001c");
expectEscapeSequenceForAllEncodings ('\x1d', "\\u001d");
expectEscapeSequenceForAllEncodings ('\x1e', "\\u001e");
expectEscapeSequenceForAllEncodings ('\x1f', "\\u001f");
}
beginTest ("Only special ASCII characters are escaped");
{
for (juce_wchar c = 32; CharacterFunctions::isAscii (c); ++c)
{
if (c != '"')
expectEscapeSequenceForAllEncodings ('"', R"(\")");
else if (c != '\\')
expectEscapeSequenceForAllEncodings ('\\', R"(\\)");
else
expectNoEscapeSequence (c);
}
}
beginTest ("Unicode characters are escaped for ASCII encoding only");
{
// First and last 2 byte UTF-8 code points
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0x0080, "\\u0080");
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0x07FF, "\\u07ff");
// First and last 3 byte UTF-8 code points
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0x0800, "\\u0800");
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0xffff, "\\uffff");
// Code points at the UTF-16 surrogate boundaries
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0xd7ff, "\\ud7ff");
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0xe000, "\\ue000");
// First and last 4 byte UTF-8 code points (also first and last UTF-16 surrogate pairs)
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0x010000, "\\ud800\\udc00");
expectEscapeSequenceForAsciiEncodingOnly ((juce_wchar) 0x10ffff, "\\udbff\\udfff");
}
beginTest ("Fuzz tests");
{
auto r = getRandom();
expect (JSON::parse (String()) == var());
expect (JSON::parse ("{}").isObject());
expect (JSON::parse ("[]").isArray());
expect (JSON::parse ("[ 1234 ]")[0].isInt());
expect (JSON::parse ("[ 12345678901234 ]")[0].isInt64());
expect (JSON::parse ("[ 1.123e3 ]")[0].isDouble());
expect (JSON::parse ("[ -1234]")[0].isInt());
expect (JSON::parse ("[-12345678901234]")[0].isInt64());
expect (JSON::parse ("[-1.123e3]")[0].isDouble());
for (int i = 100; --i >= 0;)
{
var v;
if (i > 0)
v = createRandomVar (r, 0);
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);
}
}
}
};
static JSONTests JSONUnitTests;
#endif
} // namespace juce

View file

@ -0,0 +1,231 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Contains static methods for converting JSON-formatted text to and from var objects.
The var class is structurally compatible with JSON-formatted data, so these
functions allow you to parse JSON into a var object, and to convert a var
object to JSON-formatted text.
@see var
@tags{Core}
*/
class JUCE_API JSON
{
public:
//==============================================================================
/** Parses a string of JSON-formatted text, and returns a result code containing
any parse errors.
This will return the parsed structure in the parsedResult parameter, and will
return a Result object to indicate whether parsing was successful, and if not,
it will contain an error message.
If you're not interested in the error message, you can use one of the other
shortcut parse methods, which simply return a var() if the parsing fails.
Note that this will only parse valid JSON, which means that the item given must
be either an object or an array definition. If you want to also be able to parse
any kind of primitive JSON object, use the fromString() method.
*/
static Result parse (const String& text, var& parsedResult);
/** Attempts to parse some JSON-formatted text, and returns the result as a var object.
If the parsing fails, this simply returns var() - if you need to find out more
detail about the parse error, use the alternative parse() method which returns a Result.
Note that this will only parse valid JSON, which means that the item given must
be either an object or an array definition. If you want to also be able to parse
any kind of primitive JSON object, use the fromString() method.
*/
static var parse (const String& text);
/** Attempts to parse some JSON-formatted text from a file, and returns the result
as a var object.
Note that this is just a short-cut for reading the entire file into a string and
parsing the result.
If the parsing fails, this simply returns var() - if you need to find out more
detail about the parse error, use the alternative parse() method which returns a Result.
*/
static var parse (const File& file);
/** Attempts to parse some JSON-formatted text from a stream, and returns the result
as a var object.
Note that this is just a short-cut for reading the entire stream into a string and
parsing the result.
If the parsing fails, this simply returns var() - if you need to find out more
detail about the parse error, use the alternative parse() method which returns a Result.
*/
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
};
enum class Encoding
{
utf8, ///< Use UTF-8 avoiding escape sequences for non-ASCII characters, this is the default behaviour
ascii, ///< Use ASCII characters only, unicode characters will be encoded using UTF-16 escape sequences
};
/**
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 a copy of this Formatter with the specified encoding.
Use this to force a JSON to be ASCII characters only.
*/
FormatOptions withEncoding (Encoding x) const
{
return withMember (*this, &FormatOptions::encoding, 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; }
/** Returns the encoding of this Formatter. */
Encoding getEncoding() const { return encoding; }
private:
Spacing spacing = Spacing::multiLine;
Encoding encoding = Encoding::utf8;
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
with no carriage-returns. If false, it will be laid-out in a more human-readable format.
The maximumDecimalPlaces parameter determines the precision of floating point numbers
in scientific notation.
@see writeToStream
*/
static String toString (const var& objectToFormat,
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
either.
*/
static var fromString (StringRef);
/** Writes a JSON-formatted representation of the var object to the given stream.
If allOnOneLine is true, the result will be compacted into a single line of text
with no carriage-returns. If false, it will be laid-out in a more human-readable format.
The maximumDecimalPlaces parameter determines the precision of floating point numbers
in scientific notation.
@see toString
*/
static void writeToStream (OutputStream& output,
const var& objectToFormat,
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);
/** Parses a quoted string-literal in JSON format, returning the un-escaped result in the
result parameter, and an error message in case the content was illegal.
This advances the text parameter, leaving it positioned after the closing quote.
*/
static Result parseQuotedString (String::CharPointerType& text, var& result);
private:
//==============================================================================
JSON() = delete; // This class can't be instantiated - just use its static methods.
};
} // namespace juce

View file

@ -0,0 +1,546 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
/**
Options that control conversion from arbitrary types to juce::var.
@see ToVar
@tags{Core}
*/
class ToVarOptions
{
public:
/** By default, conversion will serialise the type using the marshallingVersion defined for
that type. Setting an explicit version allows the type to be serialised as an earlier
version.
*/
[[nodiscard]] ToVarOptions withExplicitVersion (std::optional<int> x) const { return withMember (*this, &ToVarOptions::explicitVersion, x); }
/** By default, conversion will include version information for any type with a non-null
marshallingVersion. Setting versionIncluded to false will cause the version info to be
omitted, which is useful in situations where the version information is not needed
(e.g. when presenting transient information to the user, rather than writing data to
disk that must be deserialised in the future).
*/
[[nodiscard]] ToVarOptions withVersionIncluded (bool x) const { return withMember (*this, &ToVarOptions::versionIncluded, x); }
/** @see withExplicitVersion() */
[[nodiscard]] auto getExplicitVersion() const { return explicitVersion; }
/** @see withVersionIncluded(). */
[[nodiscard]] auto getVersionIncluded() const { return versionIncluded; }
private:
std::optional<std::optional<int>> explicitVersion;
bool versionIncluded = true;
};
/**
Allows converting an object of arbitrary type to var.
To use this, you must first ensure that the type passed to convert is set up for serialisation.
For details of what this entails, see the docs for SerialisationTraits.
In short, the constant 'marshallingVersion', and either the single function 'serialise()', or
the function pair 'load()' and 'save()' must be defined for the type. These may be defined
as public members of the type T itself, or as public members of juce::SerialisationTraits<T>,
which is a specialisation of the SerialisationTraits template struct for the type T.
@see FromVar
@tags{Core}
*/
class ToVar
{
public:
using Options = ToVarOptions;
/** Attempts to convert the argument to a var using the serialisation utilities specified for
that type.
This will return a non-null optional if conversion succeeds, or nullopt if conversion fails.
*/
template <typename T>
static std::optional<var> convert (const T& t, const Options& options = {})
{
return Visitor::convert (t, options);
}
private:
class Visitor
{
public:
template <typename T>
static std::optional<var> convert (const T& t, const Options& options)
{
constexpr auto fallbackVersion = detail::ForwardingSerialisationTraits<T>::marshallingVersion;
const auto versionToUse = options.getExplicitVersion()
.value_or (fallbackVersion);
if (versionToUse > fallbackVersion)
{
// The requested explicit version is higher than the declared version of the type.
return std::nullopt;
}
Visitor visitor { versionToUse, options.getVersionIncluded() };
detail::doSave (visitor, t);
return visitor.value;
}
std::optional<int> getVersion() const { return version; }
template <typename... Ts>
void operator() (Ts&&... ts)
{
(visit (std::forward<Ts> (ts)), ...);
}
private:
Visitor (const std::optional<int>& explicitVersion, bool includeVersion)
: version (explicitVersion),
value ([&]() -> var
{
if (! (version.has_value() && includeVersion))
return var();
auto obj = std::make_unique<DynamicObject>();
obj->setProperty ("__version__", *version);
return obj.release();
}()),
versionIncluded (includeVersion) {}
template <typename T>
void visit (const T& t)
{
if constexpr (std::is_integral_v<T>)
{
push ((int64) t);
}
else if constexpr (std::is_floating_point_v<T>)
{
push ((double) t);
}
else if (auto converted = convert (t))
{
push (*converted);
}
else
{
value.reset();
}
}
template <typename T>
void visit (const Named<T>& named)
{
if (! value.has_value())
return;
if (value == var())
value = new DynamicObject;
auto* obj = value->getDynamicObject();
if (obj == nullptr)
{
// Serialisation failure! This may be caused by archiving a primitive or
// SerialisationSize, and then attempting to archive a named pair to the same
// archive instance.
// When using named pairs, *all* items serialised with a particular archiver must be
// named pairs.
jassertfalse;
value.reset();
return;
}
if (! trySetProperty (*obj, named))
value.reset();
}
template <typename T>
void visit (const SerialisationSize<T>&)
{
push (Array<var>{});
}
void visit (const bool& t)
{
push (t);
}
void visit (const String& t)
{
push (t);
}
void visit (const var& t)
{
push (t);
}
template <typename T>
std::optional<var> convert (const T& t)
{
return convert (t, Options{}.withVersionIncluded (versionIncluded));
}
void push (var v)
{
if (! value.has_value())
return;
if (*value == var())
*value = v;
else if (auto* array = value->getArray())
array->add (v);
else
value.reset();
}
template <typename T>
bool trySetProperty (DynamicObject& obj, const Named<T>& n)
{
if (const auto converted = convert (n.value))
{
obj.setProperty (Identifier (std::string (n.name)), *converted);
return true;
}
return false;
}
std::optional<int> version;
std::optional<var> value;
bool versionIncluded = true;
};
};
//==============================================================================
/**
Allows converting a var to an object of arbitrary type.
To use this, you must first ensure that the type passed to convert is set up for serialisation.
For details of what this entails, see the docs for SerialisationTraits.
In short, the constant 'marshallingVersion', and either the single function 'serialise()', or
the function pair 'load()' and 'save()' must be defined for the type. These may be defined
as public members of the type T itself, or as public members of juce::SerialisationTraits<T>,
which is a specialisation of the SerialisationTraits template struct for the type T.
@see ToVar
@tags{Core}
*/
class FromVar
{
public:
/** Attempts to convert a var to an instance of type T.
This will return a non-null optional if conversion succeeds, or nullopt if conversion fails.
*/
template <typename T>
static std::optional<T> convert (const var& v)
{
return Visitor::convert<T> (v);
}
private:
class Visitor
{
public:
template <typename T>
static std::optional<T> convert (const var& v)
{
const auto version = [&]() -> std::optional<int>
{
if (auto* obj = v.getDynamicObject())
if (obj->hasProperty ("__version__"))
return (int) obj->getProperty ("__version__");
return std::nullopt;
}();
Visitor visitor { version, v };
T t{};
detail::doLoad (visitor, t);
return ! visitor.failed ? std::optional<T> (std::move (t))
: std::nullopt;
}
std::optional<int> getVersion() const { return version; }
template <typename... Ts>
void operator() (Ts&&... ts)
{
(visit (std::forward<Ts> (ts)), ...);
}
private:
Visitor (std::optional<int> vn, const var& i)
: version (vn), input (i) {}
template <typename T>
void visit (T& t)
{
if constexpr (std::is_integral_v<T>)
{
readPrimitive (std::in_place_type<int64>, t);
}
else if constexpr (std::is_floating_point_v<T>)
{
readPrimitive (std::in_place_type<double>, t);
}
else
{
auto node = getNodeToRead();
if (! node.has_value())
return;
auto converted = convert<T> (*node);
if (converted.has_value())
t = *converted;
else
failed = true;
}
}
template <typename T>
void visit (const Named<T>& named)
{
auto node = getNodeToRead();
if (! node.has_value())
return;
auto* obj = node->getDynamicObject();
failed = obj == nullptr || ! tryGetProperty (*obj, named);
}
template <typename T>
void visit (const SerialisationSize<T>& t)
{
if (failed)
return;
if (auto* array = input.getArray())
{
t.size = static_cast<T> (array->size());
currentArrayIndex = 0;
}
else
{
failed = true;
}
}
void visit (bool& t)
{
readPrimitive (std::in_place_type<bool>, t);
}
void visit (String& t)
{
readPrimitive (std::in_place_type<String>, t);
}
void visit (var& t)
{
t = input;
}
static std::optional<double> pullTyped (std::in_place_type_t<double>, const var& source)
{
return source.isDouble() ? std::optional<double> ((double) source) : std::nullopt;
}
static std::optional<int64> pullTyped (std::in_place_type_t<int64>, const var& source)
{
return source.isInt() || source.isInt64() ? std::optional<int64> ((int64) source) : std::nullopt;
}
static std::optional<bool> pullTyped (std::in_place_type_t<bool>, const var& source)
{
return std::optional<bool> ((bool) source);
}
static std::optional<String> pullTyped (std::in_place_type_t<String>, const var& source)
{
return source.isString() ? std::optional<String> (source.toString()) : std::nullopt;
}
std::optional<var> getNodeToRead()
{
if (failed)
return std::nullopt;
if (currentArrayIndex == std::numeric_limits<size_t>::max())
return input;
const auto* array = input.getArray();
if (array == nullptr)
return input;
if ((int) currentArrayIndex < array->size())
return array->getReference ((int) currentArrayIndex++);
failed = true;
return std::nullopt;
}
template <typename TypeToRead, typename T>
void readPrimitive (std::in_place_type_t<TypeToRead> tag, T& t)
{
auto node = getNodeToRead();
if (! node.has_value())
return;
auto typed = pullTyped (tag, *node);
if (typed.has_value())
t = static_cast<T> (*typed);
else
failed = true;
}
template <typename T>
static bool tryGetProperty (const DynamicObject& obj, const Named<T>& n)
{
const Identifier identifier (String (n.name.data(), n.name.size()));
if (! obj.hasProperty (identifier))
return false;
const auto converted = convert<T> (obj.getProperty (identifier));
if (! converted.has_value())
return false;
n.value = *converted;
return true;
}
std::optional<int> version;
var input;
size_t currentArrayIndex = std::numeric_limits<size_t>::max();
bool failed = false;
};
};
//==============================================================================
/**
This template-overloaded class can be used to convert between var and custom types.
If not specialised, the variant converter will attempt to use serialisation functions
if they are detected for the given type.
For details of what this entails, see the docs for SerialisationTraits.
In short, the constant 'marshallingVersion', and either the single function 'serialise()', or
the function pair 'load()' and 'save()' must be defined for the type. These may be defined
as public members of the type T itself, or as public members of juce::SerialisationTraits<T>,
which is a specialisation of the SerialisationTraits template struct for the type T.
@see ToVar, FromVar
@tags{Core}
*/
template <typename Type>
struct VariantConverter
{
static Type fromVar (const var& v)
{
return static_cast<Type> (v);
}
static var toVar (const Type& t)
{
return t;
}
};
#ifndef DOXYGEN
template <>
struct VariantConverter<String>
{
static String fromVar (const var& v) { return v.toString(); }
static var toVar (const String& s) { return s; }
};
#endif
/**
A helper type that can be used to implement specialisations of VariantConverter that use
FromVar::convert and ToVar::convert internally.
If you've already implemented SerialisationTraits for a specific type, and don't want to write
a custom VariantConverter that duplicates that implementation, you can instead write:
@code
template <>
struct juce::VariantConverter<MyType> : public juce::StrictVariantConverter<MyType> {};
@endcode
@tags{Core}
*/
template <typename Type>
struct StrictVariantConverter
{
static_assert (detail::serialisationKind<Type> != detail::SerialisationKind::none);
static Type fromVar (const var& v)
{
auto converted = FromVar::convert<Type> (v);
jassert (converted.has_value());
return std::move (converted).value_or (Type{});
}
static var toVar (const Type& t)
{
auto converted = ToVar::convert<> (t);
jassert (converted.has_value());
return std::move (converted).value_or (var{});
}
};
} // namespace juce

View file

@ -0,0 +1,629 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
struct TypeWithExternalUnifiedSerialisation
{
int a;
std::string b;
std::vector<int> c;
std::map<std::string, int> d;
auto operator== (const TypeWithExternalUnifiedSerialisation& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.a, x.b, x.c, x.d); };
return tie (*this) == tie (other);
}
auto operator!= (const TypeWithExternalUnifiedSerialisation& other) const { return ! operator== (other); }
};
template <>
struct SerialisationTraits<TypeWithExternalUnifiedSerialisation>
{
static constexpr auto marshallingVersion = 2;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (named ("a", t.a),
named ("b", t.b),
named ("c", t.c),
named ("d", t.d));
}
};
// Now that the serialiser trait is visible, it should be detected
static_assert (detail::serialisationKind<TypeWithExternalUnifiedSerialisation> == detail::SerialisationKind::external);
struct TypeWithInternalUnifiedSerialisation
{
double a;
float b;
String c;
StringArray d;
auto operator== (const TypeWithInternalUnifiedSerialisation& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.a, x.b, x.c, x.d); };
return tie (*this) == tie (other);
}
auto operator!= (const TypeWithInternalUnifiedSerialisation& other) const { return ! operator== (other); }
static constexpr auto marshallingVersion = 5;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (named ("a", t.a),
named ("b", t.b),
named ("c", t.c),
named ("d", t.d));
}
};
static_assert (detail::serialisationKind<TypeWithInternalUnifiedSerialisation> == detail::SerialisationKind::internal);
struct TypeWithExternalSplitSerialisation
{
std::optional<String> a;
Array<int> b;
auto operator== (const TypeWithExternalSplitSerialisation& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.a, x.b); };
return tie (*this) == tie (other);
}
auto operator!= (const TypeWithExternalSplitSerialisation& other) const { return ! operator== (other); }
};
template <>
struct SerialisationTraits<TypeWithExternalSplitSerialisation>
{
static constexpr auto marshallingVersion = 10;
template <typename Archive>
static void load (Archive& archive, TypeWithExternalSplitSerialisation& t)
{
std::optional<String> a;
Array<String> hexStrings;
archive (named ("a", a), named ("b", hexStrings));
Array<int> b;
for (auto& i : hexStrings)
b.add (i.getHexValue32());
t = { a, b };
}
template <typename Archive>
static void save (Archive& archive, const TypeWithExternalSplitSerialisation& t)
{
Array<String> hexStrings;
for (auto& i : t.b)
hexStrings.add ("0x" + String::toHexString (i));
archive (named ("a", t.a), named ("b", hexStrings));
}
};
// Now that the serialiser trait is visible, it should be detected
static_assert (detail::serialisationKind<TypeWithExternalSplitSerialisation> == detail::SerialisationKind::external);
// Check that serialisation kinds are correctly detected for primitives
static_assert (detail::serialisationKind<bool> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind< int8_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind< uint8_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind< int16_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind<uint16_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind< int32_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind<uint32_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind< int64_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind<uint64_t> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind<float> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind<double> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind<std::byte> == detail::SerialisationKind::primitive);
static_assert (detail::serialisationKind<String> == detail::SerialisationKind::primitive);
// Check that serialisation is disabled for types with no serialsation defined
static_assert (detail::serialisationKind<Logger> == detail::SerialisationKind::none);
static_assert (detail::serialisationKind<CriticalSection> == detail::SerialisationKind::none);
struct TypeWithInternalSplitSerialisation
{
std::string a;
Array<int> b;
auto operator== (const TypeWithInternalSplitSerialisation& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.a, x.b); };
return tie (*this) == tie (other);
}
auto operator!= (const TypeWithInternalSplitSerialisation& other) const { return ! operator== (other); }
static constexpr auto marshallingVersion = 1;
template <typename Archive>
static void load (Archive& archive, TypeWithInternalSplitSerialisation& t)
{
std::string a;
Array<String> hexStrings;
archive (named ("a", a), named ("b", hexStrings));
Array<int> b;
for (auto& i : hexStrings)
b.add (i.getHexValue32());
t = { a, b };
}
template <typename Archive>
static void save (Archive& archive, const TypeWithInternalSplitSerialisation& t)
{
Array<String> hexStrings;
for (auto& i : t.b)
hexStrings.add ("0x" + String::toHexString (i));
archive (named ("a", t.a), named ("b", hexStrings));
}
};
static_assert (detail::serialisationKind<TypeWithInternalSplitSerialisation> == detail::SerialisationKind::internal);
struct TypeWithBrokenObjectSerialisation
{
int a;
int b;
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
// Archiving a named value will start reading/writing an object
archive (named ("a", t.a));
// Archiving a non-named value will assume that the current node is convertible
archive (t.b);
}
};
struct TypeWithBrokenPrimitiveSerialisation
{
int a;
int b;
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
// Archiving a non-named value will assume that the current node is convertible
archive (t.a);
// Archiving a named value will fail if the current node holds a non-object type
archive (named ("b", t.b));
}
};
struct TypeWithBrokenArraySerialisation
{
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T&)
{
size_t size = 5;
archive (size);
// serialisationSize should always be serialised first!
archive (serialisationSize (size));
}
};
struct TypeWithBrokenNestedSerialisation
{
int a;
TypeWithBrokenObjectSerialisation b;
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (named ("a", t.a), named ("b", t.b));
}
};
struct TypeWithBrokenDynamicSerialisation
{
std::vector<TypeWithBrokenObjectSerialisation> a;
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (t.a);
}
};
struct TypeWithVersionedSerialisation
{
int a{}, b{}, c{}, d{};
bool operator== (const TypeWithVersionedSerialisation& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.a, x.b, x.c, x.d); };
return tie (*this) == tie (other);
}
bool operator!= (const TypeWithVersionedSerialisation& other) const { return ! operator== (other); }
static constexpr auto marshallingVersion = 3;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (named ("a", t.a));
if (archive.getVersion() >= 1)
archive (named ("b", t.b));
if (archive.getVersion() >= 2)
archive (named ("c", t.c));
if (archive.getVersion() >= 3)
archive (named ("d", t.d));
}
};
struct TypeWithRawVarLast
{
int status = 0;
String message;
var extended;
bool operator== (const TypeWithRawVarLast& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.status, x.message, x.extended); };
return tie (*this) == tie (other);
}
bool operator!= (const TypeWithRawVarLast& other) const { return ! operator== (other); }
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (named ("status", t.status),
named ("message", t.message),
named ("extended", t.extended));
}
};
struct TypeWithRawVarFirst
{
int status = 0;
String message;
var extended;
bool operator== (const TypeWithRawVarFirst& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.status, x.message, x.extended); };
return tie (*this) == tie (other);
}
bool operator!= (const TypeWithRawVarFirst& other) const { return ! operator== (other); }
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (named ("extended", t.extended),
named ("status", t.status),
named ("message", t.message));
}
};
struct TypeWithInnerVar
{
int eventId = 0;
var payload;
bool operator== (const TypeWithInnerVar& other) const
{
const auto tie = [] (const auto& x) { return std::tie (x.eventId, x.payload); };
return tie (*this) == tie (other);
}
bool operator!= (const TypeWithInnerVar& other) const { return ! operator== (other); }
static constexpr auto marshallingVersion = std::nullopt;
template <typename Archive, typename T>
static void serialise (Archive& archive, T& t)
{
archive (named ("eventId", t.eventId),
named ("payload", t.payload));
}
};
class JSONSerialisationTest final : public UnitTest
{
public:
JSONSerialisationTest() : UnitTest ("JSONSerialisation", UnitTestCategories::json) {}
void runTest() override
{
beginTest ("ToVar");
{
expectDeepEqual (ToVar::convert (false), false);
expectDeepEqual (ToVar::convert (true), true);
expectDeepEqual (ToVar::convert (1), 1);
expectDeepEqual (ToVar::convert (5.0f), 5.0);
expectDeepEqual (ToVar::convert (6LL), 6);
expectDeepEqual (ToVar::convert ("hello world"), "hello world");
expectDeepEqual (ToVar::convert (String ("hello world")), "hello world");
expectDeepEqual (ToVar::convert (std::vector<int> { 1, 2, 3 }), Array<var> { 1, 2, 3 });
expectDeepEqual (ToVar::convert (TypeWithExternalUnifiedSerialisation { 7,
"hello world",
{ 5, 6, 7 },
{ { "foo", 4 }, { "bar", 5 } } }),
JSONUtils::makeObject ({ { "__version__", 2 },
{ "a", 7 },
{ "b", "hello world" },
{ "c", Array<var> { 5, 6, 7 } },
{ "d",
Array<var> { JSONUtils::makeObject ({ { "first", "bar" },
{ "second", 5 } }),
JSONUtils::makeObject ({ { "first", "foo" },
{ "second", 4 } }) } } }));
expectDeepEqual (ToVar::convert (TypeWithInternalUnifiedSerialisation { 7.89,
4.321f,
"custom string",
{ "foo", "bar", "baz" } }),
JSONUtils::makeObject ({ { "__version__", 5 },
{ "a", 7.89 },
{ "b", 4.321f },
{ "c", "custom string" },
{ "d", Array<var> { "foo", "bar", "baz" } } }));
expectDeepEqual (ToVar::convert (TypeWithExternalSplitSerialisation { "string", { 1, 2, 3 } }),
JSONUtils::makeObject ({ { "__version__", 10 },
{ "a", JSONUtils::makeObject ({ { "engaged", true }, { "value", "string" } }) },
{ "b", Array<var> { "0x1", "0x2", "0x3" } } }));
expectDeepEqual (ToVar::convert (TypeWithInternalSplitSerialisation { "string", { 16, 32, 48 } }),
JSONUtils::makeObject ({ { "__version__", 1 },
{ "a", "string" },
{ "b", Array<var> { "0x10", "0x20", "0x30" } } }));
expect (ToVar::convert (TypeWithBrokenObjectSerialisation { 1, 2 }) == std::nullopt);
expect (ToVar::convert (TypeWithBrokenPrimitiveSerialisation { 1, 2 }) == std::nullopt);
expect (ToVar::convert (TypeWithBrokenArraySerialisation {}) == std::nullopt);
expect (ToVar::convert (TypeWithBrokenNestedSerialisation {}) == std::nullopt);
expect (ToVar::convert (TypeWithBrokenDynamicSerialisation { std::vector<TypeWithBrokenObjectSerialisation> (10) }) == std::nullopt);
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }),
JSONUtils::makeObject ({ { "__version__", 3 },
{ "a", 1 },
{ "b", 2 },
{ "c", 3 },
{ "d", 4 } }));
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }, ToVar::Options {}.withVersionIncluded (false)),
JSONUtils::makeObject ({ { "a", 1 },
{ "b", 2 },
{ "c", 3 },
{ "d", 4 } }));
// Requested explicit version is higher than the type's declared version
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }, ToVar::Options {}.withExplicitVersion (4)),
std::nullopt);
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }, ToVar::Options {}.withExplicitVersion (3)),
JSONUtils::makeObject ({ { "__version__", 3 },
{ "a", 1 },
{ "b", 2 },
{ "c", 3 },
{ "d", 4 } }));
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }, ToVar::Options {}.withExplicitVersion (2)),
JSONUtils::makeObject ({ { "__version__", 2 },
{ "a", 1 },
{ "b", 2 },
{ "c", 3 } }));
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }, ToVar::Options {}.withExplicitVersion (1)),
JSONUtils::makeObject ({ { "__version__", 1 },
{ "a", 1 },
{ "b", 2 } }));
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }, ToVar::Options {}.withExplicitVersion (0)),
JSONUtils::makeObject ({ { "__version__", 0 },
{ "a", 1 } }));
expectDeepEqual (ToVar::convert (TypeWithVersionedSerialisation { 1, 2, 3, 4 }, ToVar::Options {}.withExplicitVersion (std::nullopt)),
JSONUtils::makeObject ({ { "a", 1 } }));
expectDeepEqual (ToVar::convert (TypeWithRawVarLast { 200, "success", true }),
JSONUtils::makeObject ({ { "status", 200 }, { "message", "success" }, { "extended", true } }));
expectDeepEqual (ToVar::convert (TypeWithRawVarLast { 200,
"success",
JSONUtils::makeObject ({ { "status", 123.456 },
{ "message", "failure" },
{ "extended", true } }) }),
JSONUtils::makeObject ({ { "status", 200 },
{ "message", "success" },
{ "extended", JSONUtils::makeObject ({ { "status", 123.456 },
{ "message", "failure" },
{ "extended", true } }) } }));
expectDeepEqual (ToVar::convert (TypeWithRawVarFirst { 200, "success", true }),
JSONUtils::makeObject ({ { "status", 200 }, { "message", "success" }, { "extended", true } }));
expectDeepEqual (ToVar::convert (TypeWithRawVarFirst { 200,
"success",
JSONUtils::makeObject ({ { "status", 123.456 },
{ "message", "failure" },
{ "extended", true } }) }),
JSONUtils::makeObject ({ { "status", 200 },
{ "message", "success" },
{ "extended", JSONUtils::makeObject ({ { "status", 123.456 },
{ "message", "failure" },
{ "extended", true } }) } }));
const auto payload = JSONUtils::makeObject ({ { "foo", 1 }, { "bar", 2 } });
expectDeepEqual (ToVar::convert (TypeWithInnerVar { 404, payload }),
JSONUtils::makeObject ({ { "eventId", 404 }, { "payload", payload } }));
}
beginTest ("FromVar");
{
expect (FromVar::convert<bool> (JSON::fromString ("false")) == false);
expect (FromVar::convert<bool> (JSON::fromString ("true")) == true);
expect (FromVar::convert<bool> (JSON::fromString ("0")) == false);
expect (FromVar::convert<bool> (JSON::fromString ("1")) == true);
expect (FromVar::convert<int> (JSON::fromString ("1")) == 1);
expect (FromVar::convert<float> (JSON::fromString ("5.0f")) == 5.0f);
expect (FromVar::convert<int64> (JSON::fromString ("6")) == 6);
expect (FromVar::convert<String> (JSON::fromString ("\"hello world\"")) == "hello world");
expect (FromVar::convert<std::vector<int>> (JSON::fromString ("[1,2,3]")) == std::vector<int> { 1, 2, 3 });
expect (FromVar::convert<TypeWithExternalUnifiedSerialisation> (JSONUtils::makeObject ({ { "__version__", 2 },
{ "a", 7 },
{ "b", "hello world" },
{ "c", Array<var> { 5, 6, 7 } },
{ "d",
Array<var> { JSONUtils::makeObject ({ { "first", "bar" },
{ "second", 5 } }),
JSONUtils::makeObject ({ { "first", "foo" },
{ "second", 4 } }) } } }))
== TypeWithExternalUnifiedSerialisation { 7,
"hello world",
{ 5, 6, 7 },
{ { "foo", 4 }, { "bar", 5 } } });
expect (FromVar::convert<TypeWithInternalUnifiedSerialisation> (JSONUtils::makeObject ({ { "__version__", 5 },
{ "a", 7.89 },
{ "b", 4.321f },
{ "c", "custom string" },
{ "d", Array<var> { "foo", "bar", "baz" } } }))
== TypeWithInternalUnifiedSerialisation { 7.89,
4.321f,
"custom string",
{ "foo", "bar", "baz" } });
expect (FromVar::convert<TypeWithExternalSplitSerialisation> (JSONUtils::makeObject ({ { "__version__", 10 },
{ "a", JSONUtils::makeObject ({ { "engaged", true }, { "value", "string" } }) },
{ "b", Array<var> { "0x1", "0x2", "0x3" } } }))
== TypeWithExternalSplitSerialisation { "string", { 1, 2, 3 } });
expect (FromVar::convert<TypeWithInternalSplitSerialisation> (JSONUtils::makeObject ({ { "__version__", 1 },
{ "a", "string" },
{ "b", Array<var> { "0x10", "0x20", "0x30" } } }))
== TypeWithInternalSplitSerialisation { "string", { 16, 32, 48 } });
expect (FromVar::convert<TypeWithBrokenObjectSerialisation> (JSON::fromString ("null")) == std::nullopt);
expect (FromVar::convert<TypeWithBrokenPrimitiveSerialisation> (JSON::fromString ("null")) == std::nullopt);
expect (FromVar::convert<TypeWithBrokenArraySerialisation> (JSON::fromString ("null")) == std::nullopt);
expect (FromVar::convert<TypeWithBrokenNestedSerialisation> (JSON::fromString ("null")) == std::nullopt);
expect (FromVar::convert<TypeWithBrokenDynamicSerialisation> (JSON::fromString ("null")) == std::nullopt);
expect (FromVar::convert<TypeWithInternalUnifiedSerialisation> (JSONUtils::makeObject ({ { "a", 7.89 },
{ "b", 4.321f } }))
== std::nullopt);
expect (FromVar::convert<TypeWithVersionedSerialisation> (JSONUtils::makeObject ({ { "__version__", 3 },
{ "a", 1 },
{ "b", 2 },
{ "c", 3 },
{ "d", 4 } }))
== TypeWithVersionedSerialisation { 1, 2, 3, 4 });
expect (FromVar::convert<TypeWithVersionedSerialisation> (JSONUtils::makeObject ({ { "__version__", 4 },
{ "a", 1 },
{ "b", 2 },
{ "c", 3 },
{ "d", 4 } }))
== TypeWithVersionedSerialisation { 1, 2, 3, 4 });
expect (FromVar::convert<TypeWithVersionedSerialisation> (JSONUtils::makeObject ({ { "__version__", 2 },
{ "a", 1 },
{ "b", 2 },
{ "c", 3 } }))
== TypeWithVersionedSerialisation { 1, 2, 3, 0 });
expect (FromVar::convert<TypeWithVersionedSerialisation> (JSONUtils::makeObject ({ { "__version__", 1 },
{ "a", 1 },
{ "b", 2 } }))
== TypeWithVersionedSerialisation { 1, 2, 0, 0 });
expect (FromVar::convert<TypeWithVersionedSerialisation> (JSONUtils::makeObject ({ { "__version__", 0 },
{ "a", 1 } }))
== TypeWithVersionedSerialisation { 1, 0, 0, 0 });
expect (FromVar::convert<TypeWithVersionedSerialisation> (JSONUtils::makeObject ({ { "a", 1 } }))
== TypeWithVersionedSerialisation { 1, 0, 0, 0 });
const auto raw = JSONUtils::makeObject ({ { "status", 200 }, { "message", "success" }, { "extended", "another string" } });
expect (FromVar::convert<TypeWithRawVarLast> (raw) == TypeWithRawVarLast { 200, "success", "another string" });
expect (FromVar::convert<TypeWithRawVarFirst> (raw) == TypeWithRawVarFirst { 200, "success", "another string" });
const var payloads[] { JSONUtils::makeObject ({ { "foo", 1 }, { "bar", 2 } }),
var (Array<var> { 1, 2 }),
var() };
for (const auto& payload : payloads)
{
const auto objectWithPayload = JSONUtils::makeObject ({ { "eventId", 404 }, { "payload", payload } });
expect (FromVar::convert<TypeWithInnerVar> (objectWithPayload) == TypeWithInnerVar { 404, payload });
}
}
}
private:
void expectDeepEqual (const std::optional<var>& a, const std::optional<var>& b)
{
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)
{
if (a.has_value() && b.has_value())
return JSONUtils::deepEqual (*a, *b);
return a == b;
}
};
static JSONSerialisationTest jsonSerialisationTest;
} // namespace juce

View file

@ -0,0 +1,230 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
var JSONUtils::makeObject (const std::map<Identifier, var>& source)
{
auto result = std::make_unique<DynamicObject>();
for (const auto& [name, value] : source)
result->setProperty (name, value);
return var (result.release());
}
var JSONUtils::makeObjectWithKeyFirst (const std::map<Identifier, var>& source,
Identifier key)
{
auto result = std::make_unique<DynamicObject>();
if (const auto iter = source.find (key); iter != source.end())
result->setProperty (key, iter->second);
for (const auto& [name, value] : source)
if (name != key)
result->setProperty (name, value);
return var (result.release());
}
std::optional<var> JSONUtils::setPointer (const var& v,
String pointer,
const var& newValue)
{
if (pointer.isEmpty())
return newValue;
if (! pointer.startsWith ("/"))
{
// This is not a well-formed JSON pointer
jassertfalse;
return {};
}
const auto findResult = pointer.indexOfChar (1, '/');
const auto pos = findResult < 0 ? pointer.length() : findResult;
const String head (pointer.begin() + 1, pointer.begin() + pos);
const String tail (pointer.begin() + pos, pointer.end());
const auto unescaped = head.replace ("~1", "/").replace ("~0", "~");
if (auto* object = v.getDynamicObject())
{
if (const auto newProperty = setPointer (object->getProperty (unescaped), tail, newValue))
{
auto cloned = object->clone();
cloned->setProperty (unescaped, *newProperty);
return var (cloned.release());
}
}
else if (auto* array = v.getArray())
{
const auto index = [&]() -> size_t
{
if (unescaped == "-")
return (size_t) array->size();
if (unescaped == "0")
return 0;
if (! unescaped.startsWith ("0"))
return (size_t) unescaped.getLargeIntValue();
return std::numeric_limits<size_t>::max();
}();
if (const auto newIndex = setPointer ((*array)[(int) index], tail, newValue))
{
auto copied = *array;
if ((int) index == copied.size())
copied.add ({});
if (isPositiveAndBelow (index, copied.size()))
{
copied.getReference ((int) index) = *newIndex;
return var (copied);
}
}
}
return {};
}
bool JSONUtils::deepEqual (const var& a, const var& b)
{
const auto compareObjects = [] (const DynamicObject& x, const DynamicObject& y)
{
if (x.getProperties().size() != y.getProperties().size())
return false;
for (const auto& [key, value] : x.getProperties())
{
if (! y.hasProperty (key))
return false;
if (! deepEqual (value, y.getProperty (key)))
return false;
}
return true;
};
if (auto* i = a.getDynamicObject())
if (auto* j = b.getDynamicObject())
return compareObjects (*i, *j);
if (auto* i = a.getArray())
if (auto* j = b.getArray())
return std::equal (i->begin(), i->end(), j->begin(), j->end(), [] (const var& x, const var& y) { return deepEqual (x, y); });
return a == b;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class JSONUtilsTests final : public UnitTest
{
public:
JSONUtilsTests() : UnitTest ("JSONUtils", UnitTestCategories::json) {}
void runTest() override
{
beginTest ("JSON pointers");
{
const auto obj = JSON::parse (R"({ "name": "PIANO 4"
, "lfoSpeed": 30
, "lfoWaveform": "triangle"
, "pitchEnvelope": { "rates": [94,67,95,60], "levels": [50,50,50,50] }
})");
expectDeepEqual (JSONUtils::setPointer (obj, "", "hello world"), var ("hello world"));
expectDeepEqual (JSONUtils::setPointer (obj, "/lfoWaveform/foobar", "str"), std::nullopt);
expectDeepEqual (JSONUtils::setPointer (JSON::parse (R"({"foo":0,"bar":1})"), "/foo", 2), JSON::parse (R"({"foo":2,"bar":1})"));
expectDeepEqual (JSONUtils::setPointer (JSON::parse (R"({"foo":0,"bar":1})"), "/baz", 2), JSON::parse (R"({"foo":0,"bar":1,"baz":2})"));
expectDeepEqual (JSONUtils::setPointer (JSON::parse (R"({"foo":{},"bar":{}})"), "/foo/bar", 2), JSON::parse (R"({"foo":{"bar":2},"bar":{}})"));
expectDeepEqual (JSONUtils::setPointer (obj, "/pitchEnvelope/rates/01", "str"), std::nullopt);
expectDeepEqual (JSONUtils::setPointer (obj, "/pitchEnvelope/rates/10", "str"), std::nullopt);
expectDeepEqual (JSONUtils::setPointer (obj, "/lfoSpeed", 10), JSON::parse (R"({ "name": "PIANO 4"
, "lfoSpeed": 10
, "lfoWaveform": "triangle"
, "pitchEnvelope": { "rates": [94,67,95,60], "levels": [50,50,50,50] }
})"));
expectDeepEqual (JSONUtils::setPointer (JSON::parse (R"([0,1,2])"), "/0", "bang"), JSON::parse (R"(["bang",1,2])"));
expectDeepEqual (JSONUtils::setPointer (JSON::parse (R"([0,1,2])"), "/0", "bang"), JSON::parse (R"(["bang",1,2])"));
expectDeepEqual (JSONUtils::setPointer (JSON::parse (R"({"/":"fizz"})"), "/~1", "buzz"), JSON::parse (R"({"/":"buzz"})"));
expectDeepEqual (JSONUtils::setPointer (JSON::parse (R"({"~":"fizz"})"), "/~0", "buzz"), JSON::parse (R"({"~":"buzz"})"));
expectDeepEqual (JSONUtils::setPointer (obj, "/pitchEnvelope/rates/0", 80), JSON::parse (R"({ "name": "PIANO 4"
, "lfoSpeed": 30
, "lfoWaveform": "triangle"
, "pitchEnvelope": { "rates": [80,67,95,60], "levels": [50,50,50,50] }
})"));
expectDeepEqual (JSONUtils::setPointer (obj, "/pitchEnvelope/levels/0", 80), JSON::parse (R"({ "name": "PIANO 4"
, "lfoSpeed": 30
, "lfoWaveform": "triangle"
, "pitchEnvelope": { "rates": [94,67,95,60], "levels": [80,50,50,50] }
})"));
expectDeepEqual (JSONUtils::setPointer (obj, "/pitchEnvelope/levels/-", 100), JSON::parse (R"({ "name": "PIANO 4"
, "lfoSpeed": 30
, "lfoWaveform": "triangle"
, "pitchEnvelope": { "rates": [94,67,95,60], "levels": [50,50,50,50,100] }
})"));
}
}
void expectDeepEqual (const std::optional<var>& a, const std::optional<var>& b)
{
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)
{
if (a.has_value() && b.has_value())
return JSONUtils::deepEqual (*a, *b);
return a == b;
}
};
static JSONUtilsTests jsonUtilsTests;
#endif
} // namespace juce

View file

@ -0,0 +1,79 @@
/*
==============================================================================
This file is part of the JUCE framework.
Copyright (c) Raw Material Software Limited
JUCE is an open source framework subject to commercial or open source
licensing.
By downloading, installing, or using the JUCE framework, or combining the
JUCE framework with any other source code, object code, content or any other
copyrightable work, you agree to the terms of the JUCE End User Licence
Agreement, and all incorporated terms including the JUCE Privacy Policy and
the JUCE Website Terms of Service, as applicable, which will bind you. If you
do not agree to the terms of these agreements, we will not license the JUCE
framework to you, and you must discontinue the installation or download
process and cease use of the JUCE framework.
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
Or:
You may also use this code under the terms of the AGPLv3:
https://www.gnu.org/licenses/agpl-3.0.en.html
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
==============================================================================
*/
namespace juce
{
/**
A mini namespace to hold utility functions for working with juce::vars.
@tags{Core}
*/
struct JSONUtils
{
/** No constructor. */
JSONUtils() = delete;
/** Given a JSON array/object 'v', a string representing a JSON pointer,
and a new property value 'newValue', returns a copy of 'v' where the
property or array index referenced by the pointer has been set to 'newValue'.
If the pointer cannot be followed, due to referencing missing array indices
or fields, then this returns nullopt.
For more details, check the JSON Pointer RFC 6901:
https://datatracker.ietf.org/doc/html/rfc6901
*/
static std::optional<var> setPointer (const var& v, String pointer, const var& newValue);
/** Converts the provided key/value pairs into a JSON object. */
static var makeObject (const std::map<Identifier, var>& source);
/** Converts the provided key/value pairs into a JSON object with the provided
key at the first position in the object.
This is useful because the MIDI-CI spec requires that certain fields (e.g.
status) should be placed at the beginning of a MIDI-CI header.
*/
static var makeObjectWithKeyFirst (const std::map<Identifier, var>& source, Identifier key);
/** Returns true if and only if the contents of a match the contents of b.
Unlike var::operator==, this will recursively check that contained DynamicObject and Array
instances compare equal.
*/
static bool deepEqual (const var& a, const var& b);
};
} // namespace juce