Skip to content

Commit 21b6749

Browse files
committed
feature: allow customizing enum value serialization via annotations
* Require the latest Dart SDK Annotated enums fail without the CFE * Require the latest pkg:analyzer Enum metadata parsing was just recently added * Require the latest pkg:source_gen Uses multiple return values to allow generating shared helpers * Deprecates enum helpers in json_annotation They can now be generated as part of the source, which minimizes versioning issues with helpers. * Small, but breaking changes to Generator API to return multiple values * Added `addMember` API to Context classes to allow helpers to be generated. * Updated integration test code to include an annotated enum * Updated yaml test code to remove now superfluous convert logic and use annotated enums instead Fixes #38
1 parent 29d3b51 commit 21b6749

24 files changed

+455
-146
lines changed

json_annotation/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.2.10
2+
3+
* Deprecated `$enumDecode` and `$enumDecodeNullable` which are no longer needed
4+
by the latest release of `package:json_serializable`.
5+
16
## 0.2.9
27

38
* When `FormatException` is caught in "checked mode", use the `message`

json_annotation/lib/src/json_serializable.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ class JsonKey {
195195
}
196196

197197
// Until enum supports parse: github.com/dart-lang/sdk/issues/33244
198+
/// **DEPRECATED** No longer used by `package:source_gen` `^0.6.0`.
199+
///
198200
/// *Helper function used in generated code with `enum` values – should not be
199201
/// used directly.*
200202
///
@@ -209,12 +211,17 @@ class JsonKey {
209211
/// ```dart
210212
/// $enumDecode('Example', Example.values, 'desiredValue')
211213
/// ```
214+
@Deprecated('This member is no longer needed by code generated by '
215+
'package:json_serializable ^0.6.0. Upgrade your dependency and regenerate '
216+
'your code to remove the deprecation warning.')
212217
T $enumDecode<T>(String enumName, List<T> values, String enumValue) =>
213218
values.singleWhere((e) => e.toString() == '$enumName.$enumValue',
214219
orElse: () => throw new ArgumentError(
215220
'`$enumValue` is not one of the supported values: '
216221
'${values.map(_nameForEnumValue).join(', ')}'));
217222

223+
/// **DEPRECATED** No longer used by `package:source_gen` `^0.6.0`.
224+
///
218225
/// *Helper function used in generated code with `enum` values – should not be
219226
/// used directly.*
220227
///
@@ -230,6 +237,9 @@ T $enumDecode<T>(String enumName, List<T> values, String enumValue) =>
230237
/// ```dart
231238
/// $enumDecodeNullable('Example', Example.values, 'desiredValue')
232239
/// ```
240+
@Deprecated('This member is no longer needed by code generated by '
241+
'package:json_serializable ^0.6.0. Upgrade your dependency and regenerate '
242+
'your code to remove the deprecation warning.')
233243
T $enumDecodeNullable<T>(String enumName, List<T> values, String enumValue) =>
234244
enumValue == null ? null : $enumDecode(enumName, values, enumValue);
235245

json_annotation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: json_annotation
2-
version: 0.2.9
2+
version: 0.2.10-dev
33
description: >-
44
Classes and helper functions that support JSON code generation via the
55
`json_serializable` package.

json_serializable/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
## 0.6.0
2+
3+
* Now supports changing the serialized values of enums using `JsonKey`.
4+
5+
```dart
6+
enum AutoApply {
7+
none,
8+
dependents,
9+
@JsonKey(name: 'all_packages')
10+
allPackages,
11+
@JsonKey(name: 'root_package')
12+
rootPackage
13+
}
14+
```
15+
16+
* `JsonSerializableGenerator.generateForAnnotatedElement` now returns
17+
`Iterable<String>` instead of `String`.
18+
19+
* `SerializeContext` and `DeserializeContext` now have an `addMember` function
20+
which allows `TypeHelper` instances to add additional members when handling
21+
a field. This is useful for generating shared helpers, for instance.
22+
123
## 0.5.8
224

325
* Small fixes to support Dart 2 runtime semantics.

json_serializable/lib/src/helper_core.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ abstract class HelperCore {
2020

2121
HelperCore(this.generator, this.element, this.annotation);
2222

23+
void addMember(String memberContent);
24+
2325
String get targetClassReference =>
2426
'${element.name}${genericClassArgumentsImpl(false)}';
2527

@@ -41,7 +43,7 @@ abstract class HelperCore {
4143
new JsonKeyWithConversion(field, annotation);
4244

4345
TypeHelperContext getHelperContext(FieldElement field) =>
44-
new TypeHelperContext(generator, field.metadata, jsonKeyFor(field));
46+
new TypeHelperContext(this, field.metadata, jsonKeyFor(field));
4547
}
4648

4749
InvalidGenerationSourceError createInvalidGenerationError(

json_serializable/lib/src/json_key_with_conversion.dart

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,13 @@ import 'package:source_gen/source_gen.dart';
1212
import 'json_literal_generator.dart';
1313
import 'utils.dart';
1414

15-
final _jsonKeyChecker = const TypeChecker.fromRuntime(JsonKey);
16-
1715
JsonKeyWithConversion _from(
1816
FieldElement element, JsonSerializable classAnnotation) {
1917
// If an annotation exists on `element` the source is a 'real' field.
2018
// If the result is `null`, check the getter – it is a property.
2119
// TODO(kevmoo) setters: github.com/dart-lang/json_serializable/issues/24
22-
var obj = _jsonKeyChecker.firstAnnotationOfExact(element) ??
23-
_jsonKeyChecker.firstAnnotationOfExact(element.getter);
20+
var obj = jsonKeyChecker.firstAnnotationOfExact(element) ??
21+
jsonKeyChecker.firstAnnotationOfExact(element.getter);
2422

2523
if (obj == null) {
2624
return new JsonKeyWithConversion._(classAnnotation);
@@ -79,15 +77,13 @@ JsonKeyWithConversion _from(
7977
var defaultValueObject = obj.getField('defaultValue');
8078

8179
Object defaultValueLiteral;
82-
if (isEnum(defaultValueObject.type)) {
83-
var interfaceType = defaultValueObject.type as InterfaceType;
84-
var allowedValues = interfaceType.element.fields
85-
.where((p) => !p.isSynthetic)
86-
.map((p) => p.name)
87-
.toList();
80+
81+
var enumFields = iterateEnumFields(defaultValueObject.type);
82+
if (enumFields != null) {
83+
var allowedValues = enumFields.map((p) => p.name).toList();
8884
var enumValueIndex = defaultValueObject.getField('index').toIntValue();
8985
defaultValueLiteral =
90-
'${interfaceType.name}.${allowedValues[enumValueIndex]}';
86+
'${defaultValueObject.type.name}.${allowedValues[enumValueIndex]}';
9187
} else {
9288
defaultValueLiteral = _getLiteral(defaultValueObject, []);
9389
if (defaultValueLiteral != null) {

json_serializable/lib/src/json_serializable_generator.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class JsonSerializableGenerator
158158
new List.unmodifiable(typeHelpers.followedBy(_defaultHelpers)));
159159

160160
@override
161-
String generateForAnnotatedElement(
161+
Iterable<String> generateForAnnotatedElement(
162162
Element element, ConstantReader annotation, BuildStep buildStep) {
163163
if (element is! ClassElement) {
164164
var name = element.name;
@@ -170,7 +170,7 @@ class JsonSerializableGenerator
170170
var classElement = element as ClassElement;
171171
var classAnnotation = valueForAnnotation(annotation);
172172
var helper = new _GeneratorHelper(this, classElement, classAnnotation);
173-
return helper._generate().join('\n\n');
173+
return helper._generate();
174174
}
175175
}
176176

@@ -179,7 +179,15 @@ class _GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
179179
JsonSerializable annotation)
180180
: super(generator, element, annotation);
181181

182+
final _addedMembers = new Set<String>();
183+
184+
@override
185+
void addMember(String memberContent) {
186+
_addedMembers.add(memberContent);
187+
}
188+
182189
Iterable<String> _generate() sync* {
190+
assert(_addedMembers.isEmpty);
183191
var sortedFields = _createSortedFieldSet(element);
184192

185193
// Used to keep track of why a field is ignored. Useful for providing
@@ -223,6 +231,8 @@ class _GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
223231
if (annotation.createToJson) {
224232
yield* createToJson(accessibleFieldSet);
225233
}
234+
235+
yield* _addedMembers;
226236
}
227237
}
228238

json_serializable/lib/src/type_helper.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@ import 'package:analyzer/dart/element/element.dart';
66
import 'package:analyzer/dart/element/type.dart';
77

88
abstract class SerializeContext {
9+
/// `true` if [serialize] should handle the case of `expression` being null.
910
bool get nullable;
1011
bool get useWrappers;
1112

1213
/// [expression] may be just the name of the field or it may an expression
1314
/// representing the serialization of a value.
1415
String serialize(DartType fieldType, String expression);
1516
List<ElementAnnotation> get metadata;
17+
18+
/// Adds [memberContent] to the set of generated, top-level members.
19+
void addMember(String memberContent);
1620
}
1721

1822
abstract class DeserializeContext {
23+
/// `true` if [deserialize] should handle the case of `expression` being null.
1924
bool get nullable;
2025
String deserialize(DartType fieldType, String expression);
2126
List<ElementAnnotation> get metadata;
27+
28+
/// Adds [memberContent] to the set of generated, top-level members.
29+
void addMember(String memberContent);
2230
}
2331

2432
abstract class TypeHelper {

json_serializable/lib/src/type_helper_context.dart

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,38 @@
55
import 'package:analyzer/dart/element/element.dart';
66
import 'package:analyzer/dart/element/type.dart';
77

8+
import 'helper_core.dart';
89
import 'json_key_with_conversion.dart';
910
import 'json_serializable_generator.dart';
1011
import 'type_helper.dart';
1112

1213
class TypeHelperContext implements SerializeContext, DeserializeContext {
14+
final HelperCore _helperCore;
15+
1316
@override
1417
final List<ElementAnnotation> metadata;
1518

16-
final JsonSerializableGenerator _generator;
1719
final JsonKeyWithConversion _key;
1820

1921
@override
20-
bool get useWrappers => _generator.useWrappers;
22+
bool get useWrappers => _helperCore.generator.useWrappers;
2123

22-
bool get anyMap => _generator.anyMap;
24+
bool get anyMap => _helperCore.generator.anyMap;
2325

24-
bool get explicitToJson => _generator.explicitToJson;
26+
bool get explicitToJson => _helperCore.generator.explicitToJson;
2527

2628
@override
2729
bool get nullable => _key.nullable;
2830

2931
ConvertData get fromJsonData => _key.fromJsonData;
3032
ConvertData get toJsonData => _key.toJsonData;
3133

32-
TypeHelperContext(this._generator, this.metadata, this._key);
34+
TypeHelperContext(this._helperCore, this.metadata, this._key);
35+
36+
@override
37+
void addMember(String memberContent) {
38+
_helperCore.addMember(memberContent);
39+
}
3340

3441
@override
3542
String serialize(DartType targetType, String expression) => _run(
@@ -45,7 +52,8 @@ class TypeHelperContext implements SerializeContext, DeserializeContext {
4552

4653
String _run(DartType targetType, String expression,
4754
String invoke(TypeHelper instance)) =>
48-
allHelpersImpl(_generator).map(invoke).firstWhere((r) => r != null,
55+
allHelpersImpl(_helperCore.generator).map(invoke).firstWhere(
56+
(r) => r != null,
4957
orElse: () => throw new UnsupportedTypeError(
5058
targetType, expression, _notSupportedWithTypeHelpersMsg));
5159
}

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,75 @@ class EnumHelper extends TypeHelper {
1515
@override
1616
String serialize(
1717
DartType targetType, String expression, SerializeContext context) {
18-
if (!isEnum(targetType)) {
18+
var memberContent = _enumValueMapFromType(targetType);
19+
20+
if (memberContent == null) {
1921
return null;
2022
}
2123

22-
var nullableLiteral = context.nullable ? '?' : '';
24+
context.addMember(memberContent);
2325

24-
return '$expression$nullableLiteral.toString()'
25-
"$nullableLiteral.split('.')$nullableLiteral.last";
26+
return '${_constMapName(targetType)}[$expression]';
2627
}
2728

2829
@override
2930
String deserialize(
3031
DartType targetType, String expression, DeserializeContext context) {
31-
if (!isEnum(targetType)) {
32+
var memberContent = _enumValueMapFromType(targetType);
33+
34+
if (memberContent == null) {
3235
return null;
3336
}
3437

38+
context.addMember(_enumDecodeHelper);
39+
40+
if (context.nullable) {
41+
context.addMember(_enumDecodeHelperNullable);
42+
}
43+
44+
context.addMember(memberContent);
45+
3546
var functionName =
36-
context.nullable ? r'$enumDecodeNullable' : r'$enumDecode';
37-
return "$functionName('$targetType', $targetType.values, "
47+
context.nullable ? r'_$enumDecodeNullable' : r'_$enumDecode';
48+
return '$functionName(${_constMapName(targetType)}, '
3849
'$expression as String)';
3950
}
4051
}
52+
53+
String _constMapName(DartType targetType) => '_\$${targetType.name}EnumMap';
54+
55+
String _enumValueMapFromType(DartType targetType) {
56+
var enumMap = enumFieldsMap(targetType);
57+
58+
if (enumMap == null) {
59+
return null;
60+
}
61+
62+
var items = enumMap.entries
63+
.map((e) => " ${targetType.name}.${e.key.name} : '${e.value}'");
64+
65+
return 'const ${_constMapName(targetType)} = '
66+
'const <${targetType.name}, String>{\n${items.join(',\n')}\n};';
67+
}
68+
69+
const _enumDecodeHelper = r'''
70+
T _$enumDecode<T>(Map<T, String> enumValues, String source) {
71+
if (source == null) {
72+
throw new ArgumentError('A value must be provided. Supported values: '
73+
'${enumValues.values.join(', ')}');
74+
}
75+
return enumValues.entries
76+
.singleWhere((e) => e.value == source,
77+
orElse: () => throw new ArgumentError(
78+
'`$source` is not one of the supported values: '
79+
'${enumValues.values.join(', ')}'))
80+
.key;
81+
}''';
82+
83+
const _enumDecodeHelperNullable = r'''
84+
T _$enumDecodeNullable<T>(Map<T, String> enumValues, String source) {
85+
if (source == null) {
86+
return null;
87+
}
88+
return _$enumDecode<T>(enumValues, source);
89+
}''';

0 commit comments

Comments
 (0)