Skip to content

Commit 989ddc3

Browse files
committed
Custom Map Types
Custom Map Types
1 parent a36f266 commit 989ddc3

File tree

3 files changed

+202
-12
lines changed

3 files changed

+202
-12
lines changed

json_serializable/lib/src/type_helpers/json_helper.dart

+158-12
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ import 'package:source_gen/source_gen.dart';
1111
import 'package:source_helper/source_helper.dart';
1212

1313
import '../default_container.dart';
14+
import '../shared_checkers.dart';
1415
import '../type_helper.dart';
16+
import '../unsupported_type_error.dart';
1517
import '../utils.dart';
1618
import 'config_types.dart';
1719
import 'generic_factory_helper.dart';
20+
import 'to_from_string.dart';
1821

1922
const _helperLambdaParam = 'value';
2023

@@ -49,11 +52,13 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
4952

5053
toJsonArgs.addAll(
5154
_helperParams(
55+
context,
5256
context.serialize,
5357
_encodeHelper,
5458
interfaceType,
5559
toJson.parameters.where((element) => element.isRequiredPositional),
5660
toJson,
61+
isSerializing: true,
5762
),
5863
);
5964
}
@@ -109,11 +114,13 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
109114
final args = [
110115
output,
111116
..._helperParams(
117+
context,
112118
context.deserialize,
113119
_decodeHelper,
114120
targetType,
115121
positionalParams.skip(1),
116122
fromJsonCtor,
123+
isSerializing: false,
117124
),
118125
];
119126

@@ -137,13 +144,16 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
137144
}
138145

