|
| 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