Add option to do permissive parsing of proto3 json enums (#294)
diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md
index 4ff3e89..e3e0336 100644
--- a/protobuf/CHANGELOG.md
+++ b/protobuf/CHANGELOG.md
@@ -1,8 +1,9 @@
## 0.14.4
-* Added 'ensureX' methods on GeneratedMessage classes for each message field X.
+* Add `permissiveEnums` option to `mergeFromProto3Json`.
+ It will do a case-insensitive matching of enum values ignoring `-` and `_`.
- The method `ensureX()` will set X to an empty instance if `hasX()` returns false and then returns the value of X.
+* Add support for 'ensureX' methods generated by `protoc_plugin` 19.0.0.
* Add specialized getters for `String`, `int`, and `bool` with usual default values.
* Shrink dart2js generated code for `getDefault()`.
@@ -21,12 +22,12 @@
The generated code for a protofile `a.proto` that `import public "b.proto"` will export the
generated code for `b.proto`.
-
+
See https://developers.google.com/protocol-buffers/docs/proto#importing-definitions.
## 0.14.0
-* Support for proto3 json (json with field names as keys)
+* Support for proto3 json (json with field names as keys)
- encoding and decoding.
- Support for well-known types.
- Use `GeneratedMessage.toProto3Json()` to encode and `GeneratedMessage.mergeFromProto3Json(json)`
@@ -54,7 +55,7 @@
## 0.13.15
-* Add new getter `GeneratedMessage.isFrozen` to query if the message has been frozen.
+* Add new getter `GeneratedMessage.isFrozen` to query if the message has been frozen.
## 0.13.14
@@ -109,7 +110,7 @@
* Fix issue with parsing map field entries. The values for two different keys would sometimes be
merged.
-
+
* Deprecated `PBMap.add`.
## 0.13.2
diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart
index 88b54ae..03ec80c 100644
--- a/protobuf/lib/src/protobuf/generated_message.dart
+++ b/protobuf/lib/src/protobuf/generated_message.dart
@@ -234,6 +234,13 @@
/// underscores.
/// If `false` only the JSON names are supported.
///
+ /// If [permissiveEnums] is `true` (default `false`) enum values in the
+ /// JSON will be matched without case insensitivity and ignoring `-`s and `_`.
+ /// This allows JSON values like `camelCase` and `kebab-case` to match the
+ /// enum values `CAMEL_CASE` and `KEBAB_CASE`.
+ /// In case of ambiguities between the enum values, the first matching value
+ /// will be found.
+ ///
/// The [typeRegistry] is be used for decoding `Any` messages. If an `Any`
/// message encoding a type not in [typeRegistry] is encountered, a
/// [FormatException] is thrown.
@@ -243,9 +250,10 @@
void mergeFromProto3Json(Object json,
{TypeRegistry typeRegistry = const TypeRegistry.empty(),
bool ignoreUnknownFields = false,
- bool supportNamesWithUnderscores = true}) =>
+ bool supportNamesWithUnderscores = true,
+ bool permissiveEnums = false}) =>
_mergeFromProto3Json(json, _fieldSet, typeRegistry, ignoreUnknownFields,
- supportNamesWithUnderscores);
+ supportNamesWithUnderscores, permissiveEnums);
/// Merges field values from [data], a JSON object, encoded as described by
/// [GeneratedMessage.writeToJson].
diff --git a/protobuf/lib/src/protobuf/json_parsing_context.dart b/protobuf/lib/src/protobuf/json_parsing_context.dart
index ad4e139..7f27bbb 100644
--- a/protobuf/lib/src/protobuf/json_parsing_context.dart
+++ b/protobuf/lib/src/protobuf/json_parsing_context.dart
@@ -7,8 +7,10 @@
final List<String> _path = <String>[];
final bool ignoreUnknownFields;
final bool supportNamesWithUnderscores;
- JsonParsingContext(
- this.ignoreUnknownFields, this.supportNamesWithUnderscores);
+ final bool permissiveEnums;
+
+ JsonParsingContext(this.ignoreUnknownFields, this.supportNamesWithUnderscores,
+ this.permissiveEnums);
void addMapIndex(String index) {
_path.add(index);
diff --git a/protobuf/lib/src/protobuf/mixins/well_known.dart b/protobuf/lib/src/protobuf/mixins/well_known.dart
index 06fd629..e77a8ad 100644
--- a/protobuf/lib/src/protobuf/mixins/well_known.dart
+++ b/protobuf/lib/src/protobuf/mixins/well_known.dart
@@ -123,7 +123,8 @@
..mergeFromProto3Json(subJson,
typeRegistry: typeRegistry,
supportNamesWithUnderscores: context.supportNamesWithUnderscores,
- ignoreUnknownFields: context.ignoreUnknownFields);
+ ignoreUnknownFields: context.ignoreUnknownFields,
+ permissiveEnums: context.permissiveEnums);
any.value = packedMessage.writeToBuffer();
any.typeUrl = typeUrl;
diff --git a/protobuf/lib/src/protobuf/proto3_json.dart b/protobuf/lib/src/protobuf/proto3_json.dart
index d3bae84..07581f2 100644
--- a/protobuf/lib/src/protobuf/proto3_json.dart
+++ b/protobuf/lib/src/protobuf/proto3_json.dart
@@ -116,9 +116,10 @@
_FieldSet fieldSet,
TypeRegistry typeRegistry,
bool ignoreUnknownFields,
- bool supportNamesWithUnderscores) {
- JsonParsingContext context =
- JsonParsingContext(ignoreUnknownFields, supportNamesWithUnderscores);
+ bool supportNamesWithUnderscores,
+ bool permissiveEnums) {
+ JsonParsingContext context = JsonParsingContext(
+ ignoreUnknownFields, supportNamesWithUnderscores, permissiveEnums);
void recursionHelper(Object json, _FieldSet fieldSet) {
int tryParse32Bit(String s) {
@@ -195,9 +196,14 @@
case PbFieldType._ENUM_BIT:
if (value is String) {
// TODO(sigurdm): Do we want to avoid linear search here? Measure...
- return fieldInfo.enumValues.firstWhere((e) => e.name == value,
- orElse: () =>
- throw context.parseException('Unknown enum value', value));
+ final result = permissiveEnums
+ ? fieldInfo.enumValues.firstWhere(
+ (e) => _permissiveCompare(e.name, value),
+ orElse: () => null)
+ : fieldInfo.enumValues
+ .firstWhere((e) => e.name == value, orElse: () => null);
+ if (result != null) return result;
+ throw context.parseException('Unknown enum value', value);
} else if (value is int) {
return fieldInfo.valueOf(value) ??
(throw context.parseException('Unknown enum value', value));
@@ -400,3 +406,42 @@
recursionHelper(json, fieldSet);
}
+
+bool _isAsciiLetter(int char) {
+ const lowerA = 97;
+ const lowerZ = 122;
+ const capitalA = 65;
+ char |= lowerA ^ capitalA;
+ return lowerA <= char && char <= lowerZ;
+}
+
+/// Returns true if [a] and [b] are the same ignoring case and all instances of
+/// `-` and `_`.
+bool _permissiveCompare(String a, String b) {
+ const dash = 45;
+ const underscore = 95;
+
+ // Enum names are always ascii.
+ int i = 0;
+ int j = 0;
+
+ outer:
+ while (i < a.length && j < b.length) {
+ int ca = a.codeUnitAt(i);
+ if (ca == dash || ca == underscore) {
+ i++;
+ continue;
+ }
+ int cb = b.codeUnitAt(j);
+ while (cb == dash || cb == underscore) {
+ j++;
+ if (j == b.length) break outer;
+ cb = b.codeUnitAt(j);
+ }
+
+ if (ca != cb && (ca ^ cb != 0x20 || !_isAsciiLetter(ca))) return false;
+ i++;
+ j++;
+ }
+ return true;
+}
diff --git a/protoc_plugin/CHANGELOG.md b/protoc_plugin/CHANGELOG.md
index 56960e1..d37ed94 100644
--- a/protoc_plugin/CHANGELOG.md
+++ b/protoc_plugin/CHANGELOG.md
@@ -2,6 +2,8 @@
* Breaking: Generates code that requires at least `protobuf` 0.14.4.
- GeneratedMessage classes now have methods `ensureX` for each message field X.
- Add specialized getters for `String`, `int`, and `bool` with usual default values.
+* Breaking: Use unmangled names for the string representation of enum values.
+ Mangled names would lead to wrong proto3 json en- and decoding.
## 18.0.2
@@ -42,7 +44,7 @@
## 17.0.5
-* Remove unnecessary cast from generated grpc stubs.
+* Remove unnecessary cast from generated grpc stubs.
## 17.0.4
diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile
index f43fed7..f797168 100644
--- a/protoc_plugin/Makefile
+++ b/protoc_plugin/Makefile
@@ -27,6 +27,7 @@
google/protobuf/wrappers \
dart_name \
enum_extension \
+ enum_name \
extend_unittest \
ExtensionEnumNameConflict \
ExtensionNameConflict \
diff --git a/protoc_plugin/lib/enum_generator.dart b/protoc_plugin/lib/enum_generator.dart
index f3c1a9f..0899a18 100644
--- a/protoc_plugin/lib/enum_generator.dart
+++ b/protoc_plugin/lib/enum_generator.dart
@@ -108,7 +108,7 @@
final name = dartNames[val.name];
out.printlnAnnotated(
'static const ${classname} $name = '
- "${classname}._(${val.number}, ${singleQuote(name)});",
+ "${classname}._(${val.number}, ${singleQuote(val.name)});",
[
NamedLocation(
name: name,
diff --git a/protoc_plugin/test/proto3_json_test.dart b/protoc_plugin/test/proto3_json_test.dart
index d9c1fa8..50ebb1c 100644
--- a/protoc_plugin/test/proto3_json_test.dart
+++ b/protoc_plugin/test/proto3_json_test.dart
@@ -19,6 +19,7 @@
import '../out/protos/google/protobuf/unittest_well_known_types.pb.dart';
import '../out/protos/google/protobuf/wrappers.pb.dart';
+import '../out/protos/enum_name.pb.dart';
import '../out/protos/map_field.pb.dart';
import 'test_util.dart';
@@ -453,6 +454,230 @@
}, supportNamesWithUnderscores: false),
parseFailure(['optional_foreign_message']));
});
+ test('permissive enums', () {
+ final sparseB = SparseEnumMessage()..sparseEnum = TestSparseEnum.SPARSE_B;
+ expect(
+ SparseEnumMessage()..mergeFromProto3Json({'sparseEnum': 'SPARSE_B'}),
+ sparseB);
+ expect(
+ () => SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'sparse_b'}),
+ parseFailure(['sparseEnum']));
+ expect(
+ () => SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'SPARSE-B'}),
+ parseFailure(['sparseEnum']));
+ expect(
+ () => SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'sPaRsE_b'}),
+ parseFailure(['sparseEnum']));
+ expect(
+ () => SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'sparseB'}),
+ parseFailure(['sparseEnum']));
+ expect(
+ () => SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'spaRSEB'}),
+ parseFailure(['sparseEnum']));
+
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'x'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'X'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'x_'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'X_'}),
+ parseFailure(['a']));
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_x'}), AMessage()..a = A.x_);
+ expect(() => AMessage()..mergeFromProto3Json({'a': '_X'}),
+ parseFailure(['a']));
+
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'y'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'Y'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'y_'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'Y_'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': '_y'}),
+ parseFailure(['a']));
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_Y'}), AMessage()..a = A.Y_);
+
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'z'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'Z'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'z_'}),
+ parseFailure(['a']));
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'Z_'}), AMessage()..a = A.Z_);
+ expect(() => AMessage()..mergeFromProto3Json({'a': '_z'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': '_Z'}),
+ parseFailure(['a']));
+
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'a_a'}),
+ parseFailure(['a']));
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'A_A'}), AMessage()..a = A.A_A);
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'aA'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'AA'}),
+ parseFailure(['a']));
+
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'b_b'}), AMessage()..a = A.b_b);
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'B_B'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'bB'}),
+ parseFailure(['a']));
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'BB'}),
+ parseFailure(['a']));
+
+ expect(() => AMessage()..mergeFromProto3Json({'a': 'CAMEL_CASE'}),
+ parseFailure(['a']));
+ expect(AMessage()..mergeFromProto3Json({'a': 'camelCase'}),
+ AMessage()..a = A.camelCase);
+
+ expect(AMessage()..mergeFromProto3Json({'a': 'x'}, permissiveEnums: true),
+ AMessage()..a = A.x_);
+ expect(AMessage()..mergeFromProto3Json({'a': 'X'}, permissiveEnums: true),
+ AMessage()..a = A.x_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'x_'}, permissiveEnums: true),
+ AMessage()..a = A.x_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'X_'}, permissiveEnums: true),
+ AMessage()..a = A.x_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_x'}, permissiveEnums: true),
+ AMessage()..a = A.x_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_X'}, permissiveEnums: true),
+ AMessage()..a = A.x_);
+
+ expect(AMessage()..mergeFromProto3Json({'a': 'y'}, permissiveEnums: true),
+ AMessage()..a = A.Y_);
+ expect(AMessage()..mergeFromProto3Json({'a': 'Y'}, permissiveEnums: true),
+ AMessage()..a = A.Y_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'y_'}, permissiveEnums: true),
+ AMessage()..a = A.Y_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'Y_'}, permissiveEnums: true),
+ AMessage()..a = A.Y_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_y'}, permissiveEnums: true),
+ AMessage()..a = A.Y_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_Y'}, permissiveEnums: true),
+ AMessage()..a = A.Y_);
+
+ expect(AMessage()..mergeFromProto3Json({'a': 'z'}, permissiveEnums: true),
+ AMessage()..a = A.Z_);
+ expect(AMessage()..mergeFromProto3Json({'a': 'Z'}, permissiveEnums: true),
+ AMessage()..a = A.Z_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'z_'}, permissiveEnums: true),
+ AMessage()..a = A.Z_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'Z_'}, permissiveEnums: true),
+ AMessage()..a = A.Z_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_z'}, permissiveEnums: true),
+ AMessage()..a = A.Z_);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': '_Z'}, permissiveEnums: true),
+ AMessage()..a = A.Z_);
+
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'a_a'}, permissiveEnums: true),
+ AMessage()..a = A.A_A);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'A_A'}, permissiveEnums: true),
+ AMessage()..a = A.A_A);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'aA'}, permissiveEnums: true),
+ AMessage()..a = A.A_A);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'AA'}, permissiveEnums: true),
+ AMessage()..a = A.A_A);
+
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'b_b'}, permissiveEnums: true),
+ AMessage()..a = A.b_b);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'B_B'}, permissiveEnums: true),
+ AMessage()..a = A.b_b);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'bB'}, permissiveEnums: true),
+ AMessage()..a = A.b_b);
+ expect(
+ AMessage()..mergeFromProto3Json({'a': 'BB'}, permissiveEnums: true),
+ AMessage()..a = A.b_b);
+
+ expect(
+ AMessage()
+ ..mergeFromProto3Json({'a': 'CAMEL_CASE'}, permissiveEnums: true),
+ AMessage()..a = A.camelCase);
+ expect(
+ AMessage()
+ ..mergeFromProto3Json({'a': 'camelCase'}, permissiveEnums: true),
+ AMessage()..a = A.camelCase);
+
+ expect(
+ SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'sparse_b'},
+ permissiveEnums: true),
+ sparseB);
+ expect(
+ SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'SPARSE-B'},
+ permissiveEnums: true),
+ sparseB);
+ expect(
+ SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'S-P-A-R-S-E-B'},
+ permissiveEnums: true),
+ sparseB);
+ expect(
+ SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'sPaRsE_b'},
+ permissiveEnums: true),
+ sparseB);
+ expect(
+ SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'sparseB'},
+ permissiveEnums: true),
+ sparseB);
+ expect(
+ SparseEnumMessage()
+ ..mergeFromProto3Json({'sparseEnum': 'spaRSEB'},
+ permissiveEnums: true),
+ sparseB);
+ expect(
+ () => Any()
+ ..mergeFromProto3Json({
+ '@type':
+ 'type.googleapis.com/protobuf_unittest.SparseEnumMessage',
+ 'sparseEnum': 'SPARSEB'
+ }, typeRegistry: TypeRegistry([SparseEnumMessage()])),
+ parseFailure(['sparseEnum']));
+ expect(
+ Any()
+ ..mergeFromProto3Json({
+ '@type':
+ 'type.googleapis.com/protobuf_unittest.SparseEnumMessage',
+ 'sparseEnum': 'SPARSEB'
+ },
+ typeRegistry: TypeRegistry([SparseEnumMessage()]),
+ permissiveEnums: true),
+ Any.pack(sparseB),
+ reason: 'Parsing options are passed through Any messages');
+ });
test('map value', () {
TestMap expected = TestMap()
diff --git a/protoc_plugin/test/protos/enum_name.proto b/protoc_plugin/test/protos/enum_name.proto
new file mode 100644
index 0000000..9adce88
--- /dev/null
+++ b/protoc_plugin/test/protos/enum_name.proto
@@ -0,0 +1,17 @@
+syntax = "proto2";
+
+package enum.name;
+
+// Enum with non-standard value-names.
+enum A {
+ _x = 0;
+ _Y = 1;
+ Z_ = 2;
+ A_A = 4;
+ b_b = 5;
+ camelCase = 6;
+}
+
+message AMessage {
+ optional A a = 1;
+}