Skip to content

Commit a83e3d7

Browse files
authored
Validate one-off property sets (#168)
1 parent f841c8e commit a83e3d7

File tree

4 files changed

+149
-101
lines changed

4 files changed

+149
-101
lines changed

json_serializable/lib/src/generator_helper.dart

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,28 +124,56 @@ class _GeneratorHelper {
124124
});
125125

126126
if (_annotation.createFactory) {
127-
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';
128-
129127
_buffer.writeln();
128+
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';
129+
_buffer.writeln('${_element.name} ${_prefix}FromJson($mapType json) =>');
130130

131131
String deserializeFun(String paramOrFieldName,
132132
{ParameterElement ctorParam}) =>
133133
_deserializeForField(accessibleFields[paramOrFieldName],
134134
ctorParam: ctorParam);
135135

136-
var tempBuffer = new StringBuffer();
137-
var fieldsSetByFactory = writeConstructorInvocation(
138-
tempBuffer,
139-
_element,
140-
accessibleFields.keys,
141-
accessibleFields.values
142-
.where((fe) => !fe.isFinal)
143-
.map((fe) => fe.name)
144-
.toList(),
145-
unavailableReasons,
146-
deserializeFun);
147-
136+
Set<String> fieldsSetByFactory;
148137
if (_generator.checked) {
138+
var classLiteral = escapeDartString(_element.displayName);
139+
140+
_buffer.writeln('\$checkedNew($classLiteral, json, ()');
141+
142+
var data = writeConstructorInvocation(
143+
_element,
144+
accessibleFields.keys,
145+
accessibleFields.values
146+
.where((fe) => !fe.isFinal)
147+
.map((fe) => fe.name)
148+
.toList(),
149+
unavailableReasons,
150+
deserializeFun);
151+
152+
fieldsSetByFactory = data.usedCtorParamsAndFields;
153+
154+
if (data.fieldsToSet.isEmpty) {
155+
// Use simple arrow syntax for the constructor invocation.
156+
// There are no fields to set
157+
_buffer.write(' => ${data.content}');
158+
} else {
159+
// If there are fields to set, create a full function body and
160+
// create a temporary variable to hold the instance so we can make
161+
// wrapped calls to all of the fields value assignments.
162+
_buffer.writeln('{ var val = ');
163+
_buffer.write(data.content);
164+
_buffer.writeln(';');
165+
166+
for (var field in data.fieldsToSet) {
167+
_buffer.writeln();
168+
_buffer.write('\$checkedConvert(json, ${_safeNameAccess(
169+
accessibleFields[field])}, (v) => ');
170+
_buffer.write('val.$field = ');
171+
_buffer.write(_deserializeForField(accessibleFields[field],
172+
checkedProperty: true));
173+
_buffer.write(');');
174+
}
175+
_buffer.writeln('return val; }');
176+
}
149177
var fieldKeyMap = new Map.fromEntries(fieldsSetByFactory
150178
.map((k) => new MapEntry(k, _nameAccess(accessibleFields[k])))
151179
.where((me) => me.key != me.value));
@@ -158,15 +186,27 @@ class _GeneratorHelper {
158186
fieldKeyMapArg = ', fieldKeyMap: $mapLiteral';
159187
}
160188

161-
var classLiteral = escapeDartString(_element.displayName);
162-
163-
_buffer.writeln(
164-
'${_element.displayName} ${_prefix}FromJson($mapType json) =>'
165-
'\$checkedNew($classLiteral, json, () => $tempBuffer'
166-
'$fieldKeyMapArg)');
189+
_buffer.writeln('$fieldKeyMapArg)');
167190
} else {
168-
_buffer.writeln('${_element.name} '
169-
'${_prefix}FromJson($mapType json) => $tempBuffer');
191+
var data = writeConstructorInvocation(
192+
_element,
193+
accessibleFields.keys,
194+
accessibleFields.values
195+
.where((fe) => !fe.isFinal)
196+
.map((fe) => fe.name)
197+
.toList(),
198+
unavailableReasons,
199+
deserializeFun);
200+
201+
fieldsSetByFactory = data.usedCtorParamsAndFields;
202+
203+
_buffer.writeln(data.content);
204+
for (var field in data.fieldsToSet) {
205+
_buffer.writeln();
206+
_buffer.write(' ..$field = ');
207+
_buffer.write(deserializeFun(field));
208+
}
209+
_buffer.writeln();
170210
}
171211
_buffer.writeln(';');
172212

