Skip to content

I big refactor and another integration test #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 72 additions & 69 deletions json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:collection';

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
Expand Down Expand Up @@ -78,67 +77,15 @@ class JsonSerializableGenerator
var classElement = element as ClassElement;

// Get all of the fields that need to be assigned
var sortedFieldList = listFields(classElement);

// Used to keep track of why a field is ignored. Useful for providing
// helpful errors when generating constructor calls that try to use one of
// these fields.
var unavailableReasons = <String, String>{};

var accessibleFieldList = sortedFieldList.where((field) {
if (!field.isPublic) {
unavailableReasons[field.name] = 'It is assigned to a private field.';
return false;
}

if (_jsonKeyFor(field).ignore == true) {
unavailableReasons[field.name] = 'It is assigned to an ignored field.';
return false;
}

return true;
}).toList();

// Explicitly using `LinkedHashMap` – we want these ordered.
var fields = new LinkedHashMap<String, FieldElement>.fromIterable(
accessibleFieldList,
key: (f) => (f as FieldElement).name);

var prefix = '_\$${classElement.name}';
var classAnnotation = _valueForAnnotation(annotation);

var buffer = new StringBuffer();
var accessibleFields = _writeCtor(buffer, classAnnotation, classElement);

final classAnnotation = _valueForAnnotation(annotation);

if (classAnnotation.createFactory) {
buffer.writeln();
buffer.writeln(
'${classElement.name} ${prefix}FromJson(Map<String, dynamic> json) =>');

String deserializeFun(String paramOrFieldName,
{ParameterElement ctorParam}) =>
_deserializeForField(
fields[paramOrFieldName], classAnnotation.nullable,
ctorParam: ctorParam);

var fieldsSetByFactory = writeConstructorInvocation(
buffer,
classElement,
fields.keys.toSet(),
fields.values.where((fe) => !fe.isFinal).map((fe) => fe.name).toSet(),
unavailableReasons,
deserializeFun);

// If there are fields that are final – that are not set via the generated
// constructor, then don't output them when generating the `toJson` call.
fields.removeWhere((key, field) => !fieldsSetByFactory.contains(key));
}

// Now we check for duplicate JSON keys due to colliding annotations.
// Check for duplicate JSON keys due to colliding annotations.
// We do this now, since we have a final field list after any pruning done
// by `createFactory`.

