Skip to content

Commit f540ca6

Browse files
committed
Add an option to JsonSerializableGenerator to opt-in to wrappers
1 parent 128a1f7 commit f540ca6

20 files changed

+1058
-202
lines changed

json_annotation/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## 0.2.1
22

3+
* Added a helper class – `$JsonMapWrapper` – and helper functions – `$wrapMap`,
4+
`$wrapMapHandleNull`, `$wrapList`, and `$wrapListHandleNull` – to support
5+
the `useWrappers` option added to `JsonSerializableGenerator` in `v0.3.0` of
6+
`package:json_serializable`.
7+
38
* `JsonSerializable` class annotation
49
* Added `nullable` field.
510
* Fixed doc comment.

json_annotation/lib/src/json_serializable.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:collection';
6+
57
class JsonSerializable {
68
// TODO(kevmoo): document these fields
79
final bool createFactory;
@@ -68,3 +70,55 @@ class JsonKey {
6870
/// Only required when the default behavior is not desired.
6971
const JsonKey({this.name, this.nullable, this.includeIfNull});
7072
}
73+
74+
// TODO(kevmoo): Add documentation
75+
abstract class $JsonMapWrapper extends UnmodifiableMapBase<String, dynamic> {}
76+
77+
Map<String, dynamic> $wrapMap<K, V>(
78+
Map<K, V> source, dynamic converter(V key)) =>
79+
new _MappingMap(source, converter);
80+
81+
Map<String, dynamic> $wrapMapHandleNull<K, V>(
82+
Map<K, V> source, dynamic converter(V key)) =>
83+
source == null ? null : new _MappingMap(source, converter);
84+
85+
List<dynamic> $wrapList<T>(List<T> source, dynamic converter(T key)) =>
86+
new _MappingList(source, converter);
87+
88+
List<dynamic> $wrapListHandleNull<T>(
89+
List<T> source, dynamic converter(T key)) =>
90+
source == null ? null : new _MappingList(source, converter);
91+
92+
typedef dynamic _Convert<S>(S value);
93+
94+
class _MappingList<S> extends ListBase<dynamic> {
95+
final List<S> _source;
96+
final _Convert<S> _converter;
97+
98+
_MappingList(this._source, this._converter);
99+
100+
@override
101+
dynamic operator [](int index) => _converter(_source[index]);
102+
103+
@override
104+
operator []=(int index, dynamic value) => throw new UnsupportedError('');
105+
106+
@override
107+
int get length => _source.length;
108+
109+
@override
110+
set length(int value) => throw new UnsupportedError('');
111+
}
112+
113+
class _MappingMap<K, V> extends UnmodifiableMapBase<String, dynamic> {
114+
final Map<K, V> _source;
115+
final _Convert<V> _converter;
116+
117+
_MappingMap(this._source, this._converter);
118+
119+
@override
120+
Iterable<String> get keys => _source.keys.map((k) => k as String);
121+
122+
@override
123+
dynamic operator [](Object key) => _converter(_source[key]);
124+
}

json_serializable/CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
## 0.2.5
1+
## 0.3.0
2+
3+
* **BREAKING** The arguments to `TypeHelper` `serialize` and `deserialize` have
4+
changed.
5+
* `SerializeContext` and `DeserializeContext` (new classes) are now passed
6+
instead of the `TypeHelperGenerator` typedef (which has been deleted).
7+
8+
* `JsonSerializableGenerator` now supports an optional `useWrappers` argument
9+
when generates and uses wrapper classes to (hopefully) improve the speed and
10+
memory usage of serialization – at the cost of more code.
211

312
* Throw an exception if a duplicate JSON key is detected.
413

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,26 @@ class JsonSerializableGenerator
3636

3737
final List<TypeHelper> _typeHelpers;
3838

39+
final bool useWrappers;
40+
3941
/// Creates an instance of [JsonSerializableGenerator].
4042
///
4143
/// If [typeHelpers] is not provided, two built-in helpers are used:
4244
/// [JsonHelper] and [DateTimeHelper].
43-
const JsonSerializableGenerator({List<TypeHelper> typeHelpers})
44-
: this._typeHelpers = typeHelpers ?? _defaultHelpers;
45+
const JsonSerializableGenerator(
46+
{List<TypeHelper> typeHelpers, bool useWrappers: false})
47+
: this.useWrappers = useWrappers ?? false,
48+
this._typeHelpers = typeHelpers ?? _defaultHelpers;
4549

4650
/// Creates an instance of [JsonSerializableGenerator].
4751
///
4852
/// [typeHelpers] provides a set of [TypeHelper] that will be used along with
4953
/// the built-in helpers: [JsonHelper] and [DateTimeHelper].
5054
factory JsonSerializableGenerator.withDefaultHelpers(
51-
Iterable<TypeHelper> typeHelpers) =>
55+
Iterable<TypeHelper> typeHelpers,
56+
{bool useWrappers: false}) =>
5257
new JsonSerializableGenerator(
58+
useWrappers: useWrappers,
5359
typeHelpers: new List.unmodifiable(
5460
[typeHelpers, _defaultHelpers].expand((e) => e)));
5561

