Skip to content

feature: allow customizing enum value serialization via annotations #251

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
Jun 29, 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
8 changes: 8 additions & 0 deletions json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.3.0

* Added `JsonValue` class for annotating `enum` fields with a custom
serialization value.

* Removed `$checkAllowedKeys`, `$enumDecode` and `$enumDecodeNullable` which are
no longer needed by the latest release of `package:json_serializable`.

## 0.2.9

* When `FormatException` is caught in "checked mode", use the `message`
Expand Down
11 changes: 0 additions & 11 deletions json_annotation/lib/src/allowed_keys_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,6 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// **DEPRECATED** Helper function used in generated code when
/// `JsonSerializable.disallowUnrecognizedKeys` is `true`.
///
/// Should not be used directly.
@Deprecated('Code generated with the latest `json_serializable` will use '
'`\$checkKeys` instead. This function will be removed in the next major '
'release.')
void $checkAllowedKeys(Map map, Iterable<String> allowedKeys) {
$checkKeys(map, allowedKeys: allowedKeys?.toList());
}

/// Helper function used in generated `fromJson` code when
/// `JsonSerializable.disallowUnrecognizedKeys` is true for an annotated type or
/// `JsonKey.required` is `true` for any annotated fields.
Expand Down
50 changes: 9 additions & 41 deletions json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,44 +194,12 @@ class JsonKey {
});
}

// Until enum supports parse: github.com/dart-lang/sdk/issues/33244
/// *Helper function used in generated code with `enum` values – should not be
/// used directly.*
///
/// Returns an enum instance corresponding to [enumValue] from the enum named
/// [enumName] with [values].
///
/// If [enumValue] is null or no corresponding values exists, an `ArgumentError`
/// is thrown.
///
/// Given an enum named `Example`, an invocation would look like
///
/// ```dart
/// $enumDecode('Example', Example.values, 'desiredValue')
/// ```
T $enumDecode<T>(String enumName, List<T> values, String enumValue) =>
values.singleWhere((e) => e.toString() == '$enumName.$enumValue',
orElse: () => throw new ArgumentError(
'`$enumValue` is not one of the supported values: '
'${values.map(_nameForEnumValue).join(', ')}'));

/// *Helper function used in generated code with `enum` values – should not be
/// used directly.*
///
/// Returns an enum instance corresponding to [enumValue] from the enum named
/// [enumName] with [values].
///
/// If [enumValue] is `null`, `null` is returned.
///
/// If no corresponding values exists, an `ArgumentError` is thrown.
///
/// Given an enum named `Example`, an invocation would look like
///
/// ```dart
/// $enumDecodeNullable('Example', Example.values, 'desiredValue')
/// ```
T $enumDecodeNullable<T>(String enumName, List<T> values, String enumValue) =>
enumValue == null ? null : $enumDecode(enumName, values, enumValue);

// Until enum has a name property: github.com/dart-lang/sdk/issues/21712
String _nameForEnumValue(Object value) => value.toString().split('.')[1];
/// An annotation used to specify how a enum value is serialized.
class JsonValue {
/// The value to use when serializing and deserializing.
///
/// Can be a [String] or an [int].
final dynamic value;

const JsonValue(this.value);
}
2 changes: 1 addition & 1 deletion json_annotation/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: json_annotation
version: 0.2.9
version: 0.3.0-dev
description: >-
Classes and helper functions that support JSON code generation via the
`json_serializable` package.
Expand Down
22 changes: 22 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## 0.6.0

* Now supports changing the serialized values of enums using `JsonValue`.

```dart
enum AutoApply {
none,
dependents,
@JsonValue('all_packages')
allPackages,
@JsonValue('root_package')
rootPackage
}
```

* `JsonSerializableGenerator.generateForAnnotatedElement` now returns
`Iterable<String>` instead of `String`.

* `SerializeContext` and `DeserializeContext` now have an `addMember` function
which allows `TypeHelper` instances to add additional members when handling
a field. This is useful for generating shared helpers, for instance.

## 0.5.8

* Small fixes to support Dart 2 runtime semantics.
Expand Down
4 changes: 3 additions & 1 deletion json_serializable/lib/src/helper_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ abstract class HelperCore {

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

void addMember(String memberContent);

String get targetClassReference =>
'${element.name}${genericClassArgumentsImpl(false)}';

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

TypeHelperContext getHelperContext(FieldElement field) =>
new TypeHelperContext(generator, field.metadata, jsonKeyFor(field));
new TypeHelperContext(this, field.metadata, jsonKeyFor(field));
}

InvalidGenerationSourceError createInvalidGenerationError(
Expand Down
12 changes: 5 additions & 7 deletions json_serializable/lib/src/json_key_with_conversion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,13 @@ JsonKeyWithConversion _from(
var defaultValueObject = obj.getField('defaultValue');

Object defaultValueLiteral;
if (isEnum(defaultValueObject.type)) {
var interfaceType = defaultValueObject.type as InterfaceType;
var allowedValues = interfaceType.element.fields
.where((p) => !p.isSynthetic)
.map((p) => p.name)
.toList();

var enumFields = iterateEnumFields(defaultValueObject.type);
if (enumFields != null) {
var allowedValues = enumFields.map((p) => p.name).toList();
var enumValueIndex = defaultValueObject.getField('index').toIntValue();
defaultValueLiteral =
'${interfaceType.name}.${allowedValues[enumValueIndex]}';
'${defaultValueObject.type.name}.${allowedValues[enumValueIndex]}';
} else {
defaultValueLiteral = _getLiteral(defaultValueObject, []);
if (defaultValueLiteral != null) {
Expand Down
2 changes: 1 addition & 1 deletion json_serializable/lib/src/json_literal_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ String jsonLiteralAsDart(dynamic value, bool asConst) {

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

if (value is Map) return jsonMapAsDart(value, asConst);
Expand Down
14 changes: 12 additions & 2 deletions json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class JsonSerializableGenerator
new List.unmodifiable(typeHelpers.followedBy(_defaultHelpers)));

@override
String generateForAnnotatedElement(
Iterable<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) {
if (element is! ClassElement) {
var name = element.name;
Expand All @@ -170,7 +170,7 @@ class JsonSerializableGenerator
var classElement = element as ClassElement;
var classAnnotation = valueForAnnotation(annotation);
var helper = new _GeneratorHelper(this, classElement, classAnnotation);
return helper._generate().join('\n\n');
return helper._generate();
}
}

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

final _addedMembers = new Set<String>();

@override
void addMember(String memberContent) {
_addedMembers.add(memberContent);
}

Iterable<String> _generate() sync* {
assert(_addedMembers.isEmpty);
var sortedFields = _createSortedFieldSet(element);

// Used to keep track of why a field is ignored. Useful for providing
Expand Down Expand Up @@ -223,6 +231,8 @@ class _GeneratorHelper extends HelperCore with EncodeHelper, DecodeHelper {
if (annotation.createToJson) {
yield* createToJson(accessibleFieldSet);
}

yield* _addedMembers;
}
}

Expand Down
8 changes: 8 additions & 0 deletions json_serializable/lib/src/type_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';

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

/// [expression] may be just the name of the field or it may an expression
/// representing the serialization of a value.
String serialize(DartType fieldType, String expression);
List<ElementAnnotation> get metadata;

/// Adds [memberContent] to the set of generated, top-level members.
void addMember(String memberContent);
}

abstract class DeserializeContext {
/// `true` if [deserialize] should handle the case of `expression` being null.
bool get nullable;
String deserialize(DartType fieldType, String expression);
List<ElementAnnotation> get metadata;

/// Adds [memberContent] to the set of generated, top-level members.
void addMember(String memberContent);
}

abstract class TypeHelper {
Expand Down
20 changes: 14 additions & 6 deletions json_serializable/lib/src/type_helper_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,38 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';

import 'helper_core.dart';
import 'json_key_with_conversion.dart';
import 'json_serializable_generator.dart';
import 'type_helper.dart';

class TypeHelperContext implements SerializeContext, DeserializeContext {
final HelperCore _helperCore;

@override
final List<ElementAnnotation> metadata;

final JsonSerializableGenerator _generator;
final JsonKeyWithConversion _key;

@override
bool get useWrappers => _generator.useWrappers;
bool get useWrappers => _helperCore.generator.useWrappers;

bool get anyMap => _generator.anyMap;
bool get anyMap => _helperCore.generator.anyMap;

bool get explicitToJson => _generator.explicitToJson;
bool get explicitToJson => _helperCore.generator.explicitToJson;

@override
bool get nullable => _key.nullable;

ConvertData get fromJsonData => _key.fromJsonData;
ConvertData get toJsonData => _key.toJsonData;

TypeHelperContext(this._generator, this.metadata, this._key);
TypeHelperContext(this._helperCore, this.metadata, this._key);

@override
void addMember(String memberContent) {
_helperCore.addMember(memberContent);
}

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

String _run(DartType targetType, String expression,
String invoke(TypeHelper instance)) =>
allHelpersImpl(_generator).map(invoke).firstWhere((r) => r != null,
allHelpersImpl(_helperCore.generator).map(invoke).firstWhere(
(r) => r != null,
orElse: () => throw new UnsupportedTypeError(
targetType, expression, _notSupportedWithTypeHelpersMsg));
}
Expand Down
66 changes: 58 additions & 8 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

import '../json_literal_generator.dart';
import '../type_helper.dart';
import '../utils.dart';

Expand All @@ -15,26 +16,75 @@ class EnumHelper extends TypeHelper {
@override
String serialize(
DartType targetType, String expression, SerializeContext context) {
if (!isEnum(targetType)) {
var memberContent = _enumValueMapFromType(targetType);

if (memberContent == null) {
return null;
}

var nullableLiteral = context.nullable ? '?' : '';
context.addMember(memberContent);

return '$expression$nullableLiteral.toString()'
"$nullableLiteral.split('.')$nullableLiteral.last";
return '${_constMapName(targetType)}[$expression]';
}

@override
String deserialize(
DartType targetType, String expression, DeserializeContext context) {
if (!isEnum(targetType)) {
var memberContent = _enumValueMapFromType(targetType);

if (memberContent == null) {
return null;
}

context.addMember(_enumDecodeHelper);
Copy link
Collaborator

Choose a reason for hiding this comment

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

So we will actually be adding one of these each time this helper is called right? Is that once per field using the enum or more than that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

once per field using the enum – but then they are collapsed.

The storage is a set, so it's only recorded once.

Copy link
Collaborator

Choose a reason for hiding this comment

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

kk


if (context.nullable) {
context.addMember(_enumDecodeHelperNullable);
}

context.addMember(memberContent);

var functionName =
context.nullable ? r'$enumDecodeNullable' : r'$enumDecode';
return "$functionName('$targetType', $targetType.values, "
'$expression as String)';
context.nullable ? r'_$enumDecodeNullable' : r'_$enumDecode';
return '$functionName(${_constMapName(targetType)}, '
'$expression)';
}
}

String _constMapName(DartType targetType) => '_\$${targetType.name}EnumMap';

String _enumValueMapFromType(DartType targetType) {
var enumMap = enumFieldsMap(targetType);

if (enumMap == null) {
return null;
}

var items = enumMap.entries.map((e) =>
' ${targetType.name}.${e.key.name}: ${jsonLiteralAsDart(e.value, false)}');

return 'const ${_constMapName(targetType)} = '
'const <${targetType.name}, dynamic>{\n${items.join(',\n')}\n};';
}

const _enumDecodeHelper = r'''
T _$enumDecode<T>(Map<T, dynamic> enumValues, dynamic source) {
if (source == null) {
throw new ArgumentError('A value must be provided. Supported values: '
'${enumValues.values.join(', ')}');
}
return enumValues.entries
.singleWhere((e) => e.value == source,
orElse: () => throw new ArgumentError(
'`$source` is not one of the supported values: '
'${enumValues.values.join(', ')}'))
.key;
}''';

const _enumDecodeHelperNullable = r'''
T _$enumDecodeNullable<T>(Map<T, dynamic> enumValues, dynamic source) {
if (source == null) {
return null;
}
return _$enumDecode<T>(enumValues, source);
}''';
Loading