Skip to content

Validate one-off property sets #168

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 2 commits into from
May 21, 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
93 changes: 70 additions & 23 deletions json_serializable/lib/src/generator_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,28 +124,56 @@ class _GeneratorHelper {
});

if (_annotation.createFactory) {
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';

_buffer.writeln();
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';
_buffer.writeln('${_element.name} ${_prefix}FromJson($mapType json) =>');

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

var tempBuffer = new StringBuffer();
var fieldsSetByFactory = writeConstructorInvocation(
tempBuffer,
_element,
accessibleFields.keys,
accessibleFields.values
.where((fe) => !fe.isFinal)
.map((fe) => fe.name)
.toList(),
unavailableReasons,
deserializeFun);

Set<String> fieldsSetByFactory;
if (_generator.checked) {
var classLiteral = escapeDartString(_element.displayName);

_buffer.writeln('\$checkedNew($classLiteral, json, ()');

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

fieldsSetByFactory = data.usedCtorParamsAndFields;

if (data.fieldsToSet.isEmpty) {
// Use simple arrow syntax for the constructor invocation.
// There are no fields to set
_buffer.write(' => ${data.content}');
} else {
// If there are fields to set, create a full function body and
// create a temporary variable to hold the instance so we can make
// wrapped calls to all of the fields value assignments.
_buffer.writeln('{ var val = ');
_buffer.write(data.content);
_buffer.writeln(';');

for (var field in data.fieldsToSet) {
_buffer.writeln();
_buffer.write('\$checkedConvert(json, ${_safeNameAccess(
accessibleFields[field])}, (v) => ');
_buffer.write('val.$field = ');
_buffer.write(_deserializeForField(accessibleFields[field],
checkedProperty: true));
_buffer.write(');');
}
_buffer.writeln('return val; }');
}
var fieldKeyMap = new Map.fromEntries(fieldsSetByFactory
.map((k) => new MapEntry(k, _nameAccess(accessibleFields[k])))
.where((me) => me.key != me.value));
Expand All @@ -158,15 +186,27 @@ class _GeneratorHelper {
fieldKeyMapArg = ', fieldKeyMap: $mapLiteral';
}

var classLiteral = escapeDartString(_element.displayName);

_buffer.writeln(
'${_element.displayName} ${_prefix}FromJson($mapType json) =>'
'\$checkedNew($classLiteral, json, () => $tempBuffer'
'$fieldKeyMapArg)');
_buffer.writeln('$fieldKeyMapArg)');
} else {
_buffer.writeln('${_element.name} '
'${_prefix}FromJson($mapType json) => $tempBuffer');
var data = writeConstructorInvocation(
_element,
accessibleFields.keys,
accessibleFields.values
.where((fe) => !fe.isFinal)
.map((fe) => fe.name)
.toList(),
unavailableReasons,
deserializeFun);

fieldsSetByFactory = data.usedCtorParamsAndFields;

_buffer.writeln(data.content);
for (var field in data.fieldsToSet) {
_buffer.writeln();
_buffer.write(' ..$field = ');
_buffer.write(deserializeFun(field));
}
_buffer.writeln();
}
_buffer.writeln(';');

Expand Down Expand Up @@ -315,16 +355,23 @@ void $toJsonMapHelperName(String key, dynamic value) {
}

