Skip to content

Commit 60b29d1

Browse files
authored
Finish implementation of defaultValue (#191)
Implementation for `checked` option Improve error messages on unsupported types Explicitly disallow defaultValue + non-nullable field support serialization of single enum values Fixes #74 Also added $enumDecode[Nullable] helpers to json_annotation
1 parent 89bef38 commit 60b29d1

24 files changed

+401
-182
lines changed

json_annotation/CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
* Added `JsonKey.defaultValue`.
44

5+
* Added helpers for deserialization of `enum` values.
6+
These functions starting with `$` are referenced by generated code.
7+
They are not meant for direct use.
8+
59
## 0.2.5
610

711
* Added `CheckedFromJsonException` which is thrown by code generated when
812
`checked` is enabled in `json_serializable`.
913

10-
* Added functions to support the `checked` generation option. These
11-
functions start with `$` are referenced by generated code. They are not meant
12-
for direct use.
14+
* Added functions to support the `checked` generation option.
15+
These functions starting with `$` are referenced by generated code.
16+
They are not meant for direct use.
1317

1418
## 0.2.4
1519

json_annotation/lib/src/json_serializable.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,45 @@ class JsonKey {
133133
this.toJson,
134134
this.defaultValue});
135135
}
136+
137+
// Until enum supports parse: github.com/dart-lang/sdk/issues/33244
138+
/// *Helper function used in generated code with `enum` values – should not be
139+
/// used directly.*
140+
///
141+
/// Returns an enum instance corresponding to [enumValue] from the enum named
142+
/// [enumName] with [values].
143+
///
144+
/// If [enumValue] is null or no corresponding values exists, an `ArgumentError`
145+
/// is thrown.
146+
///
147+
/// Given an enum named `Example`, an invocation would look like
148+
///
149+
/// ```dart
150+
/// $enumDecode('Example', Example.values, 'desiredValue')
151+
/// ```
152+
T $enumDecode<T>(String enumName, List<T> values, String enumValue) =>
153+
values.singleWhere((e) => e.toString() == '$enumName.$enumValue',
154+
orElse: () => throw new ArgumentError(
155+
'`$enumValue` is not one of the supported values: '
156+
'${values.map(_nameForEnumValue).join(', ')}'));
157+
158+
/// *Helper function used in generated code with `enum` values – should not be
159+
/// used directly.*
160+
///
161+
/// Returns an enum instance corresponding to [enumValue] from the enum named
162+
/// [enumName] with [values].
163+
///
164+
/// If [enumValue] is `null`, `null` is returned.
165+
///
166+
/// If no corresponding values exists, an `ArgumentError` is thrown.
167+
///
168+
/// Given an enum named `Example`, an invocation would look like
169+
///
170+
/// ```dart
171+
/// $enumDecodeNullable('Example', Example.values, 'desiredValue')
172+
/// ```
173+
T $enumDecodeNullable<T>(String enumName, List<T> values, String enumValue) =>
174+
enumValue == null ? null : $enumDecode(enumName, values, enumValue);
175+
176+
// Until enum has a name property: github.com/dart-lang/sdk/issues/21712
177+
String _nameForEnumValue(Object value) => value.toString().split('.')[1];

json_serializable/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
* Added support for `JsonKey.defaultValue`.
44

5+
* `enum` deserialization now uses helpers provided by `json_annotation`.
6+
57
* Small change to how nullable `Map` values are deserialized.
68

79
* Small whitespace changes to `JsonLiteral` generation to align with `dartfmt`.

