Skip to content
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

Add aliases to JsonValue to enum value to be decoded from different JSON values #1459

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
33 changes: 28 additions & 5 deletions _test_yaml/test/src/build_config.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions json_annotation/lib/src/enum_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,53 @@ K? $enumDecodeNullable<K extends Enum, V>(
return unknownValue;
}

/// Returns the key associated with value [source] from [decodeMap], if one
/// exists.
///
/// If [unknownValue] is not `null` and [source] is not a value in [decodeMap],
/// [unknownValue] is returned. Otherwise, an [ArgumentError] is thrown.
///
/// If [source] is `null`, `null` is returned.
///
/// Exposed only for code generated by `package:json_serializable`.
/// Not meant to be used directly by user code.
V? $enumDecodeNullableWithDecodeMap<K, V extends Enum>(
Map<K, V> decodeMap,
Object? source, {
Enum? unknownValue,
}) {
if (source == null) {
return null;
}

final decodedValue = decodeMap[source];

if (decodedValue != null) {
return decodedValue;
}

if (unknownValue == JsonKey.nullForUndefinedEnumValue) {
return null;
}

if (unknownValue == null) {
throw ArgumentError(
'`$source` is not one of the supported values: '
'${decodeMap.keys.join(', ')}',
);
}

if (unknownValue is! V) {
throw ArgumentError.value(
unknownValue,
'unknownValue',
'Must by of type `$K` or `JsonKey.nullForUndefinedEnumValue`.',
);
}

return unknownValue;
}

/// Returns the key associated with value [source] from [enumValues], if one
/// exists.
///
Expand Down Expand Up @@ -88,3 +135,41 @@ K $enumDecode<K extends Enum, V>(

return unknownValue;
}

/// Returns the key associated with value [source] from [decodeMap], if one
/// exists.
///
/// If [unknownValue] is not `null` and [source] is not a value in [decodeMap],
/// [unknownValue] is returned. Otherwise, an [ArgumentError] is thrown.
///
/// If [source] is `null`, an [ArgumentError] is thrown.
///
/// Exposed only for code generated by `package:json_serializable`.
/// Not meant to be used directly by user code.
V $enumDecodeWithDecodeMap<K, V extends Enum>(
Map<K, V> decodeMap,
Object? source, {
V? unknownValue,
}) {
if (source == null) {
throw ArgumentError(
'A value must be provided. Supported values: '
'${decodeMap.keys.join(', ')}',
);
}

final decodedValue = decodeMap[source];

if (decodedValue != null) {
return decodedValue;
}

if (unknownValue == null) {
throw ArgumentError(
'`$source` is not one of the supported values: '
'${decodeMap.keys.join(', ')}',
);
}

return unknownValue;
}
7 changes: 6 additions & 1 deletion json_annotation/lib/src/json_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@ class JsonValue {
/// Can be a [String] or an [int].
final dynamic value;

const JsonValue(this.value);
/// Optional values that can be used when deserializing.
///
/// The elements of [aliases] must be either [String] or [int].
final Set<Object> aliases;

const JsonValue(this.value, {this.aliases = const {}});
}
109 changes: 100 additions & 9 deletions json_serializable/lib/src/enum_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import 'utils.dart';
String constMapName(DartType targetType) =>
'_\$${targetType.element!.name}EnumMap';

String constDecodeMapName(DartType targetType) =>
'_\$${targetType.element!.name}EnumDecodeMap';

/// If [targetType] is not an enum, return `null`.
///
/// Otherwise, returns `true` if [targetType] is nullable OR if one of the
Expand All @@ -31,21 +34,45 @@ bool? enumFieldWithNullInEncodeMap(DartType targetType) {
return enumMap.values.contains(null);
}

