Permit unknown enum values when ignoreUnknownFields=true. (#354)

This change allows for the backend to add additional values to an enum without parsing failures occurring on the client.

This is consistent with Java behavior.
diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart
index 13f4dfb..a22d8c4 100644
--- a/protobuf/lib/src/protobuf/generated_message.dart
+++ b/protobuf/lib/src/protobuf/generated_message.dart
@@ -227,8 +227,8 @@
   /// Well-known types and their special JSON encoding are supported.
   ///
   /// If [ignoreUnknownFields] is `false` (the default) an
-  /// [FormatException] is be thrown if an unknown field name
-  /// is encountered. Otherwise the unknown field is ignored.
+  /// [FormatException] is be thrown if an unknown field or enum name
+  /// is encountered. Otherwise the unknown field or enum is ignored.
   ///
   /// If [supportNamesWithUnderscores] is `true` (the default) field names in
   /// the JSON can be represented as either camel-case JSON-names or names with
diff --git a/protobuf/lib/src/protobuf/proto3_json.dart b/protobuf/lib/src/protobuf/proto3_json.dart
index f0a03b2..f76f065 100644
--- a/protobuf/lib/src/protobuf/proto3_json.dart
+++ b/protobuf/lib/src/protobuf/proto3_json.dart
@@ -202,11 +202,14 @@
                     orElse: () => null)
                 : fieldInfo.enumValues
                     .firstWhere((e) => e.name == value, orElse: () => null);
-            if (result != null) return result;
+            if ((result != null) || ignoreUnknownFields) 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));
+                (ignoreUnknownFields
+                    ? null
+                    : (throw context.parseException(
+                        'Unknown enum value', value)));
           }
           throw context.parseException(
               'Expected enum as a string or integer', value);
diff --git a/protobuf/test/json_test.dart b/protobuf/test/json_test.dart
index ce2f792..c94c304 100644
--- a/protobuf/test/json_test.dart
+++ b/protobuf/test/json_test.dart
@@ -4,10 +4,11 @@
 library json_test;
 
 import 'dart:convert';
+
 import 'package:fixnum/fixnum.dart' show Int64;
 import 'package:test/test.dart';
 
