diff --git a/benchmarks/bin/to_proto3_json_string.dart b/benchmarks/bin/to_proto3_json_string.dart index 1a3407d27..7a6247b6a 100644 --- a/benchmarks/bin/to_proto3_json_string.dart +++ b/benchmarks/bin/to_proto3_json_string.dart @@ -2,8 +2,6 @@ // 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 'dart:convert' show jsonEncode; - import 'package:protobuf_benchmarks/benchmark_base.dart'; import 'package:protobuf_benchmarks/generated/google_message1_proto2.pb.dart' as p2; @@ -26,9 +24,9 @@ class Benchmark extends BenchmarkBase { @override void run() { - jsonEncode(_message1Proto2.toProto3Json()); - jsonEncode(_message1Proto3.toProto3Json()); - jsonEncode(_message2.toProto3Json()); + _message1Proto2.toProto3JsonString(); + _message1Proto3.toProto3JsonString(); + _message2.toProto3JsonString(); } } diff --git a/benchmarks/pubspec.lock b/benchmarks/pubspec.lock index 151ca4a60..c99963eac 100644 --- a/benchmarks/pubspec.lock +++ b/benchmarks/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + jsontool: + dependency: transitive + description: + name: jsontool + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" lints: dependency: "direct dev" description: diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md index 3e59845e8..092ff6103 100644 --- a/protobuf/CHANGELOG.md +++ b/protobuf/CHANGELOG.md @@ -20,6 +20,11 @@ missing. ([#719], [#745]) * Fix updating frozen (immutable) messages with merge methods (`mergeFromBuffer`, `mergeFromProto3Json`, ...). ([#489], [#727]) +* New `GeneratedMessage` method `toProto3JsonString` added to generate proto3 + JSON string of a message. This method is much more efficient than generating + proto3 JSON object of a message with `toProto3Json` and then encoding that + object with `dart:convert`'s `jsonEncode`. ([#683]) +* `GeneratedMessage.writeToJson` performance improved. ([#683]) [#183]: https://github.com/google/protobuf.dart/issues/183 [#644]: https://github.com/google/protobuf.dart/pull/644 @@ -37,6 +42,8 @@ [#745]: https://github.com/google/protobuf.dart/pull/745 [#489]: https://github.com/google/protobuf.dart/issues/489 [#727]: https://github.com/google/protobuf.dart/pull/727 +[jsontool]: https://pub.dev/packages/jsontool +[#683]: https://github.com/google/protobuf.dart/pull/683 ## 2.1.0 diff --git a/protobuf/lib/meta.dart b/protobuf/lib/meta.dart index 519bbd052..5ad59a640 100644 --- a/protobuf/lib/meta.dart +++ b/protobuf/lib/meta.dart @@ -52,6 +52,7 @@ const GeneratedMessage_reservedNames = [ 'toBuilder', 'toDebugString', 'toProto3Json', + 'toProto3JsonString', 'toString', 'unknownFields', 'writeToBuffer', @@ -79,6 +80,7 @@ const GeneratedMessage_reservedNames = [ r'$_setSignedInt32', r'$_setString', r'$_setUnsignedInt32', + r'$_toProto3JsonSink', r'$_whichOneof', ]; diff --git a/protobuf/lib/protobuf.dart b/protobuf/lib/protobuf.dart index b4d4ca996..f7e2ea52f 100644 --- a/protobuf/lib/protobuf.dart +++ b/protobuf/lib/protobuf.dart @@ -8,12 +8,12 @@ library protobuf; import 'dart:collection' show ListBase, MapBase; -import 'dart:convert' - show base64Decode, base64Encode, jsonEncode, jsonDecode, Utf8Codec; +import 'dart:convert' show base64Decode, base64Encode, jsonDecode, Utf8Codec; import 'dart:math' as math; import 'dart:typed_data' show TypedData, Uint8List, ByteData, Endian; import 'package:fixnum/fixnum.dart' show Int64; +import 'package:jsontool/jsontool.dart'; import 'package:meta/meta.dart' show UseResult; import 'src/protobuf/json_parsing_context.dart'; @@ -38,11 +38,13 @@ part 'src/protobuf/field_set.dart'; part 'src/protobuf/field_type.dart'; part 'src/protobuf/generated_message.dart'; part 'src/protobuf/generated_service.dart'; -part 'src/protobuf/json.dart'; +part 'src/protobuf/json_reader.dart'; +part 'src/protobuf/json_writer.dart'; part 'src/protobuf/pb_list.dart'; part 'src/protobuf/pb_map.dart'; part 'src/protobuf/protobuf_enum.dart'; -part 'src/protobuf/proto3_json.dart'; +part 'src/protobuf/proto3_json_reader.dart'; +part 'src/protobuf/proto3_json_writer.dart'; part 'src/protobuf/rpc_client.dart'; part 'src/protobuf/unknown_field_set.dart'; part 'src/protobuf/utils.dart'; diff --git a/protobuf/lib/src/protobuf/builder_info.dart b/protobuf/lib/src/protobuf/builder_info.dart index 36d04d7be..c560da43b 100644 --- a/protobuf/lib/src/protobuf/builder_info.dart +++ b/protobuf/lib/src/protobuf/builder_info.dart @@ -41,11 +41,15 @@ class BuilderInfo { List? _sortedByTag; - // For well-known types. - final Object? Function(GeneratedMessage message, TypeRegistry typeRegistry)? + /// JSON generator for well-known types. + final void Function( + GeneratedMessage msg, TypeRegistry typeRegistry, JsonSink jsonSink)? toProto3Json; + + /// JSON parser for well-known types. final Function(GeneratedMessage targetMessage, Object json, TypeRegistry typeRegistry, JsonParsingContext context)? fromProto3Json; + final CreateBuilderFunc? createEmptyInstance; BuilderInfo(String? messageName, diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart index c77a17e2e..aab0e35c3 100644 --- a/protobuf/lib/src/protobuf/generated_message.dart +++ b/protobuf/lib/src/protobuf/generated_message.dart @@ -200,7 +200,14 @@ abstract class GeneratedMessage { /// Returns the JSON encoding of this message as a Dart [Map]. /// /// The encoding is described in [GeneratedMessage.writeToJson]. - Map writeToJsonMap() => _writeToJsonMap(_fieldSet); + Map writeToJsonMap() { + Object? object; + final objectSink = jsonObjectWriter((newObject) { + object = newObject; + }); + _writeToJsonMapSink(_fieldSet, objectSink); + return object as Map; + } /// Returns a JSON string that encodes this message. /// @@ -215,24 +222,57 @@ abstract class GeneratedMessage { /// represented as their integer value. /// /// For the proto3 JSON format use: [toProto3Json]. - String writeToJson() => jsonEncode(writeToJsonMap()); + String writeToJson() { + final buf = StringBuffer(); + final stringSink = jsonStringWriter(buf); + _writeToJsonMapSink(_fieldSet, stringSink); + return buf.toString(); + } - /// Returns an Object representing Proto3 JSON serialization of `this`. + /// Returns Dart JSON object encoding this message, following proto3 JSON + /// format. /// - /// The key for each field is be the camel-cased name of the field. + /// Key for a field is the the camel-case name of the field. /// - /// Well-known types and their special JSON encoding are supported. - /// If a well-known type cannot be encoded (eg. a `google.protobuf.Timestamp` - /// with negative `nanoseconds`) an error is thrown. + /// Well-known types and their special JSON encodings are supported. /// /// Extensions and unknown fields are not encoded. /// - /// The [typeRegistry] is be used for encoding `Any` messages. If an `Any` - /// message encoding a type not in [typeRegistry] is encountered, an - /// error is thrown. + /// [typeRegistry] is used for encoding `Any` messages. + /// + /// Throws [ArgumentError] if type of an `Any` message is not in + /// [typeRegistry]. + /// + /// Throws [ArgumentError] if a well-known type cannot be encoded. For + /// example, when a `google.protobuf.Timestamp` has negative `nanoseconds`. Object? toProto3Json( - {TypeRegistry typeRegistry = const TypeRegistry.empty()}) => - _writeToProto3Json(_fieldSet, typeRegistry); + {TypeRegistry typeRegistry = const TypeRegistry.empty()}) { + Object? object; + final objectSink = jsonObjectWriter((newObject) { + object = newObject; + }); + _writeToProto3JsonSink(_fieldSet, typeRegistry, objectSink); + return object; + } + + /// Returns a proto3 JSON string encoding of this message. + /// + /// See [toProto3Json] for details. + String toProto3JsonString( + {TypeRegistry typeRegistry = const TypeRegistry.empty()}) { + final buf = StringBuffer(); + final stringSink = jsonStringWriter(buf); + _writeToProto3JsonSink(_fieldSet, typeRegistry, stringSink); + return buf.toString(); + } + + /// For generated code only. + /// @nodoc + void $_toProto3JsonSink(TypeRegistry typeRegistry, JsonSink jsonSink, + {bool newMessage = true}) { + _writeToProto3JsonSink(_fieldSet, typeRegistry, jsonSink, + newMessage: newMessage); + } /// Merges field values from [json], a JSON object using proto3 encoding. /// diff --git a/protobuf/lib/src/protobuf/json.dart b/protobuf/lib/src/protobuf/json_reader.dart similarity index 70% rename from protobuf/lib/src/protobuf/json.dart rename to protobuf/lib/src/protobuf/json_reader.dart index 6d9875cc5..ee1d5357c 100644 --- a/protobuf/lib/src/protobuf/json.dart +++ b/protobuf/lib/src/protobuf/json_reader.dart @@ -4,94 +4,6 @@ part of protobuf; -Map _writeToJsonMap(_FieldSet fs) { - dynamic convertToMap(dynamic fieldValue, int fieldType) { - var baseType = PbFieldType._baseType(fieldType); - - if (_isRepeated(fieldType)) { - final PbList list = fieldValue; - return List.from(list.map((e) => convertToMap(e, baseType))); - } - - switch (baseType) { - case PbFieldType._BOOL_BIT: - case PbFieldType._STRING_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); - case PbFieldType._ENUM_BIT: - final ProtobufEnum enum_ = fieldValue; - return enum_.value; // assume |value| < 2^52 - case PbFieldType._INT64_BIT: - case PbFieldType._SINT64_BIT: - case PbFieldType._SFIXED64_BIT: - return fieldValue.toString(); - case PbFieldType._UINT64_BIT: - case PbFieldType._FIXED64_BIT: - final Int64 int_ = fieldValue; - return int_.toStringUnsigned(); - case PbFieldType._GROUP_BIT: - case PbFieldType._MESSAGE_BIT: - final GeneratedMessage msg = fieldValue; - return msg.writeToJsonMap(); - default: - throw 'Unknown type $fieldType'; - } - } - - List _writeMap(PbMap fieldValue, MapFieldInfo fi) => - List.from(fieldValue.entries.map((MapEntry e) => { - '${PbMap._keyFieldNumber}': convertToMap(e.key, fi.keyFieldType), - '${PbMap._valueFieldNumber}': - convertToMap(e.value, fi.valueFieldType) - })); - - var result = {}; - for (var fi in fs._infosSortedByTag) { - var value = fs._values[fi.index!]; - if (value == null || (value is List && value.isEmpty)) { - continue; // It's missing, repeated, or an empty byte array. - } - if (_isMapField(fi.type)) { - result['${fi.tagNumber}'] = - _writeMap(value, fi as MapFieldInfo); - continue; - } - result['${fi.tagNumber}'] = convertToMap(value, fi.type); - } - final extensions = fs._extensions; - if (extensions != null) { - for (var tagNumber in _sorted(extensions._tagNumbers)) { - var value = extensions._values[tagNumber]; - if (value is List && value.isEmpty) { - continue; // It's repeated or an empty byte array. - } - var fi = extensions._getInfoOrNull(tagNumber)!; - result['$tagNumber'] = convertToMap(value, fi.type); - } - } - return result; -} - // Merge fields from a previously decoded JSON object. // (Called recursively on nested messages.) void _mergeFromJsonMap( @@ -146,14 +58,14 @@ void _appendJsonMap(BuilderInfo meta, _FieldSet fs, List jsonList, final convertedKey = _convertJsonValue( entryMeta, entryFieldSet, - jsonEntry['${PbMap._keyFieldNumber}'], + jsonEntry[PbMap._keyFieldNumberString], PbMap._keyFieldNumber, fi.keyFieldType, registry); var convertedValue = _convertJsonValue( entryMeta, entryFieldSet, - jsonEntry['${PbMap._valueFieldNumber}'], + jsonEntry[PbMap._valueFieldNumberString], PbMap._valueFieldNumber, fi.valueFieldType, registry); diff --git a/protobuf/lib/src/protobuf/json_writer.dart b/protobuf/lib/src/protobuf/json_writer.dart new file mode 100644 index 000000000..a40e23f6c --- /dev/null +++ b/protobuf/lib/src/protobuf/json_writer.dart @@ -0,0 +1,138 @@ +// Copyright (c) 2015, 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; + +void _writeToJsonMapSink(_FieldSet fs, JsonSink jsonSink) { + dynamic convertToMap(dynamic fieldValue, int fieldType) { + var baseType = PbFieldType._baseType(fieldType); + + if (_isRepeated(fieldType)) { + final List listValue = fieldValue; + jsonSink.startArray(); + for (final value in listValue) { + convertToMap(value, baseType); + } + jsonSink.endArray(); + return; + } + + switch (baseType) { + case PbFieldType._BOOL_BIT: + jsonSink.addBool(fieldValue); + return; + + case PbFieldType._STRING_BIT: + jsonSink.addString(fieldValue); + return; + + case PbFieldType._INT32_BIT: + case PbFieldType._SINT32_BIT: + case PbFieldType._UINT32_BIT: + case PbFieldType._FIXED32_BIT: + case PbFieldType._SFIXED32_BIT: + jsonSink.addNumber(fieldValue); + return; + + case PbFieldType._FLOAT_BIT: + case PbFieldType._DOUBLE_BIT: + final double doubleValue = fieldValue; + if (doubleValue.isNaN) { + jsonSink.addString(_nan); + return; + } + if (doubleValue.isInfinite) { + jsonSink.addString( + doubleValue.isNegative ? _negativeInfinity : _infinity); + return; + } + if (fieldValue.toInt() == fieldValue) { + jsonSink.addNumber(fieldValue.toInt()); + return; + } + jsonSink.addNumber(doubleValue); + return; + + case PbFieldType._BYTES_BIT: + // Encode 'bytes' as a base64-encoded string. + final List listValue = fieldValue; + jsonSink.addString(base64Encode(listValue)); + return; + + case PbFieldType._ENUM_BIT: + final ProtobufEnum enum_ = fieldValue; + jsonSink.addNumber(enum_.value); // assume |value| < 2^52 + return; + + case PbFieldType._INT64_BIT: + case PbFieldType._SINT64_BIT: + case PbFieldType._SFIXED64_BIT: + jsonSink.addString(fieldValue.toString()); + return; + + case PbFieldType._UINT64_BIT: + case PbFieldType._FIXED64_BIT: + final Int64 int_ = fieldValue; + jsonSink.addString(int_.toStringUnsigned()); + return; + + case PbFieldType._GROUP_BIT: + case PbFieldType._MESSAGE_BIT: + final GeneratedMessage messageValue = fieldValue; + _writeToJsonMapSink(messageValue._fieldSet, jsonSink); + return; + + default: + throw 'Unknown type $fieldType'; + } + } + + void _writeMap(PbMap fieldValue, MapFieldInfo fi) { + jsonSink.startArray(); + for (final e in fieldValue.entries) { + jsonSink.startObject(); + + jsonSink.addKey(PbMap._keyFieldNumberString); + convertToMap(e.key, fi.keyFieldType); + + jsonSink.addKey(PbMap._valueFieldNumberString); + convertToMap(e.value, fi.valueFieldType); + + jsonSink.endObject(); + } + jsonSink.endArray(); + } + + jsonSink.startObject(); + + for (var fi in fs._infosSortedByTag) { + var value = fs._values[fi.index!]; + if (value == null || (value is List && value.isEmpty)) { + continue; // It's missing, repeated, or an empty byte array. + } + if (_isMapField(fi.type)) { + final MapFieldInfo mapFi = fi as dynamic; + jsonSink.addKey(fi.tagNumber.toString()); + _writeMap(value, mapFi); + continue; + } + jsonSink.addKey(fi.tagNumber.toString()); + convertToMap(value, fi.type); + } + + final extensions = fs._extensions; + if (extensions != null) { + for (var tagNumber in _sorted(extensions._tagNumbers)) { + var value = extensions._values[tagNumber]; + if (value is List && value.isEmpty) { + continue; // It's repeated or an empty byte array. + } + var fi = extensions._getInfoOrNull(tagNumber)!; + jsonSink.addKey(tagNumber.toString()); + convertToMap(value, fi.type); + } + } + + jsonSink.endObject(); +} diff --git a/protobuf/lib/src/protobuf/mixins/well_known.dart b/protobuf/lib/src/protobuf/mixins/well_known.dart index 66854c9eb..2ad8a74b8 100644 --- a/protobuf/lib/src/protobuf/mixins/well_known.dart +++ b/protobuf/lib/src/protobuf/mixins/well_known.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:fixnum/fixnum.dart'; +import 'package:jsontool/jsontool.dart'; import '../../../protobuf.dart'; import '../json_parsing_context.dart'; @@ -76,23 +77,25 @@ abstract class AnyMixin implements GeneratedMessage { // "@type": "type.googleapis.com/google.protobuf.Duration", // "value": "1.212s" // } - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { var any = message as AnyMixin; - var info = typeRegistry.lookup(_typeNameFromUrl(any.typeUrl)); + final info = typeRegistry.lookup(_typeNameFromUrl(any.typeUrl)); if (info == null) { throw ArgumentError( 'The type of the Any message (${any.typeUrl}) is not in the given typeRegistry.'); } - var unpacked = info.createEmptyInstance!()..mergeFromBuffer(any.value); - var proto3Json = unpacked.toProto3Json(typeRegistry: typeRegistry); + final unpacked = info.createEmptyInstance!()..mergeFromBuffer(any.value); + jsonSink.startObject(); + jsonSink.addKey('@type'); + jsonSink.addString(any.typeUrl); if (info.toProto3Json == null) { - var map = proto3Json as Map; - map['@type'] = any.typeUrl; - return map; + unpacked.$_toProto3JsonSink(typeRegistry, jsonSink, newMessage: false); } else { - return {'@type': any.typeUrl, 'value': proto3Json}; + jsonSink.addKey('value'); + unpacked.$_toProto3JsonSink(typeRegistry, jsonSink, newMessage: true); } + jsonSink.endObject(); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -191,8 +194,8 @@ abstract class TimestampMixin { // // For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past // 01:30 UTC on January 15, 2017. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { var timestamp = message as TimestampMixin; var dateTime = timestamp.toDateTime(); @@ -225,7 +228,7 @@ abstract class TimestampMixin { .padLeft(9, '0') .replaceFirst(finalGroupsOfThreeZeroes, ''); } - return '$y-$m-${d}T$h:$min:$sec${secFrac}Z'; + jsonSink.addString('$y-$m-${d}T$h:$min:$sec${secFrac}Z'); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -268,8 +271,8 @@ abstract class DurationMixin { static final RegExp finalZeroes = RegExp(r'0+$'); - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { var duration = message as DurationMixin; var secFrac = duration.nanos // nanos and seconds should always have the same sign. @@ -278,7 +281,7 @@ abstract class DurationMixin { .padLeft(9, '0') .replaceFirst(finalZeroes, ''); var secPart = secFrac == '' ? '' : '.$secFrac'; - return '${duration.seconds}${secPart}s'; + jsonSink.addString('${duration.seconds}${secPart}s'); } static final RegExp durationPattern = RegExp(r'(-?\d*)(?:\.(\d*))?s$'); @@ -312,11 +315,16 @@ abstract class StructMixin implements GeneratedMessage { // From google/protobuf/struct.proto: // The JSON representation for `Struct` is JSON object. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { var struct = message as StructMixin; - return struct.fields.map((key, value) => - MapEntry(key, ValueMixin.toProto3JsonHelper(value, typeRegistry))); + + jsonSink.startObject(); + for (var entry in struct.fields.entries) { + jsonSink.addKey(entry.key); + ValueMixin.toProto3JsonHelper(entry.value, typeRegistry, jsonSink); + } + jsonSink.endObject(); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -370,23 +378,24 @@ abstract class ValueMixin implements GeneratedMessage { // From google/protobuf/struct.proto: // The JSON representation for `Value` is JSON value - static Object? toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { var value = message as ValueMixin; // This would ideally be a switch, but we cannot import the enum we are // switching over. if (value.hasNullValue()) { - return null; + jsonSink.addNull(); } else if (value.hasNumberValue()) { - return value.numberValue; + jsonSink.addNumber(value.numberValue); } else if (value.hasStringValue()) { - return value.stringValue; + jsonSink.addString(value.stringValue); } else if (value.hasBoolValue()) { - return value.boolValue; + jsonSink.addBool(value.boolValue); } else if (value.hasStructValue()) { - return StructMixin.toProto3JsonHelper(value.structValue, typeRegistry); + StructMixin.toProto3JsonHelper(value.structValue, typeRegistry, jsonSink); } else if (value.hasListValue()) { - return ListValueMixin.toProto3JsonHelper(value.listValue, typeRegistry); + ListValueMixin.toProto3JsonHelper( + value.listValue, typeRegistry, jsonSink); } else { throw ArgumentError('Serializing google.protobuf.Value with no value'); } @@ -429,12 +438,14 @@ abstract class ListValueMixin implements GeneratedMessage { // From google/protobuf/struct.proto: // The JSON representation for `ListValue` is JSON array. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - var list = message as ListValueMixin; - return list.values - .map((value) => ValueMixin.toProto3JsonHelper(value, typeRegistry)) - .toList(); + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + final list = message as ListValueMixin; + jsonSink.startArray(); + for (final value in list.values) { + ValueMixin.toProto3JsonHelper(value, typeRegistry, jsonSink); + } + jsonSink.endArray(); } static const _valueFieldTagNumber = 1; @@ -467,8 +478,8 @@ abstract class FieldMaskMixin { // In JSON, a field mask is encoded as a single string where paths are // separated by a comma. Fields name in each path are converted // to/from lower-camel naming conventions. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { var fieldMask = message as FieldMaskMixin; for (var path in fieldMask.paths) { if (path.contains(RegExp('[A-Z]|_[^a-z]'))) { @@ -476,7 +487,7 @@ abstract class FieldMaskMixin { 'Bad fieldmask $path. Does not round-trip to json.'); } } - return fieldMask.paths.map(_toCamelCase).join(','); + jsonSink.addString(fieldMask.paths.map(_toCamelCase).join(',')); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -516,9 +527,9 @@ abstract class DoubleValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `DoubleValue` is JSON number. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as DoubleValueMixin).value; + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addNumber((message as DoubleValueMixin).value); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -542,9 +553,9 @@ abstract class FloatValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `FloatValue` is JSON number. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as FloatValueMixin).value; + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addNumber((message as FloatValueMixin).value); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -568,9 +579,9 @@ abstract class Int64ValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `Int64Value` is JSON string. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as Int64ValueMixin).value.toString(); + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addString((message as Int64ValueMixin).value.toString()); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -596,9 +607,9 @@ abstract class UInt64ValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `UInt64Value` is JSON string. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as UInt64ValueMixin).value.toStringUnsigned(); + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addString((message as UInt64ValueMixin).value.toStringUnsigned()); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -625,9 +636,9 @@ abstract class Int32ValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `Int32Value` is JSON number. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as Int32ValueMixin).value; + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addNumber((message as Int32ValueMixin).value); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -648,9 +659,10 @@ abstract class Int32ValueMixin { abstract class UInt32ValueMixin { int get value; set value(int value); - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as UInt32ValueMixin).value; + + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addNumber((message as UInt32ValueMixin).value); } // From google/protobuf/wrappers.proto: @@ -676,9 +688,9 @@ abstract class BoolValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `BoolValue` is JSON `true` and `false` - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as BoolValueMixin).value; + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addBool((message as BoolValueMixin).value); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -697,9 +709,9 @@ abstract class StringValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `StringValue` is JSON string. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return (message as StringValueMixin).value; + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addString((message as StringValueMixin).value); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, @@ -718,9 +730,9 @@ abstract class BytesValueMixin { // From google/protobuf/wrappers.proto: // The JSON representation for `BytesValue` is JSON string. - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { - return base64.encode((message as BytesValueMixin).value); + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addString(base64.encode((message as BytesValueMixin).value)); } static void fromProto3JsonHelper(GeneratedMessage message, Object json, diff --git a/protobuf/lib/src/protobuf/pb_map.dart b/protobuf/lib/src/protobuf/pb_map.dart index e2d41f64d..c2fffb969 100644 --- a/protobuf/lib/src/protobuf/pb_map.dart +++ b/protobuf/lib/src/protobuf/pb_map.dart @@ -19,7 +19,9 @@ class PbMap extends MapBase { final int valueFieldType; static const int _keyFieldNumber = 1; + static const String _keyFieldNumberString = '1'; static const int _valueFieldNumber = 2; + static const String _valueFieldNumberString = '2'; final Map _wrappedMap; diff --git a/protobuf/lib/src/protobuf/proto3_json.dart b/protobuf/lib/src/protobuf/proto3_json_reader.dart similarity index 75% rename from protobuf/lib/src/protobuf/proto3_json.dart rename to protobuf/lib/src/protobuf/proto3_json_reader.dart index 33f9e2223..49fbb18dc 100644 --- a/protobuf/lib/src/protobuf/proto3_json.dart +++ b/protobuf/lib/src/protobuf/proto3_json_reader.dart @@ -4,152 +4,6 @@ part of protobuf; -Object? _writeToProto3Json(_FieldSet fs, TypeRegistry typeRegistry) { - String? convertToMapKey(dynamic key, int keyType) { - var baseType = PbFieldType._baseType(keyType); - - assert(!_isRepeated(keyType)); - - switch (baseType) { - case PbFieldType._BOOL_BIT: - return key ? 'true' : 'false'; - case PbFieldType._STRING_BIT: - return key; - case PbFieldType._UINT64_BIT: - return (key as Int64).toStringUnsigned(); - case PbFieldType._INT32_BIT: - case PbFieldType._SINT32_BIT: - case PbFieldType._UINT32_BIT: - case PbFieldType._FIXED32_BIT: - case PbFieldType._SFIXED32_BIT: - case PbFieldType._INT64_BIT: - case PbFieldType._SINT64_BIT: - case PbFieldType._SFIXED64_BIT: - case PbFieldType._FIXED64_BIT: - return key.toString(); - default: - throw StateError('Not a valid key type $keyType'); - } - } - - Object? valueToProto3Json(dynamic fieldValue, int? fieldType) { - if (fieldValue == null) return null; - - if (_isGroupOrMessage(fieldType!)) { - return _writeToProto3Json( - (fieldValue as GeneratedMessage)._fieldSet, typeRegistry); - } else if (_isEnum(fieldType)) { - return (fieldValue as ProtobufEnum).name; - } else { - var baseType = PbFieldType._baseType(fieldType); - switch (baseType) { - case PbFieldType._BOOL_BIT: - return fieldValue ? true : false; - case PbFieldType._STRING_BIT: - return fieldValue; - case PbFieldType._INT32_BIT: - case PbFieldType._SINT32_BIT: - case PbFieldType._UINT32_BIT: - case PbFieldType._FIXED32_BIT: - case PbFieldType._SFIXED32_BIT: - return fieldValue; - case PbFieldType._INT64_BIT: - case PbFieldType._SINT64_BIT: - case PbFieldType._SFIXED64_BIT: - case PbFieldType._FIXED64_BIT: - return fieldValue.toString(); - case PbFieldType._FLOAT_BIT: - case PbFieldType._DOUBLE_BIT: - double value = fieldValue; - if (value.isNaN) { - return _nan; - } - if (value.isInfinite) { - return value.isNegative ? _negativeInfinity : _infinity; - } - if (fieldValue.toInt() == fieldValue) { - return fieldValue.toInt(); - } - return value; - case PbFieldType._UINT64_BIT: - return (fieldValue as Int64).toStringUnsigned(); - case PbFieldType._BYTES_BIT: - return base64Encode(fieldValue); - default: - throw StateError( - 'Invariant violation: unexpected value type $fieldType'); - } - } - } - - final meta = fs._meta; - if (meta.toProto3Json != null) { - return meta.toProto3Json!(fs._message!, typeRegistry); - } - - var result = {}; - for (var fieldInfo in fs._infosSortedByTag) { - var value = fs._values[fieldInfo.index!]; - if (value == null || (value is List && value.isEmpty)) { - continue; // It's missing, repeated, or an empty byte array. - } - dynamic jsonValue; - if (fieldInfo.isMapField) { - jsonValue = (value as PbMap).map((key, entryValue) { - var mapEntryInfo = fieldInfo as MapFieldInfo; - return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType), - valueToProto3Json(entryValue, mapEntryInfo.valueFieldType)); - }); - } else if (fieldInfo.isRepeated) { - jsonValue = (value as PbList) - .map((element) => valueToProto3Json(element, fieldInfo.type)) - .toList(); - } else { - jsonValue = valueToProto3Json(value, fieldInfo.type); - } - result[fieldInfo.name] = jsonValue; - } - // Extensions and unknown fields are not encoded by proto3 JSON. - return result; -} - -int _tryParse32BitProto3(String s, JsonParsingContext context) { - return int.tryParse(s) ?? - (throw context.parseException('expected integer', s)); -} - -int _check32BitSignedProto3(int n, JsonParsingContext context) { - if (n < -2147483648 || n > 2147483647) { - throw context.parseException('expected 32 bit signed integer', n); - } - return n; -} - -int _check32BitUnsignedProto3(int n, JsonParsingContext context) { - if (n < 0 || n > 0xFFFFFFFF) { - throw context.parseException('expected 32 bit unsigned integer', n); - } - return n; -} - -Int64 _tryParse64BitProto3(Object? json, String s, JsonParsingContext context) { - try { - return Int64.parseInt(s); - } on FormatException { - throw context.parseException('expected integer', json); - } -} - -/// TODO(paulberry): find a better home for this? -extension _FindFirst on Iterable { - E? findFirst(bool Function(E) test) { - for (var element in this) { - if (test(element)) return element; - } - return null; - } -} - void _mergeFromProto3Json( Object? json, _FieldSet fieldSet, @@ -415,3 +269,40 @@ void _mergeFromProto3Json( recursionHelper(json, fieldSet); } + +int _tryParse32BitProto3(String s, JsonParsingContext context) { + return int.tryParse(s) ?? + (throw context.parseException('expected integer', s)); +} + +int _check32BitSignedProto3(int n, JsonParsingContext context) { + if (n < -2147483648 || n > 2147483647) { + throw context.parseException('expected 32 bit signed integer', n); + } + return n; +} + +int _check32BitUnsignedProto3(int n, JsonParsingContext context) { + if (n < 0 || n > 0xFFFFFFFF) { + throw context.parseException('expected 32 bit unsigned integer', n); + } + return n; +} + +Int64 _tryParse64BitProto3(Object? json, String s, JsonParsingContext context) { + try { + return Int64.parseInt(s); + } on FormatException { + throw context.parseException('expected integer', json); + } +} + +/// TODO(paulberry): find a better home for this? +extension _FindFirst on Iterable { + E? findFirst(bool Function(E) test) { + for (var element in this) { + if (test(element)) return element; + } + return null; + } +} diff --git a/protobuf/lib/src/protobuf/proto3_json_writer.dart b/protobuf/lib/src/protobuf/proto3_json_writer.dart new file mode 100644 index 000000000..bacc9e28b --- /dev/null +++ b/protobuf/lib/src/protobuf/proto3_json_writer.dart @@ -0,0 +1,157 @@ +// Copyright (c) 2019, 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; + +void _writeToProto3JsonSink( + _FieldSet fs, TypeRegistry typeRegistry, JsonSink jsonSink, + {bool newMessage = true}) { + final wellKnownConverter = fs._meta.toProto3Json; + if (wellKnownConverter != null) { + wellKnownConverter(fs._message!, typeRegistry, jsonSink); + return; + } + + if (newMessage) { + jsonSink.startObject(); // start message + } + + for (var fieldInfo in fs._infosSortedByTag) { + var value = fs._values[fieldInfo.index!]; + + if (value == null || (value is List && value.isEmpty)) { + continue; // It's missing, repeated, or an empty byte array. + } + + jsonSink.addKey(fieldInfo.name); + + if (fieldInfo.isMapField) { + jsonSink.startObject(); // start map field + final MapFieldInfo mapFieldInfo = fieldInfo as dynamic; + final Map mapValue = value; + for (var entry in mapValue.entries) { + final key = entry.key; + final value = entry.value; + _writeMapKey(key, mapFieldInfo.keyFieldType, jsonSink); + _writeFieldValue( + value, mapFieldInfo.valueFieldType, jsonSink, typeRegistry); + } + jsonSink.endObject(); // end map field + } else if (fieldInfo.isRepeated) { + jsonSink.startArray(); // start repeated field + final List listValue = value; + for (final element in listValue) { + _writeFieldValue(element, fieldInfo.type, jsonSink, typeRegistry); + } + jsonSink.endArray(); // end repeated field + } else { + _writeFieldValue(value, fieldInfo.type, jsonSink, typeRegistry); + } + } + + if (newMessage) { + jsonSink.endObject(); // end message + } +} + +void _writeMapKey(dynamic key, int keyType, JsonSink jsonSink) { + var baseType = PbFieldType._baseType(keyType); + + assert(!_isRepeated(keyType)); + + switch (baseType) { + case PbFieldType._BOOL_BIT: + final bool boolKey = key; + jsonSink.addKey(boolKey.toString()); + break; + case PbFieldType._STRING_BIT: + final String stringKey = key; + jsonSink.addKey(stringKey); + break; + case PbFieldType._UINT64_BIT: + final Int64 intKey = key; + jsonSink.addKey(intKey.toStringUnsigned().toString()); + break; + case PbFieldType._INT32_BIT: + case PbFieldType._SINT32_BIT: + case PbFieldType._UINT32_BIT: + case PbFieldType._FIXED32_BIT: + case PbFieldType._SFIXED32_BIT: + case PbFieldType._INT64_BIT: + case PbFieldType._SINT64_BIT: + case PbFieldType._SFIXED64_BIT: + case PbFieldType._FIXED64_BIT: + jsonSink.addKey(key.toString()); + break; + default: + throw StateError('Not a valid key type $keyType'); + } +} + +void _writeFieldValue(dynamic fieldValue, int fieldType, JsonSink jsonSink, + TypeRegistry typeRegistry) { + if (fieldValue == null) { + jsonSink.addNull(); + return; + } + + if (_isGroupOrMessage(fieldType)) { + final GeneratedMessage messageValue = fieldValue; + _writeToProto3JsonSink(messageValue._fieldSet, typeRegistry, jsonSink); + } else if (_isEnum(fieldType)) { + final ProtobufEnum enumValue = fieldValue; + jsonSink.addString(enumValue.name); + } else { + final baseType = PbFieldType._baseType(fieldType); + switch (baseType) { + case PbFieldType._BOOL_BIT: + jsonSink.addBool(fieldValue); + break; + case PbFieldType._STRING_BIT: + jsonSink.addString(fieldValue); + break; + case PbFieldType._INT32_BIT: + case PbFieldType._SINT32_BIT: + case PbFieldType._UINT32_BIT: + case PbFieldType._FIXED32_BIT: + case PbFieldType._SFIXED32_BIT: + jsonSink.addNumber(fieldValue); + break; + case PbFieldType._INT64_BIT: + case PbFieldType._SINT64_BIT: + case PbFieldType._SFIXED64_BIT: + case PbFieldType._FIXED64_BIT: + jsonSink.addString(fieldValue.toString()); + break; + case PbFieldType._FLOAT_BIT: + case PbFieldType._DOUBLE_BIT: + double value = fieldValue; + if (value.isNaN) { + jsonSink.addString(_nan); + break; + } + if (value.isInfinite) { + jsonSink.addString(value.isNegative ? _negativeInfinity : _infinity); + break; + } + final intValue = value.toInt(); + if (intValue == value) { + jsonSink.addNumber(intValue); + break; + } + jsonSink.addNumber(value); + break; + case PbFieldType._UINT64_BIT: + final Int64 intValue = fieldValue; + jsonSink.addString(intValue.toStringUnsigned()); + break; + case PbFieldType._BYTES_BIT: + jsonSink.addString(base64Encode(fieldValue)); + break; + default: + throw StateError( + 'Invariant violation: unexpected value type $fieldType'); + } + } +} diff --git a/protobuf/pubspec.yaml b/protobuf/pubspec.yaml index b1640c585..64b88f057 100644 --- a/protobuf/pubspec.yaml +++ b/protobuf/pubspec.yaml @@ -9,8 +9,9 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: - fixnum: ^1.0.0 collection: ^1.15.0 + fixnum: ^1.0.0 + jsontool: ^1.1.2 meta: ^1.7.0 dev_dependencies: