Account for double.infinity and double.nan in json serializers (#652)

Port of cl/398287072
diff --git a/protobuf/lib/protobuf.dart b/protobuf/lib/protobuf.dart
index f60d5ee..97e9044 100644
--- a/protobuf/lib/protobuf.dart
+++ b/protobuf/lib/protobuf.dart
@@ -21,6 +21,7 @@
 part 'src/protobuf/coded_buffer.dart';
 part 'src/protobuf/coded_buffer_reader.dart';
 part 'src/protobuf/coded_buffer_writer.dart';
+part 'src/protobuf/consts.dart';
 part 'src/protobuf/builder_info.dart';
 part 'src/protobuf/event_plugin.dart';
 part 'src/protobuf/exceptions.dart';
diff --git a/protobuf/lib/src/protobuf/consts.dart b/protobuf/lib/src/protobuf/consts.dart
new file mode 100644
index 0000000..a4fcd16
--- /dev/null
+++ b/protobuf/lib/src/protobuf/consts.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2021, 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.
+
+part of protobuf;
+
+/// Constant string value of double.infinity.toString() and the infinity value
+/// recognized by double.parse(..).
+const infinity = 'Infinity';
+
+/// Constant string value of double.negativeInfinity.toString() and the negative
+/// infinity value recognized by double.parse(..).
+const negativeInfinity = '-Infinity';
+
+/// Constant string value of double.nan.toString() and the NaN (not a number)
+/// value recognized by double.parse(..).
+const nan = 'NaN';
diff --git a/protobuf/lib/src/protobuf/json.dart b/protobuf/lib/src/protobuf/json.dart
index f42d12e..011323f 100644
--- a/protobuf/lib/src/protobuf/json.dart
+++ b/protobuf/lib/src/protobuf/json.dart
@@ -15,14 +15,25 @@
     switch (baseType) {
       case PbFieldType._BOOL_BIT:
       case PbFieldType._STRING_BIT:
-      case PbFieldType._FLOAT_BIT:
-      case PbFieldType._DOUBLE_BIT:
       case PbFieldType._INT32_BIT:
       case PbFieldType._SINT32_BIT:
       case PbFieldType._UINT32_BIT:
       case PbFieldType._FIXED32_BIT:
       case PbFieldType._SFIXED32_BIT:
         return fieldValue;
+      case PbFieldType._FLOAT_BIT:
+      case PbFieldType._DOUBLE_BIT:
+        final value = fieldValue as double;
+        if (value.isNaN) {
+          return nan;
+        }
+        if (value.isInfinite) {
+          return value.isNegative ? negativeInfinity : infinity;
+        }
+        if (fieldValue.toInt() == fieldValue) {
+          return fieldValue.toInt();
+        }
+        return value;
       case PbFieldType._BYTES_BIT:
         // Encode 'bytes' as a base64-encoded string.
         return base64Encode(fieldValue as List<int>);
diff --git a/protobuf/lib/src/protobuf/proto3_json.dart b/protobuf/lib/src/protobuf/proto3_json.dart
index ade908b..ac2fb63 100644
--- a/protobuf/lib/src/protobuf/proto3_json.dart
+++ b/protobuf/lib/src/protobuf/proto3_json.dart
@@ -61,13 +61,11 @@
         case PbFieldType._FLOAT_BIT:
         case PbFieldType._DOUBLE_BIT:
           double value = fieldValue;
-          if (value.isNaN) return 'NaN';
+          if (value.isNaN) {
+            return nan;
+          }
           if (value.isInfinite) {
-            if (value.isNegative) {
-              return '-Infinity';
-            } else {
-              return 'Infinity';
-            }
+            return value.isNegative ? negativeInfinity : infinity;
           }
           return value;
         case PbFieldType._UINT64_BIT:
diff --git a/protobuf/test/json_test.dart b/protobuf/test/json_test.dart
index 6291ad8..bf72b2b 100644
--- a/protobuf/test/json_test.dart
+++ b/protobuf/test/json_test.dart
@@ -105,7 +105,7 @@
     expect(decoded.int64, value);
   });
 