-import 'mock_util.dart' show T;
+import 'mock_util.dart' show T, mockEnumValues;
 
 void main() {
   var example = T()
@@ -15,6 +16,54 @@
     ..str = 'hello'
     ..int32s.addAll(<int>[1, 2, 3]);
 
+  test('testProto3JsonEnum', () {
+    // No enum value specified.
+    expect(example.hasEnm, isFalse);
+    // Defaults to first when it doesn't exist.
+    expect(example.enm, equals(mockEnumValues.first));
+    expect((example..mergeFromProto3Json({'enm': 'a'})).enm.name, equals('a'));
+    // Now it's explicitly set after merging.
+    expect(example.hasEnm, isTrue);
+
+    expect((example..mergeFromProto3Json({'enm': 'b'})).enm.name, equals('b'));
+    // "c" is not a legal enum value.
+    expect(
+        () => example..mergeFromProto3Json({'enm': 'c'}),
+        throwsA(allOf(isFormatException,
+            predicate((e) => e.message.contains('Unknown enum value')))));
+    // `example` hasn't changed.
+    expect(example.hasEnm, isTrue);
+    expect(example.enm.name, equals('b'));
+
+    // "c" is not a legal enum value, but we are ignoring unknown fields, so
+    // default behavior is to unset `enm`, returning the default value "a"
+    expect(
+        (example..mergeFromProto3Json({'enm': 'c'}, ignoreUnknownFields: true))
+            .enm
+            .name,
+        equals('a'));
+    expect(example.hasEnm, isFalse);
+
+    // Same for index values...
+    expect((example..mergeFromProto3Json({'enm': 2})).enm.name, 'b');
+    expect(
+        () => example..mergeFromProto3Json({'enm': 3}),
+        throwsA(allOf(isFormatException,
+            predicate((e) => e.message.contains('Unknown enum value')))));
+    // `example` hasn't changed.
+    expect(example.hasEnm, isTrue);
+    expect(example.enm.name, equals('b'));
+
+    // "c" is not a legal enum value, but we are ignoring unknown fields, so
+    // default behavior is to unset `enm`, returning the default value "a"
+    expect(
+        (example..mergeFromProto3Json({'enm': 3}, ignoreUnknownFields: true))
+            .enm
+            .name,
+        equals('a'));
+    expect(example.hasEnm, isFalse);
+  });
+
   test('testWriteToJson', () {
     var json = example.writeToJson();
     checkJsonMap(jsonDecode(json));
diff --git a/protobuf/test/map_mixin_test.dart b/protobuf/test/map_mixin_test.dart
index ec60cbb..e37529d 100644
--- a/protobuf/test/map_mixin_test.dart
+++ b/protobuf/test/map_mixin_test.dart
@@ -33,7 +33,7 @@
 
     expect(r.isEmpty, false);
     expect(r.isNotEmpty, true);
-    expect(r.keys, ['val', 'str', 'child', 'int32s', 'int64']);
+    expect(r.keys, ['val', 'str', 'child', 'int32s', 'int64', 'enm']);
 
     expect(r['val'], 42);
     expect(r['str'], '');
@@ -42,11 +42,12 @@
     expect(r['int32s'], []);
 
     var v = r.values;
-    expect(v.length, 5);
+    expect(v.length, 6);
     expect(v.first, 42);
     expect(v.toList()[1], '');
     expect(v.toList()[3].toString(), '[]');
-    expect(v.last, 0);
+    expect(v.toList()[4], 0);
+    expect(v.toList()[5].name, 'a');
   });
 
   test('operator []= sets record fields', () {
diff --git a/protobuf/test/mock_util.dart b/protobuf/test/mock_util.dart
index 2153dfc..565a5bf 100644
--- a/protobuf/test/mock_util.dart
+++ b/protobuf/test/mock_util.dart
@@ -6,15 +6,27 @@
 
 import 'package:fixnum/fixnum.dart' show Int64;
 import 'package:protobuf/protobuf.dart'
-    show GeneratedMessage, BuilderInfo, CreateBuilderFunc, PbFieldType;
+    show
+        BuilderInfo,
+        CreateBuilderFunc,
+        GeneratedMessage,
+        PbFieldType,
+        ProtobufEnum;
 
+final mockEnumValues = [ProtobufEnum(1, 'a'), ProtobufEnum(2, 'b')];
 BuilderInfo mockInfo(String className, CreateBuilderFunc create) {
   return BuilderInfo(className)
     ..a(1, 'val', PbFieldType.O3, defaultOrMaker: 42)
     ..a(2, 'str', PbFieldType.OS)
     ..a(3, 'child', PbFieldType.OM, defaultOrMaker: create, subBuilder: create)
     ..p<int>(4, 'int32s', PbFieldType.P3)
-    ..a(5, 'int64', PbFieldType.O6);
+    ..a(5, 'int64', PbFieldType.O6)
+    // 6 is reserved for extensions in other tests.
+    ..e(7, 'enm', PbFieldType.OE,
+        defaultOrMaker: mockEnumValues.first,
+        valueOf: (i) =>
+            mockEnumValues.firstWhere((e) => e.value == i, orElse: () => null),
+        enumValues: mockEnumValues);
 }
 
 /// A minimal protobuf implementation for testing.
@@ -37,6 +49,9 @@
   Int64 get int64 => $_get(4, Int64(0));
   set int64(x) => setField(5, x);
 
+  ProtobufEnum get enm => $_getN(5);
+  bool get hasEnm => $_has(5);
+
   @override
   GeneratedMessage clone() {
     var create = info_.byName['child'].subBuilder;