@@ -125,6 +131,7 @@ class JsonSerializableGenerator
125131

126132
if (classAnnotation.createToJson) {
127133
var mixClassName = '${prefix}SerializerMixin';
134+
var helpClassName = '${prefix}JsonMapWrapper';
128135

129136
//
130137
// Generate the mixin class
@@ -138,23 +145,95 @@ class JsonSerializableGenerator
138145
buffer.writeln(' ${field.type} get ${field.name};');
139146
}
140147

141-
buffer.writeln(' Map<String, dynamic> toJson() ');
142-
if (fieldsList
143-
.every((e) => _includeIfNull(e, classAnnotation.includeIfNull))) {
144-
// write simple `toJson` method that includes all keys...
145-
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
148+
buffer.write(' Map<String, dynamic> toJson() ');
149+
150+
if (useWrappers) {
151+
buffer.writeln('=> new $helpClassName(this);');
146152
} else {
147-
// At least one field should be excluded if null
148-
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
153+
if (fieldsList
154+
.every((e) => _includeIfNull(e, classAnnotation.includeIfNull))) {
155+
// write simple `toJson` method that includes all keys...
156+
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
157+
} else {
158+
// At least one field should be excluded if null
159+
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
160+
}
149161
}
150162

151163
// end of the mixin class
152164
buffer.writeln('}');
165+
166+
if (useWrappers) {
167+
_writeWrapper(
168+
buffer, helpClassName, mixClassName, classAnnotation, fields);
169+
}
153170
}
154171

155172
return buffer.toString();
156173
}
157174