String? enumValueMapFromType(
String? enumMapsFromType(
DartType targetType, {
bool nullWithNoAnnotation = false,
}) {
final enumMap =
_enumMap(targetType, nullWithNoAnnotation: nullWithNoAnnotation);

if (enumMap == null) return null;

final items = enumMap.entries
.map((e) => ' ${targetType.element!.name}.${e.key.name}: '
'${jsonLiteralAsDart(e.value)},')
.join();

return 'const ${constMapName(targetType)} = {\n$items\n};';
final enumAliases =
_enumAliases(targetType, nullWithNoAnnotation: nullWithNoAnnotation);

final valuesItems = enumMap == null
? null
: [
for (final MapEntry(:key, :value) in enumMap.entries)
' ${targetType.element!.name}.${key.name}: '
'${jsonLiteralAsDart(value)},',
].join();

final valuesMap = valuesItems == null
? null
: '// ignore: unused_element\n'
'const ${constMapName(targetType)} = {\n$valuesItems\n};';

final decodeItems = enumAliases == null
? null
: [
for (final MapEntry(:key, :value) in enumAliases.entries)
' ${jsonLiteralAsDart(key)}: '
'${targetType.element!.name}.${value.name},',
].join();

final decodeMap = decodeItems == null
? null
: '// ignore: unused_element\n'
'const ${constDecodeMapName(targetType)} = {\n$decodeItems\n};';

return valuesMap == null && decodeMap == null
? null
: [valuesMap, decodeMap].join('\n\n');
}

Map<FieldElement, Object?>? _enumMap(
Expand Down Expand Up @@ -73,6 +100,34 @@ Map<FieldElement, Object?>? _enumMap(
};
}

Map<Object?, FieldElement>? _enumAliases(
DartType targetType, {
bool nullWithNoAnnotation = false,
}) {
final targetTypeElement = targetType.element;
if (targetTypeElement == null) return null;
final annotation = _jsonEnumChecker.firstAnnotationOf(targetTypeElement);
final jsonEnum = _fromAnnotation(annotation);

final enumFields = iterateEnumFields(targetType);

if (enumFields == null || (nullWithNoAnnotation && !jsonEnum.alwaysCreate)) {
return null;
}

return {
for (var field in enumFields) ...{
_generateEntry(
field: field,
jsonEnum: jsonEnum,
targetType: targetType,
): field,
for (var alias in _generateAliases(field: field, targetType: targetType))
alias: field,
},
};
}

Object? _generateEntry({
required FieldElement field,
required JsonEnum jsonEnum,
Expand Down Expand Up @@ -138,6 +193,36 @@ Object? _generateEntry({
}
}

List<Object?> _generateAliases({
required FieldElement field,
required DartType targetType,
}) {
final annotation =
const TypeChecker.fromRuntime(JsonValue).firstAnnotationOfExact(field);

if (annotation == null) {
return const [];
} else {
final reader = ConstantReader(annotation);

final valueReader = reader.read('aliases');

if (valueReader.validAliasesType) {
return [
for (final value in valueReader.setValue)
ConstantReader(value).literalValue,
];
} else {
final targetTypeCode = typeToCode(targetType);
throw InvalidGenerationSourceError(
'The `JsonValue` annotation on `$targetTypeCode.${field.name}` aliases '
'should all be of type String or int.',
element: field,
);
}
}
}

const _jsonEnumChecker = TypeChecker.fromRuntime(JsonEnum);

JsonEnum _fromAnnotation(DartObject? dartObject) {
Expand All @@ -154,4 +239,10 @@ JsonEnum _fromAnnotation(DartObject? dartObject) {

extension on ConstantReader {
bool get validValueType => isString || isNull || isInt;

bool get validAliasesType =>
isSet &&
setValue.every((element) =>
(element.type?.isDartCoreString ?? false) ||
(element.type?.isDartCoreInt ?? false));
}
2 changes: 1 addition & 1 deletion json_serializable/lib/src/json_enum_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class JsonEnumGenerator extends GeneratorForAnnotation<JsonEnum> {
}

final value =
enumValueMapFromType(element.thisType, nullWithNoAnnotation: true);
enumMapsFromType(element.thisType, nullWithNoAnnotation: true);

return [
if (value != null) value,
Expand Down
10 changes: 5 additions & 5 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
String expression,
TypeHelperContextWithConfig context,
) {
final memberContent = enumValueMapFromType(targetType);
final memberContent = enumMapsFromType(targetType);

if (memberContent == null) {
return null;
Expand All @@ -44,7 +44,7 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
TypeHelperContextWithConfig context,
bool defaultProvided,
) {
final memberContent = enumValueMapFromType(targetType);
final memberContent = enumMapsFromType(targetType);

if (memberContent == null) {
return null;
Expand All @@ -64,15 +64,15 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {

String functionName;
if (targetType.isNullableType || defaultProvided) {
functionName = r'$enumDecodeNullable';
functionName = r'$enumDecodeNullableWithDecodeMap';
} else {
functionName = r'$enumDecode';
functionName = r'$enumDecodeWithDecodeMap';
}

context.addMember(memberContent);

final args = [
constMapName(targetType),
constDecodeMapName(targetType),
expression,
if (jsonKey.unknownEnumValue != null)
'unknownValue: ${jsonKey.unknownEnumValue}',
Expand Down
Loading