From 861fadb968c24b467bc0ac473f5e6db10990b340 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis <25266387+Leptopoda@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:56:19 +0100 Subject: [PATCH] feat: support ignoring custom types that StandardJsonPlugin should leave as lists (#1296) Signed-off-by: Nikolas Rimikis Co-authored-by: Nikolas Rimikis --- CHANGELOG.md | 4 ++ built_value/lib/serializer.dart | 2 +- built_value/lib/standard_json_plugin.dart | 19 ++++--- end_to_end_test/lib/records.dart | 56 +++++++++++++++++++ end_to_end_test/lib/records.g.dart | 33 +++++++++-- .../test/records_serializer_test.dart | 32 +++++++++++ 6 files changed, 134 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413b1f84..24110bd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +# 8.9.0-wip + +- Add ignoring types that the StandardJsonPlugin should leave as a List. + # 8.8.1 - Fix codegen for enum wire keys when there is a `$` in the field name. diff --git a/built_value/lib/serializer.dart b/built_value/lib/serializer.dart index 22e5163c..c2e0f62e 100644 --- a/built_value/lib/serializer.dart +++ b/built_value/lib/serializer.dart @@ -334,7 +334,7 @@ abstract class PrimitiveSerializer implements Serializer { /// the type being serialized is provided in [specifiedType]. /// /// Returns a value that can be represented as a JSON primitive: a boolean, - /// an integer, a double, or a String. + /// an integer, a double, a String or a List. /// /// TODO(davidmorgan): document the wire format. Object serialize(Serializers serializers, T object, diff --git a/built_value/lib/standard_json_plugin.dart b/built_value/lib/standard_json_plugin.dart index 6d1c2001..ab0e3bd1 100644 --- a/built_value/lib/standard_json_plugin.dart +++ b/built_value/lib/standard_json_plugin.dart @@ -24,11 +24,19 @@ class StandardJsonPlugin implements SerializerPlugin { /// The field used to specify the value type if needed. Defaults to `$`. final String discriminator; - // The key used when there is just a single value, for example if serializing - // an `int`. + /// The key used when there is just a single value, for example if serializing + /// an `int`. final String valueKey; - StandardJsonPlugin({this.discriminator = r'$', this.valueKey = ''}); + /// The types to leave as a list when converting into json. + final BuiltSet typesToLeaveAsList; + + StandardJsonPlugin({ + this.discriminator = r'$', + this.valueKey = '', + Iterable? typesToLeaveAsList, + }) : typesToLeaveAsList = BuiltSet( + {BuiltList, BuiltSet, JsonObject, ...?typesToLeaveAsList}); @override Object? beforeSerialize(Object? object, FullType specifiedType) { @@ -41,10 +49,7 @@ class StandardJsonPlugin implements SerializerPlugin { @override Object? afterSerialize(Object? object, FullType specifiedType) { - if (object is List && - specifiedType.root != BuiltList && - specifiedType.root != BuiltSet && - specifiedType.root != JsonObject) { + if (object is List && !typesToLeaveAsList.contains(specifiedType.root)) { if (specifiedType.isUnspecified) { return _toMapWithDiscriminator(object); } else { diff --git a/end_to_end_test/lib/records.dart b/end_to_end_test/lib/records.dart index e2caa148..117cefcb 100644 --- a/end_to_end_test/lib/records.dart +++ b/end_to_end_test/lib/records.dart @@ -66,6 +66,7 @@ abstract class SerializableRecordValue int get value; RecordOfIntInt? get record; + RecordOfIntOrList? get intOrList; factory SerializableRecordValue( [void Function(SerializableRecordValueBuilder) updates]) = @@ -93,3 +94,58 @@ class RecordOfIntIntSerializer implements StructuredSerializer { @override String get wireName => 'RecordOfIntInt'; } + +typedef RecordOfIntOrList = (int?, BuiltList?); + +class RecordOfIntOrListSerializer + implements PrimitiveSerializer { + const RecordOfIntOrListSerializer(); + + @override + Iterable get types => const [RecordOfIntOrList]; + + @override + String get wireName => 'RecordOfIntOrList'; + + @override + Object serialize( + Serializers serializers, + RecordOfIntOrList object, { + FullType specifiedType = FullType.unspecified, + }) { + dynamic value; + value = object.$1; + if (value != null) { + return serializers.serialize(value, specifiedType: const FullType(int))!; + } + value = object.$2; + if (value != null) { + return serializers.serialize(value, + specifiedType: const FullType(BuiltList, [FullType(String)]))!; + } + + throw StateError('Tried to serialize without any value.'); + } + + @override + RecordOfIntOrList deserialize( + Serializers serializers, + Object data, { + FullType specifiedType = FullType.unspecified, + }) { + BuiltList? list; + try { + list = serializers.deserialize( + data, + specifiedType: const FullType(BuiltList, [FullType(String)]), + )! as BuiltList; + } catch (_) {} + int? number; + try { + number = serializers.deserialize(data, + specifiedType: const FullType(int))! as int; + } catch (_) {} + + return (number, list); + } +} diff --git a/end_to_end_test/lib/records.g.dart b/end_to_end_test/lib/records.g.dart index 72da6844..fedc6307 100644 --- a/end_to_end_test/lib/records.g.dart +++ b/end_to_end_test/lib/records.g.dart @@ -35,6 +35,13 @@ class _$SerializableRecordValueSerializer ..add(serializers.serialize(value, specifiedType: const FullType(RecordOfIntInt))); } + value = object.intOrList; + if (value != null) { + result + ..add('intOrList') + ..add(serializers.serialize(value, + specifiedType: const FullType(RecordOfIntOrList))); + } return result; } @@ -58,6 +65,11 @@ class _$SerializableRecordValueSerializer result.record = serializers.deserialize(value, specifiedType: const FullType(RecordOfIntInt)) as RecordOfIntInt?; break; + case 'intOrList': + result.intOrList = serializers.deserialize(value, + specifiedType: const FullType(RecordOfIntOrList)) + as RecordOfIntOrList?; + break; } } @@ -447,12 +459,16 @@ class _$SerializableRecordValue extends SerializableRecordValue { final int value; @override final RecordOfIntInt? record; + @override + final RecordOfIntOrList? intOrList; factory _$SerializableRecordValue( [void Function(SerializableRecordValueBuilder)? updates]) => (new SerializableRecordValueBuilder()..update(updates))._build(); - _$SerializableRecordValue._({required this.value, this.record}) : super._() { + _$SerializableRecordValue._( + {required this.value, this.record, this.intOrList}) + : super._() { BuiltValueNullFieldError.checkNotNull( value, r'SerializableRecordValue', 'value'); } @@ -472,7 +488,8 @@ class _$SerializableRecordValue extends SerializableRecordValue { final dynamic _$dynamicOther = other; return other is SerializableRecordValue && value == other.value && - record == _$dynamicOther.record; + record == _$dynamicOther.record && + intOrList == _$dynamicOther.intOrList; } @override @@ -480,6 +497,7 @@ class _$SerializableRecordValue extends SerializableRecordValue { var _$hash = 0; _$hash = $jc(_$hash, value.hashCode); _$hash = $jc(_$hash, record.hashCode); + _$hash = $jc(_$hash, intOrList.hashCode); _$hash = $jf(_$hash); return _$hash; } @@ -488,7 +506,8 @@ class _$SerializableRecordValue extends SerializableRecordValue { String toString() { return (newBuiltValueToStringHelper(r'SerializableRecordValue') ..add('value', value) - ..add('record', record)) + ..add('record', record) + ..add('intOrList', intOrList)) .toString(); } } @@ -506,6 +525,10 @@ class SerializableRecordValueBuilder RecordOfIntInt? get record => _$this._record; set record(RecordOfIntInt? record) => _$this._record = record; + RecordOfIntOrList? _intOrList; + RecordOfIntOrList? get intOrList => _$this._intOrList; + set intOrList(RecordOfIntOrList? intOrList) => _$this._intOrList = intOrList; + SerializableRecordValueBuilder(); SerializableRecordValueBuilder get _$this { @@ -513,6 +536,7 @@ class SerializableRecordValueBuilder if ($v != null) { _value = $v.value; _record = $v.record; + _intOrList = $v.intOrList; _$v = null; } return this; @@ -537,7 +561,8 @@ class SerializableRecordValueBuilder new _$SerializableRecordValue._( value: BuiltValueNullFieldError.checkNotNull( value, r'SerializableRecordValue', 'value'), - record: record); + record: record, + intOrList: intOrList); replace(_$result); return _$result; } diff --git a/end_to_end_test/test/records_serializer_test.dart b/end_to_end_test/test/records_serializer_test.dart index 44260b90..1081020a 100644 --- a/end_to_end_test/test/records_serializer_test.dart +++ b/end_to_end_test/test/records_serializer_test.dart @@ -4,6 +4,9 @@ import 'dart:convert'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_value/standard_json_plugin.dart'; import 'package:end_to_end_test/errors_matchers.dart'; import 'package:end_to_end_test/records.dart'; import 'package:end_to_end_test/serializers.dart'; @@ -63,4 +66,33 @@ void main() { expect(serializersWithCustomSerializer.deserialize(serialized), data); }); }); + + group('$SerializableRecordValue with a record list value', () { + var data = SerializableRecordValue((b) => b + ..value = 1 + ..intOrList = (null, BuiltList(['value0', 'value1', 'value2']))); + var serialized = json.decode(json.encode({ + 'value': 1, + 'intOrList': ['value0', 'value1', 'value2'], + })) as Object; + var serializersWithCustomSerializer = (serializers.toBuilder() + ..addPlugin( + StandardJsonPlugin(typesToLeaveAsList: {RecordOfIntOrList})) + ..add(RecordOfIntOrListSerializer())) + .build(); + + test('can be serialized with custom serializer', () { + expect( + serializersWithCustomSerializer.serialize(data, + specifiedType: FullType(SerializableRecordValue)), + serialized); + }); + + test('can be deserialized with custom deserializer', () { + expect( + serializersWithCustomSerializer.deserialize(serialized, + specifiedType: FullType(SerializableRecordValue)), + data); + }); + }); }