Fully support operator[] on QCborValue and QCborValueRef

Added operator[] to QCborValueRef and mutating operator[] both there
and in QCborValue.  If the value (referenced) is not a container, it
is replaced with a map and a reference into the result is returned.
If the value is an array and the key is a string, negative, or more
than 0xffff, the array is first converted to a map.

Change-Id: Ibbc9e480fb25eb3d05547c8a1b99e762b2a68b68
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2018-09-14 14:59:21 +02:00
parent f09fc1f352
commit 779a73762d
5 changed files with 519 additions and 12 deletions

View File

@ -272,6 +272,7 @@ private:
void detach(qsizetype reserve = 0);
friend QCborValue;
friend QCborValueRef;
explicit QCborArray(QCborContainerPrivate &dd) noexcept;
QExplicitlySharedDataPointer<QCborContainerPrivate> d;
};

View File

@ -118,6 +118,8 @@ public:
QCborValueRef item; // points to the value
friend class Iterator;
friend class QCborMap;
friend class QCborValue;
friend class QCborValueRef;
ConstIterator(QCborContainerPrivate *dd, qsizetype ii) : item(dd, ii) {}
public:
typedef std::random_access_iterator_tag iterator_category;
@ -323,9 +325,10 @@ public:
QJsonObject toJsonObject() const;
private:
friend class QCborValue;
friend class QCborValueRef;
void detach(qsizetype reserve = 0);
friend QCborValue;
explicit QCborMap(QCborContainerPrivate &dd) noexcept;
QExplicitlySharedDataPointer<QCborContainerPrivate> d;
};

View File

