Allow application of external mixins to generated dart protos.
These are defined using the new dart_option "mixins".
For example usage, see test/protos/mixins.proto.
This change requires issue 2090033002 to be submitted first, which introduces protobuf version 0.5.2.
R=skybrian@google.com
Review URL: https://chromiumcodereview.appspot.com//2086253002 .
diff --git a/Makefile b/Makefile
index e1d95c4..3f9c344 100644
--- a/Makefile
+++ b/Makefile
@@ -21,6 +21,7 @@
enum_extension \
map_api \
map_api2 \
+ mixins \
multiple_files_test \
nested_extension \
non_nested_extension \
diff --git a/lib/file_generator.dart b/lib/file_generator.dart
index 65cba47..a921786 100644
--- a/lib/file_generator.dart
+++ b/lib/file_generator.dart
@@ -4,23 +4,89 @@
part of protoc;
+final _dartIdentifier = new RegExp(r'^\w+$');
+
/// Generates the Dart output files for one .proto input file.
///
/// Outputs include .pb.dart, pbenum.dart, and .pbjson.dart.
class FileGenerator extends ProtobufContainer {
- /// Returns the the mixin to use by default in this file,
- /// or null for no mixin by default.
- static PbMixin _getDefaultMixin(FileDescriptorProto desc) {
- if (!desc.hasOptions()) return null;
- if (!desc.options.hasExtension(Dart_options.defaultMixin)) {
- return null;
+ /// Reads and the declared mixins in the file, keyed by name.
+ ///
+ /// Performs some basic validation on declared mixins, e.g. whether names
+ /// are valid dart identifiers and whether there are cycles in the `parent`
+ /// hierarchy.
+ /// Does not check for existence of import files or classes.
+ static Map<String, PbMixin> _getDeclaredMixins(FileDescriptorProto desc) {
+ String mixinError(String error) =>
+ 'Option "mixins" in file ${desc.name}: $error';
+
+ if (!desc.hasOptions() ||
+ !desc.options.hasExtension(Dart_options.importedMixins)) {
+ return <String, PbMixin>{};
}
- var name = desc.options.getExtension(Dart_options.defaultMixin);
- PbMixin mixin = findMixin(name);
- if (mixin == null) {
- throw ("unknown mixin class: ${name}");
+ var dartMixins = <String, DartMixin>{};
+ ImportedMixins importedMixins =
+ desc.options.getExtension(Dart_options.importedMixins);
+ for (DartMixin mixin in importedMixins.mixins) {
+ if (dartMixins.containsKey(mixin.name)) {
+ throw mixinError('Duplicate mixin name: "${mixin.name}"');
+ }
+ if (!mixin.name.startsWith(_dartIdentifier)) {
+ throw mixinError(
+ '"${mixin.name}" is not a valid dart class identifier');
+ }
+ if (mixin.hasParent() && !mixin.parent.startsWith(_dartIdentifier)) {
+ throw mixinError('Mixin parent "${mixin.parent}" of "${mixin.name}" is '
+ 'not a valid dart class identifier');
+ }
+ dartMixins[mixin.name] = mixin;
}
- return mixin;
+
+ // Detect cycles and unknown parents.
+ for (var mixin in dartMixins.values) {
+ if (!mixin.hasParent()) continue;
+ var currentMixin = mixin;
+ var parentChain = <String>[];
+ while (currentMixin.hasParent()) {
+ var parentName = currentMixin.parent;
+
+ bool declaredMixin = dartMixins.containsKey(parentName);
+ bool internalMixin = !declaredMixin && findMixin(parentName) != null;
+
+ if (internalMixin) break; // No further validation of parent chain.
+
+ if (!declaredMixin) {
+ throw mixinError('Unknown mixin parent "${mixin.parent}" of '
+ '"${currentMixin.name}"');
+ }
+
+ if (parentChain.contains(parentName)) {
+ var cycle = parentChain.join('->') + '->$parentName';
+ throw mixinError('Cycle in parent chain: $cycle');
+ }
+ parentChain.add(parentName);
+ currentMixin = dartMixins[parentName];
+ }
+ }
+
+ // Turn DartMixins into PbMixins.
+ final pbMixins = <String, PbMixin>{};
+ PbMixin resolveMixin(String name) {
+ if (pbMixins.containsKey(name)) return pbMixins[name];
+ if (dartMixins.containsKey(name)) {
+ var dartMixin = dartMixins[name];
+ var pbMixin = new PbMixin(dartMixin.name,
+ importFrom: dartMixin.importFrom,
+ parent: resolveMixin(dartMixin.parent));
+ pbMixins[name] = pbMixin;
+ return pbMixin;
+ }
+ return findMixin(name);
+ }
+ for (var mixin in dartMixins.values) {
+ resolveMixin(mixin.name);
+ }
+ return pbMixins;
}
final FileDescriptorProto _fileDescriptor;
@@ -45,15 +111,23 @@
throw "FAILURE: Import with absolute path is not supported";
}
- var defaultMixin = _getDefaultMixin(_fileDescriptor);
+ var declaredMixins = _getDeclaredMixins(descriptor);
+ var defaultMixinName =
+ descriptor.options?.getExtension(Dart_options.defaultMixin) ?? '';
+ var defaultMixin =
+ declaredMixins[defaultMixinName] ?? findMixin(defaultMixinName);
+ if (defaultMixin == null && defaultMixinName.isNotEmpty) {
+ throw ('Option default_mixin on file ${descriptor.name}: Unknown mixin '
+ '$defaultMixinName');
+ }
// Load and register all enum and message types.
for (EnumDescriptorProto enumType in _fileDescriptor.enumType) {
enumGenerators.add(new EnumGenerator(enumType, this));
}
for (DescriptorProto messageType in _fileDescriptor.messageType) {
- messageGenerators
- .add(new MessageGenerator(messageType, this, defaultMixin));
+ messageGenerators.add(new MessageGenerator(
+ messageType, this, declaredMixins, defaultMixin));
}
for (FieldDescriptorProto extension in _fileDescriptor.extension) {
extensionGenerators.add(new ExtensionGenerator(extension, this));
diff --git a/lib/message_generator.dart b/lib/message_generator.dart
index 4dd64bb..40c6028 100644
--- a/lib/message_generator.dart
+++ b/lib/message_generator.dart
@@ -5,18 +5,23 @@
part of protoc;
class MessageGenerator extends ProtobufContainer {
- // Returns the mixin for this message, or null if none.
- static PbMixin _getMixin(DescriptorProto desc, PbMixin defaultValue) {
- if (!desc.hasOptions()) return defaultValue;
- if (!desc.options.hasExtension(Dart_options.mixin)) return defaultValue;
+ /// Returns the mixin for this message, or null if none.
+ ///
+ /// First searches [declaredMixins], then internal mixins declared by
+ /// [findMixin].
+ static PbMixin _getMixin(DescriptorProto desc,
+ Map<String, PbMixin> declaredMixins, PbMixin defaultMixin) {
+ PbMixin getMixinByName(String name) {
+ if (name.isEmpty) return null; // don't use a mixin (override any default)
+ return declaredMixins[name] ?? findMixin(name);
+ }
+
+ if (!desc.hasOptions() || !desc.options.hasExtension(Dart_options.mixin)) {
+ return defaultMixin;
+ }
String name = desc.options.getExtension(Dart_options.mixin);
- if (name.isEmpty) return null; // don't use a mixin (override any default)
- var mixin = findMixin(name);
- if (mixin == null) {
- throw ("unknown mixin class: ${name}");
- }
- return mixin;
+ return getMixinByName(name);
}
final String classname;
@@ -33,7 +38,7 @@
List<ProtobufField> _fieldList;
MessageGenerator(DescriptorProto descriptor, ProtobufContainer parent,
- PbMixin defaultMixin)
+ Map<String, PbMixin> declaredMixins, PbMixin defaultMixin)
: _descriptor = descriptor,
_parent = parent,
classname = (parent.classname == '')
@@ -44,13 +49,14 @@
: (parent.fqname == '.'
? '.${descriptor.name}'
: '${parent.fqname}.${descriptor.name}'),
- mixin = _getMixin(descriptor, defaultMixin) {
+ mixin = _getMixin(descriptor, declaredMixins, defaultMixin) {
for (EnumDescriptorProto e in _descriptor.enumType) {
_enumGenerators.add(new EnumGenerator(e, this));
}
for (DescriptorProto n in _descriptor.nestedType) {
- _messageGenerators.add(new MessageGenerator(n, this, defaultMixin));
+ _messageGenerators
+ .add(new MessageGenerator(n, this, declaredMixins, defaultMixin));
}
for (FieldDescriptorProto x in _descriptor.extension) {
diff --git a/lib/protoc.dart b/lib/protoc.dart
index f8fde96..94cb333 100644
--- a/lib/protoc.dart
+++ b/lib/protoc.dart
@@ -3,17 +3,17 @@
import 'dart:async';
import 'dart:io';
-import 'package:protobuf/protobuf.dart';
import 'package:protobuf/mixins_meta.dart';
+import 'package:protobuf/protobuf.dart';
import 'package:path/path.dart' as path;
+import 'src/dart_options.pb.dart';
import 'src/descriptor.pb.dart';
import 'src/plugin.pb.dart';
-import 'src/dart_options.pb.dart';
import 'const_generator.dart' show writeJsonConst;
-import 'names.dart';
import 'indenting_writer.dart' show IndentingWriter;
+import 'names.dart';
part 'base_type.dart';
part 'client_generator.dart';
diff --git a/lib/src/dart_options.pb.dart b/lib/src/dart_options.pb.dart
index ea4f483..92961dd 100644
--- a/lib/src/dart_options.pb.dart
+++ b/lib/src/dart_options.pb.dart
@@ -5,13 +5,110 @@
import 'package:protobuf/protobuf.dart';
+class DartOptions extends GeneratedMessage {
+ static final BuilderInfo _i = new BuilderInfo('DartOptions')
+ ..hasRequiredFields = false
+ ;
+
+ DartOptions() : super();
+ DartOptions.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
+ DartOptions.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
+ DartOptions clone() => new DartOptions()..mergeFromMessage(this);
+ BuilderInfo get info_ => _i;
+ static DartOptions create() => new DartOptions();
+ static PbList<DartOptions> createRepeated() => new PbList<DartOptions>();
+ static DartOptions getDefault() {
+ if (_defaultInstance == null) _defaultInstance = new _ReadonlyDartOptions();
+ return _defaultInstance;
+ }
+ static DartOptions _defaultInstance;
+ static void $checkItem(DartOptions v) {
+ if (v is !DartOptions) checkItemFailed(v, 'DartOptions');
+ }
+}
+
+class _ReadonlyDartOptions extends DartOptions with ReadonlyMessageMixin {}
+
+class DartMixin extends GeneratedMessage {
+ static final BuilderInfo _i = new BuilderInfo('DartMixin')
+ ..a/*<String>*/(1, 'name', PbFieldType.OS)
+ ..a/*<String>*/(2, 'importFrom', PbFieldType.OS)
+ ..a/*<String>*/(3, 'parent', PbFieldType.OS)
+ ..hasRequiredFields = false
+ ;
+
+ DartMixin() : super();
+ DartMixin.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
+ DartMixin.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
+ DartMixin clone() => new DartMixin()..mergeFromMessage(this);
+ BuilderInfo get info_ => _i;
+ static DartMixin create() => new DartMixin();
+ static PbList<DartMixin> createRepeated() => new PbList<DartMixin>();
+ static DartMixin getDefault() {
+ if (_defaultInstance == null) _defaultInstance = new _ReadonlyDartMixin();
+ return _defaultInstance;
+ }
+ static DartMixin _defaultInstance;
+ static void $checkItem(DartMixin v) {
+ if (v is !DartMixin) checkItemFailed(v, 'DartMixin');
+ }
+
+ String get name => $_get(0, 1, '');
+ void set name(String v) { $_setString(0, 1, v); }
+ bool hasName() => $_has(0, 1);
+ void clearName() => clearField(1);
+
+ String get importFrom => $_get(1, 2, '');
+ void set importFrom(String v) { $_setString(1, 2, v); }
+ bool hasImportFrom() => $_has(1, 2);
+ void clearImportFrom() => clearField(2);
+
+ String get parent => $_get(2, 3, '');
+ void set parent(String v) { $_setString(2, 3, v); }
+ bool hasParent() => $_has(2, 3);
+ void clearParent() => clearField(3);
+}
+
+class _ReadonlyDartMixin extends DartMixin with ReadonlyMessageMixin {}
+
+class ImportedMixins extends GeneratedMessage {
+ static final BuilderInfo _i = new BuilderInfo('ImportedMixins')
+ ..pp/*<DartMixin>*/(1, 'mixins', PbFieldType.PM, DartMixin.$checkItem, DartMixin.create)
+ ..hasRequiredFields = false
+ ;
+
+ ImportedMixins() : super();
+ ImportedMixins.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
+ ImportedMixins.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
+ ImportedMixins clone() => new ImportedMixins()..mergeFromMessage(this);
+ BuilderInfo get info_ => _i;
+ static ImportedMixins create() => new ImportedMixins();
+ static PbList<ImportedMixins> createRepeated() => new PbList<ImportedMixins>();
+ static ImportedMixins getDefault() {
+ if (_defaultInstance == null) _defaultInstance = new _ReadonlyImportedMixins();
+ return _defaultInstance;
+ }
+ static ImportedMixins _defaultInstance;
+ static void $checkItem(ImportedMixins v) {
+ if (v is !ImportedMixins) checkItemFailed(v, 'ImportedMixins');
+ }
+
+ List<DartMixin> get mixins => $_get(0, 1, null);
+}
+
+class _ReadonlyImportedMixins extends ImportedMixins with ReadonlyMessageMixin {}
+
class Dart_options {
+ static final Extension importedMixins = new Extension<ImportedMixins>('FileOptions', 'importedMixins', 28125061, PbFieldType.OM, ImportedMixins.getDefault, ImportedMixins.create);
static final Extension defaultMixin = new Extension<String>('FileOptions', 'defaultMixin', 96128839, PbFieldType.OS);
static final Extension mixin = new Extension<String>('MessageOptions', 'mixin', 96128839, PbFieldType.OS);
+ static final Extension override = new Extension<bool>('FieldOptions', 'override', 28205290, PbFieldType.OB);
static final Extension dartName = new Extension<String>('FieldOptions', 'dartName', 28700919, PbFieldType.OS);
static void registerAllExtensions(ExtensionRegistry registry) {
+ registry.add(importedMixins);
registry.add(defaultMixin);
registry.add(mixin);
+ registry.add(override);
registry.add(dartName);
}
}
diff --git a/lib/testing/mixins.dart b/lib/testing/mixins.dart
new file mode 100644
index 0000000..694840c
--- /dev/null
+++ b/lib/testing/mixins.dart
@@ -0,0 +1,15 @@
+abstract class Mixin1 {
+ String get overriddenString => "mixin1";
+
+ String get interfaceString;
+ set interfaceString(String string);
+ bool hasInterfaceString();
+}
+
+abstract class Mixin2 {
+ String get overriddenString => "mixin2";
+
+ bool hasOverriddenHasMethod() => false;
+}
+
+abstract class Mixin3 {}
diff --git a/pubspec.yaml b/pubspec.yaml
index 7bab074..4acb45c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -8,7 +8,7 @@
dependencies:
fixnum: ^0.10.5
path: ^1.0.0
- protobuf: ^0.5.1
+ protobuf: ^0.5.2
dev_dependencies:
browser: any
test: ^0.12.0
diff --git a/test/all_tests.dart b/test/all_tests.dart
index c0e1e9d..3e6cbf9 100755
--- a/test/all_tests.dart
+++ b/test/all_tests.dart
@@ -18,6 +18,7 @@
import 'map_test.dart' as map;
import 'message_generator_test.dart' as message_generator;
import 'message_test.dart' as message;
+import 'mixin_test.dart' as mixin;
import 'protoc_options_test.dart' as protoc_options;
import 'repeated_field_test.dart' as repeated_field;
import 'service_test.dart' as service;
@@ -40,6 +41,7 @@
map.main();
message_generator.main();
message.main();
+ mixin.main();
protoc_options.main();
repeated_field.main();
service.main();
diff --git a/test/message_generator_test.dart b/test/message_generator_test.dart
index 9cb0a33..e28c401 100755
--- a/test/message_generator_test.dart
+++ b/test/message_generator_test.dart
@@ -7,9 +7,10 @@
import 'package:protoc_plugin/indenting_writer.dart';
import 'package:protoc_plugin/protoc.dart';
+import 'package:test/test.dart';
+
import 'package:protoc_plugin/src/descriptor.pb.dart';
import 'package:protoc_plugin/src/plugin.pb.dart';
-import 'package:test/test.dart';
void main() {
test('testMessageGenerator', () {
@@ -127,7 +128,7 @@
new CodeGeneratorRequest(), new CodeGeneratorResponse());
FileGenerator fg = new FileGenerator(fd);
- MessageGenerator mg = new MessageGenerator(md, fg, null);
+ MessageGenerator mg = new MessageGenerator(md, fg, {}, null);
var ctx = new GenerationContext(options);
mg.register(ctx);
diff --git a/test/mixin_test.dart b/test/mixin_test.dart
new file mode 100644
index 0000000..2e8c789
--- /dev/null
+++ b/test/mixin_test.dart
@@ -0,0 +1,67 @@
+import 'package:test/test.dart';
+
+import '../out/protos/mixins.pb.dart' as pb;
+import 'package:protoc_plugin/testing/mixins.dart';
+
+void main() {
+ group('Proto with Mixin1', () {
+ pb.Mixin1PB proto;
+
+ setUp(() {
+ proto = new pb.Mixin1PB();
+ });
+
+ test('is a Mixin1', () {
+ expect(proto is Mixin1, isTrue);
+ expect(proto is Mixin2, isFalse);
+ });
+
+ test('implements interface defined by mixins', () {
+ proto.interfaceString = 'test';
+ expect(proto.hasInterfaceString(), isTrue);
+ expect(proto.interfaceString, equals('test'));
+ });
+ });
+
+ group('Proto with Mixin2', () {
+ pb.Mixin2PB proto;
+
+ setUp(() {
+ proto = new pb.Mixin2PB();
+ });
+
+ test('overrides has method', () {
+ expect(proto.hasOverriddenHasMethod(), isFalse);
+ proto.overriddenHasMethod = 'test';
+
+ expect(proto.hasOverriddenHasMethod(), isTrue);
+ });
+ });
+
+ group('Proto without mixins', () {
+ pb.NoMixinPB proto;
+
+ setUp(() {
+ proto = new pb.NoMixinPB();
+ });
+
+ test('is neither Mixin1 nor Mixin2', () {
+ expect(proto is Mixin1, isFalse);
+ expect(proto is Mixin2, isFalse);
+ });
+ });
+
+ group('Proto with Mixin3', () {
+ pb.Mixin3PB proto;
+
+ setUp(() {
+ proto = new pb.Mixin3PB();
+ });
+
+ test('is both Mixin1 (from parent) and Mixin3', () {
+ expect(proto is Mixin1, isTrue);
+ expect(proto is Mixin2, isFalse);
+ expect(proto is Mixin3, isTrue);
+ });
+ });
+}
diff --git a/test/protos/dart_options.proto b/test/protos/dart_options.proto
index 7a16d81..7de2170 100644
--- a/test/protos/dart_options.proto
+++ b/test/protos/dart_options.proto
@@ -10,8 +10,38 @@
import "descriptor_2_5_opensource.proto";
+// A mixin to be applied in the 'with' clause of a generated proto in dart.
+message DartMixin {
+ // Class name of the mixin class.
+ optional string name = 1;
+
+ // File from which the mixin class is imported.
+ // The generated Dart code will use this in an import statement.
+ optional string import_from = 2;
+
+ // Used to apply multiple mixins to a proto.
+ // The mixin listed in parent will always be applied before this one,
+ // making the generated proto implement both mixins.
+ optional string parent = 3;
+}
+
+// Defines mixins imported from dart files to be used on messages in this file.
+message ImportedMixins {
+
+ // The DartMixins to be imported.
+ repeated DartMixin mixins = 1;
+}
+
extend google.protobuf.FileOptions {
+ // Defines the named mixins to be used on messages in this file.
+ // These mixins are in addition to internally defined mixins (e.g PbMapMixin)
+ // and may override them.
+ // Warning: there is no checking at compile time for name conflicts between
+ // identifiers defined in the mixin and proto-generated identifiers.
+ // Running dartanalyzer on the generated file might find some of them.
+ optional ImportedMixins imported_mixins = 28125061;
+
// Applies the named mixin to all messages in this file.
// (May be overridden by the "mixin" option on a message.)
// For now, "PbMapMixin" is the only available mixin.
diff --git a/test/protos/mixins.proto b/test/protos/mixins.proto
new file mode 100644
index 0000000..fccd674
--- /dev/null
+++ b/test/protos/mixins.proto
@@ -0,0 +1,41 @@
+syntax = "proto2";
+
+import "dart_options.proto";
+
+option (dart_options.imported_mixins) = {
+ mixins: [{
+ name: "Mixin1"
+ import_from: "package:protoc_plugin/testing/mixins.dart"
+ },
+ {
+ name: "Mixin2"
+ import_from: "package:protoc_plugin/testing/mixins.dart"
+ },
+ {
+ name: "Mixin3"
+ import_from: "package:protoc_plugin/testing/mixins.dart"
+ parent: "Mixin1"
+ }];
+};
+
+option (dart_options.default_mixin) = "Mixin1";
+
+message NoMixinPB {
+ option (dart_options.mixin) = "";
+}
+
+message Mixin1PB {
+ optional string interface_string = 1;
+}
+
+message Mixin2PB {
+ option (dart_options.mixin) = "Mixin2";
+
+ optional string overridden_has_method = 3;
+}
+
+message Mixin3PB {
+ option (dart_options.mixin) = "Mixin3";
+
+ optional string interface_string = 1;
+}