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:
parent
637226addc
commit
df6f3f8e28
69 changed files with 941 additions and 1351 deletions
868
modules/juce_core/json/juce_JSON.cpp
Normal file
868
modules/juce_core/json/juce_JSON.cpp
Normal 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
|
||||
231
modules/juce_core/json/juce_JSON.h
Normal file
231
modules/juce_core/json/juce_JSON.h
Normal 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
|
||||
546
modules/juce_core/json/juce_JSONSerialisation.h
Normal file
546
modules/juce_core/json/juce_JSONSerialisation.h
Normal 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
|
||||
629
modules/juce_core/json/juce_JSONSerialisation_test.cpp
Normal file
629
modules/juce_core/json/juce_JSONSerialisation_test.cpp
Normal 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
|
||||
230
modules/juce_core/json/juce_JSONUtils.cpp
Normal file
230
modules/juce_core/json/juce_JSONUtils.cpp
Normal 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
|
||||
79
modules/juce_core/json/juce_JSONUtils.h
Normal file
79
modules/juce_core/json/juce_JSONUtils.h
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue