blob: 5da0531526925be7361affffd2d36b734e9a982f [file] [log] [blame]
// 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 'internal.dart';
// Public because this is called from the mixins library.
Object writeToProto3JsonAny(
FieldSet fs,
String typeUrl,
TypeRegistry typeRegistry,
) {
final result = _writeToProto3Json(fs, typeRegistry);
final wellKnownType = fs._meta._wellKnownType;
if (wellKnownType != null) {
switch (wellKnownType) {
case WellKnownType.any:
case WellKnownType.timestamp:
case WellKnownType.duration:
case WellKnownType.struct:
case WellKnownType.value:
case WellKnownType.listValue:
case WellKnownType.fieldMask:
case WellKnownType.doubleValue:
case WellKnownType.floatValue:
case WellKnownType.int64Value:
case WellKnownType.uint64Value:
case WellKnownType.int32Value:
case WellKnownType.uint32Value:
case WellKnownType.boolValue:
case WellKnownType.stringValue:
case WellKnownType.bytesValue:
return {'@type': typeUrl, 'value': result};
}
}
(result as Map<String, dynamic>)['@type'] = typeUrl;
return result;
}
Object? _writeToProto3Json(FieldSet fs, TypeRegistry typeRegistry) {
String? convertToMapKey(dynamic key, int keyType) {
final baseType = PbFieldType.baseType(keyType);
assert(!PbFieldType.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 (PbFieldType.isGroupOrMessage(fieldType!)) {
return _writeToProto3Json(
(fieldValue as GeneratedMessage)._fieldSet,
typeRegistry,
);
} else if (PbFieldType.isEnum(fieldType)) {
return (fieldValue as ProtobufEnum).name;
} else {
final baseType = PbFieldType.baseType(fieldType);
switch (baseType) {
case PbFieldType.BOOL_BIT:
return fieldValue as bool;
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:
final double value = fieldValue;
if (value.isNaN) {
return nan;
}
if (value.isInfinite) {
return value.isNegative ? negativeInfinity : infinity;
}
if (value.toInt() == fieldValue) {
return value.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;
final wellKnownType = meta._wellKnownType;
if (wellKnownType != null) {
switch (wellKnownType) {
case WellKnownType.any:
return AnyMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.timestamp:
return TimestampMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.duration:
return DurationMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.struct:
return StructMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.value:
return ValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.listValue:
return ListValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.fieldMask:
return FieldMaskMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.doubleValue:
return DoubleValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.floatValue:
return FloatValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.int64Value:
return Int64ValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.uint64Value:
return UInt64ValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.int32Value:
return Int32ValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.uint32Value:
return UInt32ValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.boolValue:
return BoolValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.stringValue:
return StringValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
case WellKnownType.bytesValue:
return BytesValueMixin.toProto3JsonHelper(fs._message!, typeRegistry);
}
// [WellKnownType] could be used to for messages which have special
// encodings in other codecs. The set of messages which special encodings in
// proto3json is handled here, so we intentionally fall through to the
// default message handling rather than throwing.
}
final result = <String, dynamic>{};
for (final fieldInfo in fs._infosSortedByTag) {
final 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) {
final 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;
}
/// TODO(paulberry): find a better home for this?
extension _FindFirst<E> on Iterable<E> {
E? findFirst(bool Function(E) test) {
for (final element in this) {
if (test(element)) return element;
}
return null;
}
}
// Public because this is called from the mixins library.
void mergeFromProto3JsonAny(
Object? json,
FieldSet fieldSet,
TypeRegistry typeRegistry,
JsonParsingContext context,
) {
if (json is! Map<String, dynamic>) {
throw context.parseException('Expected JSON object', json);
}
final wellKnownType = fieldSet._meta._wellKnownType;
if (wellKnownType != null) {
switch (wellKnownType) {
case WellKnownType.any:
case WellKnownType.timestamp:
case WellKnownType.duration:
case WellKnownType.struct:
case WellKnownType.value:
case WellKnownType.listValue:
case WellKnownType.fieldMask:
case WellKnownType.doubleValue:
case WellKnownType.floatValue:
case WellKnownType.int64Value:
case WellKnownType.uint64Value:
case WellKnownType.int32Value:
case WellKnownType.uint32Value:
case WellKnownType.boolValue:
case WellKnownType.stringValue:
case WellKnownType.bytesValue:
final value = json['value'];
return _mergeFromProto3JsonWithContext(
value,
fieldSet,
typeRegistry,
context,
);
}
}
// TODO(sigurdm): avoid cloning [object] here.
final withoutType = Map<String, dynamic>.from(json)..remove('@type');
return _mergeFromProto3JsonWithContext(
withoutType,
fieldSet,
typeRegistry,
context,
);
}
/// Merge a JSON object representing a message in proto3 JSON format ([json])
/// to [fieldSet].
void _mergeFromProto3Json(
Object? json,
FieldSet fieldSet,
TypeRegistry typeRegistry,
bool ignoreUnknownFields,
bool supportNamesWithUnderscores,
bool permissiveEnums,
) {
final context = JsonParsingContext(
ignoreUnknownFields,
supportNamesWithUnderscores,
permissiveEnums,
);
return _mergeFromProto3JsonWithContext(json, fieldSet, typeRegistry, context);
}
/// Merge a JSON object representing a message in proto3 JSON format ([json])
/// to [fieldSet].
void _mergeFromProto3JsonWithContext(
Object? json,
FieldSet fieldSet,
TypeRegistry typeRegistry,
JsonParsingContext context,
) {
fieldSet._ensureWritable();
void recursionHelper(Object? json, FieldSet fieldSet) {
// Convert a JSON object to proto object. Returns `null` on unknown enum
// values when [ignoreUnknownFields] is [true].
Object? convertProto3JsonValue(Object value, FieldInfo fieldInfo) {
final 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) {
try {
return base64Decode(value);
} on FormatException {
throw context.parseException(
'Expected bytes encoded as base64 String',
json,
);
}
}
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 =
context.permissiveEnums
? fieldInfo.enumValues!.findFirst(
(e) => permissiveCompare(e.name, value),
)
: fieldInfo.enumValues!.findFirst((e) => e.name == value);
if ((result != null) || context.ignoreUnknownFields) return result;
throw context.parseException('Unknown enum value', value);
} else if (value is int) {
return fieldInfo.valueOf!(value) ??
(context.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 = Proto3ParseUtils.tryParse32Bit(value, context);
} else {
throw context.parseException(
'Expected int or stringified int',
value,
);
}
return Proto3ParseUtils.check32BitUnsigned(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 = Proto3ParseUtils.tryParse32Bit(value, context);
} else {
throw context.parseException(
'Expected int or stringified int',
value,
);
}
Proto3ParseUtils.check32BitSigned(result, context);
return result;
case PbFieldType.UINT64_BIT:
Int64 result;
if (value is int) {
result = Int64(value);
} else if (value is String) {
result = Proto3ParseUtils.tryParse64Bit(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) {
try {
return Int64.parseInt(value);
} on FormatException {
throw context.parseException(
'Expected int or stringified int',
value,
);
}
}
throw context.parseException(
'Expected int or stringified int',
value,
);
case PbFieldType.GROUP_BIT:
case PbFieldType.MESSAGE_BIT:
final 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,
);
}
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 Proto3ParseUtils.tryParse64Bit(json, key, context);
case PbFieldType.INT64_BIT:
case PbFieldType.SINT64_BIT:
case PbFieldType.SFIXED64_BIT:
case PbFieldType.FIXED64_BIT:
return Proto3ParseUtils.tryParse64Bit(json, key, context);
case PbFieldType.INT32_BIT:
case PbFieldType.SINT32_BIT:
case PbFieldType.FIXED32_BIT:
case PbFieldType.SFIXED32_BIT:
return Proto3ParseUtils.check32BitSigned(
Proto3ParseUtils.tryParse32Bit(key, context),
context,
);
case PbFieldType.UINT32_BIT:
return Proto3ParseUtils.check32BitUnsigned(
Proto3ParseUtils.tryParse32Bit(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 wellKnownType = meta._wellKnownType;
if (wellKnownType != null) {
switch (wellKnownType) {
case WellKnownType.any:
AnyMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.timestamp:
TimestampMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.duration:
DurationMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.struct:
StructMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.value:
ValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.listValue:
ListValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.fieldMask:
FieldMaskMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.doubleValue:
DoubleValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.floatValue:
FloatValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.int64Value:
Int64ValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.uint64Value:
UInt64ValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.int32Value:
Int32ValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.uint32Value:
UInt32ValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.boolValue:
BoolValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.stringValue:
StringValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
case WellKnownType.bytesValue:
BytesValueMixin.fromProto3JsonHelper(
fieldSet._message!,
json,
typeRegistry,
context,
);
return;
}
// [WellKnownType] could be used to for messages which have special
// encodings in other codecs. The set of messages which special encodings
// in proto3json is handled here, so we intentionally fall through to the
// default message handling rather than throwing.
}
if (json is Map) {
final byName = meta.byName;
json.forEach((key, Object? value) {
if (value == null) {
return;
}
if (key is! String) {
throw context.parseException('Key was not a String', key);
}
context.addMapIndex(key);
var fieldInfo = byName[key];
if (fieldInfo == null && context.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 (context.ignoreUnknownFields) {
return;
} else {
throw context.parseException('Unknown field name \'$key\'', key);
}
}
if (PbFieldType.isMapField(fieldInfo.type)) {
if (value is Map) {
final mapFieldInfo = fieldInfo as MapFieldInfo<dynamic, dynamic>;
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);
final key = decodeMapKey(subKey, mapFieldInfo.keyFieldType);
final value = convertProto3JsonValue(
subValue,
mapFieldInfo.valueFieldInfo,
);
if (value != null) {
fieldValues[key] = value;
}
context.popIndex();
});
} else {
throw context.parseException('Expected a map', value);
}
} else if (PbFieldType.isRepeated(fieldInfo.type)) {
if (value is List) {
final values = fieldSet._ensureRepeatedField(meta, fieldInfo);
for (var i = 0; i < value.length; i++) {
final entry = value[i];
context.addListIndex(i);
final parsedValue = convertProto3JsonValue(entry, fieldInfo);
if (parsedValue != null) {
values.add(parsedValue);
}
context.popIndex();
}
} else {
throw context.parseException('Expected a list', value);
}
} else if (PbFieldType.isGroupOrMessage(fieldInfo.type)) {
// TODO(sigurdm) consider a cleaner separation between parsing and
// merging.
final parsedSubMessage =
convertProto3JsonValue(value, fieldInfo) as GeneratedMessage;
final GeneratedMessage? original = fieldSet._values[fieldInfo.index!];
if (original == null) {
fieldSet._setNonExtensionFieldUnchecked(
meta,
fieldInfo,
parsedSubMessage,
);
} else {
original.mergeFromMessage(parsedSubMessage);
}
} else {
final parsedValue = convertProto3JsonValue(value, fieldInfo);
if (parsedValue == null) {
// Unknown enum
if (!context.ignoreUnknownFields) {
throw context.parseException('Unknown enum value', value);
}
} else {
fieldSet._setFieldUnchecked(meta, fieldInfo, parsedValue);
}
}
context.popIndex();
});
} else {
throw context.parseException('Expected JSON object', json);
}
}
recursionHelper(json, fieldSet);
}