Skip to content

Commit a32605e

Browse files
committed
Implement aliases
1 parent bc8fcb9 commit a32605e

23 files changed

+821
-168
lines changed

json_annotation/lib/src/enum_helpers.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,53 @@ K? $enumDecodeNullable<K extends Enum, V>(
5151
return unknownValue;
5252
}
5353

54+
/// Returns the key associated with value [source] from [decodeMap], if one
55+
/// exists.
56+
///
57+
/// If [unknownValue] is not `null` and [source] is not a value in [decodeMap],
58+
/// [unknownValue] is returned. Otherwise, an [ArgumentError] is thrown.
59+
///
60+
/// If [source] is `null`, `null` is returned.
61+
///
62+
/// Exposed only for code generated by `package:json_serializable`.
63+
/// Not meant to be used directly by user code.
64+
V? $enumDecodeNullableWithDecodeMap<K, V extends Enum>(
65+
Map<K, V> decodeMap,
66+
Object? source, {
67+
Enum? unknownValue,
68+
}) {
69+
if (source == null) {
70+
return null;
71+
}
72+
73+
final decodedValue = decodeMap[source];
74+
75+
if (decodedValue != null) {
76+
return decodedValue;
77+
}
78+
79+
if (unknownValue == JsonKey.nullForUndefinedEnumValue) {
80+
return null;
81+
}
82+
83+
if (unknownValue == null) {
84+
throw ArgumentError(
85+
'`$source` is not one of the supported values: '
86+
'${decodeMap.keys.join(', ')}',
87+
);
88+
}
89+
90+
if (unknownValue is! V) {
91+
throw ArgumentError.value(
92+
unknownValue,
93+
'unknownValue',
94+
'Must by of type `$K` or `JsonKey.nullForUndefinedEnumValue`.',
95+
);
96+
}
97+
98+
return unknownValue;
99+
}
100+
54101
/// Returns the key associated with value [source] from [enumValues], if one
55102
/// exists.
56103
///
@@ -88,3 +135,41 @@ K $enumDecode<K extends Enum, V>(
88135

89136
return unknownValue;
90137
}
138+
139+
/// Returns the key associated with value [source] from [decodeMap], if one
140+
/// exists.
141+
///
142+
/// If [unknownValue] is not `null` and [source] is not a value in [decodeMap],
143+
/// [unknownValue] is returned. Otherwise, an [ArgumentError] is thrown.
144+
///
145+
/// If [source] is `null`, an [ArgumentError] is thrown.
146+
///
147+
/// Exposed only for code generated by `package:json_serializable`.
148+
/// Not meant to be used directly by user code.
149+
V $enumDecodeWithDecodeMap<K, V extends Enum>(
150+
Map<K, V> decodeMap,
151+
Object? source, {
152+
V? unknownValue,
153+
}) {
154+
if (source == null) {
155+
throw ArgumentError(
156+
'A value must be provided. Supported values: '
157+
'${decodeMap.keys.join(', ')}',
158+
);
159+
}
160+
161+
final decodedValue = decodeMap[source];
162+
163+
if (decodedValue != null) {
164+
return decodedValue;
165+
}
166+
167+
if (unknownValue == null) {
168+
throw ArgumentError(
169+
'`$source` is not one of the supported values: '
170+
'${decodeMap.keys.join(', ')}',
171+
);
172+
}
173+
174+
return unknownValue;
175+
}

json_annotation/lib/src/json_value.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@ class JsonValue {
99
/// Can be a [String] or an [int].
1010
final dynamic value;
1111

12-
const JsonValue(this.value);
12+
/// Optional values that can be used when deserializing.
13+
///
14+
/// The elements of [aliases] must be either [String] or [int].
15+
final Set<Object> aliases;
16+
17+
const JsonValue(this.value, {this.aliases = const {}});
1318
}

json_serializable/lib/src/enum_utils.dart

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import 'utils.dart';
1515
String constMapName(DartType targetType) =>
1616
'_\$${targetType.element!.name}EnumMap';
1717

18+
String constDecodeMapName(DartType targetType) =>
19+
'_\$${targetType.element!.name}EnumDecodeMap';
20+
1821
/// If [targetType] is not an enum, return `null`.
1922
///
2023
/// Otherwise, returns `true` if [targetType] is nullable OR if one of the
@@ -40,17 +43,40 @@ String? enumValueMapFromType(
4043
nullWithNoAnnotation: nullWithNoAnnotation,
4144
);
4245

43-
if (enumMap == null) return null;
46+
final enumAliases = _enumAliases(
47+
targetType,
48+
nullWithNoAnnotation: nullWithNoAnnotation,
49+
);
4450

45-
final items = enumMap.entries
51+
final valuesItems = enumMap?.entries
4652
.map(
4753
(e) =>
48-
' ${targetType.element!.name}.${e.key.name3}: '
54+
' ${targetType.element3!.name3}.${e.key.name3}: '
4955
'${jsonLiteralAsDart(e.value)},',
5056
)
5157
.join();
5258