String _deserializeForField(FieldElement field,
{ParameterElement ctorParam}) {
{ParameterElement ctorParam, bool checkedProperty}) {
checkedProperty ??= false;
var jsonKey = _safeNameAccess(field);

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

try {
if (_generator.checked) {
var value = _getHelperContext(field).deserialize(targetType, 'v');
if (checkedProperty) {
return value;
}

return '\$checkedConvert(json, $jsonKey, (v) => $value)';
}
assert(!checkedProperty,
'should only be true if `_generator.checked` is true.');
return _getHelperContext(field).deserialize(targetType, 'json[$jsonKey]');
} on UnsupportedTypeError catch (e) {
throw _createInvalidGenerationError('fromJson', field, e);
Expand Down
36 changes: 14 additions & 22 deletions json_serializable/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,7 @@ int _sortByLocation(FieldElement a, FieldElement b) {

final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);

/// Writes the invocation of the default constructor – `new Class(...)` for the
/// type defined in [classElement] to the provided [buffer].
///
/// If an parameter is required to invoke the constructor,
/// If a parameter is required to invoke the constructor,
/// [availableConstructorParameters] is checked to see if it is available. If
/// [availableConstructorParameters] does not contain the parameter name,
/// an [UnsupportedError] is thrown.
Expand All @@ -190,11 +187,7 @@ final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);
///
/// [writeableFields] are also populated, but only if they have not already
/// been defined by a constructor parameter with the same name.
///
/// Set set of all constructor parameters and and fields that are populated is
/// returned.
Set<String> writeConstructorInvocation(
StringBuffer buffer,
CtorData writeConstructorInvocation(
ClassElement classElement,
Iterable<String> availableConstructorParameters,
Iterable<String> writeableFields,
Expand Down Expand Up @@ -250,9 +243,7 @@ Set<String> writeConstructorInvocation(
var remainingFieldsForInvocationBody =
writeableFields.toSet().difference(usedCtorParamsAndFields);

//
// Generate the static factory method
//
var buffer = new StringBuffer();
buffer.write('new $className(');
buffer.writeAll(
constructorArguments.map((paramElement) =>
Expand All @@ -267,17 +258,18 @@ Set<String> writeConstructorInvocation(
}), ', ');

buffer.write(')');
if (remainingFieldsForInvocationBody.isNotEmpty) {
for (var field in remainingFieldsForInvocationBody) {
buffer.writeln();
buffer.write(' ..$field = ');
buffer.write(deserializeForField(field));
usedCtorParamsAndFields.add(field);
}
}
buffer.writeln();

return usedCtorParamsAndFields;
usedCtorParamsAndFields.addAll(remainingFieldsForInvocationBody);

return new CtorData(buffer.toString(), remainingFieldsForInvocationBody,
usedCtorParamsAndFields);
}

class CtorData {
final String content;
final Set<String> fieldsToSet;
final Set<String> usedCtorParamsAndFields;
CtorData(this.content, this.fieldsToSet, this.usedCtorParamsAndFields);
}

void _validateConstructorArguments(
Expand Down
3 changes: 1 addition & 2 deletions json_serializable/test/kitchen_sink_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,7 @@ Matcher _getMatcher(bool checked, String expectedKey, bool checkedAssignment) {

if (checked) {
if (checkedAssignment &&
const ['intIterable', 'datetime-iterable', 'validatedPropertyNo42']
.contains(expectedKey)) {
const ['intIterable', 'datetime-iterable'].contains(expectedKey)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jakemac53 – this is where the rubber hits the road. Now the expected key for validatedPropertyNo42 is not null

expectedKey = null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,61 +10,71 @@ part of 'kitchen_sink.non_nullable.checked.dart';
// Generator: JsonSerializableGenerator
// **************************************************************************

KitchenSink _$KitchenSinkFromJson(Map json) => $checkedNew(
'KitchenSink',
json,
() => new KitchenSink(
ctorValidatedNo42: $checkedConvert(json, 'no-42', (v) => v as int),
iterable: $checkedConvert(json, 'iterable', (v) => v as List),
dynamicIterable:
$checkedConvert(json, 'dynamicIterable', (v) => v as List),
objectIterable:
$checkedConvert(json, 'objectIterable', (v) => v as List),
intIterable: $checkedConvert(
json, 'intIterable', (v) => (v as List).map((e) => e as int)),
dateTimeIterable: $checkedConvert(json, 'datetime-iterable',
(v) => (v as List).map((e) => DateTime.parse(e as String))))
..dateTime = $checkedConvert(
json, 'dateTime', (v) => DateTime.parse(v as String))
..list = $checkedConvert(json, 'list', (v) => v as List)
..dynamicList = $checkedConvert(json, 'dynamicList', (v) => v as List)
..objectList = $checkedConvert(json, 'objectList', (v) => v as List)
..intList = $checkedConvert(
json, 'intList', (v) => (v as List).map((e) => e as int).toList())
..dateTimeList = $checkedConvert(
json,
'dateTimeList',
(v) =>
(v as List).map((e) => DateTime.parse(e as String)).toList())
..map = $checkedConvert(json, 'map', (v) => v as Map)
..stringStringMap = $checkedConvert(json, 'stringStringMap',
(v) => new Map<String, String>.from(v as Map))
..dynamicIntMap = $checkedConvert(
json, 'dynamicIntMap', (v) => new Map<String, int>.from(v as Map))
..objectDateTimeMap = $checkedConvert(
json,
'objectDateTimeMap',
(v) => (v as Map)
.map((k, e) => new MapEntry(k, DateTime.parse(e as String))))
..crazyComplex = $checkedConvert(
json,
'crazyComplex',
(v) => (v as List)
.map((e) => (e as Map).map((k, e) => new MapEntry(
KitchenSink _$KitchenSinkFromJson(Map json) =>
$checkedNew('KitchenSink', json, () {
var val = new KitchenSink(
ctorValidatedNo42: $checkedConvert(json, 'no-42', (v) => v as int),
iterable: $checkedConvert(json, 'iterable', (v) => v as List),
dynamicIterable:
$checkedConvert(json, 'dynamicIterable', (v) => v as List),
objectIterable:
$checkedConvert(json, 'objectIterable', (v) => v as List),
intIterable: $checkedConvert(
json, 'intIterable', (v) => (v as List).map((e) => e as int)),
dateTimeIterable: $checkedConvert(json, 'datetime-iterable',
(v) => (v as List).map((e) => DateTime.parse(e as String))));

$checkedConvert(
json, 'dateTime', (v) => val.dateTime = DateTime.parse(v as String));
$checkedConvert(json, 'list', (v) => val.list = v as List);
$checkedConvert(json, 'dynamicList', (v) => val.dynamicList = v as List);
$checkedConvert(json, 'objectList', (v) => val.objectList = v as List);
$checkedConvert(json, 'intList',
(v) => val.intList = (v as List).map((e) => e as int).toList());
$checkedConvert(
json,
'dateTimeList',
(v) => val.dateTimeList =
(v as List).map((e) => DateTime.parse(e as String)).toList());
$checkedConvert(json, 'map', (v) => val.map = v as Map);
$checkedConvert(json, 'stringStringMap',
(v) => val.stringStringMap = new Map<String, String>.from(v as Map));
$checkedConvert(json, 'dynamicIntMap',
(v) => val.dynamicIntMap = new Map<String, int>.from(v as Map));
$checkedConvert(
json,
'objectDateTimeMap',
(v) => val.objectDateTimeMap = (v as Map)
.map((k, e) => new MapEntry(k, DateTime.parse(e as String))));
$checkedConvert(
json,
'crazyComplex',
(v) => val.crazyComplex = (v as List)
.map((e) => (e as Map).map((k, e) => new MapEntry(
k as String,
(e as Map).map((k, e) => new MapEntry(
k as String,
(e as Map).map((k, e) =>
new MapEntry(k as String, (e as List).map((e) => (e as List).map((e) => DateTime.parse(e as String)).toList()).toList())))))
.toList())
..val = $checkedConvert(json, 'val', (v) => new Map<String, bool>.from(v as Map))
..writeNotNull = $checkedConvert(json, 'writeNotNull', (v) => v as bool)
..string = $checkedConvert(json, r'$string', (v) => v as String)
..simpleObject = $checkedConvert(json, 'simpleObject', (v) => new SimpleObject.fromJson(v as Map))
..validatedPropertyNo42 = $checkedConvert(json, 'validatedPropertyNo42', (v) => v as int),
fieldKeyMap: const {
'ctorValidatedNo42': 'no-42',
'dateTimeIterable': 'datetime-iterable',
'string': r'$string'
});
(e as List)
.map((e) => (e as List)
.map((e) => DateTime.parse(e as String))
.toList())
.toList())))))
.toList());
$checkedConvert(
json, 'val', (v) => val.val = new Map<String, bool>.from(v as Map));
$checkedConvert(
json, 'writeNotNull', (v) => val.writeNotNull = v as bool);
$checkedConvert(json, r'$string', (v) => val.string = v as String);
$checkedConvert(json, 'simpleObject',
(v) => val.simpleObject = new SimpleObject.fromJson(v as Map));
$checkedConvert(json, 'validatedPropertyNo42',
(v) => val.validatedPropertyNo42 = v as int);
return val;
}, fieldKeyMap: const {
'ctorValidatedNo42': 'no-42',
'dateTimeIterable': 'datetime-iterable',
'string': r'$string'
});

abstract class _$KitchenSinkSerializerMixin {
int get ctorValidatedNo42;
Expand Down