-  test('tesFrozentInt64JsonEncoding', () {
+  test('testFrozentInt64JsonEncoding', () {
     final value = Int64.parseInt('1234567890123456789');
     final frozen = T()
       ..int64 = value
diff --git a/protoc_plugin/test/json_test.dart b/protoc_plugin/test/json_test.dart
index 15c9326..8e5f6f1 100755
--- a/protoc_plugin/test/json_test.dart
+++ b/protoc_plugin/test/json_test.dart
@@ -14,19 +14,19 @@
 
 void main() {
   final testAllJsonTypes = '{"1":101,"2":"102","3":103,"4":"104",'
-      '"5":105,"6":"106","7":107,"8":"108","9":109,"10":"110","11":111.0,'
-      '"12":112.0,"13":true,"14":"115","15":"MTE2","16":{"17":117},'
+      '"5":105,"6":"106","7":107,"8":"108","9":109,"10":"110","11":111,'
+      '"12":112,"13":true,"14":"115","15":"MTE2","16":{"17":117},'
       '"18":{"1":118},"19":{"1":119},"20":{"1":120},"21":3,"22":6,"23":9,'
       '"24":"124","25":"125","31":[201,301],"32":["202","302"],'
       '"33":[203,303],"34":["204","304"],"35":[205,305],"36":["206","306"],'
       '"37":[207,307],"38":["208","308"],"39":[209,309],"40":["210","310"],'
-      '"41":[211.0,311.0],"42":[212.0,312.0],"43":[true,false],'
+      '"41":[211,311],"42":[212,312],"43":[true,false],'
       '"44":["215","315"],"45":["MjE2","MzE2"],"46":[{"47":217},{"47":317}],'
       '"48":[{"1":218},{"1":318}],"49":[{"1":219},{"1":319}],'
       '"50":[{"1":220},{"1":320}],"51":[2,3],"52":[5,6],"53":[8,9],'
       '"54":["224","324"],"55":["225","325"],"61":401,"62":"402","63":403,'
       '"64":"404","65":405,"66":"406","67":407,"68":"408","69":409,'
-      '"70":"410","71":411.0,"72":412.0,"73":false,"74":"415","75":"NDE2",'
+      '"70":"410","71":411,"72":412,"73":false,"74":"415","75":"NDE2",'
       '"81":1,"82":4,"83":7,"84":"424","85":"425"}';
 
   // Checks that message once serialized to JSON
@@ -128,6 +128,37 @@
     expect(parsed, expected);
   });
 
+  group('testConvertDouble', () {
+    test('WithDecimal', () {
+      final json = '{"12":1.2}';
+      TestAllTypes proto = TestAllTypes()..optionalDouble = 1.2;
+      expect(TestAllTypes.fromJson(json), proto);
+      expect(proto.writeToJson(), json);
+    });
+
+    test('WholeNumber', () {
+      final json = '{"12":5}';
+      TestAllTypes proto = TestAllTypes()..optionalDouble = 5.0;
+      expect(TestAllTypes.fromJson(json), proto);
+      expect(proto.writeToJson(), json);
+    });
+
+    test('Infinity', () {
+      final json = '{"12":"Infinity"}';
+      TestAllTypes proto = TestAllTypes()..optionalDouble = double.infinity;
+      expect(TestAllTypes.fromJson(json), proto);
+      expect(proto.writeToJson(), json);
+    });
+
+    test('NegativeInfinity', () {
+      final json = '{"12":"-Infinity"}';
+      TestAllTypes proto = TestAllTypes()
+        ..optionalDouble = double.negativeInfinity;
+      expect(TestAllTypes.fromJson(json), proto);
+      expect(proto.writeToJson(), json);
+    });
+  });
+
   test('testParseUnsignedLegacy', () {
     var parsed = TestAllTypes.fromJson(
         '{"4":"-1152921500311945216","8":"-1152921500311945215"}');
diff --git a/protoc_plugin/test/proto3_json_test.dart b/protoc_plugin/test/proto3_json_test.dart
index 791ae39..0856ee4 100644
--- a/protoc_plugin/test/proto3_json_test.dart
+++ b/protoc_plugin/test/proto3_json_test.dart
@@ -1258,4 +1258,35 @@
     expectSecondSet(Foo()..mergeFromProto3Json({'second': 1}));
     expectOneofNotSet(Foo()..mergeFromProto3Json({}));
   });
+
+  group('Convert Double', () {
+    test('With Decimal', () {
+      final json = {'optionalDouble': 1.2};
+      TestAllTypes proto = TestAllTypes()..optionalDouble = 1.2;
+      expect(TestAllTypes()..mergeFromProto3Json(json), proto);
+      expect(proto.toProto3Json(), json);
+    });
+
+    test('Whole Number', () {
+      final json = {'optionalDouble': 5};
+      TestAllTypes proto = TestAllTypes()..optionalDouble = 5.0;
+      expect(TestAllTypes()..mergeFromProto3Json(json), proto);
+      expect(proto.toProto3Json(), json);
+    });
+
+    test('Infinity', () {
+      final json = {'optionalDouble': 'Infinity'};
+      TestAllTypes proto = TestAllTypes()..optionalDouble = double.infinity;
+      expect(TestAllTypes()..mergeFromProto3Json(json), proto);
+      expect(proto.toProto3Json(), json);
+    });
+
+    test('Negative Infinity', () {
+      final json = {'optionalDouble': '-Infinity'};
+      TestAllTypes proto = TestAllTypes()
+        ..optionalDouble = double.negativeInfinity;
+      expect(TestAllTypes()..mergeFromProto3Json(json), proto);
+      expect(proto.toProto3Json(), json);
+    });
+  });
 }