json_serializable/lib/src/generator_helper.dart

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -382,34 +382,37 @@ class ${_wrapperClassName(true)} extends \$JsonMapWrapper {
382382
String _deserializeForField(FieldElement field,
383383
{ParameterElement ctorParam, bool checkedProperty}) {
384384
checkedProperty ??= false;
385-
var defaultValue = jsonKeyFor(field).defaultValue;
386385
var jsonKeyName = _safeNameAccess(field);
387-
388386
var targetType = ctorParam?.type ?? field.type;
387+
var contextHelper = _getHelperContext(field);
389388

389+
String value;
390390
try {
391391
if (_generator.checked) {
392-
// TODO: default value fun here!
393-
var value = _getHelperContext(field).deserialize(targetType, 'v');
394-
if (checkedProperty) {
395-
return value;
392+
value = contextHelper.deserialize(targetType, 'v');
393+
if (!checkedProperty) {
394+
value = '\$checkedConvert(json, $jsonKeyName, (v) => $value)';
396395
}
396+
} else {
397+
assert(!checkedProperty,
398+
'should only be true if `_generator.checked` is true.');
397399

398-
return '\$checkedConvert(json, $jsonKeyName, (v) => $value)';
399-
}
400-
assert(!checkedProperty,
401-
'should only be true if `_generator.checked` is true.');
402-
403-
var value = _getHelperContext(field)
404-
.deserialize(targetType, 'json[$jsonKeyName]');
405-
406-
if (defaultValue != null) {
407-
value = '$value ?? $defaultValue';
400+
value = contextHelper.deserialize(targetType, 'json[$jsonKeyName]');
408401
}
409-
return value;
410402
} on UnsupportedTypeError catch (e) {
411403
throw _createInvalidGenerationError('fromJson', field, e);
412404
}
405+
406+
var defaultValue = jsonKeyFor(field).defaultValue;
407+
if (defaultValue != null) {
408+
if (!contextHelper.nullable) {
409+
throwUnsupported(field,
410+
'Cannot use `defaultValue` on a field with `nullable` false.');
411+
}
412+
413+
value = '$value ?? $defaultValue';
414+
}
415+
return value;
413416
}
414417

415418
TypeHelperContext _getHelperContext(FieldElement field) {

json_serializable/lib/src/json_key_helpers.dart

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import 'package:meta/meta.dart' show alwaysThrows;
1111
import 'package:source_gen/source_gen.dart';
1212

1313
import 'json_literal_generator.dart';
14+
import 'utils.dart';
1415

1516
@alwaysThrows
16-
T _throwUnsupported<T>(FieldElement element, String message) =>
17+
void throwUnsupported(FieldElement element, String message) =>
1718
throw new InvalidGenerationSourceError(
1819
'Error with `@JsonKey` on `${element.name}`. $message',
1920
element: element);
@@ -45,41 +46,71 @@ JsonKeyWithConversion _from(FieldElement element) {
4546
var fromJsonName = _getFunctionName(obj, element, true);
4647
var toJsonName = _getFunctionName(obj, element, false);
4748

48-
Object _getLiteral(DartObject dartObject) {
49+
Object _getLiteral(DartObject dartObject, Iterable<String> things) {
4950
if (dartObject.isNull) {
5051
return null;
5152
}
5253

5354
var reader = new ConstantReader(dartObject);
5455

56+
String badType;
5557
if (reader.isSymbol) {
56-
_throwUnsupported(element,
57-
'Values of type `Symbol` are not supported for `defaultValue`.');
58+
badType = 'Symbol';
5859
} else if (reader.isType) {
59-
_throwUnsupported(element,
60-
'Values of type `Type` are not supported for `defaultValue`.');
60+
badType = 'Type';
61+
} else if (dartObject.type is FunctionType) {
62+
// TODO(kevmoo): Support calling function for the default value?
63+
badType = 'Function';
6164
} else if (!reader.isLiteral) {
62-
_throwUnsupported(
63-
element, 'The provided `defaultValue` is not a literal: $dartObject');
65+
badType = dartObject.type.name;
66+
}
67+
68+
if (badType != null) {
69+
badType = things.followedBy([badType]).join(' > ');
70+
throwUnsupported(
71+
element, '`defaultValue` is `$badType`, it must be a literal.');
6472
}
6573

6674
var literal = reader.literalValue;
75+
6776
if (literal is num || literal is String || literal is bool) {
6877
return literal;
6978
} else if (literal is List<DartObject>) {
70-
return literal.map(_getLiteral).toList();
71-
} else if (literal is Map<DartObject, DartObject>) {
7279
return literal
73-
.map((k, v) => new MapEntry(_getLiteral(k), _getLiteral(v)));
80+
.map((e) => _getLiteral(e, things.followedBy(['List'])))
81+
.toList();
82+
} else if (literal is Map<DartObject, DartObject>) {
83+
var mapThings = things.followedBy(['Map']);
84+
return literal.map((k, v) =>
85+
new MapEntry(_getLiteral(k, mapThings), _getLiteral(v, mapThings)));
7486
}
75-
_throwUnsupported(
76-
element, 'The provided value is not supported: $dartObject');
77-
}
7887

79-
var defaultValueLiteral = _getLiteral(obj.getField('defaultValue'));
88+
badType = things.followedBy(['$dartObject']).join(' > ');
8089

81-
if (defaultValueLiteral != null) {
82-
defaultValueLiteral = jsonLiteralAsDart(defaultValueLiteral, false);
90+
throwUnsupported(
91+
element,
92+
'The provided value is not supported: $badType. '
93+
'This may be an error in package:json_serializable. '
94+
'Please rerun your build with `--verbose` and file an issue.');
95+
}
96+
97+
var defaultValueObject = obj.getField('defaultValue');
98+
99+
Object defaultValueLiteral;
100+
if (isEnum(defaultValueObject.type)) {
101+
var interfaceType = defaultValueObject.type as InterfaceType;
102+
var allowedValues = interfaceType.accessors
103+
.where((p) => p.returnType == interfaceType)
104+
.map((p) => p.name)
105+
.toList();
106+
var enumValueIndex = defaultValueObject.getField('index').toIntValue();
107+
defaultValueLiteral =
108+
'${interfaceType.name}.${allowedValues[enumValueIndex]}';
109+
} else {
110+
defaultValueLiteral = _getLiteral(defaultValueObject, []);
111+
if (defaultValueLiteral != null) {
112+
defaultValueLiteral = jsonLiteralAsDart(defaultValueLiteral, false);
113+
}
83114
}
84115

85116
return new JsonKeyWithConversion._(
@@ -130,7 +161,7 @@ ConvertData _getFunctionName(
130161
var type = objectValue.type as FunctionType;
131162

132163
if (type.element is MethodElement) {
133-
_throwUnsupported(
164+
throwUnsupported(
134165
element,
135166
'The function provided for `$paramName` must be top-level. '
136167
'Static class methods (`${type.element.name}`) are not supported.');
@@ -140,7 +171,7 @@ ConvertData _getFunctionName(
140171
if (functionElement.parameters.isEmpty ||
141172
functionElement.parameters.first.isNamed ||
142173
functionElement.parameters.where((pe) => !pe.isOptional).length > 1) {
143-
_throwUnsupported(
174+
throwUnsupported(
144175
element,
145176
'The `$paramName` function `${functionElement.name}` must have one '
146177
'positional paramater.');
@@ -155,7 +186,7 @@ ConvertData _getFunctionName(
155186
// to the `fromJson` function.
156187
// TODO: consider adding error checking here if there is confusion.
157188
} else if (!returnType.isAssignableTo(element.type)) {
158-
_throwUnsupported(
189+
throwUnsupported(
159190
element,
160191
'The `$paramName` function `${functionElement.name}` return type '
161192
'`$returnType` is not compatible with field type `${element.type}`.');
@@ -166,7 +197,7 @@ ConvertData _getFunctionName(
166197
// to the `fromJson` function.
167198
// TODO: consider adding error checking here if there is confusion.
168199
} else if (!element.type.isAssignableTo(argType)) {
169-
_throwUnsupported(
200+
throwUnsupported(
170201
element,
171202
'The `$paramName` function `${functionElement.name}` argument type '
172203
'`$argType` is not compatible with field type'

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import 'package:analyzer/dart/element/type.dart';
66

7-
import '../constants.dart' as consts;
87
import '../type_helper.dart';
98
import '../utils.dart';
109

@@ -31,18 +30,9 @@ class EnumHelper extends TypeHelper {
3130
return null;
3231
}
3332

34-
var wrappedExpression =
35-
simpleExpression.hasMatch(expression) ? expression : '{$expression}';
36-
37-
var closureArg = consts.closureArg;
38-
if (closureArg == wrappedExpression) {
39-
closureArg = '${closureArg}2';
40-
}
41-
42-
return commonNullPrefix(
43-
context.nullable,
44-
expression,
45-
'$targetType.values.singleWhere(($closureArg) => $closureArg.toString()'
46-
" == '$targetType.\$$wrappedExpression')");
33+
var functionName =
34+
context.nullable ? r'$enumDecodeNullable' : r'$enumDecode';
35+
return "$functionName('$targetType', $targetType.values, "
36+
'$expression as String)';
4737
}
4838
}

json_serializable/test/default_value/default_value.non_nullable.dart renamed to json_serializable/test/default_value/default_value.checked.dart

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,27 @@
55
// GENERATED CODE - DO NOT MODIFY BY HAND
66

77
// **************************************************************************
8-
// Generator: _NonNullableGenerator
8+
// Generator: _CheckedGenerator
99
// **************************************************************************
1010

1111
// ignore_for_file: annotate_overrides
1212

1313
import 'package:json_annotation/json_annotation.dart';
1414

15-
part 'default_value.non_nullable.g.dart';
15+
import 'default_value_interface.dart' as dvi hide Greek;
16+
import 'default_value_interface.dart' show Greek;
17+
18+
part 'default_value.checked.g.dart';
1619

1720
const _intValue = 42;
1821

19-
@JsonSerializable(nullable: false)
20-
class DefaultValue extends Object with _$DefaultValueSerializerMixin {
22+
dvi.DefaultValue fromJson(Map<String, dynamic> json) =>
23+
_$DefaultValueFromJson(json);
24+
25+
@JsonSerializable()
26+
class DefaultValue extends Object
27+
with _$DefaultValueSerializerMixin
28+
implements dvi.DefaultValue {
2129
@JsonKey(defaultValue: true)
2230
bool fieldBool;
2331

@@ -47,6 +55,9 @@ class DefaultValue extends Object with _$DefaultValueSerializerMixin {
4755
})
4856
Map<String, List<String>> fieldMapListString;
4957

58+
@JsonKey(defaultValue: Greek.beta)
59+
Greek fieldEnum;
60+
5061
DefaultValue();
5162

5263
factory DefaultValue.fromJson(Map<String, dynamic> json) =>

0 commit comments

Comments
 (0)