diff --git a/protobuf/benchmarks/common.dart b/protobuf/benchmarks/common.dart index ccda311b8..c26e0f84e 100644 --- a/protobuf/benchmarks/common.dart +++ b/protobuf/benchmarks/common.dart @@ -8,7 +8,7 @@ /// both on the VM and when compiled to JavaScript. library common; -import 'dart:convert' show jsonDecode, jsonEncode; +import 'dart:convert' show jsonEncode; import 'dart:typed_data'; import 'package:benchmark_harness/benchmark_harness.dart'; @@ -101,35 +101,35 @@ class Factories { fromBuffer: (List binary) => p2.GoogleMessage1.fromBuffer(binary), fromJson: (String json) => p2.GoogleMessage1.fromJson(json), fromProto3JsonString: (String json) => - p2.GoogleMessage1.create()..mergeFromProto3Json(jsonDecode(json)), + p2.GoogleMessage1.create()..mergeFromProto3JsonString(json), fromProto3JsonObject: (Object json) => p2.GoogleMessage1.create()..mergeFromProto3Json(json)), 'benchmarks.proto3.GoogleMessage1': Factories._( fromBuffer: (List binary) => p3.GoogleMessage1.fromBuffer(binary), fromJson: (String json) => p3.GoogleMessage1.fromJson(json), fromProto3JsonString: (String json) => - p3.GoogleMessage1.create()..mergeFromProto3Json(jsonDecode(json)), + p3.GoogleMessage1.create()..mergeFromProto3JsonString(json), fromProto3JsonObject: (Object json) => p3.GoogleMessage1.create()..mergeFromProto3Json(json)), 'benchmarks.proto2.GoogleMessage2': Factories._( fromBuffer: (List binary) => GoogleMessage2.fromBuffer(binary), fromJson: (String json) => GoogleMessage2.fromJson(json), fromProto3JsonString: (String json) => - GoogleMessage2.create()..mergeFromProto3Json(jsonDecode(json)), + GoogleMessage2.create()..mergeFromProto3JsonString(json), fromProto3JsonObject: (Object json) => GoogleMessage2.create()..mergeFromProto3Json(json)), 'benchmarks.google_message3.GoogleMessage3': Factories._( fromBuffer: (List binary) => GoogleMessage3.fromBuffer(binary), fromJson: (String json) => GoogleMessage3.fromJson(json), fromProto3JsonString: (String json) => - GoogleMessage3.create()..mergeFromProto3Json(jsonDecode(json)), + GoogleMessage3.create()..mergeFromProto3JsonString(json), fromProto3JsonObject: (Object json) => GoogleMessage3.create()..mergeFromProto3Json(json)), 'benchmarks.google_message4.GoogleMessage4': Factories._( fromBuffer: (List binary) => GoogleMessage4.fromBuffer(binary), fromJson: (String json) => GoogleMessage4.fromJson(json), fromProto3JsonString: (String json) => - GoogleMessage4.create()..mergeFromProto3Json(jsonDecode(json)), + GoogleMessage4.create()..mergeFromProto3JsonString(json), fromProto3JsonObject: (Object json) => GoogleMessage4.create()..mergeFromProto3Json(json)), }; @@ -231,7 +231,7 @@ class ToProto3JsonStringBenchmark extends _ProtobufBenchmark { void run() { for (final ds in datasets) { for (final unpacked in ds.unpacked) { - jsonEncode(unpacked.toProto3Json()); + unpacked.toProto3JsonString(); } } } diff --git a/protobuf/lib/meta.dart b/protobuf/lib/meta.dart index 519bbd052..72e830495 100644 --- a/protobuf/lib/meta.dart +++ b/protobuf/lib/meta.dart @@ -44,6 +44,8 @@ const GeneratedMessage_reservedNames = [ 'mergeFromJsonMap', 'mergeFromMessage', 'mergeFromProto3Json', + 'mergeFromProto3JsonReader', + 'mergeFromProto3JsonString', 'mergeUnknownFields', 'noSuchMethod', 'runtimeType', @@ -52,6 +54,8 @@ const GeneratedMessage_reservedNames = [ 'toBuilder', 'toDebugString', 'toProto3Json', + 'toProto3JsonSink', + 'toProto3JsonString', 'toString', 'unknownFields', 'writeToBuffer', diff --git a/protobuf/lib/protobuf.dart b/protobuf/lib/protobuf.dart index ad7bb99fc..cec00fbf6 100644 --- a/protobuf/lib/protobuf.dart +++ b/protobuf/lib/protobuf.dart @@ -11,6 +11,7 @@ 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'; @@ -18,11 +19,11 @@ import 'src/protobuf/permissive_compare.dart'; import 'src/protobuf/type_registry.dart'; export 'src/protobuf/type_registry.dart' show TypeRegistry; +part 'src/protobuf/builder_info.dart'; 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'; part 'src/protobuf/extension.dart'; @@ -37,14 +38,16 @@ part 'src/protobuf/generated_service.dart'; part 'src/protobuf/json.dart'; part 'src/protobuf/pb_list.dart'; part 'src/protobuf/pb_map.dart'; +part 'src/protobuf/proto3_json_reader.dart'; +part 'src/protobuf/proto3_json_writer.dart'; part 'src/protobuf/protobuf_enum.dart'; -part 'src/protobuf/proto3_json.dart'; part 'src/protobuf/rpc_client.dart'; part 'src/protobuf/unknown_field_set.dart'; -part 'src/protobuf/utils.dart'; part 'src/protobuf/unpack.dart'; +part 'src/protobuf/utils.dart'; part 'src/protobuf/wire_format.dart'; +/// Used by generated code, do not use. // TODO(sra): Use Int64.parse() when available - see http://dartbug.com/21915. Int64 parseLongInt(String text) { if (text.startsWith('0x')) return Int64.parseHex(text.substring(2)); @@ -54,3 +57,19 @@ Int64 parseLongInt(String text) { } const _utf8 = Utf8Codec(allowMalformed: true); + +/// Used by generated code, do not use. +/// +/// Reads the next JSON object from the given [JsonReader]. +Object? nextJsonObject(JsonReader jsonReader) { + Object? json; + try { + final sink = jsonObjectWriter((result) { + json = result; + }); + jsonReader.expectAnyValue(sink); + } on FormatException { + json = ''; + } + return json; +} diff --git a/protobuf/lib/src/protobuf/builder_info.dart b/protobuf/lib/src/protobuf/builder_info.dart index 430bbb94c..1e0469f2d 100644 --- a/protobuf/lib/src/protobuf/builder_info.dart +++ b/protobuf/lib/src/protobuf/builder_info.dart @@ -41,18 +41,25 @@ class BuilderInfo { List? _sortedByTag; - // For well-known types. - final Object? Function(GeneratedMessage message, TypeRegistry typeRegistry)? - toProto3Json; - final Function(GeneratedMessage targetMessage, Object json, - TypeRegistry typeRegistry, JsonParsingContext context)? fromProto3Json; + /// JSON generator for well-known types. + final void Function( + GeneratedMessage msg, TypeRegistry typeRegistry, JsonSink jsonSink)? + writeToProto3JsonSink; + + /// JSON parser for well-known types. + final Function( + GeneratedMessage msg, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context)? mergeFromProto3JsonReader; + final CreateBuilderFunc? createEmptyInstance; BuilderInfo(String? messageName, {PackageName package = const PackageName(''), this.createEmptyInstance, - this.toProto3Json, - this.fromProto3Json}) + this.writeToProto3JsonSink, + this.mergeFromProto3JsonReader}) : qualifiedMessageName = '${package.prefix}$messageName'; void add( diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart index 6b5c9714b..530da7dad 100644 --- a/protobuf/lib/src/protobuf/generated_message.dart +++ b/protobuf/lib/src/protobuf/generated_message.dart @@ -208,24 +208,92 @@ abstract class GeneratedMessage { /// For the proto3 JSON format use: [toProto3Json]. String writeToJson() => jsonEncode(writeToJsonMap()); - /// Returns an Object representing Proto3 JSON serialization of `this`. + /// Returns Dart JSON object encoding this message following proto3 JSON + /// format. + /// + /// The returned object will have the same format as objects returned by + /// [jsonEncode]. + /// + /// See [toProto3JsonSink] for details. + Object? toProto3Json( + {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 this message. + /// + /// See [toProto3JsonSink] for details. + String toProto3JsonString( + {TypeRegistry typeRegistry = const TypeRegistry.empty()}) { + final buf = StringBuffer(); + final stringSink = jsonStringWriter(buf); + toProto3JsonSink(typeRegistry, stringSink); + return buf.toString(); + } + + /// Writes proto3 JSON serialization of this message to the given [JsonSink]. /// /// The key for each field is be the camel-cased 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 encoding are supported. If a + /// well-known type cannot be encoded (eg. a `google.protobuf.Timestamp` with + /// negative `nanoseconds`) an error is thrown. /// /// 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. - Object? toProto3Json( - {TypeRegistry typeRegistry = const TypeRegistry.empty()}) => - _writeToProto3Json(_fieldSet, typeRegistry); + /// message encoding a type not in [typeRegistry] is encountered, an error is + /// thrown. + /// + /// The [newMessage] argument is for use in generated code, do not use. + void toProto3JsonSink(TypeRegistry typeRegistry, JsonSink jsonSink, + {bool newMessage = true}) { + _writeToProto3JsonSink(_fieldSet, typeRegistry, jsonSink, + newMessage: newMessage); + } + + /// Merges field values from Dart representation of JSON following proto3 + /// JSON format. + /// + /// [jsonObject] should be generated by [jsonDecode] or have the same format. + /// + /// See [mergeFromProto3JsonReader] documentation for details. + void mergeFromProto3Json(Object? jsonObject, + {TypeRegistry typeRegistry = const TypeRegistry.empty(), + bool ignoreUnknownFields = false, + bool supportNamesWithUnderscores = true, + bool permissiveEnums = false}) => + mergeFromProto3JsonReader( + JsonReader.fromObject(jsonObject), + typeRegistry: typeRegistry, + ignoreUnknownFields: ignoreUnknownFields, + supportNamesWithUnderscores: supportNamesWithUnderscores, + permissiveEnums: permissiveEnums, + ); + + /// Marges field values from a [String] following proto3 JSON format. + /// + /// See [mergeFromProto3JsonReader] documentation for details. + void mergeFromProto3JsonString(String jsonString, + {TypeRegistry typeRegistry = const TypeRegistry.empty(), + bool ignoreUnknownFields = false, + bool supportNamesWithUnderscores = true, + bool permissiveEnums = false}) => + mergeFromProto3JsonReader( + JsonReader.fromString(jsonString), + typeRegistry: typeRegistry, + ignoreUnknownFields: ignoreUnknownFields, + supportNamesWithUnderscores: supportNamesWithUnderscores, + permissiveEnums: permissiveEnums, + ); - /// Merges field values from [json], a JSON object using proto3 encoding. + /// Merges field values from a [JsonReader] that reads from a JSON in proto3 + /// JSON format. /// /// Well-known types and their special JSON encodings are supported. /// @@ -252,13 +320,13 @@ abstract class GeneratedMessage { /// /// Throws [FormatException] if the JSON not formatted correctly (a String /// where a number was expected etc.). - void mergeFromProto3Json(Object? json, + void mergeFromProto3JsonReader(JsonReader jsonReader, {TypeRegistry typeRegistry = const TypeRegistry.empty(), bool ignoreUnknownFields = false, bool supportNamesWithUnderscores = true, bool permissiveEnums = false}) => - _mergeFromProto3Json(json, _fieldSet, typeRegistry, ignoreUnknownFields, - supportNamesWithUnderscores, permissiveEnums); + _mergeFromProto3JsonReader(jsonReader, _fieldSet, typeRegistry, + ignoreUnknownFields, supportNamesWithUnderscores, permissiveEnums); /// Merges field values from [data], a JSON object, encoded as described by /// [GeneratedMessage.writeToJson]. diff --git a/protobuf/lib/src/protobuf/mixins/well_known.dart b/protobuf/lib/src/protobuf/mixins/well_known.dart index 66854c9eb..ab8f8f2a3 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,60 +77,74 @@ 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); - if (info.toProto3Json == null) { - var map = proto3Json as Map; - map['@type'] = any.typeUrl; - return map; + final unpacked = info.createEmptyInstance!()..mergeFromBuffer(any.value); + jsonSink.startObject(); + jsonSink.addKey('@type'); + jsonSink.addString(any.typeUrl); + if (info.writeToProto3JsonSink == null) { + 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, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + // TODO: We can avoid allocating JSON objects here by masking the `@type` + // field in a `JsonReader` type + Object? json; + final sink = jsonObjectWriter((result) { + json = result; + }); + jsonReader.expectAnyValue(sink); - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { if (json is! Map) { throw context.parseException( 'Expected Any message encoded as {@type,...},', json); } - final object = json; - final typeUrl = object['@type']; - if (typeUrl is String) { - var any = message as AnyMixin; - var info = typeRegistry.lookup(_typeNameFromUrl(typeUrl)); - if (info == null) { - throw context.parseException( - 'Decoding Any of type $typeUrl not in TypeRegistry $typeRegistry', - json); - } + final object = json as Map; + final typeUrl = object['@type']; - Object? subJson = info.fromProto3Json == null - // TODO(sigurdm): avoid cloning [object] here. - ? (Map.from(object)..remove('@type')) - : object['value']; - // TODO(sigurdm): We lose [context.path]. - var packedMessage = info.createEmptyInstance!() - ..mergeFromProto3Json(subJson, - typeRegistry: typeRegistry, - supportNamesWithUnderscores: context.supportNamesWithUnderscores, - ignoreUnknownFields: context.ignoreUnknownFields, - permissiveEnums: context.permissiveEnums); - - any.value = packedMessage.writeToBuffer(); - any.typeUrl = typeUrl; - } else { + if (typeUrl is! String) { throw context.parseException('Expected a string', json); } + + var any = message as AnyMixin; + var info = typeRegistry.lookup(_typeNameFromUrl(typeUrl)); + if (info == null) { + throw context.parseException( + 'Decoding Any of type $typeUrl not in TypeRegistry $typeRegistry', + json); + } + + Object? subJson = info.mergeFromProto3JsonReader == null + // TODO(sigurdm): avoid cloning [object] here. + ? (Map.from(object)..remove('@type')) + : object['value']; + // TODO(sigurdm): We lose [context.path]. + var packedMessage = info.createEmptyInstance!() + ..mergeFromProto3Json(subJson, + typeRegistry: typeRegistry, + supportNamesWithUnderscores: context.supportNamesWithUnderscores, + ignoreUnknownFields: context.ignoreUnknownFields, + permissiveEnums: context.permissiveEnums); + + any.value = packedMessage.writeToBuffer(); + any.typeUrl = typeUrl; } } @@ -191,8 +206,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,37 +240,43 @@ abstract class TimestampMixin { .padLeft(9, '0') .replaceFirst(finalGroupsOfThreeZeroes, ''); } - return '$y-$m-${d}T$h:$min:$sec${secFrac}Z'; - } - - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is String) { - var jsonWithoutFracSec = json; - var nanos = 0; - Match? fracSecsMatch = RegExp(r'\.(\d+)').firstMatch(json); - if (fracSecsMatch != null) { - var fracSecs = fracSecsMatch[1]!; - if (fracSecs.length > 9) { - throw context.parseException( - 'Timestamp can have at most than 9 decimal digits', json); - } - nanos = int.parse(fracSecs.padRight(9, '0')); - jsonWithoutFracSec = - json.replaceRange(fracSecsMatch.start, fracSecsMatch.end, ''); + jsonSink.addString('$y-$m-${d}T$h:$min:$sec${secFrac}Z'); + } + + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var str = jsonReader.tryString(); + if (str == null) { + throw context.parseException('Expected timestamp represented as String', + nextJsonObject(jsonReader)); + } + + final json = str; + + var jsonWithoutFracSec = json; + var nanos = 0; + final Match? fracSecsMatch = RegExp(r'\.(\d+)').firstMatch(json); + if (fracSecsMatch != null) { + final fracSecs = fracSecsMatch[1]!; + if (fracSecs.length > 9) { + throw context.parseException( + 'Timestamp can have at most than 9 decimal digits', json); } - var dateTimeWithoutFractionalSeconds = - DateTime.tryParse(jsonWithoutFracSec) ?? - (throw context.parseException( - 'Timestamp not well formatted. ', json)); - - var timestamp = message as TimestampMixin; - setFromDateTime(timestamp, dateTimeWithoutFractionalSeconds); - timestamp.nanos = nanos; - } else { - throw context.parseException( - 'Expected timestamp represented as String', json); + nanos = int.parse(fracSecs.padRight(9, '0')); + jsonWithoutFracSec = + json.replaceRange(fracSecsMatch.start, fracSecsMatch.end, ''); } + final dateTimeWithoutFractionalSeconds = + DateTime.tryParse(jsonWithoutFracSec) ?? + (throw context.parseException( + 'Timestamp not well formatted. ', str)); + + final timestamp = message as TimestampMixin; + setFromDateTime(timestamp, dateTimeWithoutFractionalSeconds); + timestamp.nanos = nanos; } } @@ -268,8 +289,8 @@ abstract class DurationMixin { static final RegExp finalZeroes = RegExp(r'0+$'); - static Object toProto3JsonHelper( - GeneratedMessage message, TypeRegistry typeRegistry) { + static 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,30 +299,38 @@ 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$'); - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { var duration = message as DurationMixin; - if (json is String) { - var match = durationPattern.matchAsPrefix(json); - if (match == null) { - throw context.parseException( - 'Expected a String of the form `.s`', json); - } else { - var secondsString = match[1]!; - var seconds = - secondsString == '' ? Int64.ZERO : Int64.parseInt(secondsString); - duration.seconds = seconds; - var nanos = int.parse((match[2] ?? '').padRight(9, '0')); - duration.nanos = seconds < 0 ? -nanos : nanos; - } - } else { + + var str = jsonReader.tryString(); + if (str == null) { + throw context.parseException( + 'Expected a String of the form `.s`', + nextJsonObject(jsonReader)); + } + + var json = str; + + var match = durationPattern.matchAsPrefix(json); + if (match == null) { throw context.parseException( 'Expected a String of the form `.s`', json); + } else { + var secondsString = match[1]!; + var seconds = + secondsString == '' ? Int64.ZERO : Int64.parseInt(secondsString); + duration.seconds = seconds; + var nanos = int.parse((match[2] ?? '').padRight(9, '0')); + duration.nanos = seconds < 0 ? -nanos : nanos; } } } @@ -312,38 +341,46 @@ 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))); - } - - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is Map) { - // Check for emptiness to avoid setting `.fields` if there are no - // values. - if (json.isNotEmpty) { - var fields = (message as StructMixin).fields; - var valueCreator = - (message.info_.fieldInfo[_fieldsFieldTagNumber] as MapFieldInfo) - .valueCreator!; - - json.forEach((key, value) { - if (key is! String) { - throw context.parseException('Expected String key', json); - } - var v = valueCreator() as ValueMixin; - context.addMapIndex(key); - ValueMixin.fromProto3JsonHelper(v, value, typeRegistry, context); - context.popIndex(); - fields[key] = v; - }); - } - } else { + + 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, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + if (!jsonReader.tryObject()) { throw context.parseException( - 'Expected a JSON object literal (map)', json); + 'Expected a JSON object literal (map)', nextJsonObject(jsonReader)); + } + + // Check for emptiness to avoid setting `.fields` if there are no + // values. + var nextKey = jsonReader.nextKey(); + if (nextKey != null) { + var fields = (message as StructMixin).fields; + var valueCreator = + (message.info_.fieldInfo[_fieldsFieldTagNumber] as MapFieldInfo) + .valueCreator!; + + while (nextKey != null) { + final key = nextKey; + final v = valueCreator() as ValueMixin; + context.addMapIndex(key); + ValueMixin.fromProto3JsonHelper(v, jsonReader, typeRegistry, context); + context.popIndex(); + fields[key] = v; + nextKey = jsonReader.nextKey(); + } } } } @@ -370,57 +407,84 @@ 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); + return StructMixin.toProto3JsonHelper( + value.structValue, typeRegistry, jsonSink); } else if (value.hasListValue()) { - return ListValueMixin.toProto3JsonHelper(value.listValue, typeRegistry); + return ListValueMixin.toProto3JsonHelper( + value.listValue, typeRegistry, jsonSink); } else { throw ArgumentError('Serializing google.protobuf.Value with no value'); } } - static void fromProto3JsonHelper(GeneratedMessage message, Object? json, - TypeRegistry typeRegistry, JsonParsingContext context) { - var value = message as ValueMixin; - if (json == null) { + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + final value = message as ValueMixin; + + if (jsonReader.tryNull()) { // Rely on the getter retrieving the default to provide an instance. value.nullValue = value.nullValue; - } else if (json is num) { - value.numberValue = json.toDouble(); - } else if (json is String) { - value.stringValue = json; - } else if (json is bool) { - value.boolValue = json; - } else if (json is Map) { + return; + } + + var num_ = jsonReader.tryNum(); + if (num_ != null) { + value.numberValue = num_.toDouble(); + return; + } + + var str = jsonReader.tryString(); + if (str != null) { + value.stringValue = str; + return; + } + + var bool_ = jsonReader.tryBool(); + if (bool_ != null) { + value.boolValue = bool_; + return; + } + + // Copy the reader first as `StructMixin` will be entering the object. + // TODO: This should be cheap? + if (jsonReader.copy().tryObject()) { // Clone because the default instance is frozen. - var structValue = value.structValue.deepCopy(); + final structValue = value.structValue.deepCopy(); StructMixin.fromProto3JsonHelper( - structValue, json, typeRegistry, context); + structValue, jsonReader, typeRegistry, context); value.structValue = structValue; - } else if (json is List) { - // Clone because the default instance is frozen. - var listValue = value.listValue.deepCopy(); + return; + } + + // Ditto + if (jsonReader.copy().tryArray()) { + final listValue = value.listValue.deepCopy(); ListValueMixin.fromProto3JsonHelper( - listValue, json, typeRegistry, context); + listValue, jsonReader, typeRegistry, context); value.listValue = listValue; - } else { - throw context.parseException( - 'Expected a json-value (Map, List, String, number, bool or null)', - json); + return; } + + throw context.parseException( + 'Expected a json-value (Map, List, String, number, bool or null)', + nextJsonObject(jsonReader)); } } @@ -429,31 +493,38 @@ 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; - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - var list = message as ListValueMixin; - if (json is List) { - var subBuilder = message.info_.subBuilder(_valueFieldTagNumber)!; - for (var i = 0; i < json.length; i++) { - Object element = json[i]; - var v = subBuilder() as ValueMixin; - context.addListIndex(i); - ValueMixin.fromProto3JsonHelper(v, element, typeRegistry, context); - context.popIndex(); - list.values.add(v); - } - } else { - throw context.parseException('Expected a json-List', json); + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + final list = message as ListValueMixin; + + if (!jsonReader.tryArray()) { + throw context.parseException('Expected an array', json); + } + + var subBuilder = message.info_.subBuilder(_valueFieldTagNumber)!; + var i = 0; + while (jsonReader.hasNext()) { + final v = subBuilder() as ValueMixin; + context.addListIndex(i); + ValueMixin.fromProto3JsonHelper(v, jsonReader, typeRegistry, context); + context.popIndex(); + list.values.add(v); + i += 1; } } } @@ -467,8 +538,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,27 +547,30 @@ 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, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is String) { - if (json.contains('_')) { - throw context.parseException( - 'Invalid Character `_` in FieldMask', json); - } - if (json == '') { - // The empty string splits to a single value. So this is a special case. - return; - } - (message as FieldMaskMixin) - .paths - .addAll(json.split(',').map(_fromCamelCase)); - } else { + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var str = jsonReader.tryString(); + if (str == null) { throw context.parseException( - 'Expected String formatted as FieldMask', json); + 'Expected String formatted as FieldMask', nextJsonObject(jsonReader)); + } + + if (str.contains('_')) { + throw context.parseException('Invalid Character `_` in FieldMask', json); + } + if (str == '') { + // The empty string splits to a single value. So this is a special case. + return; } + (message as FieldMaskMixin) + .paths + .addAll(str.split(',').map(_fromCamelCase)); } static String _toCamelCase(String name) { @@ -516,23 +590,32 @@ 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) { + return jsonSink.addNumber((message as DoubleValueMixin).value); + } + + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var num_ = jsonReader.tryNum(); + if (num_ != null) { + (message as DoubleValueMixin).value = num_.toDouble(); + return; + } - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is num) { - (message as DoubleValueMixin).value = json.toDouble(); - } else if (json is String) { - (message as DoubleValueMixin).value = double.tryParse(json) ?? + var str = jsonReader.tryString(); + if (str != null) { + (message as DoubleValueMixin).value = double.tryParse(str) ?? (throw context.parseException( - 'Expected string to encode a double', json)); - } else { - throw context.parseException( - 'Expected a double as a String or number', json); + 'Expected string to encode a double', str)); + return; } + + throw context.parseException( + 'Expected a double as a String or number', nextJsonObject(jsonReader)); } } @@ -542,23 +625,32 @@ 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, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var num_ = jsonReader.tryNum(); + if (num_ != null) { + (message as FloatValueMixin).value = num_.toDouble(); + return; + } - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is num) { - (message as FloatValueMixin).value = json.toDouble(); - } else if (json is String) { - (message as FloatValueMixin).value = double.tryParse(json) ?? + var str = jsonReader.tryString(); + if (str != null) { + (message as FloatValueMixin).value = double.tryParse(str) ?? (throw context.parseException( - 'Expected a float as a String or number', json)); - } else { - throw context.parseException( - 'Expected a float as a String or number', json); + 'Expected a float as a String or number', str)); + return; } + + throw context.parseException( + 'Expected a float as a String or number', nextJsonObject(jsonReader)); } } @@ -568,25 +660,39 @@ 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, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var num_ = jsonReader.tryNum(); + if (num_ != null) { + if (num_ is int) { + (message as Int64ValueMixin).value = Int64(num_); + return; + } + throw context.parseException( + 'Expected an integer encoded as a String or number', json); + } - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is int) { - (message as Int64ValueMixin).value = Int64(json); - } else if (json is String) { + var str = jsonReader.tryString(); + if (str != null) { try { - (message as Int64ValueMixin).value = Int64.parseInt(json); + (message as Int64ValueMixin).value = Int64.parseInt(str); + return; } on FormatException { - throw context.parseException('Expected string to encode integer', json); + throw context.parseException('Expected string to encode integer', str); } - } else { - throw context.parseException( - 'Expected an integer encoded as a String or number', json); } + + throw context.parseException( + 'Expected an integer encoded as a String or number', + nextJsonObject(jsonReader)); } } @@ -596,26 +702,40 @@ 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, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var num_ = jsonReader.tryNum(); + if (num_ != null) { + if (num_ is int) { + (message as UInt64ValueMixin).value = Int64(num_); + return; + } + throw context.parseException( + 'Expected an unsigned integer as a String or integer', num_); + } - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is int) { - (message as UInt64ValueMixin).value = Int64(json); - } else if (json is String) { + var str = jsonReader.tryString(); + if (str != null) { try { - (message as UInt64ValueMixin).value = Int64.parseInt(json); + (message as UInt64ValueMixin).value = Int64.parseInt(str); + return; } on FormatException { throw context.parseException( - 'Expected string to encode unsigned integer', json); + 'Expected string to encode unsigned integer', str); } - } else { - throw context.parseException( - 'Expected an unsigned integer as a String or integer', json); } + + throw context.parseException( + 'Expected an unsigned integer as a String or integer', + nextJsonObject(jsonReader)); } } @@ -625,48 +745,77 @@ 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, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var num_ = jsonReader.tryNum(); + if (num_ != null) { + if (num_ is int) { + (message as Int32ValueMixin).value = num_; + return; + } + throw context.parseException( + 'Expected an integer encoded as a String or number', num_); + } - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is int) { - (message as Int32ValueMixin).value = json; - } else if (json is String) { - (message as Int32ValueMixin).value = int.tryParse(json) ?? + var str = jsonReader.tryString(); + if (str != null) { + (message as Int32ValueMixin).value = int.tryParse(str) ?? (throw context.parseException( - 'Expected string to encode integer', json)); - } else { - throw context.parseException( - 'Expected an integer encoded as a String or number', json); + 'Expected string to encode integer', str)); + return; } + + throw context.parseException( + 'Expected an integer encoded as a String or number', + nextJsonObject(jsonReader)); } } 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: // The JSON representation for `UInt32Value` is JSON number. - static void fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is int) { - (message as UInt32ValueMixin).value = json; - } else if (json is String) { - (message as UInt32ValueMixin).value = int.tryParse(json) ?? - (throw context.parseException( - 'Expected String to encode an integer', json)); - } else { + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var num_ = jsonReader.tryNum(); + if (num_ != null) { + if (num_ is int) { + (message as UInt32ValueMixin).value = num_; + return; + } throw context.parseException( - 'Expected an unsigned integer as a String or integer', json); + 'Expected an unsigned integer as a String or integer', num_); } + + var str = jsonReader.tryString(); + if (str != null) { + (message as UInt32ValueMixin).value = int.tryParse(str) ?? + (throw context.parseException( + 'Expected String to encode an integer', str)); + return; + } + + throw context.parseException( + 'Expected an unsigned integer as a String or integer', + nextJsonObject(jsonReader)); } } @@ -676,18 +825,23 @@ 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 fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is bool) { - (message as BoolValueMixin).value = json; - } else { - throw context.parseException('Expected a bool', json); + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addBool((message as BoolValueMixin).value); + } + + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var b = jsonReader.tryBool(); + if (b != null) { + (message as BoolValueMixin).value = b; + return; } + + throw context.parseException('Expected a bool', nextJsonObject(jsonReader)); } } @@ -697,18 +851,23 @@ 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 fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is String) { - (message as StringValueMixin).value = json; - } else { - throw context.parseException('Expected a String', json); + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addString((message as StringValueMixin).value); + } + + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var str = jsonReader.tryString(); + if (str != null) { + (message as StringValueMixin).value = str; + return; } + throw context.parseException( + 'Expected a String', nextJsonObject(jsonReader)); } } @@ -718,23 +877,28 @@ 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 fromProto3JsonHelper(GeneratedMessage message, Object json, - TypeRegistry typeRegistry, JsonParsingContext context) { - if (json is String) { + static void toProto3JsonHelper( + GeneratedMessage message, TypeRegistry typeRegistry, JsonSink jsonSink) { + jsonSink.addString(base64.encode((message as BytesValueMixin).value)); + } + + static void fromProto3JsonHelper( + GeneratedMessage message, + JsonReader jsonReader, + TypeRegistry typeRegistry, + JsonParsingContext context) { + var str = jsonReader.tryString(); + if (str != null) { try { - (message as BytesValueMixin).value = base64.decode(json); + (message as BytesValueMixin).value = base64.decode(str); + return; } on FormatException { throw context.parseException( - 'Expected bytes encoded as base64 String', json); + 'Expected bytes encoded as base64 String', str); } - } else { - throw context.parseException( - 'Expected bytes encoded as base64 String', json); } + + throw context.parseException( + 'Expected bytes encoded as base64 String', nextJsonObject(jsonReader)); } } diff --git a/protobuf/lib/src/protobuf/proto3_json.dart b/protobuf/lib/src/protobuf/proto3_json.dart deleted file mode 100644 index 052d10c4d..000000000 --- a/protobuf/lib/src/protobuf/proto3_json.dart +++ /dev/null @@ -1,415 +0,0 @@ -// 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; - -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; - } - 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, - TypeRegistry typeRegistry, - bool ignoreUnknownFields, - bool supportNamesWithUnderscores, - bool permissiveEnums) { - var context = JsonParsingContext( - ignoreUnknownFields, supportNamesWithUnderscores, permissiveEnums); - - void recursionHelper(Object? json, _FieldSet fieldSet) { - Object? convertProto3JsonValue(Object? value, FieldInfo fieldInfo) { - if (value == null) { - return fieldInfo.makeDefault!(); - } - var fieldType = fieldInfo.type; - switch (PbFieldType._baseType(fieldType)) { - case PbFieldType._BOOL_BIT: - if (value is bool) { - return value; - } - throw context.parseException('Expected bool value', json); - case PbFieldType._BYTES_BIT: - if (value is String) { - Uint8List result; - try { - result = base64Decode(value); - } on FormatException { - throw context.parseException( - 'Expected bytes encoded as base64 String', json); - } - return result; - } - throw context.parseException( - 'Expected bytes encoded as base64 String', value); - case PbFieldType._STRING_BIT: - if (value is String) { - return value; - } - throw context.parseException('Expected String value', value); - case PbFieldType._FLOAT_BIT: - case PbFieldType._DOUBLE_BIT: - if (value is double) { - return value; - } else if (value is num) { - return value.toDouble(); - } else if (value is String) { - return double.tryParse(value) ?? - (throw context.parseException( - 'Expected String to encode a double', value)); - } - throw context.parseException( - 'Expected a double represented as a String or number', value); - case PbFieldType._ENUM_BIT: - if (value is String) { - // TODO(sigurdm): Do we want to avoid linear search here? Measure... - final result = permissiveEnums - ? fieldInfo.enumValues! - .findFirst((e) => permissiveCompare(e.name, value)) - : fieldInfo.enumValues!.findFirst((e) => e.name == value); - if ((result != null) || ignoreUnknownFields) return result; - throw context.parseException('Unknown enum value', value); - } else if (value is int) { - return fieldInfo.valueOf!(value) ?? - (ignoreUnknownFields - ? null - : (throw context.parseException( - 'Unknown enum value', value))); - } - throw context.parseException( - 'Expected enum as a string or integer', value); - case PbFieldType._UINT32_BIT: - case PbFieldType._FIXED32_BIT: - int result; - if (value is int) { - result = value; - } else if (value is String) { - result = _tryParse32BitProto3(value, context); - } else { - throw context.parseException( - 'Expected int or stringified int', value); - } - return _check32BitUnsignedProto3(result, context); - case PbFieldType._INT32_BIT: - case PbFieldType._SINT32_BIT: - case PbFieldType._SFIXED32_BIT: - int result; - if (value is int) { - result = value; - } else if (value is String) { - result = _tryParse32BitProto3(value, context); - } else { - throw context.parseException( - 'Expected int or stringified int', value); - } - _check32BitSignedProto3(result, context); - return result; - case PbFieldType._UINT64_BIT: - Int64 result; - if (value is int) { - result = Int64(value); - } else if (value is String) { - result = _tryParse64BitProto3(json, value, context); - } else { - throw context.parseException( - 'Expected int or stringified int', value); - } - return result; - case PbFieldType._INT64_BIT: - case PbFieldType._SINT64_BIT: - case PbFieldType._FIXED64_BIT: - case PbFieldType._SFIXED64_BIT: - if (value is int) return Int64(value); - if (value is String) { - Int64 result; - try { - result = Int64.parseInt(value); - } on FormatException { - throw context.parseException( - 'Expected int or stringified int', value); - } - return result; - } - throw context.parseException( - 'Expected int or stringified int', value); - case PbFieldType._GROUP_BIT: - case PbFieldType._MESSAGE_BIT: - var subMessage = fieldInfo.subBuilder!(); - recursionHelper(value, subMessage._fieldSet); - return subMessage; - default: - throw StateError('Unknown type $fieldType'); - } - } - - Object decodeMapKey(String key, int fieldType) { - switch (PbFieldType._baseType(fieldType)) { - case PbFieldType._BOOL_BIT: - switch (key) { - case 'true': - return true; - case 'false': - return false; - default: - throw context.parseException( - 'Wrong boolean key, should be one of ("true", "false")', key); - } - // ignore: dead_code - throw StateError('(Should have been) unreachable statement'); - case PbFieldType._STRING_BIT: - return key; - case PbFieldType._UINT64_BIT: - // TODO(sigurdm): We do not throw on negative values here. - // That would probably require going via bignum. - return _tryParse64BitProto3(json, key, context); - case PbFieldType._INT64_BIT: - case PbFieldType._SINT64_BIT: - case PbFieldType._SFIXED64_BIT: - case PbFieldType._FIXED64_BIT: - return _tryParse64BitProto3(json, key, context); - case PbFieldType._INT32_BIT: - case PbFieldType._SINT32_BIT: - case PbFieldType._FIXED32_BIT: - case PbFieldType._SFIXED32_BIT: - return _check32BitSignedProto3( - _tryParse32BitProto3(key, context), context); - case PbFieldType._UINT32_BIT: - return _check32BitUnsignedProto3( - _tryParse32BitProto3(key, context), context); - default: - throw StateError('Not a valid key type $fieldType'); - } - } - - if (json == null) { - // `null` represents the default value. Do nothing more. - return; - } - - final meta = fieldSet._meta; - final wellKnownConverter = meta.fromProto3Json; - if (wellKnownConverter != null) { - wellKnownConverter(fieldSet._message!, json, typeRegistry, context); - } else { - if (json is Map) { - final byName = meta.byName; - - json.forEach((key, Object? value) { - if (key is! String) { - throw context.parseException('Key was not a String', key); - } - context.addMapIndex(key); - - var fieldInfo = byName[key]; - if (fieldInfo == null && supportNamesWithUnderscores) { - // We don't optimize for field names with underscores, instead do a - // linear search for the index. - fieldInfo = byName.values - .findFirst((FieldInfo info) => info.protoName == key); - } - if (fieldInfo == null) { - if (ignoreUnknownFields) { - return; - } else { - throw context.parseException('Unknown field name \'$key\'', key); - } - } - - if (_isMapField(fieldInfo.type)) { - if (value is Map) { - final mapFieldInfo = fieldInfo as MapFieldInfo; - final Map fieldValues = fieldSet._ensureMapField(meta, fieldInfo); - value.forEach((subKey, subValue) { - if (subKey is! String) { - throw context.parseException('Expected a String key', subKey); - } - context.addMapIndex(subKey); - fieldValues[decodeMapKey(subKey, mapFieldInfo.keyFieldType)] = - convertProto3JsonValue( - subValue, mapFieldInfo.valueFieldInfo); - context.popIndex(); - }); - } else { - throw context.parseException('Expected a map', value); - } - } else if (_isRepeated(fieldInfo.type)) { - if (value == null) { - // `null` is accepted as the empty list []. - fieldSet._ensureRepeatedField(meta, fieldInfo); - } else if (value is List) { - var values = fieldSet._ensureRepeatedField(meta, fieldInfo); - for (var i = 0; i < value.length; i++) { - final entry = value[i]; - context.addListIndex(i); - values.add(convertProto3JsonValue(entry, fieldInfo)); - context.popIndex(); - } - } else { - throw context.parseException('Expected a list', value); - } - } else if (_isGroupOrMessage(fieldInfo.type)) { - // TODO(sigurdm) consider a cleaner separation between parsing and - // merging. - var parsedSubMessage = - convertProto3JsonValue(value, fieldInfo) as GeneratedMessage; - GeneratedMessage? original = fieldSet._values[fieldInfo.index!]; - if (original == null) { - fieldSet._setNonExtensionFieldUnchecked( - meta, fieldInfo, parsedSubMessage); - } else { - original.mergeFromMessage(parsedSubMessage); - } - } else { - fieldSet._setFieldUnchecked( - meta, fieldInfo, convertProto3JsonValue(value, fieldInfo)); - } - context.popIndex(); - }); - } else { - throw context.parseException('Expected JSON object', json); - } - } - } - - recursionHelper(json, fieldSet); -} diff --git a/protobuf/lib/src/protobuf/proto3_json_reader.dart b/protobuf/lib/src/protobuf/proto3_json_reader.dart new file mode 100644 index 000000000..ff48f0c4b --- /dev/null +++ b/protobuf/lib/src/protobuf/proto3_json_reader.dart @@ -0,0 +1,364 @@ +// 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 _mergeFromProto3JsonReader( + JsonReader jsonReader, + _FieldSet fieldSet, + TypeRegistry typeRegistry, + bool ignoreUnknownFields, + bool supportNamesWithUnderscores, + bool permissiveEnums) { + var context = JsonParsingContext( + ignoreUnknownFields, supportNamesWithUnderscores, permissiveEnums); + __mergeFromProto3JsonReader(jsonReader, fieldSet, typeRegistry, context); +} + +void __mergeFromProto3JsonReader(JsonReader jsonReader, _FieldSet fieldSet, + TypeRegistry typeRegistry, JsonParsingContext context) { + if (jsonReader.tryNull()) { + return; + } + + final meta = fieldSet._meta; + + final wellKnownConverter = meta.mergeFromProto3JsonReader; + if (wellKnownConverter != null) { + wellKnownConverter(fieldSet._message!, jsonReader, typeRegistry, context); + return; + } + + if (!jsonReader.tryObject()) { + throw context.parseException( + 'Expected JSON object', nextJsonObject(jsonReader)); + } + + final fieldsByName = meta.byName; + + var key = jsonReader.nextKey(); + while (key != null) { + context.addMapIndex(key); + var fieldInfo = fieldsByName[key]; + + if (fieldInfo == null && context.supportNamesWithUnderscores) { + // We don't optimize for field names with underscores, instead do a + // linear search for the index. + fieldInfo = meta.byName.values + .findFirst((FieldInfo info) => info.protoName == key); + } + + if (fieldInfo == null) { + if (context.ignoreUnknownFields) { + jsonReader.skipAnyValue(); + context.popIndex(); + key = jsonReader.nextKey(); + continue; + } else { + throw context.parseException('Unknown field name \'$key\'', key); + } + } + + if (_isMapField(fieldInfo.type)) { + final mapFieldInfo = fieldInfo as MapFieldInfo; + final Map fieldValues = fieldSet._ensureMapField(meta, fieldInfo); + if (!jsonReader.tryObject()) { + throw context.parseException( + 'Expected a map', nextJsonObject(jsonReader)); + } + var mapKeyStr = jsonReader.nextKey(); + while (mapKeyStr != null) { + context.addMapIndex(mapKeyStr); + var mapKey = + _decodeMapKey(mapKeyStr, mapFieldInfo.keyFieldType, context); + var value = _parseProto3JsonValue( + jsonReader, mapFieldInfo.valueFieldInfo, typeRegistry, context); + fieldValues[mapKey] = value; + context.popIndex(); + + mapKeyStr = jsonReader.nextKey(); + } + } else if (_isRepeated(fieldInfo.type)) { + if (jsonReader.tryNull()) { + // `null` is accepted as the empty list []. + fieldSet._ensureRepeatedField(meta, fieldInfo); + } else { + if (!jsonReader.tryArray()) { + throw context.parseException( + 'Expected a list', nextJsonObject(jsonReader)); + } + var values = fieldSet._ensureRepeatedField(meta, fieldInfo); + var i = 0; + while (jsonReader.hasNext()) { + context.addListIndex(i); + values.add(_parseProto3JsonValue( + jsonReader, fieldInfo, typeRegistry, context)); + context.popIndex(); + i += 1; + } + } + } else if (_isGroupOrMessage(fieldInfo.type)) { + // TODO: Avoid allocating a message here + var parsedSubMessage = + _parseProto3JsonValue(jsonReader, fieldInfo, typeRegistry, context) + as GeneratedMessage; + GeneratedMessage? original = fieldSet._values[fieldInfo.index!]; + if (original == null) { + fieldSet._setNonExtensionFieldUnchecked( + meta, fieldInfo, parsedSubMessage); + } else { + original.mergeFromMessage(parsedSubMessage); + } + } else { + var value = + _parseProto3JsonValue(jsonReader, fieldInfo, typeRegistry, context); + fieldSet._setFieldUnchecked(meta, fieldInfo, value); + } + + context.popIndex(); + key = jsonReader.nextKey(); + } +} + +Object? _parseProto3JsonValue( + JsonReader jsonReader, + FieldInfo fieldInfo, + TypeRegistry typeRegistry, + JsonParsingContext context, +) { + if (jsonReader.tryNull()) { + return fieldInfo.makeDefault!(); + } + + final fieldType = fieldInfo.type; + + switch (PbFieldType._baseType(fieldType)) { + case PbFieldType._BOOL_BIT: + var b = jsonReader.tryBool(); + if (b != null) { + return b; + } + throw context.parseException( + 'Expected bool value', nextJsonObject(jsonReader)); + + case PbFieldType._BYTES_BIT: + var s = jsonReader.tryString(); + if (s != null) { + try { + return base64Decode(s); + } on FormatException { + throw context.parseException( + 'Expected bytes encoded as base64 String', s); + } + } + throw context.parseException( + 'Expected String value', nextJsonObject(jsonReader)); + + case PbFieldType._STRING_BIT: + var s = jsonReader.tryString(); + if (s != null) { + return s; + } + throw context.parseException( + 'Expected String value', nextJsonObject(jsonReader)); + + case PbFieldType._FLOAT_BIT: + case PbFieldType._DOUBLE_BIT: + var n = jsonReader.tryNum(); + if (n != null) { + return n.toDouble(); + } + var s = jsonReader.tryString(); + if (s != null) { + return double.tryParse(s) ?? + (throw context.parseException( + 'Expected String to encode a double', s)); + } + throw context.parseException( + 'Expected a double represented as a String or number', + nextJsonObject(jsonReader)); + + case PbFieldType._ENUM_BIT: + var s = jsonReader.tryString(); + if (s != null) { + // TODO(sigurdm): Do we want to avoid linear search here? Measure... + final result = context.permissiveEnums + ? fieldInfo.enumValues! + .findFirst((e) => permissiveCompare(e.name, s)) + : fieldInfo.enumValues!.findFirst((e) => e.name == s); + if ((result != null) || context.ignoreUnknownFields) { + return result; + } + throw context.parseException('Unknown enum value', s); + } + var n = jsonReader.tryNum(); + if (n != null) { + return fieldInfo.valueOf!(n as int) ?? + (context.ignoreUnknownFields + ? null + : (throw context.parseException('Unknown enum value', n))); + } + throw context.parseException( + 'Expected enum as a string or integer', nextJsonObject(jsonReader)); + + case PbFieldType._UINT32_BIT: + case PbFieldType._FIXED32_BIT: + var n = jsonReader.tryNum(); + if (n != null) { + if (n is int) { + _check32BitUnsignedProto3(n, context); + return n; + } + throw context.parseException('Expected 32-bit int', n); + } + var s = jsonReader.tryString(); + if (s != null) { + return _tryParse32BitProto3(s, context); + } + throw context.parseException( + 'Expected 32-bit int or stringified int', nextJsonObject(jsonReader)); + + case PbFieldType._INT32_BIT: + case PbFieldType._SINT32_BIT: + case PbFieldType._SFIXED32_BIT: + var n = jsonReader.tryNum(); + if (n != null) { + if (n is int) { + _check32BitSignedProto3(n, context); + return n; + } + throw context.parseException('Expected 32-bit int', n); + } + var s = jsonReader.tryString(); + if (s != null) { + return _tryParse32BitProto3(s, context); + } + throw context.parseException( + 'Expected 32-bit int or stringified int', nextJsonObject(jsonReader)); + + case PbFieldType._UINT64_BIT: + var n = jsonReader.tryNum(); + if (n != null) { + if (n is int) { + return Int64(n); + } + throw context.parseException('Expected 64-bit int', n); + } + var s = jsonReader.tryString(); + if (s != null) { + return _tryParse64BitProto3(s, s, context); + } + throw context.parseException( + 'Expected 64-bit int or stringified int', nextJsonObject(jsonReader)); + + case PbFieldType._INT64_BIT: + case PbFieldType._SINT64_BIT: + case PbFieldType._FIXED64_BIT: + case PbFieldType._SFIXED64_BIT: + var n = jsonReader.tryNum(); + if (n != null) { + if (n is int) { + return Int64(n); + } + throw context.parseException('Expected 64-bit', n); + } + var s = jsonReader.tryString(); + if (s != null) { + try { + return Int64.parseInt(s); + } on FormatException { + throw context.parseException( + 'Expected 64-bit int or stringified int', s); + } + } + throw context.parseException( + 'Expected 64-bit int or stringified int', nextJsonObject(jsonReader)); + + case PbFieldType._GROUP_BIT: + case PbFieldType._MESSAGE_BIT: + var subMessage = fieldInfo.subBuilder!(); + __mergeFromProto3JsonReader( + jsonReader, subMessage._fieldSet, typeRegistry, context); + return subMessage; + + default: + throw StateError('Unknown type $fieldType'); + } +} + +Object _decodeMapKey(String key, int fieldType, JsonParsingContext context) { + switch (PbFieldType._baseType(fieldType)) { + case PbFieldType._BOOL_BIT: + switch (key) { + case 'true': + return true; + case 'false': + return false; + default: + throw context.parseException( + 'Wrong boolean key, should be one of ("true", "false")', key); + } + // ignore: dead_code + throw StateError('(Should have been) unreachable statement'); + case PbFieldType._STRING_BIT: + return key; + case PbFieldType._UINT64_BIT: + // TODO(sigurdm): We do not throw on negative values here. + // That would probably require going via bignum. + return _tryParse64BitProto3(key, key, context); + case PbFieldType._INT64_BIT: + case PbFieldType._SINT64_BIT: + case PbFieldType._SFIXED64_BIT: + case PbFieldType._FIXED64_BIT: + return _tryParse64BitProto3(key, key, context); + case PbFieldType._INT32_BIT: + case PbFieldType._SINT32_BIT: + case PbFieldType._FIXED32_BIT: + case PbFieldType._SFIXED32_BIT: + return _check32BitSignedProto3( + _tryParse32BitProto3(key, context), context); + case PbFieldType._UINT32_BIT: + return _check32BitUnsignedProto3( + _tryParse32BitProto3(key, context), context); + default: + throw StateError('Not a valid key type $fieldType'); + } +} + +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..0296245d3 --- /dev/null +++ b/protobuf/lib/src/protobuf/proto3_json_writer.dart @@ -0,0 +1,143 @@ +// 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.writeToProto3JsonSink; + 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 mapEntryInfo = fieldInfo as MapFieldInfo; + for (var entry in (value as PbMap).entries) { + final key = entry.key; + final value = entry.value; + _writeMapKey(key, mapEntryInfo.keyFieldType, jsonSink); + _writeFieldValue( + value, mapEntryInfo.valueFieldType, jsonSink, typeRegistry); + } + jsonSink.endObject(); // end map field + } else if (fieldInfo.isRepeated) { + jsonSink.startArray(); // start repeated field + for (final element in value as PbList) { + _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: + jsonSink.addKey((key as bool).toString()); + break; + case PbFieldType._STRING_BIT: + jsonSink.addKey(key as String); + break; + case PbFieldType._UINT64_BIT: + jsonSink.addKey((key as Int64).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)) { + _writeToProto3JsonSink( + (fieldValue as GeneratedMessage)._fieldSet, typeRegistry, jsonSink); + } else if (_isEnum(fieldType)) { + jsonSink.addString((fieldValue as ProtobufEnum).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); + } else if (value.isInfinite) { + jsonSink.addString(value.isNegative ? negativeInfinity : infinity); + } else { + jsonSink.addNumber(value); + } + break; + case PbFieldType._UINT64_BIT: + jsonSink.addString((fieldValue as Int64).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 b474ba6bc..3d9e8f110 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: diff --git a/protoc_plugin/lib/src/message_generator.dart b/protoc_plugin/lib/src/message_generator.dart index a2efc02e9..47ee8ff06 100644 --- a/protoc_plugin/lib/src/message_generator.dart +++ b/protoc_plugin/lib/src/message_generator.dart @@ -304,8 +304,8 @@ class MessageGenerator extends ProtobufContainer { var packageClause = package == '' ? '' : ', package: $conditionalPackageName'; var proto3JsonClause = (mixin?.hasProto3JsonHelpers ?? false) - ? ', toProto3Json: $mixinImportPrefix.${mixin!.name}.toProto3JsonHelper, ' - 'fromProto3Json: $mixinImportPrefix.${mixin!.name}.fromProto3JsonHelper' + ? ', writeToProto3JsonSink: $mixinImportPrefix.${mixin!.name}.toProto3JsonHelper, ' + 'mergeFromProto3JsonReader: $mixinImportPrefix.${mixin!.name}.fromProto3JsonHelper' : ''; out.addAnnotatedBlock( 'class $classname extends $protobufImportPrefix.GeneratedMessage$mixinClause {', diff --git a/protoc_plugin/test/proto3_json_test.dart b/protoc_plugin/test/proto3_json_test.dart index 69acf75db..3c2f49cfd 100644 --- a/protoc_plugin/test/proto3_json_test.dart +++ b/protoc_plugin/test/proto3_json_test.dart @@ -365,8 +365,15 @@ void main() { group('decode', () { Matcher parseFailure(List expectedPath) => throwsA(predicate((e) { if (e is FormatException) { - final pathExpression = - RegExp(r'root(\["[^"]*"]*\])*').firstMatch(e.message)![0]!; + final firstMatch = + RegExp(r'root(\["[^"]*"]*\])*').firstMatch(e.message); + if (firstMatch == null) { + return false; + } + final pathExpression = firstMatch[0]; + if (pathExpression == null) { + return false; + } final actualPath = RegExp(r'\["([^"]*)"\]') .allMatches(pathExpression) .map((match) => match[1]) @@ -773,7 +780,7 @@ void main() { expect( TestMap() ..mergeFromProto3Json({ - 'int32ToInt32Field': {'2': 21} + 'int32ToInt32Field': {'2': 21} }), TestMap()..int32ToInt32Field[2] = 21); }); @@ -1184,8 +1191,8 @@ void main() { 'number': 22.3, 'string': 'foo', 'bool': false, - 'struct': {'a': 0}, - 'list': [{}, [], 'why'] + 'struct': {'a': 0}, + 'list': [{}, [], 'why'] }; final s = Struct() @@ -1205,7 +1212,7 @@ void main() { ]))); expect(Struct()..mergeFromProto3Json(f), s); - expect(Struct()..mergeFromProto3Json({'a': 12}), + expect(Struct()..mergeFromProto3Json({'a': 12}), (Struct()..fields['a'] = (Value()..numberValue = 12.0)), reason: 'Allow key type to be `dynamic`'); @@ -1242,7 +1249,7 @@ void main() { test('one-of', () { expectFirstSet(Foo()..mergeFromProto3Json({'first': 'oneof'})); expectSecondSet(Foo()..mergeFromProto3Json({'second': 1})); - expectOneofNotSet(Foo()..mergeFromProto3Json({})); + expectOneofNotSet(Foo()..mergeFromProto3Json({})); }); group('Convert Double', () {