diff --git a/modules/juce_core/containers/juce_Variant.cpp b/modules/juce_core/containers/juce_Variant.cpp index 65600d4f0b..42adaa5138 100644 --- a/modules/juce_core/containers/juce_Variant.cpp +++ b/modules/juce_core/containers/juce_Variant.cpp @@ -714,6 +714,21 @@ bool var::hasProperty (const Identifier& propertyName) const noexcept return false; } +Array var::getProperties() const +{ + if (auto* o = getDynamicObject()) + { + Array names; + + for (auto itr : o->getProperties()) + names.add (itr.name); + + return names; + } + + return {}; +} + var::NativeFunction var::getNativeFunction() const { return isMethod() && (value.methodValue != nullptr) ? *value.methodValue : nullptr; @@ -899,6 +914,75 @@ var::NativeFunctionArgs::NativeFunctionArgs (const var& t, const var* args, int { } +//============================================================================== +VarIterator::VarIterator (const var& v_, bool isEnd) + : v (v_) +{ + if (isEnd) + { + if (v.isArray()) + index = v.getArray()->size(); + else if (auto o = v.getDynamicObject()) + itr = o->getProperties().end(); + } + else + { + if (v.isArray()) + index = 0; + else if (auto o = v.getDynamicObject()) + itr = o->getProperties().begin(); + } +} + +VarIterator& VarIterator::operator++() +{ + if (v.isArray()) + { + index++; + } + else if (v.getDynamicObject()) + { + auto i = (const NamedValueSet::NamedValue*)itr; + i++; + itr = i; + } + return *this; +} + +bool VarIterator::operator== (const VarIterator& other) const +{ + return index == other.index && itr == other.itr; +} + +bool VarIterator::operator!= (const VarIterator& other) const +{ + return ! (*this == other); +} + +VarIterator::NamedValue VarIterator::operator*() const +{ + if (v.isArray()) + { + return { index, (*v.getArray())[index] }; + } + else if (v.getDynamicObject()) + { + auto i = (const NamedValueSet::NamedValue*)itr; + return { i->name.toString(), i->value }; + } + return {}; +} + +VarIterator begin (const var& v) +{ + return VarIterator (v, false); +} + +VarIterator end (const var& v) +{ + return VarIterator (v, true); +} + //============================================================================== #if JUCE_ALLOW_STATIC_NULL_VARIABLES diff --git a/modules/juce_core/containers/juce_Variant.h b/modules/juce_core/containers/juce_Variant.h index 9a853eb7c0..12c3aef559 100644 --- a/modules/juce_core/containers/juce_Variant.h +++ b/modules/juce_core/containers/juce_Variant.h @@ -262,6 +262,8 @@ public: var getProperty (const Identifier& propertyName, const var& defaultReturnValue) const; /** Returns true if this variant is an object and if it has the given property. */ bool hasProperty (const Identifier& propertyName) const noexcept; + /** Returns property names if this variant is an object. */ + Array getProperties() const; /** Invokes a named method call with no arguments. */ var call (const Identifier& method) const; @@ -354,4 +356,42 @@ JUCE_API bool operator== (const var&, const String&); JUCE_API bool operator!= (const var&, const String&); JUCE_API bool operator== (const var&, const char*); JUCE_API bool operator!= (const var&, const char*); + + +//============================================================================== +/** Iterator for a var. + You shouldn't ever need to use this class directly - it's used internally by begin() + and end() to allow range-based-for loops on a var. +*/ +struct VarIterator +{ + struct NamedValue + { + var name; + var value; + }; + + VarIterator (const var&, bool isEnd); + VarIterator& operator++(); + + bool operator== (const VarIterator&) const; + bool operator!= (const VarIterator&) const; + NamedValue operator*() const; + + using difference_type = std::ptrdiff_t; + using value_type = NamedValue; + using reference = NamedValue&; + using pointer = NamedValue*; + using iterator_category = std::forward_iterator_tag; + +private: + const var& v; + int index = 0; + const void* itr = nullptr; +}; + +VarIterator begin (const var&); +VarIterator end (const var&); + + } // namespace juce diff --git a/modules/juce_core/json/juce_JSONUtils.cpp b/modules/juce_core/json/juce_JSONUtils.cpp index 3652023af6..3f0b56a405 100644 --- a/modules/juce_core/json/juce_JSONUtils.cpp +++ b/modules/juce_core/json/juce_JSONUtils.cpp @@ -124,6 +124,125 @@ std::optional JSONUtils::setPointer (const var& v, return {}; } +bool JSONUtils::updatePointer (var& v, String pointer, const var& newValue) +{ + if (pointer.isEmpty()) + return false; + + 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 (tail.isEmpty()) + { + object->setProperty (unescaped, newValue); + return true; + } + + auto v = object->getProperty (unescaped); + return updatePointer (v, tail, newValue); + } + 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::max(); + }(); + + if (tail.isEmpty()) + { + if (isPositiveAndBelow (index, array->size())) + { + array->set (int (index), newValue); + return true; + } + + if (index == array->size()) + { + array->add (newValue); + return true; + } + } + + auto v = (*array)[(int) index]; + return updatePointer (v, tail, newValue); + } + + return false; +} + +var JSONUtils::getPointer (const var& v, String pointer, const var& defaultValue) +{ + if (pointer.isEmpty()) + return defaultValue; + + 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 (tail.isEmpty()) + return object->hasProperty (unescaped) ? object->getProperty (unescaped) : defaultValue; + else + return getPointer (object->getProperty (unescaped), tail, defaultValue); + } + 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::max(); + }(); + + if (tail.isEmpty()) + return isPositiveAndBelow (index, array->size()) ? (*array)[int (index)] : defaultValue; + else + return getPointer ((*array)[(int) index], tail, defaultValue); + } + + return defaultValue; +} + bool JSONUtils::deepEqual (const var& a, const var& b) { const auto compareObjects = [] (const DynamicObject& x, const DynamicObject& y) @@ -172,6 +291,13 @@ public: , "lfoWaveform": "triangle" , "pitchEnvelope": { "rates": [94,67,95,60], "levels": [50,50,50,50] } })"); + + expect (JSONUtils::getPointer (obj, "/name", {}) == "PIANO 4"); + expect (JSONUtils::getPointer (obj, "/lfoSpeed", {}) == var (30)); + expect (JSONUtils::getPointer (obj, "/pitchEnvelope/rates/1", {}) == var (67)); + expect (JSONUtils::getPointer (obj, "/pitchEnvelope/levels/2", {}) == var (50)); + expect (JSONUtils::getPointer (obj, "/pitchEnvelope/levels/10", {}) == var()); + 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})")); diff --git a/modules/juce_core/json/juce_JSONUtils.h b/modules/juce_core/json/juce_JSONUtils.h index 5ea77bab21..dc6d69dc9a 100644 --- a/modules/juce_core/json/juce_JSONUtils.h +++ b/modules/juce_core/json/juce_JSONUtils.h @@ -57,6 +57,29 @@ struct JSONUtils */ static std::optional setPointer (const var& v, String pointer, const var& newValue); + /** Given a JSON array/object 'v', a string representing a JSON pointer, + and a new property value 'newValue', updates '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 false. + + For more details, check the JSON Pointer RFC 6901: + https://datatracker.ietf.org/doc/html/rfc6901 + */ + static bool updatePointer (var& v, String pointer, const var& newValue); + + /** Given a JSON array/object 'v', a string representing a JSON pointer, + returns the value of the property or array index referenced by the pointer + + If the pointer cannot be followed, due to referencing missing array indices + or fields, then this returns defaultValue. + + For more details, check the JSON Pointer RFC 6901: + https://datatracker.ietf.org/doc/html/rfc6901 + */ + static var getPointer (const var& v, String pointer, const var& defaultValue); + /** Converts the provided key/value pairs into a JSON object. */ static var makeObject (const std::map& source);