// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:analyzer/dart/element/element.dart'; import 'package:json_annotation/json_annotation.dart'; import 'constants.dart'; import 'helper_core.dart'; import 'type_helper.dart'; abstract class EncodeHelper implements HelperCore { String _fieldAccess(FieldElement field) { var fieldAccess = field.name; if (config.generateToJsonFunction) { fieldAccess = '$_toJsonParamName.$fieldAccess'; } return fieldAccess; } String _mixinClassName(bool withConstraints) => '${prefix}SerializerMixin${genericClassArgumentsImpl(withConstraints)}'; String _wrapperClassName([bool withConstraints]) => '${prefix}JsonMapWrapper${genericClassArgumentsImpl(withConstraints)}'; Iterable createToJson(Set accessibleFields) sync* { assert(config.createToJson); final buffer = StringBuffer(); if (config.generateToJsonFunction) { final functionName = '${prefix}ToJson${genericClassArgumentsImpl(true)}'; buffer.write('Map $functionName' '($targetClassReference $_toJsonParamName) '); } else { // // Generate the mixin class // buffer.writeln('abstract class ${_mixinClassName(true)} {'); // write copies of the fields - this allows the toJson method to access // the fields of the target class for (final field in accessibleFields) { //TODO - handle aliased imports buffer.writeln(' ${field.type} get ${field.name};'); } buffer.write(' Map toJson() '); } final writeNaive = accessibleFields.every(_writeJsonValueNaive); if (config.useWrappers) { final param = config.generateToJsonFunction ? _toJsonParamName : 'this'; buffer.writeln('=> ${_wrapperClassName(false)}($param);'); } else { if (writeNaive) { // write simple `toJson` method that includes all keys... _writeToJsonSimple(buffer, accessibleFields); } else { // At least one field should be excluded if null _writeToJsonWithNullChecks(buffer, accessibleFields); } } if (!config.generateToJsonFunction) { // end of the mixin class buffer.writeln('}'); } yield buffer.toString(); if (config.useWrappers) { yield _createWrapperClass(accessibleFields); } } String _createWrapperClass(Iterable fields) { final buffer = StringBuffer(); buffer.writeln(); // TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported final fieldType = config.generateToJsonFunction ? targetClassReference : _mixinClassName(false); buffer.writeln(''' class ${_wrapperClassName(true)} extends \$JsonMapWrapper { final $fieldType _v; ${_wrapperClassName()}(this._v); '''); if (fields.every(_writeJsonValueNaive)) { // TODO(kevmoo): consider just doing one code path – if it's fast // enough final jsonKeys = fields.map(safeNameAccess).join(', '); // TODO(kevmoo): maybe put this in a static field instead? // const lists have unfortunate overhead buffer.writeln(''' @override Iterable get keys => const [$jsonKeys]; '''); } else { // At least one field should be excluded if null buffer.writeln(' @override\n Iterable get keys sync* {'); for (final field in fields) { final nullCheck = !_writeJsonValueNaive(field); if (nullCheck) { buffer.write(' if (_v.${field.name} != null) {\n '); } buffer.writeln(' yield ${safeNameAccess(field)};'); if (nullCheck) { buffer.writeln(' }'); } } buffer.writeln(' }\n'); } buffer.writeln(''' @override dynamic operator [](Object key) { if (key is String) { switch (key) {'''); for (final field in fields) { final valueAccess = '_v.${field.name}'; buffer.writeln(''' case ${safeNameAccess(field)}: return ${_serializeField(field, valueAccess)};'''); } buffer.writeln(''' } } return null; }'''); buffer.writeln('}'); return buffer.toString(); } void _writeToJsonSimple(StringBuffer buffer, Iterable fields) { buffer.writeln('=> {'); buffer.writeAll(fields.map((field) { final access = _fieldAccess(field); final value = '${safeNameAccess(field)}: ${_serializeField(field, access)}'; return ' $value'; }), ',\n'); if (fields.isNotEmpty) { buffer.write('\n '); } buffer.writeln('};'); } /// Name of the parameter used when generating top-level `toJson` functions /// if [JsonSerializable.generateToJsonFunction] is `true`. static const _toJsonParamName = 'instance'; void _writeToJsonWithNullChecks( StringBuffer buffer, Iterable fields) { buffer.writeln('{'); buffer.writeln(' final $generatedLocalVarName = {'); // Note that the map literal is left open above. As long as target fields // don't need to be intercepted by the `only if null` logic, write them // to the map literal directly. In theory, should allow more efficient // serialization. var directWrite = true; for (final field in fields) { var safeFieldAccess = _fieldAccess(field); final safeJsonKeyString = safeNameAccess(field); // If `fieldName` collides with one of the local helpers, prefix // access with `this.`. if (safeFieldAccess == generatedLocalVarName || safeFieldAccess == toJsonMapHelperName) { assert(!config.generateToJsonFunction, 'This code path should only be hit during the mixin codepath.'); safeFieldAccess = 'this.$safeFieldAccess'; } final expression = _serializeField(field, safeFieldAccess); if (_writeJsonValueNaive(field)) { if (directWrite) { buffer.writeln(' $safeJsonKeyString: $expression,'); } else { buffer.writeln( ' $generatedLocalVarName[$safeJsonKeyString] = $expression;'); } } else { if (directWrite) { // close the still-open map literal buffer.writeln(' };'); buffer.writeln(); // write the helper to be used by all following null-excluding // fields buffer.writeln(''' void $toJsonMapHelperName(String key, dynamic value) { if (value != null) { $generatedLocalVarName[key] = value; } } '''); directWrite = false; } buffer.writeln( ' $toJsonMapHelperName($safeJsonKeyString, $expression);'); } } buffer.writeln(' return $generatedLocalVarName;'); buffer.writeln(' }'); } String _serializeField(FieldElement field, String accessExpression) { try { return getHelperContext(field) .serialize(field.type, accessExpression) .toString(); } on UnsupportedTypeError catch (e) { throw createInvalidGenerationError('toJson', field, e); } } /// Returns `true` if the field can be written to JSON 'naively' – meaning /// we can avoid checking for `null`. /// /// `true` if either: /// `includeIfNull` is `true` /// or /// `nullable` is `false`. bool _writeJsonValueNaive(FieldElement field) => jsonKeyFor(field).includeIfNull || !jsonKeyFor(field).nullable; }