139146
List<String> _helperParams(
147+
TypeHelperContextWithConfig context,
140148
Object? Function(DartType, String) execute,
141-
TypeParameterType Function(ParameterElement, Element) paramMapper,
149+
TypeParameterTypeWithKeyHelper Function(ParameterElement, Element)
150+
paramMapper,
142151
InterfaceType type,
143152
Iterable<ParameterElement> positionalParams,
144-
Element targetElement,
145-
) {
146-
final rest = <TypeParameterType>[];
153+
Element targetElement, {
154+
required bool isSerializing,
155+
}) {
156+
final rest = <TypeParameterTypeWithKeyHelper>[];
147157
for (var param in positionalParams) {
148158
rest.add(paramMapper(param, targetElement));
149159
}
@@ -152,18 +162,26 @@ List<String> _helperParams(
152162

153163
for (var helperArg in rest) {
154164
final typeParamIndex =
155-
type.element.typeParameters.indexOf(helperArg.element);
165+
type.element.typeParameters.indexOf(helperArg.type.element);
156166

157167
// TODO: throw here if `typeParamIndex` is -1 ?
158168
final typeArg = type.typeArguments[typeParamIndex];
159169
final body = execute(typeArg, _helperLambdaParam);
160-
args.add('($_helperLambdaParam) => $body');
170+
if (helperArg.isJsonKey) {
171+
const keyHelper = MapKeyHelper();
172+
final newBody = isSerializing
173+
? keyHelper.serialize(typeArg, '', context)
174+
: keyHelper.deserialize(typeArg, '', context, false);
175+
args.add('($_helperLambdaParam) => $newBody');
176+
} else {
177+
args.add('($_helperLambdaParam) => $body');
178+
}
161179
}
162180

163181
return args;
164182
}
165183

166-
TypeParameterType _decodeHelper(
184+
TypeParameterTypeWithKeyHelper _decodeHelper(
167185
ParameterElement param,
168186
Element targetElement,
169187
) {
@@ -178,8 +196,11 @@ TypeParameterType _decodeHelper(
178196
final funcParamType = type.normalParameterTypes.single;
179197

180198
if ((funcParamType.isDartCoreObject && funcParamType.isNullableType) ||
181-
funcParamType.isDynamic) {
182-
return funcReturnType as TypeParameterType;
199+
funcParamType.isDynamic ||
200+
funcParamType.isDartCoreString) {
201+
return TypeParameterTypeWithKeyHelper(
202+
funcReturnType as TypeParameterType,
203+
funcParamType.isDartCoreString);
183204
}
184205
}
185206
}
@@ -194,20 +215,30 @@ TypeParameterType _decodeHelper(
194215
);
195216
}
196217

197-
TypeParameterType _encodeHelper(
218+
class TypeParameterTypeWithKeyHelper {
219+
final TypeParameterType type;
220+
final bool isJsonKey;
221+
222+
TypeParameterTypeWithKeyHelper(this.type, this.isJsonKey);
223+
}
224+
225+
TypeParameterTypeWithKeyHelper _encodeHelper(
198226
ParameterElement param,
199227
Element targetElement,
200228
) {
201229
final type = param.type;
202230

203231
if (type is FunctionType &&
204-
(type.returnType.isDartCoreObject || type.returnType.isDynamic) &&
232+
(type.returnType.isDartCoreObject ||
233+
type.returnType.isDynamic ||
234+
type.returnType.isDartCoreString) &&
205235
type.normalParameterTypes.length == 1) {
206236
final funcParamType = type.normalParameterTypes.single;
207237

208238
if (param.name == toJsonForName(funcParamType.element!.name!)) {
209239
if (funcParamType is TypeParameterType) {
210-
return funcParamType;
240+
return TypeParameterTypeWithKeyHelper(
241+
funcParamType, type.returnType.isDartCoreString);
211242
}
212243
}
213244
}
@@ -290,3 +321,118 @@ ClassConfig? _annotation(ClassConfig config, InterfaceType source) {
290321
MethodElement? _toJsonMethod(DartType type) => type.typeImplementations
291322
.map((dt) => dt is InterfaceType ? dt.getMethod('toJson') : null)
292323
.firstWhereOrNull((me) => me != null);
324+
325+
class MapKeyHelper extends TypeHelper<TypeHelperContextWithConfig> {
326+
const MapKeyHelper();
327+
328+
@override
329+
String? serialize(
330+
DartType targetType,
331+
String expression,
332+
TypeHelperContextWithConfig context,
333+
) {
334+
final keyType = targetType;
335+
336+
_checkSafeKeyType(expression, keyType);
337+
338+
final subKeyValue =
339+
_forType(keyType)?.serialize(keyType, _helperLambdaParam, false) ??
340+
context.serialize(keyType, _helperLambdaParam);
341+
342+
if (_helperLambdaParam == subKeyValue) {
343+
return expression;
344+
}
345+
346+
return '$subKeyValue';
347+
}
348+
349+
@override
350+
String? deserialize(
351+
DartType targetType,
352+
String expression,
353+
TypeHelperContextWithConfig context,
354+
bool defaultProvided,
355+
) {
356+
final keyArg = targetType;
357+
358+
_checkSafeKeyType(expression, keyArg);
359+
360+
final isKeyStringable = _isKeyStringable(keyArg);
361+
if (!isKeyStringable) {
362+
throw UnsupportedTypeError(
363+
keyArg,
364+
expression,
365+
'Map keys must be one of: ${_allowedTypeNames.join(', ')}.',
366+
);
367+
}
368+
369+
String keyUsage;
370+
if (keyArg.isEnum) {
371+
keyUsage = context.deserialize(keyArg, _helperLambdaParam).toString();
372+
} else if (context.config.anyMap &&
373+
!(keyArg.isDartCoreObject || keyArg.isDynamic)) {
374+
keyUsage = '$_helperLambdaParam as String';
375+
} else if (context.config.anyMap &&
376+
keyArg.isDartCoreObject &&
377+
!keyArg.isNullableType) {
378+
keyUsage = '$_helperLambdaParam as Object';
379+
} else {
380+
keyUsage = '$_helperLambdaParam as String';
381+
}
382+
383+
final toFromString = _forType(keyArg);
384+
if (toFromString != null) {
385+
keyUsage = toFromString.deserialize(keyArg, keyUsage, false, true)!;
386+
}
387+
388+
return keyUsage;
389+
}
390+
}
391+
392+
final _intString = ToFromStringHelper('int.parse', 'toString()', 'int');
393+
394+
/// [ToFromStringHelper] instances representing non-String types that can
395+
/// be used as [Map] keys.
396+
final _instances = [
397+
bigIntString,
398+
dateTimeString,
399+
_intString,
400+
uriString,
401+
];
402+
403+
ToFromStringHelper? _forType(DartType type) =>
404+
_instances.singleWhereOrNull((i) => i.matches(type));
405+
406+
/// Returns `true` if [keyType] can be automatically converted to/from String –
407+
/// and is therefor usable as a key in a [Map].
408+
bool _isKeyStringable(DartType keyType) =>
409+
keyType.isEnum || _instances.any((inst) => inst.matches(keyType));
410+
411+
void _checkSafeKeyType(String expression, DartType keyArg) {
412+
// We're not going to handle converting key types at the moment
413+
// So the only safe types for key are dynamic/Object/String/enum
414+
if (keyArg.isDynamic ||
415+
(!keyArg.isNullableType &&
416+
(keyArg.isDartCoreObject ||
417+
coreStringTypeChecker.isExactlyType(keyArg) ||
418+
_isKeyStringable(keyArg)))) {
419+
return;
420+
}
421+
422+
throw UnsupportedTypeError(
423+
keyArg,
424+
expression,
425+
'Map keys must be one of: ${_allowedTypeNames.join(', ')}.',
426+
);
427+
}
428+
429+
/// The names of types that can be used as [Map] keys.
430+
///
431+
/// Used in [_checkSafeKeyType] to provide a helpful error with unsupported
432+
/// types.
433+
Iterable<String> get _allowedTypeNames => const [
434+
'Object',
435+
'dynamic',
436+
'enum',
437+
'String',
438+
].followedBy(_instances.map((i) => i.coreTypeName));

json_serializable/test/generic_files/generic_argument_factories.dart

+30
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,33 @@ class ConcreteClass {
5050

5151
Map<String, dynamic> toJson() => _$ConcreteClassToJson(this);
5252
}
53+
54+
class CustomMap<K, V> {
55+
final Map<K, V> map;
56+
57+
CustomMap(this.map);
58+
59+
factory CustomMap.fromJson(
60+
Map<String, dynamic> json,
61+
K Function(String?) fromJsonK,
62+
V Function(Object?) fromJsonV,
63+
) =>
64+
CustomMap(json.map<K, V>(
65+
(key, value) => MapEntry(fromJsonK(key), fromJsonV(value))));
66+
67+
Map<String?, dynamic> toJson(
68+
String? Function(K) toJsonK, Object? Function(V) toJsonV) =>
69+
map.map((key, value) => MapEntry(toJsonK(key), toJsonV(value)));
70+
}
71+
72+
@JsonSerializable()
73+
class UseOfCustomMap {
74+
final CustomMap<int, String> map;
75+
76+
UseOfCustomMap(this.map);
77+
78+
factory UseOfCustomMap.fromJson(Map<String, dynamic> json) =>
79+
_$UseOfCustomMapFromJson(json);
80+
81+
Map<String, dynamic> toJson() => _$UseOfCustomMapToJson(this);
82+
}

json_serializable/test/generic_files/generic_argument_factories.g.dart

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)