Emit binary coded descriptors from dart protoc_plugin. (#406)
These contain extensions to the descriptors, and thus can be used to
reflect over the options given to the descriptor
diff --git a/protoc_plugin/CHANGELOG.md b/protoc_plugin/CHANGELOG.md
index 0405d5f..1e41a67 100644
--- a/protoc_plugin/CHANGELOG.md
+++ b/protoc_plugin/CHANGELOG.md
@@ -1,7 +1,13 @@
+## 19.3.1
+
+* Emit binary coded descriptors, which can be used to reflect over the options
+ given to the descriptor.
+
## 19.3.0
* Generate constructors with optional named arguments for prefilling fields.
* Output language version 2.7 in generated files to support extension methods.
+
## 19.2.1
* Support optional proto3 fields.
diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile
index 1aaec06..df2b114 100644
--- a/protoc_plugin/Makefile
+++ b/protoc_plugin/Makefile
@@ -25,6 +25,7 @@
google/protobuf/unittest_well_known_types \
google/protobuf/unittest \
google/protobuf/wrappers \
+ custom_option \
dart_name \
default_value_escape \
enum_extension \
diff --git a/protoc_plugin/lib/code_generator.dart b/protoc_plugin/lib/code_generator.dart
index 753d3ac..2ae410c 100644
--- a/protoc_plugin/lib/code_generator.dart
+++ b/protoc_plugin/lib/code_generator.dart
@@ -28,6 +28,9 @@
String get fileImportPrefix => _getFileImportPrefix();
+ String get binaryDescriptorName =>
+ '${lowerCaseFirstLetter(classname)}Descriptor';
+
String _getFileImportPrefix() {
var path = fileGen.protoFileUri.toString();
if (importPrefixes.containsKey(path)) {
@@ -43,6 +46,21 @@
///
/// (Represents the .pb.dart file that we need to import in order to use it.)
FileGenerator get fileGen;
+
+ // The generator containing this entity.
+ ProtobufContainer get _parent;
+
+ /// The top-level parent of this entity. If this entity is a top-level entity,
+ /// returns this.
+ ProtobufContainer get toplevelParent {
+ if (_parent == null) {
+ return null;
+ }
+ if (_parent is FileGenerator) {
+ return this;
+ }
+ return _parent.toplevelParent;
+ }
}
class CodeGenerator extends ProtobufContainer {
@@ -112,5 +130,7 @@
@override
FileGenerator get fileGen => null;
@override
+ ProtobufContainer get _parent => null;
+ @override
List<int> get fieldPath => [];
}
diff --git a/protoc_plugin/lib/enum_generator.dart b/protoc_plugin/lib/enum_generator.dart
index c27541a..e02ac7f 100644
--- a/protoc_plugin/lib/enum_generator.dart
+++ b/protoc_plugin/lib/enum_generator.dart
@@ -11,6 +11,7 @@
}
class EnumGenerator extends ProtobufContainer {
+ @override
final ProtobufContainer _parent;
@override
final String classname;
@@ -170,6 +171,8 @@
var name = getJsonConstant(fileGen);
var json = _descriptor.writeToJsonMap();
+ out.println('@$_coreImportPrefix.Deprecated'
+ '(\'Use ${toplevelParent.binaryDescriptorName} instead\')');
out.print("const $name = ");
writeJsonConst(out, json);
out.println(";");
diff --git a/protoc_plugin/lib/file_generator.dart b/protoc_plugin/lib/file_generator.dart
index f985c64..80150dc 100644
--- a/protoc_plugin/lib/file_generator.dart
+++ b/protoc_plugin/lib/file_generator.dart
@@ -9,13 +9,18 @@
const String _protobufImportPrefix = r'$pb';
const String _asyncImportPrefix = r'$async';
const String _coreImportPrefix = r'$core';
+const String _convertImportPrefix = r'$convert';
const String _fixnumImportPrefix = r'$fixnum';
const String _grpcImportPrefix = r'$grpc';
const String _mixinImportPrefix = r'$mixin';
+const String _typedDataImportPrefix = r'$typed_data';
const String _protobufImport =
"import 'package:protobuf/protobuf.dart' as $_protobufImportPrefix;";
const String _asyncImport = "import 'dart:async' as $_asyncImportPrefix;";
const String _coreImport = "import 'dart:core' as $_coreImportPrefix;";
+const String _typedDataImport =
+ "import 'dart:typed_data' as $_typedDataImportPrefix;";
+const String _convertImport = "import 'dart:convert' as $_convertImportPrefix;";
const String _grpcImport =
"import 'package:grpc/service_api.dart' as $_grpcImportPrefix;";
@@ -207,6 +212,8 @@
@override
FileGenerator get fileGen => this;
@override
+ ProtobufContainer get _parent => null;
+ @override
List<int> get fieldPath => [];
/// Generates all the Dart files for this .proto file.
@@ -437,7 +444,8 @@
[OutputConfiguration config = const DefaultOutputConfiguration()]) {
if (!_linked) throw StateError("not linked");
var out = makeWriter();
- _writeHeading(out);
+ _writeHeading(out,
+ extraIgnores: {'deprecated_member_use_from_same_package'});
if (serviceGenerators.isNotEmpty) {
out.println(_asyncImport);
@@ -508,13 +516,27 @@
return _formatter.format(out.toString());
}
+ void writeBinaryDescriptor(IndentingWriter out, String identifierName,
+ String name, GeneratedMessage descriptor) {
+ var descriptorText = base64Encode(descriptor.writeToBuffer());
+ out.println('/// Descriptor for `$name`. Decode as a '
+ '`${descriptor.info_.qualifiedMessageName}`.');
+ out.println('final $_typedDataImportPrefix.Uint8List '
+ '$identifierName = '
+ '$_convertImportPrefix.base64Decode(\'$descriptorText\');');
+ }
+
/// Returns the contents of the .pbjson.dart file for this .proto file.
String generateJsonFile(
[OutputConfiguration config = const DefaultOutputConfiguration()]) {
if (!_linked) throw StateError("not linked");
var out = makeWriter();
- _writeHeading(out);
+ _writeHeading(out,
+ extraIgnores: {'deprecated_member_use_from_same_package'});
+ out.println(_coreImport);
+ out.println(_convertImport);
+ out.println(_typedDataImport);
// Import the .pbjson.dart files we depend on.
var imports = _findJsonProtosToImport();
for (var target in imports) {
@@ -524,12 +546,18 @@
for (var e in enumGenerators) {
e.generateConstants(out);
+ writeBinaryDescriptor(
+ out, e.binaryDescriptorName, e._descriptor.name, e._descriptor);
}
for (var m in messageGenerators) {
m.generateConstants(out);
+ writeBinaryDescriptor(
+ out, m.binaryDescriptorName, m._descriptor.name, m._descriptor);
}
for (var s in serviceGenerators) {
s.generateConstants(out);
+ writeBinaryDescriptor(
+ out, s.binaryDescriptorName, s._descriptor.name, s._descriptor);
}
return out.toString();
@@ -553,14 +581,17 @@
}
/// Writes the header at the top of the dart file.
- void _writeHeading(IndentingWriter out) {
+ void _writeHeading(IndentingWriter out,
+ {Set<String> extraIgnores = const <String>{}}) {
+ var extraIgnoresString =
+ extraIgnores.isEmpty ? '' : ',${extraIgnores.join(',')}';
out.println('''
///
// Generated code. Do not modify.
// source: ${descriptor.name}
//
// @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields$extraIgnoresString
''');
}
diff --git a/protoc_plugin/lib/grpc_generator.dart b/protoc_plugin/lib/grpc_generator.dart
index 1d67d72..71bebf1 100644
--- a/protoc_plugin/lib/grpc_generator.dart
+++ b/protoc_plugin/lib/grpc_generator.dart
@@ -181,8 +181,7 @@
factory _GrpcMethod(GrpcServiceGenerator service, GenerationContext ctx,
MethodDescriptorProto method) {
final grpcName = method.name;
- final dartName =
- grpcName.substring(0, 1).toLowerCase() + grpcName.substring(1);
+ final dartName = lowerCaseFirstLetter(grpcName);
final clientStreaming = method.clientStreaming;
final serverStreaming = method.serverStreaming;
diff --git a/protoc_plugin/lib/message_generator.dart b/protoc_plugin/lib/message_generator.dart
index 79e46d1..dd0bc0c 100644
--- a/protoc_plugin/lib/message_generator.dart
+++ b/protoc_plugin/lib/message_generator.dart
@@ -51,6 +51,7 @@
PbMixin mixin;
+ @override
final ProtobufContainer _parent;
final DescriptorProto _descriptor;
final List<EnumGenerator> _enumGenerators = <EnumGenerator>[];
@@ -683,6 +684,8 @@
var nestedEnumNames =
_enumGenerators.map((e) => e.getJsonConstant(fileGen)).toList();
+ out.println('@$_coreImportPrefix.Deprecated'
+ '(\'Use ${toplevelParent.binaryDescriptorName} instead\')');
out.addBlock("const $name = const {", "};", () {
for (var key in json.keys) {
out.print("'$key': ");
diff --git a/protoc_plugin/lib/names.dart b/protoc_plugin/lib/names.dart
index b93d79e..9e022ca 100644
--- a/protoc_plugin/lib/names.dart
+++ b/protoc_plugin/lib/names.dart
@@ -431,9 +431,8 @@
/// The name to use by default for the Dart getter and setter.
/// (A suffix will be added if there is a conflict.)
-String _defaultFieldName(String fieldMethodSuffix) {
- return '${fieldMethodSuffix[0].toLowerCase()}${fieldMethodSuffix.substring(1)}';
-}
+String _defaultFieldName(String fieldMethodSuffix) =>
+ lowerCaseFirstLetter(fieldMethodSuffix);
String _defaultHasMethodName(String fieldMethodSuffix) =>
'has$fieldMethodSuffix';
@@ -583,3 +582,6 @@
// The number of entries is one higher than the highest seen index.
return highestIndexSeen + 1;
}
+
+String lowerCaseFirstLetter(String input) =>
+ input[0].toLowerCase() + input.substring(1);
diff --git a/protoc_plugin/lib/protoc.dart b/protoc_plugin/lib/protoc.dart
index f836d94..8757465 100644
--- a/protoc_plugin/lib/protoc.dart
+++ b/protoc_plugin/lib/protoc.dart
@@ -1,5 +1,6 @@
library protoc;
+import 'dart:convert';
import 'dart:io';
import 'package:dart_style/dart_style.dart';
diff --git a/protoc_plugin/lib/service_generator.dart b/protoc_plugin/lib/service_generator.dart
index 31ed67c..af32009 100644
--- a/protoc_plugin/lib/service_generator.dart
+++ b/protoc_plugin/lib/service_generator.dart
@@ -131,8 +131,7 @@
List<MethodDescriptorProto> get _methodDescriptors => _descriptor.method;
- String _methodName(String name) =>
- name.substring(0, 1).toLowerCase() + name.substring(1);
+ String _methodName(String name) => lowerCaseFirstLetter(name);
String get _parentClass => _generatedService;
@@ -224,6 +223,9 @@
for (var key in _transitiveDeps.keys) {
typeConstants[key] = _transitiveDeps[key].getJsonConstant(fileGen);
}
+
+ out.println('@$_coreImportPrefix.Deprecated'
+ '(\'Use ${binaryDescriptorName} instead\')');
out.addBlock("const $messageJsonConstant = const {", "};", () {
for (var key in typeConstants.keys) {
var typeConst = typeConstants[key];
@@ -241,6 +243,14 @@
}
}
+ String get binaryDescriptorName {
+ var prefix = lowerCaseFirstLetter(classname);
+ if (prefix.endsWith('Base')) {
+ prefix = prefix.substring(0, prefix.length - 4);
+ }
+ return '${prefix}Descriptor';
+ }
+
static final String _future = '$_asyncImportPrefix.Future';
static final String _generatedMessage =
'$_protobufImportPrefix.GeneratedMessage';
diff --git a/protoc_plugin/pubspec.yaml b/protoc_plugin/pubspec.yaml
index 49562ba..b3e5cb9 100644
--- a/protoc_plugin/pubspec.yaml
+++ b/protoc_plugin/pubspec.yaml
@@ -1,5 +1,5 @@
name: protoc_plugin
-version: 19.3.0
+version: 19.3.1
description: Protoc compiler plugin to generate Dart code
homepage: https://github.com/dart-lang/protobuf
diff --git a/protoc_plugin/test/all_tests.dart b/protoc_plugin/test/all_tests.dart
index fb96be2..fc3408f 100755
--- a/protoc_plugin/test/all_tests.dart
+++ b/protoc_plugin/test/all_tests.dart
@@ -10,6 +10,7 @@
import 'client_generator_test.dart' as client_generator;
import 'const_generator_test.dart' as const_generator;
import 'default_value_escape_test.dart' as default_value_escape;
+import 'descriptor_test.dart' as descriptor_test;
import 'enum_generator_test.dart' as enum_generator;
import 'extension_generator_test.dart' as extension_generator_test;
import 'extension_test.dart' as extension_test;
@@ -50,6 +51,7 @@
client_generator.main();
const_generator.main();
default_value_escape.main();
+ descriptor_test.main();
enum_generator.main();
extension_generator_test.main();
extension_test.main();
diff --git a/protoc_plugin/test/descriptor_test.dart b/protoc_plugin/test/descriptor_test.dart
new file mode 100644
index 0000000..b58353a
--- /dev/null
+++ b/protoc_plugin/test/descriptor_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2020, 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.
+
+import 'package:protoc_plugin/src/descriptor.pb.dart';
+import 'package:protobuf/protobuf.dart';
+import '../out/protos/google/protobuf/unittest.pbjson.dart';
+import '../out/protos/custom_option.pb.dart';
+import '../out/protos/custom_option.pbjson.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('Can decode message descriptor', () {
+ final descriptor = DescriptorProto.fromBuffer(testAllTypesDescriptor);
+ expect(descriptor.name, 'TestAllTypes');
+ final nestedEnumDescriptor = descriptor.enumType.first;
+ expect(nestedEnumDescriptor.name, 'NestedEnum');
+ });
+ test('Can decode enum descriptor', () {
+ final descriptor = EnumDescriptorProto.fromBuffer(foreignEnumDescriptor);
+ expect(descriptor.name, 'ForeignEnum');
+ expect(descriptor.value.map((v) => v.name),
+ ['FOREIGN_FOO', 'FOREIGN_BAR', 'FOREIGN_BAZ']);
+ });
+ test('Can decode service descriptor', () {
+ final descriptor = ServiceDescriptorProto.fromBuffer(testServiceDescriptor);
+ expect(descriptor.name, 'TestService');
+ expect(descriptor.method.map((m) => m.name), ['Foo', 'Bar']);
+ });
+ test('Can read custom options', () {
+ final registry = ExtensionRegistry()..add(Custom_option.myOption);
+ final descriptor =
+ DescriptorProto.fromBuffer(myMessageDescriptor, registry);
+ final option = descriptor.options.getExtension(Custom_option.myOption);
+ expect(option, 'Hello world!');
+ });
+}
diff --git a/protoc_plugin/test/goldens/oneMessage.pbjson b/protoc_plugin/test/goldens/oneMessage.pbjson
index 0cac887..b26d869 100644
--- a/protoc_plugin/test/goldens/oneMessage.pbjson
+++ b/protoc_plugin/test/goldens/oneMessage.pbjson
@@ -3,8 +3,12 @@
// source: test
//
// @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
+@$core.Deprecated('Use phoneNumberDescriptor instead')
const PhoneNumber$json = const {
'1': 'PhoneNumber',
'2': const [
@@ -14,3 +18,5 @@
],
};
+/// Descriptor for `PhoneNumber`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List phoneNumberDescriptor = $convert.base64Decode('CgtQaG9uZU51bWJlchIWCgZudW1iZXIYASACKAlSBm51bWJlchIUCgR0eXBlGAIgASgFMgBSBHR5cGUSFQoEbmFtZRgDIAEoCToBJFIEbmFtZQ==');
diff --git a/protoc_plugin/test/goldens/service.pbserver b/protoc_plugin/test/goldens/service.pbserver
index 66d6e79..dbc1c3b 100644
--- a/protoc_plugin/test/goldens/service.pbserver
+++ b/protoc_plugin/test/goldens/service.pbserver
@@ -3,7 +3,7 @@
// source: test
//
// @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
import 'dart:async' as $async;
diff --git a/protoc_plugin/test/goldens/serviceGenerator.pb.json b/protoc_plugin/test/goldens/serviceGenerator.pb.json
index 7a33efe..30784bf 100644
--- a/protoc_plugin/test/goldens/serviceGenerator.pb.json
+++ b/protoc_plugin/test/goldens/serviceGenerator.pb.json
@@ -3,18 +3,27 @@
// source: testpkg.proto
//
// @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
import 'foobar.pbjson.dart' as $1;
+@$core.Deprecated('Use someRequestDescriptor instead')
const SomeRequest$json = const {
'1': 'SomeRequest',
};
+/// Descriptor for `SomeRequest`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List someRequestDescriptor = $convert.base64Decode('CgtTb21lUmVxdWVzdA==');
+@$core.Deprecated('Use someReplyDescriptor instead')
const SomeReply$json = const {
'1': 'SomeReply',
};
+/// Descriptor for `SomeReply`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List someReplyDescriptor = $convert.base64Decode('CglTb21lUmVwbHk=');
const TestServiceBase$json = const {
'1': 'Test',
'2': const [
@@ -23,6 +32,7 @@
],
};
+@$core.Deprecated('Use testServiceDescriptor instead')
const TestServiceBase$messageJson = const {
'.testpkg.SomeRequest': SomeRequest$json,
'.testpkg.SomeReply': SomeReply$json,
@@ -30,3 +40,5 @@
'.foo.bar.AnotherReply': $1.AnotherReply$json,
};
+/// Descriptor for `Test`. Decode as a `google.protobuf.ServiceDescriptorProto`.
+final $typed_data.Uint8List testServiceDescriptor = $convert.base64Decode('CgRUZXN0EjMKB0FNZXRob2QSFC50ZXN0cGtnLlNvbWVSZXF1ZXN0GhIudGVzdHBrZy5Tb21lUmVwbHkSPQoNQW5vdGhlck1ldGhvZBIVLmZvby5iYXIuRW1wdHlNZXNzYWdlGhUuZm9vLmJhci5Bbm90aGVyUmVwbHk=');
diff --git a/protoc_plugin/test/goldens/topLevelEnum.pbjson b/protoc_plugin/test/goldens/topLevelEnum.pbjson
index 6a549fb..03bbcd9 100644
--- a/protoc_plugin/test/goldens/topLevelEnum.pbjson
+++ b/protoc_plugin/test/goldens/topLevelEnum.pbjson
@@ -3,8 +3,12 @@
// source: test
//
// @dart = 2.7
-// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
+// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
+import 'dart:core' as $core;
+import 'dart:convert' as $convert;
+import 'dart:typed_data' as $typed_data;
+@$core.Deprecated('Use phoneTypeDescriptor instead')
const PhoneType$json = const {
'1': 'PhoneType',
'2': const [
@@ -15,3 +19,5 @@
],
};
+/// Descriptor for `PhoneType`. Decode as a `google.protobuf.EnumDescriptorProto`.
+final $typed_data.Uint8List phoneTypeDescriptor = $convert.base64Decode('CglQaG9uZVR5cGUSCgoGTU9CSUxFEAASCAoESE9NRRABEggKBFdPUksQAhIMCghCVVNJTkVTUxAC');
diff --git a/protoc_plugin/test/message_test.dart b/protoc_plugin/test/message_test.dart
index 0a43062..555b83c 100755
--- a/protoc_plugin/test/message_test.dart
+++ b/protoc_plugin/test/message_test.dart
@@ -3,6 +3,8 @@
// 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.
+// ignore_for_file: deprecated_member_use_from_same_package
+
library message_test;
import 'package:protoc_plugin/src/descriptor.pb.dart' show DescriptorProto;
diff --git a/protoc_plugin/test/protos/custom_option.proto b/protoc_plugin/test/protos/custom_option.proto
new file mode 100644
index 0000000..05d39bd
--- /dev/null
+++ b/protoc_plugin/test/protos/custom_option.proto
@@ -0,0 +1,15 @@
+// Copyright (c) 2020, 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.
+
+syntax = "proto2";
+
+import "descriptor.proto";
+
+extend google.protobuf.MessageOptions {
+ optional string my_option = 51234;
+}
+
+message MyMessage {
+ option (my_option) = "Hello world!";
+}