Skip to content

Commit 7a550cc

Browse files
authored
feature: allow customizing enum value serialization via annotations (#251)
* 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 * Removed several 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 7a550cc

31 files changed

+627
-206
lines changed

json_annotation/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 0.3.0
2+
3+
* Added `JsonValue` class for annotating `enum` fields with a custom
4+
serialization value.
5+
6+
* Removed `$checkAllowedKeys`, `$enumDecode` and `$enumDecodeNullable` which are
7+
no longer needed by the latest release of `package:json_serializable`.
8+
19
## 0.2.9
210

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

json_annotation/lib/src/allowed_keys_helpers.dart

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,6 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
/// **DEPRECATED** Helper function used in generated code when
6-
/// `JsonSerializable.disallowUnrecognizedKeys` is `true`.
7-
///
8-
/// Should not be used directly.
9-
@Deprecated('Code generated with the latest `json_serializable` will use '
10-
'`\$checkKeys` instead. This function will be removed in the next major '
11-
'release.')
12-
void $checkAllowedKeys(Map map, Iterable<String> allowedKeys) {
13-
$checkKeys(map, allowedKeys: allowedKeys?.toList());
14-
}
15-
165
/// Helper function used in generated `fromJson` code when
176
/// `JsonSerializable.disallowUnrecognizedKeys` is true for an annotated type or
187
/// `JsonKey.required` is `true` for any annotated fields.

json_annotation/lib/src/json_serializable.dart

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -194,44 +194,12 @@ class JsonKey {
194194
});
195195
}
196196

197-
// Until enum supports parse: github.com/dart-lang/sdk/issues/33244
198-
/// *Helper function used in generated code with `enum` values – should not be
199-
/// used directly.*
200-
///
201-
/// Returns an enum instance corresponding to [enumValue] from the enum named
202-
/// [enumName] with [values].
203-
///
204-
/// If [enumValue] is null or no corresponding values exists, an `ArgumentError`
205-
/// is thrown.
206-
///
207-
/// Given an enum named `Example`, an invocation would look like
208-
///
209-
/// ```dart
210-
/// $enumDecode('Example', Example.values, 'desiredValue')
211-
/// ```
212-
T $enumDecode<T>(String enumName, List<T> values, String enumValue) =>
213-
values.singleWhere((e) => e.toString() == '$enumName.$enumValue',
214-
orElse: () => throw new ArgumentError(
215-
'`$enumValue` is not one of the supported values: '
216-
'${values.map(_nameForEnumValue).join(', ')}'));
217-
218-
/// *Helper function used in generated code with `enum` values – should not be
219-
/// used directly.*
220-
///
221-
/// Returns an enum instance corresponding to [enumValue] from the enum named
222-
/// [enumName] with [values].
223-
///
224-
/// If [enumValue] is `null`, `null` is returned.
225-
///
226-
/// If no corresponding values exists, an `ArgumentError` is thrown.
227-
///
228-
/// Given an enum named `Example`, an invocation would look like
229-
///
230-
/// ```dart
231-
/// $enumDecodeNullable('Example', Example.values, 'desiredValue')
232-
/// ```
233-
T $enumDecodeNullable<T>(String enumName, List<T> values, String enumValue) =>
234-
enumValue == null ? null : $enumDecode(enumName, values, enumValue);
235-
236-
// Until enum has a name property: github.com/dart-lang/sdk/issues/21712
237-
String _nameForEnumValue(Object value) => value.toString().split('.')[1];
197+
/// An annotation used to specify how a enum value is serialized.
198+
class JsonValue {
199+
/// The value to use when serializing and deserializing.
200+
///
201+
/// Can be a [String] or an [int].
202+
final dynamic value;
203+
204+
const JsonValue(this.value);
205+
}

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.3.0-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 `JsonValue`.
4+
5+
```dart
6+
enum AutoApply {
7+
none,
8+
dependents,
9+
@JsonValue('all_packages')
10+
allPackages,
11+
@JsonValue('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: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,13 @@ JsonKeyWithConversion _from(
7979
var defaultValueObject = obj.getField('defaultValue');
8080

8181
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();
82+
83+
var enumFields = iterateEnumFields(defaultValueObject.type);
84+
if (enumFields != null) {
85+
var allowedValues = enumFields.map((p) => p.name).toList();
8886
var enumValueIndex = defaultValueObject.getField('index').toIntValue();
8987
defaultValueLiteral =
90-
'${interfaceType.name}.${allowedValues[enumValueIndex]}';
88+
'${defaultValueObject.type.name}.${allowedValues[enumValueIndex]}';
9189
} else {
9290
defaultValueLiteral = _getLiteral(defaultValueObject, []);
9391
if (defaultValueLiteral != null) {

json_serializable/lib/src/json_literal_generator.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ String jsonLiteralAsDart(dynamic value, bool asConst) {
5252

5353
if (value is List) {
5454
var listItems = value.map((v) => jsonLiteralAsDart(v, asConst)).join(', ');
55-
return '${asConst ? 'const' : ''}[$listItems]';
55+
return '${asConst ? 'const ' : ''}[$listItems]';
5656
}
5757

5858
if (value is Map) return jsonMapAsDart(value, asConst);

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: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

7+
import '../json_literal_generator.dart';
78
import '../type_helper.dart';
89
import '../utils.dart';
910

@@ -15,26 +16,75 @@ class EnumHelper extends TypeHelper {
1516
@override
1617
String serialize(
1718
DartType targetType, String expression, SerializeContext context) {
18-
if (!isEnum(targetType)) {
19+
var memberContent = _enumValueMapFromType(targetType);
20+
21+
if (memberContent == null) {
1922
return null;
2023
}
2124

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

24-
return '$expression$nullableLiteral.toString()'
25-
"$nullableLiteral.split('.')$nullableLiteral.last";
27+
return '${_constMapName(targetType)}[$expression]';
2628
}
2729

2830
@override
2931
String deserialize(
3032
DartType targetType, String expression, DeserializeContext context) {
31-
if (!isEnum(targetType)) {
33+
var memberContent = _enumValueMapFromType(targetType);
34+
35+
if (memberContent == null) {
3236
return null;
3337
}
3438

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

0 commit comments

Comments
 (0)