@@ -315,16 +355,23 @@ void $toJsonMapHelperName(String key, dynamic value) {
315355
}
316356

317357
String _deserializeForField(FieldElement field,
318-
{ParameterElement ctorParam}) {
358+
{ParameterElement ctorParam, bool checkedProperty}) {
359+
checkedProperty ??= false;
319360
var jsonKey = _safeNameAccess(field);
320361

321362
var targetType = ctorParam?.type ?? field.type;
322363

323364
try {
324365
if (_generator.checked) {
325366
var value = _getHelperContext(field).deserialize(targetType, 'v');
367+
if (checkedProperty) {
368+
return value;
369+
}
370+
326371
return '\$checkedConvert(json, $jsonKey, (v) => $value)';
327372
}
373+
assert(!checkedProperty,
374+
'should only be true if `_generator.checked` is true.');
328375
return _getHelperContext(field).deserialize(targetType, 'json[$jsonKey]');
329376
} on UnsupportedTypeError catch (e) {
330377
throw _createInvalidGenerationError('fromJson', field, e);

json_serializable/lib/src/utils.dart

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,7 @@ int _sortByLocation(FieldElement a, FieldElement b) {
176176

177177
final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);
178178

179-
/// Writes the invocation of the default constructor – `new Class(...)` for the
180-
/// type defined in [classElement] to the provided [buffer].
181-
///
182-
/// If an parameter is required to invoke the constructor,
179+
/// If a parameter is required to invoke the constructor,
183180
/// [availableConstructorParameters] is checked to see if it is available. If
184181
/// [availableConstructorParameters] does not contain the parameter name,
185182
/// an [UnsupportedError] is thrown.
@@ -190,11 +187,7 @@ final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);
190187
///
191188
/// [writeableFields] are also populated, but only if they have not already
192189
/// been defined by a constructor parameter with the same name.
193-
///
194-
/// Set set of all constructor parameters and and fields that are populated is
195-
/// returned.
196-
Set<String> writeConstructorInvocation(
197-
StringBuffer buffer,
190+
CtorData writeConstructorInvocation(
198191
ClassElement classElement,
199192
Iterable<String> availableConstructorParameters,
200193
Iterable<String> writeableFields,
@@ -250,9 +243,7 @@ Set<String> writeConstructorInvocation(
250243
var remainingFieldsForInvocationBody =
251244
writeableFields.toSet().difference(usedCtorParamsAndFields);
252245

253-
//
254-
// Generate the static factory method
255-
//
246+
var buffer = new StringBuffer();
256247
buffer.write('new $className(');
257248
buffer.writeAll(
258249
constructorArguments.map((paramElement) =>
@@ -267,17 +258,18 @@ Set<String> writeConstructorInvocation(
267258
}), ', ');
268259

269260
buffer.write(')');
270-
if (remainingFieldsForInvocationBody.isNotEmpty) {
271-
for (var field in remainingFieldsForInvocationBody) {
272-
buffer.writeln();
273-
buffer.write(' ..$field = ');
274-
buffer.write(deserializeForField(field));
275-
usedCtorParamsAndFields.add(field);
276-
}
277-
}
278-
buffer.writeln();
279261

280-
return usedCtorParamsAndFields;
262+
usedCtorParamsAndFields.addAll(remainingFieldsForInvocationBody);
263+
264+
return new CtorData(buffer.toString(), remainingFieldsForInvocationBody,
265+
usedCtorParamsAndFields);
266+
}
267+
268+
class CtorData {
269+
final String content;
270+
final Set<String> fieldsToSet;
271+
final Set<String> usedCtorParamsAndFields;
272+
CtorData(this.content, this.fieldsToSet, this.usedCtorParamsAndFields);
281273
}
282274

283275
void _validateConstructorArguments(

json_serializable/test/kitchen_sink_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,7 @@ Matcher _getMatcher(bool checked, String expectedKey, bool checkedAssignment) {
237237

238238
if (checked) {
239239
if (checkedAssignment &&
240-
const ['intIterable', 'datetime-iterable', 'validatedPropertyNo42']
241-
.contains(expectedKey)) {
240+
const ['intIterable', 'datetime-iterable'].contains(expectedKey)) {
242241
expectedKey = null;
243242
}
244243

json_serializable/test/kitchen_sink_test_files/kitchen_sink.non_nullable.checked.g.dart

Lines changed: 64 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,61 +10,71 @@ part of 'kitchen_sink.non_nullable.checked.dart';
1010
// Generator: JsonSerializableGenerator
1111
// **************************************************************************
1212

13-
KitchenSink _$KitchenSinkFromJson(Map json) => $checkedNew(
14-
'KitchenSink',
15-
json,
16-
() => new KitchenSink(
17-
ctorValidatedNo42: $checkedConvert(json, 'no-42', (v) => v as int),
18-
iterable: $checkedConvert(json, 'iterable', (v) => v as List),
19-
dynamicIterable:
20-
$checkedConvert(json, 'dynamicIterable', (v) => v as List),
21-
objectIterable:
22-
$checkedConvert(json, 'objectIterable', (v) => v as List),
23-
intIterable: $checkedConvert(
24-
json, 'intIterable', (v) => (v as List).map((e) => e as int)),
25-
dateTimeIterable: $checkedConvert(json, 'datetime-iterable',
26-
(v) => (v as List).map((e) => DateTime.parse(e as String))))
27-
..dateTime = $checkedConvert(
28-
json, 'dateTime', (v) => DateTime.parse(v as String))
29-
..list = $checkedConvert(json, 'list', (v) => v as List)
30-
..dynamicList = $checkedConvert(json, 'dynamicList', (v) => v as List)
31-
..objectList = $checkedConvert(json, 'objectList', (v) => v as List)
32-
..intList = $checkedConvert(
33-
json, 'intList', (v) => (v as List).map((e) => e as int).toList())
34-
..dateTimeList = $checkedConvert(
35-
json,
36-
'dateTimeList',
37-
(v) =>
38-
(v as List).map((e) => DateTime.parse(e as String)).toList())
39-
..map = $checkedConvert(json, 'map', (v) => v as Map)
40-
..stringStringMap = $checkedConvert(json, 'stringStringMap',
41-
(v) => new Map<String, String>.from(v as Map))
42-
..dynamicIntMap = $checkedConvert(
43-
json, 'dynamicIntMap', (v) => new Map<String, int>.from(v as Map))
44-
..objectDateTimeMap = $checkedConvert(
45-
json,
46-
'objectDateTimeMap',
47-
(v) => (v as Map)
48-
.map((k, e) => new MapEntry(k, DateTime.parse(e as String))))
49-
..crazyComplex = $checkedConvert(
50-
json,
51-
'crazyComplex',
52-
(v) => (v as List)
53-
.map((e) => (e as Map).map((k, e) => new MapEntry(
13+
KitchenSink _$KitchenSinkFromJson(Map json) =>
14+
$checkedNew('KitchenSink', json, () {
15+
var val = new KitchenSink(
16+
ctorValidatedNo42: $checkedConvert(json, 'no-42', (v) => v as int),
17+
iterable: $checkedConvert(json, 'iterable', (v) => v as List),
18+
dynamicIterable:
19+
$checkedConvert(json, 'dynamicIterable', (v) => v as List),
20+
objectIterable:
21+
$checkedConvert(json, 'objectIterable', (v) => v as List),
22+
intIterable: $checkedConvert(
23+
json, 'intIterable', (v) => (v as List).map((e) => e as int)),
24+
dateTimeIterable: $checkedConvert(json, 'datetime-iterable',
25+
(v) => (v as List).map((e) => DateTime.parse(e as String))));
26+
27+
$checkedConvert(
28+
json, 'dateTime', (v) => val.dateTime = DateTime.parse(v as String));
29+
$checkedConvert(json, 'list', (v) => val.list = v as List);
30+
$checkedConvert(json, 'dynamicList', (v) => val.dynamicList = v as List);
31+
$checkedConvert(json, 'objectList', (v) => val.objectList = v as List);
32+
$checkedConvert(json, 'intList',
33+
(v) => val.intList = (v as List).map((e) => e as int).toList());
34+
$checkedConvert(
35+
json,
36+
'dateTimeList',
37+
(v) => val.dateTimeList =
38+
(v as List).map((e) => DateTime.parse(e as String)).toList());
39+
$checkedConvert(json, 'map', (v) => val.map = v as Map);
40+
$checkedConvert(json, 'stringStringMap',
41+
(v) => val.stringStringMap = new Map<String, String>.from(v as Map));
42+
$checkedConvert(json, 'dynamicIntMap',
43+
(v) => val.dynamicIntMap = new Map<String, int>.from(v as Map));
44+
$checkedConvert(
45+
json,
46+
'objectDateTimeMap',
47+
(v) => val.objectDateTimeMap = (v as Map)
48+
.map((k, e) => new MapEntry(k, DateTime.parse(e as String))));
49+
$checkedConvert(
50+
json,
51+
'crazyComplex',
52+
(v) => val.crazyComplex = (v as List)
53+
.map((e) => (e as Map).map((k, e) => new MapEntry(
54+
k as String,
55+
(e as Map).map((k, e) => new MapEntry(
5456
k as String,
55-
(e as Map).map((k, e) =>
56-
new MapEntry(k as String, (e as List).map((e) => (e as List).map((e) => DateTime.parse(e as String)).toList()).toList())))))
57-
.toList())
58-
..val = $checkedConvert(json, 'val', (v) => new Map<String, bool>.from(v as Map))
59-
..writeNotNull = $checkedConvert(json, 'writeNotNull', (v) => v as bool)
60-
..string = $checkedConvert(json, r'$string', (v) => v as String)
61-
..simpleObject = $checkedConvert(json, 'simpleObject', (v) => new SimpleObject.fromJson(v as Map))
62-
..validatedPropertyNo42 = $checkedConvert(json, 'validatedPropertyNo42', (v) => v as int),
63-
fieldKeyMap: const {
64-
'ctorValidatedNo42': 'no-42',
65-
'dateTimeIterable': 'datetime-iterable',
66-
'string': r'$string'
67-
});
57+
(e as List)
58+
.map((e) => (e as List)
59+
.map((e) => DateTime.parse(e as String))
60+
.toList())
61+
.toList())))))
62+
.toList());
63+
$checkedConvert(
64+
json, 'val', (v) => val.val = new Map<String, bool>.from(v as Map));
65+
$checkedConvert(
66+
json, 'writeNotNull', (v) => val.writeNotNull = v as bool);
67+
$checkedConvert(json, r'$string', (v) => val.string = v as String);
68+
$checkedConvert(json, 'simpleObject',
69+
(v) => val.simpleObject = new SimpleObject.fromJson(v as Map));
70+
$checkedConvert(json, 'validatedPropertyNo42',
71+
(v) => val.validatedPropertyNo42 = v as int);
72+
return val;
73+
}, fieldKeyMap: const {
74+
'ctorValidatedNo42': 'no-42',
75+
'dateTimeIterable': 'datetime-iterable',
76+
'string': r'$string'
77+
});
6878

6979
abstract class _$KitchenSinkSerializerMixin {
7080
int get ctorValidatedNo42;

0 commit comments

Comments
 (0)