175+
void _writeWrapper(
176+
StringBuffer buffer,
177+
String helpClassName,
178+
String mixClassName,
179+
JsonSerializable classAnnotation,
180+
Map<String, FieldElement> fields) {
181+
buffer.writeln();
182+
// TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported
183+
buffer.writeln('''class $helpClassName extends \$JsonMapWrapper {
184+
final $mixClassName _v;
185+
$helpClassName(this._v);
186+
''');
187+
188+
if (fields.values
189+
.every((e) => _includeIfNull(e, classAnnotation.includeIfNull))) {
190+
// TODO(kevmoo): consider just doing one code path – if it's fast
191+
// enough
192+
var jsonKeys = fields.values.map(_safeNameAccess).join(', ');
193+
194+
// TODO(kevmoo): maybe put this in a static field instead?
195+
// const lists have unfortunate overhead
196+
buffer.writeln(''' @override
197+
Iterable<String> get keys => const [${jsonKeys}];
198+
''');
199+
} else {
200+
// At least one field should be excluded if null
201+
buffer.writeln('@override\nIterable<String> get keys sync* {');
202+
203+
for (var field in fields.values) {
204+
var nullCheck = !_includeIfNull(field, classAnnotation.includeIfNull);
205+
if (nullCheck) {
206+
buffer.writeln('if (_v.${field.name} != null) {');
207+
}
208+
buffer.writeln('yield ${_safeNameAccess(field)};');
209+
if (nullCheck) {
210+
buffer.writeln('}');
211+
}
212+
}
213+
214+
buffer.writeln('}\n');
215+
}
216+
217+
buffer.writeln('''@override
218+
dynamic operator [](Object key) {
219+
if (key is String) {
220+
switch(key) {
221+
''');
222+
223+
for (var field in fields.values) {
224+
var valueAccess = '_v.${field.name}';
225+
buffer.write('''case ${_safeNameAccess(field)}:
226+
return ${_serializeField(field, classAnnotation.nullable, accessOverride: valueAccess)};''');
227+
}
228+
229+
buffer.writeln('''
230+
}}
231+
return null;
232+
}''');
233+
234+
buffer.writeln('}');
235+
}
236+
158237
void _writeToJsonWithNullChecks(StringBuffer buffer,
159238
Iterable<FieldElement> fields, JsonSerializable classAnnotation) {
160239
buffer.writeln('{');
@@ -352,7 +431,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
352431
/// representing the serialization of a value.
353432
String _serialize(DartType targetType, String expression, bool nullable) =>
354433
_allHelpers
355-
.map((h) => h.serialize(targetType, expression, nullable, _serialize))
434+
.map((h) =>
435+
h.serialize(targetType, expression, nullable, _helperContext))
356436
.firstWhere((r) => r != null,
357437
orElse: () =>
358438
throw new UnsupportedTypeError(targetType, expression));
@@ -377,10 +457,35 @@ void $toJsonMapHelperName(String key, dynamic value) {
377457
String _deserialize(DartType targetType, String expression, bool nullable) =>
378458
_allHelpers
379459
.map((th) =>
380-
th.deserialize(targetType, expression, nullable, _deserialize))
460+
th.deserialize(targetType, expression, nullable, _helperContext))
381461
.firstWhere((r) => r != null,
382462
orElse: () =>
383463
throw new UnsupportedTypeError(targetType, expression));
464+
465+
_TypeHelperContext get _helperContext => _typeHelperContextExpando[this] ??=
466+
new _TypeHelperContext(_serialize, _deserialize, useWrappers);
467+
}
468+
469+
final _typeHelperContextExpando = new Expando<_TypeHelperContext>();
470+
471+
typedef String _TypeHelperGenerator(
472+
DartType fieldType, String expression, bool nullable);
473+
474+
class _TypeHelperContext implements SerializeContext, DeserializeContext {
475+
final _TypeHelperGenerator _serialize, _deserialize;
476+
477+
@override
478+
final bool useWrappers;
479+
480+
_TypeHelperContext(this._serialize, this._deserialize, this.useWrappers);
481+
482+
@override
483+
String serialize(DartType fieldType, String expression, bool nullable) =>
484+
_serialize(fieldType, expression, nullable);
485+
486+
@override
487+
String deserialize(DartType fieldType, String expression, bool nullable) =>
488+
_deserialize(fieldType, expression, nullable);
384489
}
385490

