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;
+}