@ -128,10 +128,11 @@ QT_BEGIN_NAMESPACE
Such values are completely valid and may appear in CBOR streams, unlike
JSON content and QJsonValue's undefined bit. But like QJsonValue's
Undefined, it is returned by QCborArray::value() when out of range or
QCborMap::operator[] when the key is not found in the container. It is not
possible to tell such a case apart from the value of Undefined, so if that
is required, check the QCborArray size and use the QCborMap iterator API.
Undefined, it is returned by a CBOR container's value() or read-only
operator[] for invalid look-ups (index out of range for QCborArray, or key
not found for QCborMap). It is not possible to tell such a case apart from
the value of Undefined, so if that is required, check the QCborArray size
and use the QCborMap iterator API.
\section1 Simple types
@ -457,7 +458,7 @@ QT_BEGIN_NAMESPACE
\fn QCborValue::QCborValue(QCborValue &&other)
\overload
Moves the contents of the \a other CBorValue object into this one and frees
Moves the contents of the \a other QCborValue object into this one and frees
the resources of this one.
*/
@ -465,7 +466,7 @@ QT_BEGIN_NAMESPACE
\fn QCborValue &&QCborValue::operator=(QCborValue &&other)
\overload
Moves the contents of the \a other CBorValue object into this one and frees
Moves the contents of the \a other QCborValue object into this one and frees
the resources of this one. Returns a reference to this object.
*/
@ -2107,15 +2108,16 @@ const QCborValue QCborValue::operator[](QLatin1String key) const
}
/*!
\overload
If this QCborValue is a QCborMap, searches elements for the value whose key
matches \a key. If this is an array, returns the element whose index is \a
key. If there's no matching value in the array or map, or if this
matches \a key. If this is a QCborArray, returns the element whose index is
\a key. If there's no matching value in the array or map, or if this
QCborValue object is not an array or map, returns the undefined value.
\sa operator[], QCborMap::operator[], QCborMap::value(),
QCborMap::find(), QCborArray::operator[], QCborArray::at()
*/
const QCborValue QCborValue::operator[](qint64 key) const
{
if (isMap())
@ -2125,6 +2127,191 @@ const QCborValue QCborValue::operator[](qint64 key) const
return QCborValue();
}
/*!
\internal
*/
static Q_DECL_COLD_FUNCTION QCborMap arrayAsMap(const QCborArray &array)
{
if (array.size())
qWarning("Using CBOR array as map forced conversion");
QCborMap map;
for (qsizetype i = array.size(); i-- > 0; ) {
QCborValue entry = array.at(i);
// Ignore padding entries that may have been added to grow the array
// when inserting past its end:
if (!entry.isInvalid())
map[i] = entry;
}
return map;
}
/*!
\internal
*/
static QCborContainerPrivate *maybeDetach(QCborContainerPrivate *container, qsizetype size)
{
auto replace = QCborContainerPrivate::detach(container, size);
Q_ASSERT(replace);
if (replace != container) {
if (container)
container->deref();
replace->ref.ref();
}
return replace;
}
/*!
\internal
*/
static QCborContainerPrivate *maybeGrow(QCborContainerPrivate *container, qsizetype index)
{
auto replace = QCborContainerPrivate::grow(container, index);
Q_ASSERT(replace);
if (replace != container) {
if (container)
container->deref();
replace->ref.ref();
}
if (replace->elements.size() == index)
replace->append(Undefined());
else
Q_ASSERT(replace->elements.size() > index);
return replace;
}
/*!
Returns a QCborValueRef that can be used to read or modify the entry in
this, as a map, with the given \a key. When this QCborValue is a QCborMap,
this function is equivalent to the matching operator[] on that map.
Before returning the reference: if this QCborValue was an array, it is first
converted to a map (so that \c{map[i]} is \c{array[i]} for each index, \c i,
with valid \c{array[i]}); otherwise, if it was not a map it will be
over-written with an empty map.
\sa operator[](qint64), QCborMap::operator[], QCborMap::value(),
QCborMap::find()
*/
QCborValueRef QCborValue::operator[](const QString &key)
{
if (!isMap())
*this = QCborValue(isArray() ? arrayAsMap(toArray()) : QCborMap());
const qsizetype size = container ? container->elements.size() : 0;
qsizetype index = size + 1;
bool found = false;
if (container) {
QCborMap proxy(*container);
auto it = proxy.constFind(key);
if (it < proxy.constEnd()) {
found = true;
index = it.item.i;
}
}
container = maybeDetach(container, size + (found ? 0 : 2));
Q_ASSERT(container);
if (!found) {
container->append(key);
container->append(QCborValue());
}
Q_ASSERT(index & 1 && !(container->elements.size() & 1));
Q_ASSERT(index < container->elements.size());
return { container, index };
}
/*!
\overload
Returns a QCborValueRef that can be used to read or modify the entry in
this, as a map, with the given \a key. When this QCborValue is a QCborMap,
this function is equivalent to the matching operator[] on that map.
Before returning the reference: if this QCborValue was an array, it is first
converted to a map (so that \c{map[i]} is \c{array[i]} for each index, \c i,
with valid \c{array[i]}); otherwise, if it was not a map it will be
over-written with an empty map.
\sa operator[](qint64), QCborMap::operator[], QCborMap::value(),
QCborMap::find()
*/
QCborValueRef QCborValue::operator[](QLatin1String key)
{
if (!isMap())
*this = QCborValue(isArray() ? arrayAsMap(toArray()) : QCborMap());
const qsizetype size = container ? container->elements.size() : 0;
qsizetype index = size + 1;
bool found = false;
if (container) {
QCborMap proxy(*container);
auto it = proxy.constFind(key);
if (it < proxy.constEnd()) {
found = true;
index = it.item.i;
}
}
container = maybeDetach(container, size + (found ? 0 : 2));
Q_ASSERT(container);
if (!found) {
container->append(key);
container->append(QCborValue());
}
Q_ASSERT(index & 1 && !(container->elements.size() & 1));
Q_ASSERT(index < container->elements.size());
return { container, index };
}
/*!
\overload
Returns a QCborValueRef that can be used to read or modify the entry in
this, as a map or array, with the given \a key. When this QCborValue is a
QCborMap or, for 0 <= key < 0x10000, a QCborArray, this function is
equivalent to the matching operator[] on that map or array.
Before returning the reference: if this QCborValue was an array but the key
is out of range, the array is first converted to a map (so that \c{map[i]}
is \c{array[i]} for each index, \c i, with valid \c{array[i]}); otherwise,
if it was not a map it will be over-written with an empty map.
\sa operator[], QCborMap::operator[], QCborMap::value(),
QCborMap::find(), QCborArray::operator[], QCborArray::at()
*/
QCborValueRef QCborValue::operator[](qint64 key)
{
if (isArray() && key >= 0 && key < 0x10000) {
container = maybeGrow(container, key);
return { container, qsizetype(key) };
}
if (!isMap())
*this = QCborValue(isArray() ? arrayAsMap(toArray()) : QCborMap());
const qsizetype size = container ? container->elements.size() : 0;
Q_ASSERT(!(size & 1));
qsizetype index = size + 1;
bool found = false;
if (container) {
QCborMap proxy(*container);
auto it = proxy.constFind(key);
if (it < proxy.constEnd()) {
found = true;
index = it.item.i;
}
}
container = maybeDetach(container, size + (found ? 0 : 2));
Q_ASSERT(container);
if (!found) {
container->append(key);
container->append(QCborValue());
}
Q_ASSERT(index & 1 && !(container->elements.size() & 1));
Q_ASSERT(index < container->elements.size());
return { container, index };
}
/*!
Decodes one item from the CBOR stream found in \a reader and returns the
equivalent representation. This function is recursive: if the item is a map
@ -2389,6 +2576,255 @@ QCborValue::Type QCborValueRef::concreteType(QCborValueRef self) noexcept
return self.d->elements.at(self.i).type;
}
/*!
If this QCborValueRef refers to a QCborMap, searches elements for the value
whose key matches \a key. If there's no key matching \a key in the map or if
this QCborValueRef object is not a map, returns the undefined value.
This function is equivalent to:
\code
value.toMap().value(key);
\endcode
\sa operator[](qint64), QCborMap::operator[], QCborMap::value(),
QCborMap::find()
*/
const QCborValue QCborValueRef::operator[](const QString &key) const
{
const QCborValue item = d->valueAt(i);
return item[key];
}
/*!
\overload
If this QCborValueRef refers to a QCborMap, searches elements for the value
whose key matches \a key. If there's no key matching \a key in the map or if
this QCborValueRef object is not a map, returns the undefined value.
This function is equivalent to:
\code
value.toMap().value(key);
\endcode
\sa operator[](qint64), QCborMap::operator[], QCborMap::value(),
QCborMap::find()
*/
const QCborValue QCborValueRef::operator[](QLatin1String key) const
{
const QCborValue item = d->valueAt(i);
return item[key];
}
/*!
\overload
If this QCborValueRef refers to a QCborMap, searches elements for the value
whose key matches \a key. If this is a QCborArray, returns the element whose
index is \a key. If there's no matching value in the array or map, or if
this QCborValueRef object is not an array or map, returns the undefined
value.
\sa operator[], QCborMap::operator[], QCborMap::value(),
QCborMap::find(), QCborArray::operator[], QCborArray::at()
*/
const QCborValue QCborValueRef::operator[](qint64 key) const
{
const QCborValue item = d->valueAt(i);
return item[key];
}
/*!
Returns a QCborValueRef that can be used to read or modify the entry in
this, as a map, with the given \a key. When this QCborValueRef refers to a
QCborMap, this function is equivalent to the matching operator[] on that
map.
Before returning the reference: if the QCborValue referenced was an array,
it is first converted to a map (so that \c{map[i]} is \c{array[i]} for each
index, \c i, with valid \c{array[i]}); otherwise, if it was not a map it
will be over-written with an empty map.
\sa operator[](qint64), QCborMap::operator[], QCborMap::value(),
QCborMap::find()
*/
QCborValueRef QCborValueRef::operator[](const QString &key)
{
auto &e = d->elements[i];
qsizetype size = 0;
if (e.flags & QtCbor::Element::IsContainer) {
if (e.container) {
if (e.type == QCborValue::Array) {
QCborValue repack = QCborValue(arrayAsMap(QCborArray(*e.container)));
qSwap(e.container, repack.container);
} else if (e.type != QCborValue::Map) {
e.container->deref();
e.container = nullptr;
}
}
e.type = QCborValue::Map;
if (e.container)
size = e.container->elements.size();
} else {
// Stomp any prior e.value, replace with a map (that we'll grow)
e.container = nullptr;
e.type = QCborValue::Map;
e.flags = QtCbor::Element::IsContainer;
}
qsizetype index = size + 1;
bool found = false;
if (e.container) {
QCborMap proxy(*e.container);
auto it = proxy.constFind(key);
if (it < proxy.constEnd()) {
found = true;
index = it.item.i;
}
}
e.container = maybeDetach(e.container, size + (found ? 0 : 2));
Q_ASSERT(e.container);
if (!found) {
e.container->append(key);
e.container->append(QCborValue());
}
Q_ASSERT(index & 1 && !(e.container->elements.size() & 1));
Q_ASSERT(index < e.container->elements.size());
return { e.container, index };
}
/*!
\overload
Returns a QCborValueRef that can be used to read or modify the entry in
this, as a map, with the given \a key. When this QCborValue is a QCborMap,
this function is equivalent to the matching operator[] on that map.
Before returning the reference: if the QCborValue referenced was an array,
it is first converted to a map (so that \c{map[i]} is \c{array[i]} for each
index, \c i, with valid \c{array[i]}); otherwise, if it was not a map it
will be over-written with an empty map.
\sa operator[](qint64), QCborMap::operator[], QCborMap::value(),
QCborMap::find()
*/
QCborValueRef QCborValueRef::operator[](QLatin1String key)
{
auto &e = d->elements[i];
qsizetype size = 0;
if (e.flags & QtCbor::Element::IsContainer) {
if (e.container) {
if (e.type == QCborValue::Array) {
QCborValue repack = QCborValue(arrayAsMap(QCborArray(*e.container)));
qSwap(e.container, repack.container);
} else if (e.type != QCborValue::Map) {
e.container->deref();
e.container = nullptr;
}
}
e.type = QCborValue::Map;
if (e.container)
size = e.container->elements.size();
} else {
// Stomp any prior e.value, replace with a map (that we'll grow)
e.container = nullptr;
e.type = QCborValue::Map;
e.flags = QtCbor::Element::IsContainer;
}
qsizetype index = size + 1;
bool found = false;
if (e.container) {
QCborMap proxy(*e.container);
auto it = proxy.constFind(key);
if (it < proxy.constEnd()) {
found = true;
index = it.item.i;
}
}
e.container = maybeDetach(e.container, size + (found ? 0 : 2));
Q_ASSERT(e.container);
if (!found) {
e.container->append(key);
e.container->append(QCborValue());
}
Q_ASSERT(index & 1 && !(e.container->elements.size() & 1));
Q_ASSERT(index < e.container->elements.size());
return { e.container, index };
}
/*!
\overload
Returns a QCborValueRef that can be used to read or modify the entry in
this, as a map or array, with the given \a key. When this QCborValue is a
QCborMap or, for 0 <= key < 0x10000, a QCborArray, this function is
equivalent to the matching operator[] on that map or array.
Before returning the reference: if the QCborValue referenced was an array
but the key is out of range, the array is first converted to a map (so that
\c{map[i]} is \c{array[i]} for each index, \c i, with valid \c{array[i]});
otherwise, if it was not a map it will be over-written with an empty map.
\sa operator[], QCborMap::operator[], QCborMap::value(),
QCborMap::find(), QCborArray::operator[], QCborArray::at()
*/
QCborValueRef QCborValueRef::operator[](qint64 key)
{
auto &e = d->elements[i];
if (e.type == QCborValue::Array && key >= 0 && key < 0x10000) {
e.container = maybeGrow(e.container, key);
return { e.container, qsizetype(key) };
}
qsizetype size = 0;
if (e.flags & QtCbor::Element::IsContainer) {
if (e.container) {
if (e.type == QCborValue::Array) {
QCborValue repack = QCborValue(arrayAsMap(QCborArray(*e.container)));
qSwap(e.container, repack.container);
} else if (e.type != QCborValue::Map) {
e.container->deref();
e.container = nullptr;
}
}
e.type = QCborValue::Map;
if (e.container)
size = e.container->elements.size();
} else {
// Stomp any prior e.value, replace with a map (that we'll grow)
e.container = nullptr;
e.type = QCborValue::Map;
e.flags = QtCbor::Element::IsContainer;
}
Q_ASSERT(!(size & 1));
qsizetype index = size + 1;
bool found = false;
if (e.container) {
QCborMap proxy(*e.container);
auto it = proxy.constFind(key);
if (it < proxy.constEnd()) {
found = true;
index = it.item.i;
}
}
e.container = maybeDetach(e.container, size + (found ? 0 : 2));
Q_ASSERT(e.container);
if (!found) {
e.container->append(key);
e.container->append(QCborValue());
}
Q_ASSERT(index & 1 && !(e.container->elements.size() & 1));
Q_ASSERT(index < e.container->elements.size());
return { e.container, index };
}
inline QCborArray::QCborArray(QCborContainerPrivate &dd) noexcept
: d(&dd)
{

View File

@ -77,6 +77,7 @@ struct QCborParserError
QString errorString() const { return error.toString(); }
};
class QCborValueRef;
class QCborContainerPrivate;
class Q_CORE_EXPORT QCborValue
{
@ -246,6 +247,9 @@ public:
const QCborValue operator[](const QString &key) const;
const QCborValue operator[](QLatin1String key) const;
const QCborValue operator[](qint64 key) const;
QCborValueRef operator[](qint64 key);
QCborValueRef operator[](QLatin1String key);
QCborValueRef operator[](const QString & key);
int compare(const QCborValue &other) const;
#if 0 && QT_HAS_INCLUDE(<compare>)
@ -387,6 +391,13 @@ public:
QCborMap toMap() const;
QCborMap toMap(const QCborMap &m) const;
const QCborValue operator[](const QString &key) const;
const QCborValue operator[](QLatin1String key) const;
const QCborValue operator[](qint64 key) const;
QCborValueRef operator[](qint64 key);
QCborValueRef operator[](QLatin1String key);
QCborValueRef operator[](const QString & key);
int compare(const QCborValue &other) const
{ return concrete().compare(other); }
#if 0 && QT_HAS_INCLUDE(<compare>)
@ -417,6 +428,7 @@ public:
{ return concrete().toDiagnosticNotation(opt); }
private:
friend class QCborValue;
friend class QCborArray;
friend class QCborMap;
friend class QCborContainerPrivate;

View File

@ -384,11 +384,17 @@ void tst_QCborValue::arrayDefaultInitialization()
QVERIFY(v.isArray());
QVERIFY(!v.isMap());
QVERIFY(!v.isTag());
QVERIFY(v[0].isUndefined());
QCborArray a2 = v.toArray();
QVERIFY(a2.isEmpty());
QCOMPARE(a2, a);
auto front = v[0];
QVERIFY(front.isUndefined());
front = 1;
QCOMPARE(v[0], 1);
QVERIFY(a2.isEmpty());
a2 = v.toArray();
QCOMPARE(a2.size(), 1);
}
void tst_QCborValue::mapDefaultInitialization()
@ -425,7 +431,7 @@ void tst_QCborValue::mapDefaultInitialization()
QVERIFY(m == QCborMap{});
QVERIFY(QCborMap{} == m);
QCborValue v(m);
const QCborValue v(m);
QVERIFY(v.isMap());
QVERIFY(!v.isArray());
QVERIFY(!v.isTag());
@ -727,6 +733,31 @@ void tst_QCborValue::arrayMutation()
QCOMPARE(a.at(1), QCborValue(-1));
QCOMPARE(a2.at(1), QCborValue(nullptr));
QCOMPARE(++it, end);
// Array accessed via value:
QCborValue val(a);
val[2] = QCborArray{2, 3, 5, 7};
QCOMPARE(a.size(), 2); // Unchanged
QVERIFY(val.isArray());
QCOMPARE(val.toArray().size(), 3);
val[2][4] = 17;
QVERIFY(val.isArray());
QVERIFY(val[2].isArray());
QCOMPARE(val[2].toArray().size(), 5);
QCOMPARE(val[2][4], 17);
QCOMPARE(val.toArray().size(), 3);
val[3] = 42;
QVERIFY(val.isArray());
QCOMPARE(val.toArray().size(), 4);
QCOMPARE(val[3], 42);
// Coerce to map on string key:
const QLatin1String any("any");
val[any] = any;
QVERIFY(val.isMap());
QCOMPARE(val.toMap().size(), 5);
QVERIFY(val[2].isArray());
QCOMPARE(val[2].toArray().size(), 5);
}
void tst_QCborValue::mapMutation()
@ -782,6 +813,30 @@ void tst_QCborValue::mapMutation()
QCOMPARE((m.end() - 1)->toInteger(), -1);
QVERIFY((m2.end() - 1)->isNull());
QCOMPARE(++it, end);
// Map accessed via value:
QCborValue val(m);
val[7] = QCborMap({{0, 2}, {1, 3}, {2, 5}});
QCOMPARE(m.size(), 2); // Unchanged
QVERIFY(val.isMap());
QCOMPARE(val.toMap().size(), 3);
val[7][3] = 11;
QVERIFY(val.isMap());
QVERIFY(val[7].isMap());
QCOMPARE(val[7].toMap().size(), 4);
val[14] = 42;
QVERIFY(val.isMap());
QCOMPARE(val.toMap().size(), 4);
const QLatin1String any("any");
const QString hello(QStringLiteral("Hello World"));
val[any][3][hello] = any;
QVERIFY(val.isMap());
QCOMPARE(val.toMap().size(), 5);
QVERIFY(val[any].isMap());
QCOMPARE(val[any].toMap().size(), 1);
QVERIFY(val[any][3].isMap());
QCOMPARE(val[any][3].toMap().size(), 1);
}
void tst_QCborValue::arrayPrepend()