386491
String _safeNameAccess(FieldElement field) {

json_serializable/lib/src/type_helper.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ List<DartType> typeArgumentsOf(DartType type, TypeChecker checker) {
1515
return implementation?.typeArguments;
1616
}
1717

18-
typedef String TypeHelperGenerator(
19-
DartType fieldType, String expression, bool nullable);
18+
abstract class SerializeContext {
19+
bool get useWrappers;
20+
String serialize(DartType fieldType, String expression, bool nullable);
21+
}
22+
23+
abstract class DeserializeContext {
24+
String deserialize(DartType fieldType, String expression, bool nullable);
25+
}
2026

2127
abstract class TypeHelper {
2228
const TypeHelper();
@@ -36,9 +42,9 @@ abstract class TypeHelper {
3642
/// String serialize(DartType targetType, String expression) =>
3743
/// "$expression.id";
3844
/// ```.
39-
// TODO(kevmoo) – document `serializeNested`
45+
// TODO(kevmoo) – document `context`
4046
String serialize(DartType targetType, String expression, bool nullable,
41-
TypeHelperGenerator serializeNested);
47+
SerializeContext context);
4248

4349
/// Returns Dart code that deserializes an [expression] representing a JSON
4450
/// literal to into [targetType].
@@ -63,9 +69,9 @@ abstract class TypeHelper {
6369
/// String deserialize(DartType targetType, String expression) =>
6470
/// "new ${targetType.name}.fromInt($expression)";
6571
/// ```.
66-
// TODO(kevmoo) – document `deserializeNested`
72+
// TODO(kevmoo) – document `context`
6773
String deserialize(DartType targetType, String expression, bool nullable,
68-
TypeHelperGenerator deserializeNested);
74+
DeserializeContext context);
6975
}
7076

7177
/// A [TypeChecker] for [String], [bool] and [num].

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class EnumHelper extends TypeHelper {
77

88
@override
99
String serialize(DartType targetType, String expression, bool nullable,
10-
TypeHelperGenerator serializeNested) {
10+
SerializeContext context) {
1111
if (targetType is InterfaceType && targetType.element.isEnum) {
1212
return commonNullPrefix(
1313
nullable, expression, "$expression.toString().split('.')[1]");
@@ -18,7 +18,7 @@ class EnumHelper extends TypeHelper {
1818

1919
@override
2020
String deserialize(DartType targetType, String expression, bool nullable,
21-
TypeHelperGenerator deserializeNested) {
21+
DeserializeContext context) {
2222
if (targetType is InterfaceType && targetType.element.isEnum) {
2323
return commonNullPrefix(
2424
nullable,

json_serializable/lib/src/type_helpers/iterable_helper.dart

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ class IterableHelper extends TypeHelper {
1010

1111
@override
1212
String serialize(DartType targetType, String expression, bool nullable,
13-
TypeHelperGenerator serializeNested) {
13+
SerializeContext context) {
1414
if (!_coreIterableChecker.isAssignableFromType(targetType)) {
1515
return null;
1616
}
1717

18+
var args = typeArgumentsOf(targetType, _coreIterableChecker);
19+
assert(args.length == 1);
20+
21+
var keyType = args[0];
22+
1823
// This block will yield a regular list, which works fine for JSON
1924
// Although it's possible that child elements may be marked unsafe
2025

2126
var isList = _coreListChecker.isAssignableFromType(targetType);
22-
var subFieldValue = serializeNested(
27+
var subFieldValue = context.serialize(
2328
_getIterableGenericType(targetType), _closureArg, nullable);
2429

2530
var optionalQuestion = nullable ? '?' : '';
@@ -28,6 +33,15 @@ class IterableHelper extends TypeHelper {
2833
// will be identical to `substitute` – so no explicit mapping is needed.
2934
// If they are not equal, then we to write out the substitution.
3035
if (subFieldValue != _closureArg) {
36+
if (context.useWrappers && isList) {
37+
var method = '\$wrapList';
38+
if (nullable) {
39+
method = '${method}HandleNull';
40+
}
41+
42+
return '$method<$keyType>($expression, ($_closureArg) => $subFieldValue)';
43+
}
44+
3145
// TODO: the type could be imported from a library with a prefix!
3246
expression =
3347
'${expression}${optionalQuestion}.map(($_closureArg) => $subFieldValue)';
@@ -47,15 +61,15 @@ class IterableHelper extends TypeHelper {
4761

4862
@override
4963
String deserialize(DartType targetType, String expression, bool nullable,
50-
TypeHelperGenerator deserializeNested) {
64+
DeserializeContext context) {
5165
if (!_coreIterableChecker.isAssignableFromType(targetType)) {
5266
return null;
5367
}
5468

5569
var iterableGenericType = _getIterableGenericType(targetType);
5670

5771
var itemSubVal =
58-
deserializeNested(iterableGenericType, _closureArg, nullable);
72+
context.deserialize(iterableGenericType, _closureArg, nullable);
5973

6074
// If `itemSubVal` is the same, then we don't need to do anything fancy
6175
if (_closureArg == itemSubVal) {

0 commit comments

Comments
 (0)