53-
return 'const ${constMapName(targetType)} = {\n$items\n};';
59+
final valuesMap = valuesItems == null
60+
? null
61+
: '// ignore: unused_element\n'
62+
'const ${constMapName(targetType)} = {\n$valuesItems\n};';
63+
64+
final decodeItems = enumAliases?.entries
65+
.map(
66+
(e) =>
67+
' ${jsonLiteralAsDart(e.key)}: '
68+
'${targetType.element!.name}.${e.value.name3},',
69+
)
70+
.join();
71+
72+
final decodeMap = decodeItems == null
73+
? null
74+
: '// ignore: unused_element\n'
75+
'const ${constDecodeMapName(targetType)} = {\n$decodeItems\n};';
76+
77+
return valuesMap == null && decodeMap == null
78+
? null
79+
: [valuesMap, decodeMap].join('\n\n');
5480
}
5581

5682
Map<FieldElement2, Object?>? _enumMap(
@@ -78,6 +104,31 @@ Map<FieldElement2, Object?>? _enumMap(
78104
};
79105
}
80106

107+
Map<Object?, FieldElement2>? _enumAliases(
108+
DartType targetType, {
109+
bool nullWithNoAnnotation = false,
110+
}) {
111+
final targetTypeElement = targetType.element;
112+
if (targetTypeElement == null) return null;
113+
final annotation = _jsonEnumChecker.firstAnnotationOf(targetTypeElement);
114+
final jsonEnum = _fromAnnotation(annotation);
115+
116+
final enumFields = iterateEnumFields(targetType);
117+
118+
if (enumFields == null || (nullWithNoAnnotation && !jsonEnum.alwaysCreate)) {
119+
return null;
120+
}
121+
122+
return {
123+
for (var field in enumFields) ...{
124+
_generateEntry(field: field, jsonEnum: jsonEnum, targetType: targetType):
125+
field,
126+
for (var alias in _generateAliases(field: field, targetType: targetType))
127+
alias: field,
128+
},
129+
};
130+
}
131+
81132
Object? _generateEntry({
82133
required FieldElement2 field,
83134
required JsonEnum jsonEnum,
@@ -144,6 +195,37 @@ Object? _generateEntry({
144195
}
145196
}
146197

198+
List<Object?> _generateAliases({
199+
required FieldElement2 field,
200+
required DartType targetType,
201+
}) {
202+
final annotation = const TypeChecker.fromRuntime(
203+
JsonValue,
204+
).firstAnnotationOfExact(field);
205+
206+
if (annotation == null) {
207+
return const [];
208+
} else {
209+
final reader = ConstantReader(annotation);
210+
211+
final valueReader = reader.read('aliases');
212+
213+
if (valueReader.validAliasesType) {
214+
return [
215+
for (final value in valueReader.setValue)
216+
ConstantReader(value).literalValue,
217+
];
218+
} else {
219+
final targetTypeCode = typeToCode(targetType);
220+
throw InvalidGenerationSourceError(
221+
'The `JsonValue` annotation on `$targetTypeCode.${field.name3}` '
222+
'aliases should all be of type String or int.',
223+
element: field,
224+
);
225+
}
226+
}
227+
}
228+
147229
const _jsonEnumChecker = TypeChecker.fromRuntime(JsonEnum);
148230

149231
JsonEnum _fromAnnotation(DartObject? dartObject) {
@@ -160,4 +242,12 @@ JsonEnum _fromAnnotation(DartObject? dartObject) {
160242

161243
extension on ConstantReader {
162244
bool get validValueType => isString || isNull || isInt;
245+
246+
bool get validAliasesType =>
247+
isSet &&
248+
setValue.every(
249+
(element) =>
250+
(element.type?.isDartCoreString ?? false) ||
251+
(element.type?.isDartCoreInt ?? false),
252+
);
163253
}

json_serializable/lib/src/type_helpers/enum_helper.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ class EnumHelper extends TypeHelper<TypeHelperContextWithConfig> {
6464

6565
String functionName;
6666
if (targetType.isNullableType || defaultProvided) {
67-
functionName = r'$enumDecodeNullable';
67+
functionName = r'$enumDecodeNullableWithDecodeMap';
6868
} else {
69-
functionName = r'$enumDecode';
69+
functionName = r'$enumDecodeWithDecodeMap';
7070
}
7171

7272
context.addMember(memberContent);
7373

7474
final args = [
75-
constMapName(targetType),
75+
constDecodeMapName(targetType),
7676
expression,
7777
if (jsonKey.unknownEnumValue != null)
7878
'unknownValue: ${jsonKey.unknownEnumValue}',

json_serializable/test/default_value/default_value.g.dart

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

json_serializable/test/default_value/default_value.g_any_map__checked.g.dart

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

json_serializable/test/default_value/implicit_default_value.g.dart

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

json_serializable/test/integration/converter_examples.g.dart

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)