fields.values.fold(new Set<String>(), (Set<String> set, fe) {
// by `_writeCtor`.
accessibleFields.fold(new Set<String>(), (Set<String> set, fe) {
var jsonKey = _jsonKeyFor(fe).name ?? fe.name;
if (!set.add(jsonKey)) {
throw new InvalidGenerationSourceError(
Expand All @@ -149,6 +96,7 @@ class JsonSerializableGenerator
});

if (classAnnotation.createToJson) {
var prefix = _prefix(classElement);
var mixClassName = '${prefix}SerializerMixin';
var helpClassName = '${prefix}JsonMapWrapper';

Expand All @@ -159,57 +107,110 @@ class JsonSerializableGenerator

// write copies of the fields - this allows the toJson method to access
// the fields of the target class
for (var field in fields.values) {
for (var field in accessibleFields) {
//TODO - handle aliased imports
buffer.writeln(' ${field.type} get ${field.name};');
}

buffer.write(' Map<String, dynamic> toJson() ');

var writeNaive = accessibleFieldList
var writeNaive = accessibleFields
.every((e) => _writeJsonValueNaive(e, classAnnotation));

if (useWrappers) {
buffer.writeln('=> new $helpClassName(this);');
} else {
if (writeNaive) {
// write simple `toJson` method that includes all keys...
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
_writeToJsonSimple(
buffer, accessibleFields, classAnnotation.nullable);
} else {
// At least one field should be excluded if null
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
_writeToJsonWithNullChecks(buffer, accessibleFields, classAnnotation);
}
}

// end of the mixin class
buffer.writeln('}');

if (useWrappers) {
_writeWrapper(
buffer, helpClassName, mixClassName, classAnnotation, fields);
_writeWrapper(buffer, helpClassName, mixClassName, classAnnotation,
accessibleFields);
}
}

return buffer.toString();
}

Set<FieldElement> _writeCtor(StringBuffer buffer,
JsonSerializable classAnnotation, ClassElement classElement) {
// Used to keep track of why a field is ignored. Useful for providing
// helpful errors when generating constructor calls that try to use one of
// these fields.
var unavailableReasons = <String, String>{};

var sortedFields = createSortedFieldSet(classElement);

var accessibleFields = sortedFields.fold<Map<String, FieldElement>>(
<String, FieldElement>{}, (map, field) {
if (!field.isPublic) {
unavailableReasons[field.name] = 'It is assigned to a private field.';
} else if (_jsonKeyFor(field).ignore == true) {
unavailableReasons[field.name] = 'It is assigned to an ignored field.';
} else {
map[field.name] = field;
}

return map;
});

if (classAnnotation.createFactory) {
buffer.writeln();
buffer.writeln('${classElement.name} '
'${_prefix(classElement)}FromJson(Map<String, dynamic> json) =>');

String deserializeFun(String paramOrFieldName,
{ParameterElement ctorParam}) =>
_deserializeForField(
accessibleFields[paramOrFieldName], classAnnotation.nullable,
ctorParam: ctorParam);

var fieldsSetByFactory = writeConstructorInvocation(
buffer,
classElement,
accessibleFields.keys,
accessibleFields.values
.where((fe) => !fe.isFinal)
.map((fe) => fe.name)
.toList(),
unavailableReasons,
deserializeFun);

// If there are fields that are final – that are not set via the generated
// constructor, then don't output them when generating the `toJson` call.
accessibleFields
.removeWhere((name, fe) => !fieldsSetByFactory.contains(name));
}
return accessibleFields.values.toSet();
}

void _writeWrapper(
StringBuffer buffer,
String helpClassName,
String mixClassName,
JsonSerializable classAnnotation,
Map<String, FieldElement> fields) {
Iterable<FieldElement> fields) {
buffer.writeln();
// TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported
buffer.writeln('''class $helpClassName extends \$JsonMapWrapper {
final $mixClassName _v;
$helpClassName(this._v);
''');

if (fields.values.every((e) => _writeJsonValueNaive(e, classAnnotation))) {
if (fields.every((e) => _writeJsonValueNaive(e, classAnnotation))) {
// TODO(kevmoo): consider just doing one code path – if it's fast
// enough
var jsonKeys = fields.values.map(_safeNameAccess).join(', ');
var jsonKeys = fields.map(_safeNameAccess).join(', ');

// TODO(kevmoo): maybe put this in a static field instead?
// const lists have unfortunate overhead
Expand All @@ -220,7 +221,7 @@ class JsonSerializableGenerator
// At least one field should be excluded if null
buffer.writeln('@override\nIterable<String> get keys sync* {');

for (var field in fields.values) {
for (var field in fields) {
var nullCheck = !_writeJsonValueNaive(field, classAnnotation);
if (nullCheck) {
buffer.writeln('if (_v.${field.name} != null) {');
Expand All @@ -240,7 +241,7 @@ class JsonSerializableGenerator
switch(key) {
''');

for (var field in fields.values) {
for (var field in fields) {
var valueAccess = '_v.${field.name}';
buffer.write('''case ${_safeNameAccess(field)}:
return ${_serializeField(
Expand Down Expand Up @@ -393,6 +394,8 @@ class _TypeHelperContext implements SerializeContext, DeserializeContext {
targetType, expression, _notSupportedWithTypeHelpersMsg));
}

String _prefix(ClassElement classElement) => '_\$${classElement.name}';

String _safeNameAccess(FieldElement field) {
var name = _jsonKeyFor(field).name ?? field.name;
// TODO(kevmoo): JsonKey.name could also have quotes and other silly.
Expand Down
13 changes: 7 additions & 6 deletions json_serializable/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ String friendlyNameForElement(Element element) {
return names.join(' ');
}

/// Returns a list of all instance, [FieldElement] items for [element] and
/// super classes.
List<FieldElement> listFields(ClassElement element) {
/// Returns a [Set] of all instance [FieldElement] items for [element] and
/// super classes, sorted first by their location in the inheritance hierarchy
/// (super first) and then by their location in the source file.
Set<FieldElement> createSortedFieldSet(ClassElement element) {
// Get all of the fields that need to be assigned
// TODO: support overriding the field set with an annotation option
var fieldsList = element.fields.where((e) => !e.isStatic).toList();
Expand Down Expand Up @@ -85,7 +86,7 @@ List<FieldElement> listFields(ClassElement element) {
// Sadly, `classElement.fields` puts properties after fields
fieldsList.sort(_sortByLocation);

return fieldsList;
return fieldsList.toSet();
}

int _sortByLocation(FieldElement a, FieldElement b) {
Expand Down Expand Up @@ -141,8 +142,8 @@ final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);
Set<String> writeConstructorInvocation(
StringBuffer buffer,
ClassElement classElement,
Set<String> availableConstructorParameters,
Set<String> writeableFields,
Iterable<String> availableConstructorParameters,
Iterable<String> writeableFields,
Map<String, String> unavailableReasons,
String deserializeForField(String paramOrFieldName,
{ParameterElement ctorParam})) {
Expand Down
3 changes: 3 additions & 0 deletions json_serializable/test/test_files/json_test_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class Order extends Object with _$OrderSerializerMixin {

int get price => items.fold(0, (total, item) => item.price + total);

@JsonKey(ignore: true)
bool shouldBeCached;

Order(this.category, [Iterable<Item> items])
: this.items = new UnmodifiableListView<Item>(
new List<Item>.unmodifiable(items ?? const <Item>[]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ class Order extends Object with _$OrderSerializerMixin {

int get price => items.fold(0, (total, item) => item.price + total);

@JsonKey(ignore: true)
bool shouldBeCached;

Order(this.category, [Iterable<Item> items])
: this.items = new UnmodifiableListView<Item>(
new List<Item>.unmodifiable(items ?? const <Item>[]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ class Order extends Object with _$OrderSerializerMixin {

int get price => items.fold(0, (total, item) => item.price + total);

@JsonKey(ignore: true)
bool shouldBeCached;

Order(this.category, [Iterable<Item> items])
: this.items = new UnmodifiableListView<Item>(
new List<Item>.unmodifiable(items ?? const <Item>[]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ class Order extends Object with _$OrderSerializerMixin {

int get price => items.fold(0, (total, item) => item.price + total);

@JsonKey(ignore: true)
bool shouldBeCached;

Order(this.category, [Iterable<Item> items])
: this.items = new UnmodifiableListView<Item>(
new List<Item>.unmodifiable(items ?? const <Item>[]));
Expand Down