Sync changes from internal repo. (#119)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e438523..21d492b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.10.0
+
+* Breaking change: Add `GeneratedMessage.freeze()`. A frozen message and its
+ sub-messages cannot be changed.
+
## 0.9.1
* Fix problem with encoding negative enum values.
diff --git a/lib/meta.dart b/lib/meta.dart
index dbaaffe..8ee607b 100644
--- a/lib/meta.dart
+++ b/lib/meta.dart
@@ -10,8 +10,10 @@
const List<String> GeneratedMessage_reservedNames = const [
'hashCode',
'noSuchMethod',
+ 'copyWith',
'runtimeType',
'toString',
+ 'freeze',
'fromBuffer',
'fromJson',
'hasRequiredFields',
@@ -63,5 +65,6 @@
r'$_setSignedInt32',
r'$_setUnsignedInt32',
r'$_setInt64',
+ 'toBuilder',
'toDebugString',
];
diff --git a/lib/src/protobuf/builder_info.dart b/lib/src/protobuf/builder_info.dart
index dec83b9..abc0965 100644
--- a/lib/src/protobuf/builder_info.dart
+++ b/lib/src/protobuf/builder_info.dart
@@ -94,14 +94,6 @@
tagNumber, name, fieldType, defaultOrMaker, null, valueOf, enumValues);
}
- // Repeated message.
- // TODO(skybrian): migrate to pp() and remove.
- void m<T>(int tagNumber, String name, CreateBuilderFunc subBuilder,
- MakeDefaultFunc makeDefault) {
- add<T>(tagNumber, name, PbFieldType._REPEATED_MESSAGE, makeDefault,
- subBuilder, null, null);
- }
-
// Repeated, not a message, group, or enum.
void p<T>(int tagNumber, String name, int fieldType) {
assert(!_isGroupOrMessage(fieldType) && !_isEnum(fieldType));
diff --git a/lib/src/protobuf/field_set.dart b/lib/src/protobuf/field_set.dart
index 04444bb..9448fec 100644
--- a/lib/src/protobuf/field_set.dart
+++ b/lib/src/protobuf/field_set.dart
@@ -4,6 +4,28 @@
part of protobuf;
+typedef void FrozenMessageErrorHandler(String messageName, [String methodName]);
+
+void defaultFrozenMessageModificationHandler(String messageName,
+ [String methodName]) {
+ if (methodName != null) {
+ throw new UnsupportedError(
+ "Attempted to call $methodName on a read-only message ($messageName)");
+ }
+ throw new UnsupportedError(
+ "Attempted to change a read-only message ($messageName)");
+}
+
+/// Invoked when an attempt is made to modify a frozen message.
+///
+/// This handler can log the attempt, throw an exception, or ignore the attempt
+/// altogether.
+///
+/// If the handler returns normally, the modification is allowed, and execution
+/// proceeds as if the message was writable.
+FrozenMessageErrorHandler frozenMessageModificationHandler =
+ defaultFrozenMessageModificationHandler;
+
/// All the data in a GeneratedMessage.
///
/// These fields and methods are in a separate class to avoid
@@ -13,6 +35,7 @@
final GeneratedMessage _message;
final BuilderInfo _meta;
final EventPlugin _eventPlugin;
+ bool _isReadOnly = false;
/// The value of each non-extension field in a fixed-length array.
/// The index of a field can be found in [FieldInfo.index].
@@ -41,7 +64,6 @@
// Metadata about multiple fields
String get _messageName => _meta.messageName;
- bool get _isReadOnly => _message._isReadOnly;
bool get _hasRequiredFields => _meta.hasRequiredFields;
/// The FieldInfo for each non-extension field.
@@ -92,6 +114,32 @@
return _extensions._getInfoOrNull(tagNumber);
}
+ void _markReadOnly() {
+ if (_isReadOnly) return;
+ _isReadOnly = true;
+ for (var field in _meta.sortedByTag) {
+ if (field.isRepeated) {
+ final entries = _values[field.index];
+ if (entries == null) continue;
+ if (field.isGroupOrMessage) {
+ for (var subMessage in entries as List<GeneratedMessage>) {
+ subMessage.freeze();
+ }
+ }
+ _values[field.index] = entries.toFrozenPbList();
+ } else if (field.isGroupOrMessage) {
+ final entry = _values[field.index];
+ if (entry != null) {
+ (entry as GeneratedMessage).freeze();
+ }
+ }
+ }
+ }
+
+ void _ensureWritable() {
+ if (_isReadOnly) frozenMessageModificationHandler(_messageName);
+ }
+
// Single-field operations
/// Gets a field with full error-checking.
@@ -163,6 +211,7 @@
}
void _clearField(int tagNumber) {
+ _ensureWritable();
var fi = _nonExtensionInfo(tagNumber);
if (fi != null) {
// clear a non-extension field
@@ -312,10 +361,7 @@
void _$set(int index, value) {
assert(!_nonExtensionInfoByIndex(index).isRepeated);
assert(_$check(index, value));
- if (_isReadOnly) {
- throw new UnsupportedError(
- "attempted to call a setter on a read-only message ($_messageName)");
- }
+ _ensureWritable();
if (value == null) {
_$check(index, value); // throw exception for null value
}
@@ -333,6 +379,7 @@
// Bulk operations reading or writing multiple fields
void _clear() {
+ _ensureWritable();
if (_unknownFields != null) {
_unknownFields.clear();
}
@@ -411,7 +458,7 @@
int get _hashCode {
int hash;
- void hashEnumList(PbList enums) {
+ void hashEnumList(PbListBase enums) {
for (ProtobufEnum enm in enums) {
hash = 0x1fffffff & ((31 * hash) + enm.value);
}
@@ -536,15 +583,15 @@
if (fi.isRepeated) {
if (mustClone) {
- // fieldValue must be a PbList of GeneratedMessage.
- PbList<GeneratedMessage> pbList = fieldValue;
+ // fieldValue must be a PbListBase of GeneratedMessage.
+ PbListBase<GeneratedMessage> pbList = fieldValue;
var repeatedFields = fi._ensureRepeatedField(this);
for (int i = 0; i < pbList.length; ++i) {
repeatedFields.add(_cloneMessage(pbList[i]));
}
} else {
- // fieldValue must be at least a PbList.
- PbList pbList = fieldValue;
+ // fieldValue must be at least a PbListBase.
+ PbListBase pbList = fieldValue;
fi._ensureRepeatedField(this).addAll(pbList);
}
return;
@@ -572,6 +619,7 @@
/// Checks the value for a field that's about to be set.
void _validateField(FieldInfo fi, var newValue) {
+ _ensureWritable();
var message = _getFieldError(fi.type, newValue);
if (message != null) {
throw new ArgumentError(_setFieldFailedMessage(fi, newValue, message));
diff --git a/lib/src/protobuf/generated_message.dart b/lib/src/protobuf/generated_message.dart
index 2f655da..fb22b2d 100644
--- a/lib/src/protobuf/generated_message.dart
+++ b/lib/src/protobuf/generated_message.dart
@@ -46,11 +46,33 @@
/// Creates a deep copy of the fields in this message.
/// (The generated code uses [mergeFromMessage].)
+ // TODO(nichite): preserve frozen state on clone.
GeneratedMessage clone();
UnknownFieldSet get unknownFields => _fieldSet._ensureUnknownFields();
- bool get _isReadOnly => this is ReadonlyMessageMixin;
+ /// Make this message read-only.
+ ///
+ /// Marks this message, and any sub-messages, as read-only.
+ GeneratedMessage freeze() {
+ _fieldSet._markReadOnly();
+ return this;
+ }
+
+ /// Returns a writable copy of this message.
+ // TODO(nichite): Return an actual builder object that lazily creates builders
+ // for sub-messages, instead of cloning everything here.
+ GeneratedMessage toBuilder() => clone();
+
+ /// Apply [updates] to a copy of this message.
+ ///
+ /// Makes a writable copy of this message, applies the [updates] to it, and
+ /// marks the copy read-only before returning it.
+ GeneratedMessage copyWith(void Function(GeneratedMessage) updates) {
+ final builder = toBuilder();
+ updates(builder);
+ return builder.freeze();
+ }
bool hasRequiredFields() => info_.hasRequiredFields;
@@ -67,6 +89,7 @@
// TODO(antonm): move to getters.
int getTagNumber(String fieldName) => info_.tagNumber(fieldName);
+ @override
bool operator ==(other) {
if (identical(this, other)) return true;
return other is GeneratedMessage
@@ -211,7 +234,7 @@
///
/// If not set, returns the extension's default value.
getExtension(Extension extension) {
- if (_isReadOnly) return extension.readonlyDefault;
+ if (_fieldSet._isReadOnly) return extension.readonlyDefault;
return _fieldSet._ensureExtensions()._getFieldOrDefault(extension);
}
@@ -291,7 +314,12 @@
_fieldSet._$get<T>(index, defaultValue);
/// For generated code only.
- T $_getN<T>(int index) => _fieldSet._$getN<T>(index);
+ T $_getN<T>(int index) {
+ if (_fieldSet == null) {
+ throw new StateError('Unable to access $index in the proto message');
+ }
+ return _fieldSet._$getN<T>(index);
+ }
/// For generated code only.
List<T> $_getList<T>(int index) => _fieldSet._$getList<T>(index);
diff --git a/lib/src/protobuf/json.dart b/lib/src/protobuf/json.dart
index f2b945d..63eb41f 100644
--- a/lib/src/protobuf/json.dart
+++ b/lib/src/protobuf/json.dart
@@ -5,12 +5,11 @@
part of protobuf;
Map<String, dynamic> _writeToJsonMap(_FieldSet fs) {
- convertToMap(fieldValue, int fieldType) {
+ convertToMap(dynamic fieldValue, int fieldType) {
int baseType = PbFieldType._baseType(fieldType);
if (_isRepeated(fieldType)) {
- PbList pbList = fieldValue;
- return new List.from(pbList.map((e) => convertToMap(e, baseType)));
+ return new List.from(fieldValue.map((e) => convertToMap(e, baseType)));
}
switch (baseType) {
diff --git a/lib/src/protobuf/pb_list.dart b/lib/src/protobuf/pb_list.dart
index d1d1aab..8517910 100644
--- a/lib/src/protobuf/pb_list.dart
+++ b/lib/src/protobuf/pb_list.dart
@@ -6,23 +6,168 @@
typedef void CheckFunc<E>(E x);
-class PbList<E> extends ListBase<E> {
+class FrozenPbList<E> extends PbListBase<E> {
+ FrozenPbList._(List<E> wrappedList) : super._(wrappedList);
+
+ factory FrozenPbList.from(PbList<E> other) =>
+ new FrozenPbList._(other._wrappedList);
+
+ UnsupportedError _unsupported(String method) =>
+ new UnsupportedError("Cannot call $method on an unmodifiable list");
+
+ void operator []=(int index, E value) => throw _unsupported("set");
+ set length(int newLength) => throw _unsupported("set length");
+ void setAll(int at, Iterable<E> iterable) => throw _unsupported("setAll");
+ void add(E value) => throw _unsupported("add");
+ void addAll(Iterable<E> iterable) => throw _unsupported("addAll");
+ void insert(int index, E element) => throw _unsupported("insert");
+ void insertAll(int at, Iterable<E> iterable) =>
+ throw _unsupported("insertAll");
+ bool remove(Object element) => throw _unsupported("remove");
+ void removeWhere(bool test(E element)) => throw _unsupported("removeWhere");
+ void retainWhere(bool test(E element)) => throw _unsupported("retainWhere");
+ void sort([Comparator<E> compare]) => throw _unsupported("sort");
+ void shuffle([math.Random random]) => throw _unsupported("shuffle");
+ void clear() => throw _unsupported("clear");
+ E removeAt(int index) => throw _unsupported("removeAt");
+ E removeLast() => throw _unsupported("removeLast");
+ void setRange(int start, int end, Iterable<E> iterable,
+ [int skipCount = 0]) =>
+ throw _unsupported("setRange");
+ void removeRange(int start, int end) => throw _unsupported("removeRange");
+ void replaceRange(int start, int end, Iterable<E> iterable) =>
+ throw _unsupported("replaceRange");
+ void fillRange(int start, int end, [E fillValue]) =>
+ throw _unsupported("fillRange");
+}
+
+class PbList<E> extends PbListBase<E> {
+ PbList({check = _checkNotNull}) : super._noList(check: check);
+
+ PbList._(List<E> wrappedList) : super._(wrappedList);
+
+ PbList.from(List from) : super._from(from);
+
+ PbList.forFieldType(int fieldType)
+ : super._noList(check: getCheckFunction(fieldType));
+
+ /// Freezes the list by converting to [FrozenPbList].
+ FrozenPbList<E> toFrozenPbList() => new FrozenPbList<E>.from(this);
+
+ /// Adds [value] at the end of the list, extending the length by one.
+ /// Throws an [UnsupportedError] if the list is not extendable.
+ void add(E value) {
+ _validate(value);
+ _wrappedList.add(value);
+ }
+
+ /// Appends all elements of the [collection] to the end of list.
+ /// Extends the length of the list by the length of [collection].
+ /// Throws an [UnsupportedError] if the list is not extendable.
+ void addAll(Iterable<E> collection) {
+ collection.forEach(_validate);
+ _wrappedList.addAll(collection);
+ }
+
+ /// Returns an [Iterable] of the objects in this list in reverse order.
+ Iterable<E> get reversed => _wrappedList.reversed;
+
+ /// Sorts this list according to the order specified by the [compare]
+ /// function.
+ void sort([int compare(E a, E b)]) => _wrappedList.sort(compare);
+
+ /// Shuffles the elements of this list randomly.
+ void shuffle([math.Random random]) => _wrappedList.shuffle(random);
+
+ /// Removes all objects from this list; the length of the list becomes zero.
+ void clear() => _wrappedList.clear();
+
+ /// Inserts a new element in the list.
+ /// The element must be valid (and not nullable) for the PbList type.
+ void insert(int index, E element) {
+ _validate(element);
+ _wrappedList.insert(index, element);
+ }
+
+ /// Inserts all elements of [iterable] at position [index] in the list.
+ ///
+ /// Elements in [iterable] must be valid and not nullable for the PbList type.
+ void insertAll(int index, Iterable<E> iterable) {
+ iterable.forEach(_validate);
+ _wrappedList.insertAll(index, iterable);
+ }
+
+ /// Overwrites elements of `this` with elements of [iterable] starting at
+ /// position [index] in the list.
+ ///
+ /// Elements in [iterable] must be valid and not nullable for the PbList type.
+ void setAll(int index, Iterable<E> iterable) {
+ iterable.forEach(_validate);
+ _wrappedList.setAll(index, iterable);
+ }
+
+ /// Removes the first occurrence of [value] from this list.
+ bool remove(Object value) => _wrappedList.remove(value);
+
+ /// Removes the object at position [index] from this list.
+ E removeAt(int index) => _wrappedList.removeAt(index);
+
+ /// Pops and returns the last object in this list.
+ E removeLast() => _wrappedList.removeLast();
+
+ /// Removes all objects from this list that satisfy [test].
+ void removeWhere(bool test(E element)) => _wrappedList.removeWhere(test);
+
+ /// Removes all objects from this list that fail to satisfy [test].
+ void retainWhere(bool test(E element)) => _wrappedList.retainWhere(test);
+
+ /// Copies [:end - start:] elements of the [from] array, starting from
+ /// [skipCount], into [:this:], starting at [start].
+ /// Throws an [UnsupportedError] if the list is not extendable.
+ void setRange(int start, int end, Iterable<E> from, [int skipCount = 0]) {
+ // NOTE: In case `take()` returns less than `end - start` elements, the
+ // _wrappedList will fail with a `StateError`.
+ from.skip(skipCount).take(end - start).forEach(_validate);
+ _wrappedList.setRange(start, end, from, skipCount);
+ }
+
+ /// Removes the objects in the range [start] inclusive to [end] exclusive.
+ void removeRange(int start, int end) => _wrappedList.removeRange(start, end);
+
+ /// Sets the objects in the range [start] inclusive to [end] exclusive to the
+ /// given [fillValue].
+ void fillRange(int start, int end, [E fillValue]) {
+ _validate(fillValue);
+ _wrappedList.fillRange(start, end, fillValue);
+ }
+
+ /// Removes the objects in the range [start] inclusive to [end] exclusive and
+ /// inserts the contents of [replacement] in its place.
+ void replaceRange(int start, int end, Iterable<E> replacement) {
+ final values = replacement.toList();
+ replacement.forEach(_validate);
+ _wrappedList.replaceRange(start, end, values);
+ }
+}
+
+abstract class PbListBase<E> extends ListBase<E> {
final List<E> _wrappedList;
final CheckFunc<E> check;
- PbList({this.check: _checkNotNull}) : _wrappedList = <E>[] {
+ PbListBase._(this._wrappedList, {this.check: _checkNotNull}) {}
+
+ PbListBase._noList({this.check: _checkNotNull}) : _wrappedList = <E>[] {
assert(check != null);
}
- PbList.from(List from)
+ PbListBase._from(List from)
// TODO(sra): Should this be validated?
: _wrappedList = new List<E>.from(from),
check = _checkNotNull;
- factory PbList.forFieldType(int fieldType) =>
- new PbList(check: getCheckFunction(fieldType));
-
- bool operator ==(other) => (other is PbList) && _areListsEqual(other, this);
+ @override
+ bool operator ==(other) =>
+ (other is PbListBase) && _areListsEqual(other, this);
int get hashCode {
int hash = 0;
@@ -40,7 +185,7 @@
Iterator<E> get iterator => _wrappedList.iterator;
/// Returns a new lazy [Iterable] with elements that are created by calling
- /// `f` on each element of this `PbList` in iteration order.
+ /// `f` on each element of this `PbListBase` in iteration order.
Iterable<T> map<T>(T f(E e)) => _wrappedList.map<T>(f);
/// Returns a new lazy [Iterable] with all elements that satisfy the predicate
@@ -132,18 +277,41 @@
/// Returns the element at the given [index] in the list or throws an
/// [IndexOutOfRangeException] if [index] is out of bounds.
+ @override
E operator [](int index) => _wrappedList[index];
+ /// Returns the number of elements in this collection.
+ int get length => _wrappedList.length;
+
+ // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
+ /// Returns the first index of [element] in this list.
+ int indexOf(Object element, [int start = 0]) =>
+ _wrappedList.indexOf(element, start);
+
+ // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
+ /// Returns the last index of [element] in this list.
+ int lastIndexOf(Object element, [int start]) =>
+ _wrappedList.lastIndexOf(element, start);
+
+ /// Returns a new list containing the objects from [start] inclusive to [end]
+ /// exclusive.
+ List<E> sublist(int start, [int end]) => _wrappedList.sublist(start, end);
+
+ /// Returns an [Iterable] that iterates over the objects in the range [start]
+ /// inclusive to [end] exclusive.
+ Iterable<E> getRange(int start, int end) => _wrappedList.getRange(start, end);
+
+ /// Returns an unmodifiable [Map] view of `this`.
+ Map<int, E> asMap() => _wrappedList.asMap();
+
/// Sets the entry at the given [index] in the list to [value].
/// Throws an [IndexOutOfRangeException] if [index] is out of bounds.
+ @override
void operator []=(int index, E value) {
_validate(value);
_wrappedList[index] = value;
}
- /// Returns the number of elements in this collection.
- int get length => _wrappedList.length;
-
/// Unsupported -- violated non-null constraint imposed by protobufs.
///
/// Changes the length of the list. If [newLength] is greater than the current
@@ -156,122 +324,6 @@
_wrappedList.length = newLength;
}
- /// Adds [value] at the end of the list, extending the length by one.
- /// Throws an [UnsupportedError] if the list is not extendable.
- void add(E value) {
- _validate(value);
- _wrappedList.add(value);
- }
-
- /// Appends all elements of the [collection] to the end of list.
- /// Extends the length of the list by the length of [collection].
- /// Throws an [UnsupportedError] if the list is not extendable.
- void addAll(Iterable<E> collection) {
- collection.forEach(_validate);
- _wrappedList.addAll(collection);
- }
-
- /// Returns an [Iterable] of the objects in this list in reverse order.
- Iterable<E> get reversed => _wrappedList.reversed;
-
- /// Sorts this list according to the order specified by the [compare]
- /// function.
- void sort([int compare(E a, E b)]) => _wrappedList.sort(compare);
-
- /// Shuffles the elements of this list randomly.
- void shuffle([math.Random random]) => _wrappedList.shuffle(random);
-
- // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
- /// Returns the first index of [element] in this list.
- int indexOf(Object element, [int start = 0]) =>
- _wrappedList.indexOf(element, start);
-
- // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
- /// Returns the last index of [element] in this list.
- int lastIndexOf(Object element, [int start]) =>
- _wrappedList.lastIndexOf(element, start);
-
- /// Removes all objects from this list; the length of the list becomes zero.
- void clear() => _wrappedList.clear();
-
- /// Inserts a new element in the list.
- /// The element must be valid (and not nullable) for the PbList type.
- void insert(int index, E element) {
- _validate(element);
- _wrappedList.insert(index, element);
- }
-
- /// Inserts all elements of [iterable] at position [index] in the list.
- ///
- /// Elements in [iterable] must be valid and not nullable for the PbList type.
- void insertAll(int index, Iterable<E> iterable) {
- iterable.forEach(_validate);
- _wrappedList.insertAll(index, iterable);
- }
-
- /// Overwrites elements of `this` with elements of [iterable] starting at
- /// position [index] in the list.
- ///
- /// Elements in [iterable] must be valid and not nullable for the PbList type.
- void setAll(int index, Iterable<E> iterable) {
- iterable.forEach(_validate);
- _wrappedList.setAll(index, iterable);
- }
-
- /// Removes the first occurrence of [value] from this list.
- bool remove(Object value) => _wrappedList.remove(value);
-
- /// Removes the object at position [index] from this list.
- E removeAt(int index) => _wrappedList.removeAt(index);
-
- /// Pops and returns the last object in this list.
- E removeLast() => _wrappedList.removeLast();
-
- /// Removes all objects from this list that satisfy [test].
- void removeWhere(bool test(E element)) => _wrappedList.removeWhere(test);
-
- /// Removes all objects from this list that fail to satisfy [test].
- void retainWhere(bool test(E element)) => _wrappedList.retainWhere(test);
-
- /// Returns a new list containing the objects from [start] inclusive to [end]
- /// exclusive.
- List<E> sublist(int start, [int end]) => _wrappedList.sublist(start, end);
-
- /// Returns an [Iterable] that iterates over the objects in the range [start]
- /// inclusive to [end] exclusive.
- Iterable<E> getRange(int start, int end) => _wrappedList.getRange(start, end);
-
- /// Copies [:end - start:] elements of the [from] array, starting from
- /// [skipCount], into [:this:], starting at [start].
- /// Throws an [UnsupportedError] if the list is not extendable.
- void setRange(int start, int end, Iterable<E> from, [int skipCount = 0]) {
- // NOTE: In case `take()` returns less than `end - start` elements, the
- // _wrappedList will fail with a `StateError`.
- from.skip(skipCount).take(end - start).forEach(_validate);
- _wrappedList.setRange(start, end, from, skipCount);
- }
-
- /// Removes the objects in the range [start] inclusive to [end] exclusive.
- void removeRange(int start, int end) => _wrappedList.removeRange(start, end);
-
- /// Sets the objects in the range [start] inclusive to [end] exclusive to the
- /// given [fillValue].
- void fillRange(int start, int end, [E fillValue]) {
- _validate(fillValue);
- _wrappedList.fillRange(start, end, fillValue);
- }
-
- /// Removes the objects in the range [start] inclusive to [end] exclusive and
- /// inserts the contents of [replacement] in its place.
- void replaceRange(int start, int end, Iterable<E> replacement) {
- final values = replacement.toList();
- replacement.forEach(_validate);
- _wrappedList.replaceRange(start, end, values);
- }
-
- /// Returns an unmodifiable [Map] view of `this`.
- Map<int, E> asMap() => _wrappedList.asMap();
-
void _validate(E val) {
check(val);
// TODO: remove after migration to check functions is finished
diff --git a/lib/src/protobuf/readonly_message.dart b/lib/src/protobuf/readonly_message.dart
index b8f03e0..a711961 100644
--- a/lib/src/protobuf/readonly_message.dart
+++ b/lib/src/protobuf/readonly_message.dart
@@ -8,6 +8,8 @@
abstract class ReadonlyMessageMixin {
BuilderInfo get info_;
+ bool get _isReadOnly => true;
+
void addExtension(Extension extension, var value) =>
_readonly("addExtension");
@@ -52,8 +54,7 @@
void _readonly(String methodName) {
String messageType = info_.messageName;
- throw new UnsupportedError(
- "attempted to call $methodName on a read-only message ($messageType)");
+ frozenMessageModificationHandler(messageType, methodName);
}
}
@@ -92,7 +93,6 @@
}
void _readonly(String methodName) {
- throw new UnsupportedError(
- "attempted to call $methodName on a read-only UnknownFieldSet");
+ frozenMessageModificationHandler('UnknownFieldSet', methodName);
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index ee29bc8..c715c6b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: protobuf
-version: 0.9.1
+version: 0.10.0
author: Dart Team <misc@dartlang.org>
description: Runtime library for protocol buffers support.
homepage: https://github.com/dart-lang/protobuf
diff --git a/test/json_test.dart b/test/json_test.dart
index 1290d0e..8abce40 100644
--- a/test/json_test.dart
+++ b/test/json_test.dart
@@ -4,6 +4,7 @@
library json_test;
import 'dart:convert';
+import 'package:fixnum/fixnum.dart' show Int64;
import 'package:test/test.dart';
import 'mock_util.dart' show T;
@@ -11,13 +12,20 @@
main() {
T example = new T()
..val = 123
- ..str = "hello";
+ ..str = "hello"
+ ..int32s.addAll(<int>[1, 2, 3]);
test('testWriteToJson', () {
String json = example.writeToJson();
checkJsonMap(jsonDecode(json));
});
+ test('testWriteFrozenToJson', () {
+ final frozen = example.clone()..freeze();
+ final json = frozen.writeToJson();
+ checkJsonMap(jsonDecode(json));
+ });
+
test('writeToJsonMap', () {
Map m = example.writeToJsonMap();
checkJsonMap(m);
@@ -34,12 +42,34 @@
t.mergeFromJsonMap({"1": 123, "2": "hello"});
checkMessage(t);
});
+
+ test('testInt64JsonEncoding', () {
+ final value = Int64.parseInt('1234567890123456789');
+ final t = new T()..int64 = value;
+ final encoded = t.writeToJsonMap();
+ expect(encoded["5"], "$value");
+ final decoded = new T()..mergeFromJsonMap(encoded);
+ expect(decoded.int64, value);
+ });
+
+ test('tesFrozentInt64JsonEncoding', () {
+ final value = Int64.parseInt('1234567890123456789');
+ final frozen = new T()
+ ..int64 = value
+ ..freeze();
+ final encoded = frozen.writeToJsonMap();
+ expect(encoded["5"], "$value");
+ final decoded = new T()..mergeFromJsonMap(encoded);
+ expect(decoded.int64, value);
+ });
}
checkJsonMap(Map m) {
- expect(m.length, 2);
+ expect(m.length, 3);
expect(m["1"], 123);
expect(m["2"], "hello");
+ print(m.toString());
+ expect(m["4"], [1, 2, 3]);
}
checkMessage(T t) {
diff --git a/test/message_test.dart b/test/message_test.dart
index 4a541d7..2b0f9e0 100644
--- a/test/message_test.dart
+++ b/test/message_test.dart
@@ -36,6 +36,20 @@
}, throwsError(ArgumentError, "tag 123 not defined in Rec"));
});
+ test('operator== and hashCode works for frozen message', () {
+ final a = new Rec()
+ ..val = 123
+ ..int32s.addAll([1, 2, 3])
+ ..freeze();
+ final b = new Rec()
+ ..val = 123
+ ..int32s.addAll([1, 2, 3]);
+
+ expect(a.hashCode, b.hashCode);
+ expect(a == b, true);
+ expect(b == a, true);
+ });
+
test('operator== and hashCode work for a simple record', () {
var a = new Rec();
expect(a == a, true);
diff --git a/test/readonly_message_test.dart b/test/readonly_message_test.dart
index 6325a0f..c127032 100644
--- a/test/readonly_message_test.dart
+++ b/test/readonly_message_test.dart
@@ -5,56 +5,246 @@
library readonly_message_test;
-import 'package:test/test.dart' show test, expect, throwsA, predicate;
+import 'package:test/test.dart';
import 'package:protobuf/protobuf.dart'
- show GeneratedMessage, ReadonlyMessageMixin, BuilderInfo;
+ show
+ BuilderInfo,
+ GeneratedMessage,
+ PbFieldType,
+ UnknownFieldSetField,
+ frozenMessageModificationHandler,
+ defaultFrozenMessageModificationHandler;
-throwsError(Type expectedType, String expectedMessage) =>
+throwsError(Type expectedType, Matcher expectedMessage) =>
throwsA(predicate((x) {
expect(x.runtimeType, expectedType);
expect(x.message, expectedMessage);
return true;
}));
-class Rec extends GeneratedMessage with ReadonlyMessageMixin {
+class Rec extends GeneratedMessage {
+ static Rec getDefault() => new Rec()..freeze();
+ static Rec create() => new Rec();
+
@override
- BuilderInfo info_ = new BuilderInfo("rec");
+ BuilderInfo info_ = new BuilderInfo('rec')
+ ..a(1, 'value', PbFieldType.O3)
+ ..pp<Rec>(2, 'sub', PbFieldType.PM, (_) {}, Rec.create)
+ ..p<int>(10, 'ints', PbFieldType.P3);
+
+ int get value => $_get(0, 0);
+ set value(int v) {
+ $_setUnsignedInt32(0, v);
+ }
+
+ bool hasValue() => $_has(0);
+
+ List<Rec> get sub => $_getList<Rec>(1);
+
+ List<int> get ints => $_getList<int>(2);
+
@override
- clone() => throw new UnimplementedError();
+ Rec clone() => new Rec()..mergeFromMessage(this);
+
+ Rec copyWith(void Function(Rec) updates) =>
+ super.copyWith((message) => updates(message as Rec));
}
main() {
- test("can write a read-only message", () {
- expect(new Rec().writeToBuffer(), []);
- expect(new Rec().writeToJson(), "{}");
+ test('can write a read-only message', () {
+ expect(Rec.getDefault().writeToBuffer(), []);
+ expect(Rec.getDefault().writeToJson(), "{}");
});
test("can't merge to a read-only message", () {
expect(
- () => new Rec().mergeFromJson("{}"),
+ () => Rec.getDefault().mergeFromJson('{"1":1}'),
throwsError(UnsupportedError,
- "attempted to call mergeFromJson on a read-only message (rec)"));
+ equals('Attempted to change a read-only message (rec)')));
});
test("can't set a field on a read-only message", () {
expect(
- () => new Rec().setField(123, 456),
+ () => Rec.getDefault().setField(1, 456),
throwsError(UnsupportedError,
- "attempted to call setField on a read-only message (rec)"));
+ equals('Attempted to change a read-only message (rec)')));
+ });
+
+ test('can set a field on a read-only message with a custom read-only handler',
+ () {
+ try {
+ int called = 0;
+
+ frozenMessageModificationHandler =
+ (String messageName, [String methodName]) {
+ expect(messageName, 'rec');
+ expect(methodName, isNull);
+ called++;
+ };
+
+ Rec.getDefault().setField(1, 456);
+ expect(called, 1);
+ } finally {
+ frozenMessageModificationHandler =
+ defaultFrozenMessageModificationHandler;
+ }
});
test("can't clear a read-only message", () {
expect(
- () => new Rec().clear(),
+ () => Rec.getDefault().clear(),
throwsError(UnsupportedError,
- "attempted to call clear on a read-only message (rec)"));
+ equals('Attempted to change a read-only message (rec)')));
+ });
+
+ test("can't clear a field on a read-only message", () {
+ expect(
+ () => Rec.getDefault().clearField(1),
+ throwsError(UnsupportedError,
+ equals('Attempted to change a read-only message (rec)')));
+ });
+
+ test("can't modify repeated fields on a read-only message", () {
+ expect(() => Rec.getDefault().sub.add(Rec.create()),
+ throwsError(UnsupportedError, contains('add')));
+ var r = Rec.create()
+ ..ints.add(10)
+ ..freeze();
+ expect(
+ () => r.ints.clear(),
+ throwsError(UnsupportedError,
+ equals('Cannot call clear on an unmodifiable list')));
+ expect(
+ () => r.ints[0] = 2,
+ throwsError(UnsupportedError,
+ equals('Cannot call set on an unmodifiable list')));
+ expect(() => r.sub.add(Rec.create()),
+ throwsError(UnsupportedError, contains('add')));
+
+ r = Rec.create()
+ ..sub.add(Rec.create())
+ ..freeze();
+ expect(
+ () => r.sub.add(Rec.create()),
+ throwsError(UnsupportedError,
+ equals('Cannot call add on an unmodifiable list')));
+ expect(() => r.ints.length = 20,
+ throwsError(UnsupportedError, contains('length')));
+ });
+
+ test("can't modify sub-messages on a read-only message", () {
+ var subMessage = Rec.create()..value = 1;
+ var r = Rec.create()
+ ..sub.add(Rec.create()..sub.add(subMessage))
+ ..freeze();
+ expect(r.sub[0].sub[0].value, 1);
+ expect(
+ () => subMessage.value = 2,
+ throwsError(UnsupportedError,
+ equals('Attempted to change a read-only message (rec)')));
});
test("can't modify unknown fields on a read-only message", () {
expect(
- () => new Rec().unknownFields.clear(),
- throwsError(UnsupportedError,
- "attempted to call clear on a read-only UnknownFieldSet"));
+ () => Rec.getDefault().unknownFields.clear(),
+ throwsError(
+ UnsupportedError,
+ equals(
+ "Attempted to call clear on a read-only message (UnknownFieldSet)")));
+ });
+
+ test("can rebuild a frozen message with merge", () {
+ final orig = Rec.create()
+ ..value = 10
+ ..freeze();
+ final rebuilt = orig.copyWith((m) => m.mergeFromJson('{"1": 7}'));
+ expect(identical(orig, rebuilt), false);
+ expect(orig.value, 10);
+ expect(rebuilt.value, 7);
+ });
+
+ test("can set a field while rebuilding a frozen message", () {
+ final orig = Rec.create()
+ ..value = 10
+ ..freeze();
+ final rebuilt = orig.copyWith((m) => m.value = 7);
+ expect(identical(orig, rebuilt), false);
+ expect(orig.value, 10);
+ expect(rebuilt.value, 7);
+ });
+
+ test("can clear while rebuilding a frozen message", () {
+ final orig = Rec.create()
+ ..value = 10
+ ..freeze();
+ final rebuilt = orig.copyWith((m) => m.clear());
+ expect(identical(orig, rebuilt), false);
+ expect(orig.value, 10);
+ expect(orig.hasValue(), true);
+ expect(rebuilt.hasValue(), false);
+ });
+
+ test("can clear a field while rebuilding a frozen message", () {
+ final orig = Rec.create()
+ ..value = 10
+ ..freeze();
+ final rebuilt = orig.copyWith((m) => m.clearField(1));
+ expect(identical(orig, rebuilt), false);
+ expect(orig.value, 10);
+ expect(orig.hasValue(), true);
+ expect(rebuilt.hasValue(), false);
+ });
+
+ test("can modify repeated fields while rebuilding a frozen message", () {
+ var orig = Rec.create()
+ ..ints.add(10)
+ ..freeze();
+ var rebuilt = orig.copyWith((m) => m.ints.add(12));
+ expect(identical(orig, rebuilt), false);
+ expect(orig.ints, [10]);
+ expect(rebuilt.ints, [10, 12]);
+
+ rebuilt = orig.copyWith((m) => m.ints.clear());
+ expect(orig.ints, [10]);
+ expect(rebuilt.ints, []);
+
+ rebuilt = orig.copyWith((m) => m.ints[0] = 2);
+ expect(orig.ints, [10]);
+ expect(rebuilt.ints, [2]);
+
+ orig = Rec.create()
+ ..sub.add(Rec.create())
+ ..freeze();
+ rebuilt = orig.copyWith((m) => m.sub.add(Rec.create()));
+ expect(orig.sub.length, 1);
+ expect(rebuilt.sub.length, 2);
+ });
+
+ test("can modify sub-messages while rebuilding a frozen message", () {
+ final subMessage = Rec.create()..value = 1;
+ final orig = Rec.create()
+ ..sub.add(Rec.create()..sub.add(subMessage))
+ ..freeze();
+
+ final rebuilt = orig.copyWith((m) {
+ expect(
+ () => subMessage.value = 5,
+ throwsError(UnsupportedError,
+ equals('Attempted to change a read-only message (rec)')));
+ m.sub[0].sub[0].value = 2;
+ });
+ expect(identical(subMessage, orig.sub[0].sub[0]), true);
+ expect(identical(subMessage, rebuilt.sub[0].sub[0]), false);
+ expect(orig.sub[0].sub[0].value, 1);
+ expect(rebuilt.sub[0].sub[0].value, 2);
+ });
+
+ test("can modify unknown fields while rebuilding a frozen message", () {
+ final orig = Rec.create()
+ ..unknownFields.addField(20, new UnknownFieldSetField()..fixed32s.add(1));
+ final rebuilt = orig.copyWith((m) => m.unknownFields.clear());
+ expect(orig.unknownFields.hasField(20), true);
+ expect(rebuilt.unknownFields.hasField(20), false);
});
}