Skip to content

Commit

Permalink
Add ReadonlyMessageMixin and the hooks needed to support it.
Browse files Browse the repository at this point in the history
The protoc plugin will use this mixin to implement a
getDefault() method for each message.

BUG=google#41

Review URL: https://chromiumcodereview.appspot.com//1228213004.
  • Loading branch information
Brian Slesinsky committed Jul 15, 2015
1 parent 16eb0d6 commit aa21cf7
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 16 deletions.
1 change: 1 addition & 0 deletions lib/protobuf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ part 'src/protobuf/generated_message.dart';
part 'src/protobuf/generated_service.dart';
part 'src/protobuf/pb_list.dart';
part 'src/protobuf/protobuf_enum.dart';
part 'src/protobuf/readonly_message.dart';
part 'src/protobuf/rpc_client.dart';
part 'src/protobuf/unknown_field_set.dart';
part 'src/protobuf/utils.dart';
Expand Down
36 changes: 23 additions & 13 deletions lib/src/protobuf/generated_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -938,24 +938,34 @@ abstract class GeneratedMessage {
/// default value if it is not set.
getField(int tagNumber) {
var value = _fieldValues[tagNumber];
if (value != null) return value;

// Initialize the field.
if (value == null) {
MakeDefaultFunc makeDefaultFunc = info_.makeDefault(tagNumber);
if (makeDefaultFunc == null) {
makeDefaultFunc = _extensions[tagNumber].makeDefault;
}
value = makeDefaultFunc();
// TODO(antonm): ugly trick which should go away imho:
// right now getField for repeated fields returns a list
// which is implicitly added to the message.
// Should return immutable empty list instead, imho.
if (value is List) {
_fieldValues[tagNumber] = value;
}
MakeDefaultFunc makeDefaultFunc = info_.makeDefault(tagNumber);
if (makeDefaultFunc == null) {
makeDefaultFunc = _extensions[tagNumber].makeDefault;
}
value = makeDefaultFunc();
if (value is List) {
return _getDefaultRepeatedField(tagNumber, value);
}
return value;
}

List _getDefaultRepeatedField(int tagNumber, List value) {
// Automatically save the repeated field so that changes won't be lost.
//
// TODO(skybrian) we could avoid this by generating another
// method for repeated fields:
//
// msg.mutableFoo().add(123);
//
// Then msg.foo could return an immutable empty list by default.
// But it doesn't seem urgent or worth the migration.
_fieldValues[tagNumber] = value;
return value;
}

/// Returns [:true:] if a value of [extension] is present.
bool hasExtension(Extension extension) {
_checkExtension(extension);
Expand Down
99 changes: 99 additions & 0 deletions lib/src/protobuf/readonly_message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of protobuf;

/// Modifies a GeneratedMessage so that it's read-only.
abstract class ReadonlyMessageMixin {
static final _emptyUnknownFields = new _ReadonlyUnknownFieldSet();
static final _emptyList = new List.unmodifiable([]);

BuilderInfo get info_;

get unknownFields => _emptyUnknownFields;

List _getDefaultRepeatedField(int tagNumber, List value) => _emptyList;

void addExtension(Extension extension, var value) =>
_readonly("addExtension");

void clear() => _readonly("clear");

void clearExtension(Extension extension) => _readonly("clearExtension");

void clearField(int tagNumber) => _readonly("clearField");

void mergeFromBuffer(List<int> input,
[ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) =>
_readonly("mergeFromBuffer");

void mergeFromCodedBufferReader(CodedBufferReader input,
[ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) =>
_readonly("mergeFromCodedBufferReader");

void mergeFromJson(String data,
[ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) =>
_readonly("mergeFromJson");

void mergeFromJsonMap(Map<String, dynamic> json,
[ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) =>
_readonly("mergeFromJsonMap");

void mergeFromMessage(GeneratedMessage other) =>
_readonly("mergeFromMessage");

void mergeUnknownFields(UnknownFieldSet unknownFieldSet) =>
_readonly("mergeUnknownFields");

void setExtension(Extension extension, var value) =>
_readonly("setExtension");

void setField(int tagNumber, var value, [int fieldType = null]) =>
_readonly("setField");

void _readonly(String methodName) {
String messageType = info_.messageName;
throw new UnsupportedError(
"attempted to call $methodName on a read-only message ($messageType)");
}
}

class _ReadonlyUnknownFieldSet extends UnknownFieldSet {

@override
void clear() => _readonly("clear");

@override
void addField(int number, UnknownFieldSetField field) =>
_readonly("addField");

@override
void mergeField(int number, UnknownFieldSetField field) =>
_readonly("mergeField");

@override
bool mergeFieldFromBuffer(int tag, CodedBufferReader input) {
_readonly("mergeFieldFromBuffer");
return false; // not reached
}

@override
void mergeFromCodedBufferReader(CodedBufferReader input) =>
_readonly("mergeFromCodedBufferReader");

@override
void mergeFromUnknownFieldSet(UnknownFieldSet other) =>
_readonly("mergeFromUnknownFieldSet");

@override
UnknownFieldSetField _getField(int number) {
_readonly("a merge method");
return null; // not reached
}

void _readonly(String methodName) {
throw new UnsupportedError(
"attempted to call $methodName on a read-only UnknownFieldSet");
}
}
4 changes: 2 additions & 2 deletions lib/src/protobuf/unknown_field_set.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class UnknownFieldSet {
mergeFromUnknownFieldSet(unknownFieldSet);
}

UnknownFieldSet clone() => new UnknownFieldSet._clone(this);
UnknownFieldSet clone() => new UnknownFieldSet._clone(this);

Map<int, UnknownFieldSetField> asMap() => new Map.from(_fields);
Map<int, UnknownFieldSetField> asMap() => new Map.from(_fields);

void clear() {
_fields.clear();
Expand Down
50 changes: 50 additions & 0 deletions test/readonly_message_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env dart
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library readonly_message_test;

import 'package:test/test.dart' show test, expect, throwsA, predicate;

import 'package:protobuf/protobuf.dart'
show GeneratedMessage, ReadonlyMessageMixin, BuilderInfo;

throwsError(Type expectedType, String expectedMessage) => throwsA(
predicate((x) {
expect(x.runtimeType, expectedType);
expect(x.message, expectedMessage);
return true;
}));

class Rec extends GeneratedMessage with ReadonlyMessageMixin {
@override
BuilderInfo info_ = new BuilderInfo("rec");
}

main() {
test("can write a read-only message", () {
expect(new Rec().writeToBuffer(), []);
expect(new Rec().writeToJson(), "{}");
});

test("can't merge to a read-only message", () {
expect(() => new Rec().mergeFromJson("{}"), throwsError(UnsupportedError,
"attempted to call mergeFromJson on a read-only message (rec)"));
});

test("can't set a field on a read-only message", () {
expect(() => new Rec().setField(123, 456), throwsError(UnsupportedError,
"attempted to call setField on a read-only message (rec)"));
});

test("can't clear a read-only message", () {
expect(() => new Rec().clear(), throwsError(UnsupportedError,
"attempted to call clear on 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"));
});
}
14 changes: 13 additions & 1 deletion test/reserved_names_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@TestOn("vm")
library reserved_names_test;

import 'package:test/test.dart' show test, expect, equals;
import 'package:test/test.dart' show test, expect, equals, fail, TestOn;

import 'package:protobuf/meta.dart' show GeneratedMessage_reservedNames;
import 'package:protobuf/mixins_meta.dart' show findMixin;
Expand All @@ -29,6 +29,18 @@ void main() {
expect(actual.toList()..sort(), equals(expected.toList()..sort()));
});

test("ReadonlyMessageMixin doesn't add any reserved names", () {
var mixinNames =
findMemberNames('package:protobuf/protobuf.dart', #ReadonlyMessageMixin);
var reservedNames = new Set<String>.from(GeneratedMessage_reservedNames);
for (var name in mixinNames) {
if (name == "ReadonlyMessageMixin" || name == "unknownFields") continue;
if (!reservedNames.contains(name)) {
fail("name from ReadonlyMessageMixin is not reserved: ${name}");
}
}
});

test('PbMapMixin reserved names are up to date', () {
var meta = findMixin("PbMapMixin");
var actual = new Set<String>.from(meta.findReservedNames());
Expand Down

0 comments on commit aa21cf7

Please sign in to comment.