Skip to content

Commit 02318e0

Browse files
authored
refactor: prepare for multi-return values in GenerateForAnnotation (#240)
JsonSerializableGenerator now returns String instead of Future<String> GeneratorHelper is now private and in the same file is the Generator Encoding and decoding have been split into mixin classes These two aspects of the generator are nicely isolated hopefully making future fixes and additions easier Many members of utils.dart have been moved closer to where they are used and made private when possible Updated tests to be sync now that the core tested function is sync
1 parent 956022a commit 02318e0

12 files changed

+845
-819
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/dart/element/element.dart';
6+
import 'package:json_annotation/json_annotation.dart';
7+
import 'package:source_gen/source_gen.dart';
8+
9+
import 'helper_core.dart';
10+
import 'json_literal_generator.dart';
11+
import 'type_helper.dart';
12+
import 'utils.dart';
13+
14+
abstract class DecodeHelper implements HelperCore {
15+
final StringBuffer _buffer = new StringBuffer();
16+
17+
String createFactory(Map<String, FieldElement> accessibleFields,
18+
Map<String, String> unavailableReasons) {
19+
assert(annotation.createFactory);
20+
assert(_buffer.isEmpty);
21+
22+
var mapType = generator.anyMap ? 'Map' : 'Map<String, dynamic>';
23+
_buffer.write('$targetClassReference '
24+
'${prefix}FromJson${genericClassArgumentsImpl(true)}'
25+
'($mapType json) {\n');
26+
27+
String deserializeFun(String paramOrFieldName,
28+
{ParameterElement ctorParam}) =>
29+
_deserializeForField(accessibleFields[paramOrFieldName],
30+
ctorParam: ctorParam);
31+
32+
Set<String> fieldsSetByFactory;
33+
if (generator.checked) {
34+
var classLiteral = escapeDartString(element.name);
35+
36+
_buffer.write('''
37+
return \$checkedNew(
38+
$classLiteral,
39+
json,
40+
() {\n''');
41+
42+
var data = _writeConstructorInvocation(
43+
element,
44+
accessibleFields.keys,
45+
accessibleFields.values
46+
.where((fe) => !fe.isFinal)
47+
.map((fe) => fe.name)
48+
.toList(),
49+
unavailableReasons,
50+
deserializeFun);
51+
52+
fieldsSetByFactory = data.usedCtorParamsAndFields;
53+
54+
_writeChecks(6, annotation, accessibleFields);
55+
_buffer.write('''
56+
var val = ${data.content};''');
57+
58+
for (var field in data.fieldsToSet) {
59+
_buffer.writeln();
60+
var safeName = safeNameAccess(accessibleFields[field]);
61+
_buffer.write('''
62+
\$checkedConvert(json, $safeName, (v) => ''');
63+
_buffer.write('val.$field = ');
64+
_buffer.write(_deserializeForField(accessibleFields[field],
65+
checkedProperty: true));
66+
_buffer.write(');');
67+
}
68+
69+
_buffer.write('''\n return val;
70+
}''');
71+
72+
var fieldKeyMap = new Map.fromEntries(fieldsSetByFactory
73+
.map((k) => new MapEntry(k, nameAccess(accessibleFields[k])))
74+
.where((me) => me.key != me.value));
75+
76+
String fieldKeyMapArg;
77+
if (fieldKeyMap.isEmpty) {
78+
fieldKeyMapArg = '';
79+
} else {
80+
var mapLiteral = jsonMapAsDart(fieldKeyMap, true);
81+
fieldKeyMapArg = ', fieldKeyMap: $mapLiteral';
82+
}
83+
84+
_buffer.write(fieldKeyMapArg);
85+
86+
_buffer.write(')');
87+
} else {
88+
var data = _writeConstructorInvocation(
89+
element,
90+
accessibleFields.keys,
91+
accessibleFields.values
92+
.where((fe) => !fe.isFinal)
93+
.map((fe) => fe.name)
94+
.toList(),
95+
unavailableReasons,
96+
deserializeFun);
97+
98+
fieldsSetByFactory = data.usedCtorParamsAndFields;
99+
100+
_writeChecks(2, annotation, accessibleFields);
101+
102+
_buffer.write('''
103+
return ${data.content}''');
104+
for (var field in data.fieldsToSet) {
105+
_buffer.writeln();
106+
_buffer.write(' ..$field = ');
107+
_buffer.write(deserializeFun(field));
108+
}
109+
}
110+
_buffer.writeln(';\n}');
111+
_buffer.writeln();
112+
113+
// If there are fields that are final – that are not set via the generated
114+
// constructor, then don't output them when generating the `toJson` call.
115+
accessibleFields
116+
.removeWhere((name, fe) => !fieldsSetByFactory.contains(name));
117+
118+
return _buffer.toString();
119+
}
120+
121+
void _writeChecks(int indent, JsonSerializable classAnnotation,
122+
Map<String, FieldElement> accessibleFields) {
123+
var args = <String>[];
124+
125+
if (classAnnotation.disallowUnrecognizedKeys) {
126+
var allowKeysLiteral = jsonLiteralAsDart(
127+
accessibleFields.values.map(nameAccess).toList(), true);
128+
129+
args.add('allowedKeys: $allowKeysLiteral');
130+
}
131+
132+
var requiredKeys =
133+
accessibleFields.values.where((fe) => jsonKeyFor(fe).required).toList();
134+
if (requiredKeys.isNotEmpty) {
135+
var requiredKeyLiteral =
136+
jsonLiteralAsDart(requiredKeys.map(nameAccess).toList(), true);
137+
138+
args.add('requiredKeys: $requiredKeyLiteral');
139+
}
140+
141+
var disallowNullKeys = accessibleFields.values
142+
.where((fe) => jsonKeyFor(fe).disallowNullValue)
143+
.toList();
144+
if (disallowNullKeys.isNotEmpty) {
145+
var dissallowNullKeyLiteral =
146+
jsonLiteralAsDart(disallowNullKeys.map(nameAccess).toList(), true);
147+
148+
args.add('disallowNullValues: $dissallowNullKeyLiteral');
149+
}
150+
151+
if (args.isNotEmpty) {
152+
_buffer.writeln('${' ' * indent}\$checkKeys(json, ${args.join(', ')});');
153+
}
154+
}
155+
156+
String _deserializeForField(FieldElement field,
157+
{ParameterElement ctorParam, bool checkedProperty}) {
158+
checkedProperty ??= false;
159+
var jsonKeyName = safeNameAccess(field);
160+
var targetType = ctorParam?.type ?? field.type;
161+
var contextHelper = getHelperContext(field);
162+
163+
String value;
164+
try {
165+
if (generator.checked) {
166+
value = contextHelper.deserialize(targetType, 'v');
167+
if (!checkedProperty) {
168+
value = '\$checkedConvert(json, $jsonKeyName, (v) => $value)';
169+
}
170+
} else {
171+
assert(!checkedProperty,
172+
'should only be true if `_generator.checked` is true.');
173+
174+
value = contextHelper.deserialize(targetType, 'json[$jsonKeyName]');
175+
}
176+
} on UnsupportedTypeError catch (e) {
177+
throw createInvalidGenerationError('fromJson', field, e);
178+
}
179+
180+
var defaultValue = jsonKeyFor(field).defaultValue;
181+
if (defaultValue != null) {
182+
if (!contextHelper.nullable) {
183+
throwUnsupported(field,
184+
'Cannot use `defaultValue` on a field with `nullable` false.');
185+
}
186+
187+
value = '$value ?? $defaultValue';
188+
}
189+
return value;
190+
}
191+
}
192+
193+
/// [availableConstructorParameters] is checked to see if it is available. If
194+
/// [availableConstructorParameters] does not contain the parameter name,
195+
/// an [UnsupportedError] is thrown.
196+
///
197+
/// To improve the error details, [unavailableReasons] is checked for the
198+
/// unavailable constructor parameter. If the value is not `null`, it is
199+
/// included in the [UnsupportedError] message.
200+
///
201+
/// [writeableFields] are also populated, but only if they have not already
202+
/// been defined by a constructor parameter with the same name.
203+
_ConstructorData _writeConstructorInvocation(
204+
ClassElement classElement,
205+
Iterable<String> availableConstructorParameters,
206+
Iterable<String> writeableFields,
207+
Map<String, String> unavailableReasons,
208+
String deserializeForField(String paramOrFieldName,
209+
{ParameterElement ctorParam})) {
210+
var className = classElement.name;
211+
212+
var ctor = classElement.unnamedConstructor;
213+
if (ctor == null) {
214+
// TODO(kevmoo): support using another ctor - dart-lang/json_serializable#50
215+
throw new UnsupportedError(
216+
'The class `$className` has no default constructor.');
217+
}
218+
219+
var usedCtorParamsAndFields = new Set<String>();
220+
var constructorArguments = <ParameterElement>[];
221+
var namedConstructorArguments = <ParameterElement>[];
222+
223+
for (var arg in ctor.parameters) {
224+
if (!availableConstructorParameters.contains(arg.name)) {
225+
if (arg.isNotOptional) {
226+
var msg = 'Cannot populate the required constructor '
227+
'argument: ${arg.name}.';
228+
229+
var additionalInfo = unavailableReasons[arg.name];
230+
231+
if (additionalInfo != null) {
232+
msg = '$msg $additionalInfo';
233+
}
234+
235+
throw new UnsupportedError(msg);
236+
}
237+
238+
continue;
239+
}
240+
241+
// TODO: validate that the types match!
242+
if (arg.isNamed) {
243+
namedConstructorArguments.add(arg);
244+
} else {
245+
constructorArguments.add(arg);
246+
}
247+
usedCtorParamsAndFields.add(arg.name);
248+
}
249+
250+
_validateConstructorArguments(
251+
classElement, constructorArguments.followedBy(namedConstructorArguments));
252+
253+
// fields that aren't already set by the constructor and that aren't final
254+
var remainingFieldsForInvocationBody =
255+
writeableFields.toSet().difference(usedCtorParamsAndFields);
256+
257+
var buffer = new StringBuffer();
258+
buffer.write('new $className${genericClassArguments(classElement, false)}(');
259+
if (constructorArguments.isNotEmpty) {
260+
buffer.writeln();
261+
buffer.writeAll(constructorArguments.map((paramElement) {
262+
var content =
263+
deserializeForField(paramElement.name, ctorParam: paramElement);
264+
return ' $content';
265+
}), ',\n');
266+
if (namedConstructorArguments.isNotEmpty) {
267+
buffer.write(',');
268+
}
269+
}
270+
if (namedConstructorArguments.isNotEmpty) {
271+
buffer.writeln();
272+
buffer.writeAll(namedConstructorArguments.map((paramElement) {
273+
var value =
274+
deserializeForField(paramElement.name, ctorParam: paramElement);
275+
return ' ${paramElement.name}: $value';
276+
}), ',\n');
277+
}
278+
279+
buffer.write(')');
280+
281+
usedCtorParamsAndFields.addAll(remainingFieldsForInvocationBody);
282+
283+
return new _ConstructorData(buffer.toString(),
284+
remainingFieldsForInvocationBody, usedCtorParamsAndFields);
285+
}
286+
287+
class _ConstructorData {
288+
final String content;
289+
final Set<String> fieldsToSet;
290+
final Set<String> usedCtorParamsAndFields;
291+
_ConstructorData(
292+
this.content, this.fieldsToSet, this.usedCtorParamsAndFields);
293+
}
294+
295+
void _validateConstructorArguments(
296+
ClassElement element, Iterable<ParameterElement> constructorArguments) {
297+
var undefinedArgs =
298+
constructorArguments.where((pe) => pe.type.isUndefined).toList();
299+
if (undefinedArgs.isNotEmpty) {
300+
var description = undefinedArgs.map((fe) => '`${fe.name}`').join(', ');
301+
302+
throw new InvalidGenerationSourceError(
303+
'At least one constructor argument has an invalid type: $description.',
304+
todo: 'Check names and imports.',
305+
element: element);
306+
}
307+
}

0 commit comments

Comments
 (0)