@@ -11,10 +11,13 @@ import 'package:source_gen/source_gen.dart';
11
11
import 'package:source_helper/source_helper.dart' ;
12
12
13
13
import '../default_container.dart' ;
14
+ import '../shared_checkers.dart' ;
14
15
import '../type_helper.dart' ;
16
+ import '../unsupported_type_error.dart' ;
15
17
import '../utils.dart' ;
16
18
import 'config_types.dart' ;
17
19
import 'generic_factory_helper.dart' ;
20
+ import 'to_from_string.dart' ;
18
21
19
22
const _helperLambdaParam = 'value' ;
20
23
@@ -49,11 +52,13 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
49
52
50
53
toJsonArgs.addAll (
51
54
_helperParams (
55
+ context,
52
56
context.serialize,
53
57
_encodeHelper,
54
58
interfaceType,
55
59
toJson.parameters.where ((element) => element.isRequiredPositional),
56
60
toJson,
61
+ isSerializing: true ,
57
62
),
58
63
);
59
64
}
@@ -109,11 +114,13 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
109
114
final args = [
110
115
output,
111
116
..._helperParams (
117
+ context,
112
118
context.deserialize,
113
119
_decodeHelper,
114
120
targetType,
115
121
positionalParams.skip (1 ),
116
122
fromJsonCtor,
123
+ isSerializing: false ,
117
124
),
118
125
];
119
126
@@ -137,13 +144,16 @@ class JsonHelper extends TypeHelper<TypeHelperContextWithConfig> {
137
144
}
138
145
139
146
List <String > _helperParams (
147
+ TypeHelperContextWithConfig context,
140
148
Object ? Function (DartType , String ) execute,
141
- TypeParameterType Function (ParameterElement , Element ) paramMapper,
149
+ TypeParameterTypeWithKeyHelper Function (ParameterElement , Element )
150
+ paramMapper,
142
151
InterfaceType type,
143
152
Iterable <ParameterElement > positionalParams,
144
- Element targetElement,
145
- ) {
146
- final rest = < TypeParameterType > [];
153
+ Element targetElement, {
154
+ required bool isSerializing,
155
+ }) {
156
+ final rest = < TypeParameterTypeWithKeyHelper > [];
147
157
for (var param in positionalParams) {
148
158
rest.add (paramMapper (param, targetElement));
149
159
}
@@ -152,18 +162,26 @@ List<String> _helperParams(
152
162
153
163
for (var helperArg in rest) {
154
164
final typeParamIndex =
155
- type.element.typeParameters.indexOf (helperArg.element);
165
+ type.element.typeParameters.indexOf (helperArg.type. element);
156
166
157
167
// TODO: throw here if `typeParamIndex` is -1 ?
158
168
final typeArg = type.typeArguments[typeParamIndex];
159
169
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
+ }
161
179
}
162
180
163
181
return args;
164
182
}
165
183
166
- TypeParameterType _decodeHelper (
184
+ TypeParameterTypeWithKeyHelper _decodeHelper (
167
185
ParameterElement param,
168
186
Element targetElement,
169
187
) {
@@ -178,8 +196,11 @@ TypeParameterType _decodeHelper(
178
196
final funcParamType = type.normalParameterTypes.single;
179
197
180
198
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);
183
204
}
184
205
}
185
206
}
@@ -194,20 +215,30 @@ TypeParameterType _decodeHelper(
194
215
);
195
216
}
196
217
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 (
198
226
ParameterElement param,
199
227
Element targetElement,
200
228
) {
201
229
final type = param.type;
202
230
203
231
if (type is FunctionType &&
204
- (type.returnType.isDartCoreObject || type.returnType.isDynamic) &&
232
+ (type.returnType.isDartCoreObject ||
233
+ type.returnType.isDynamic ||
234
+ type.returnType.isDartCoreString) &&
205
235
type.normalParameterTypes.length == 1 ) {
206
236
final funcParamType = type.normalParameterTypes.single;
207
237
208
238
if (param.name == toJsonForName (funcParamType.element! .name! )) {
209
239
if (funcParamType is TypeParameterType ) {
210
- return funcParamType;
240
+ return TypeParameterTypeWithKeyHelper (
241
+ funcParamType, type.returnType.isDartCoreString);
211
242
}
212
243
}
213
244
}
@@ -290,3 +321,118 @@ ClassConfig? _annotation(ClassConfig config, InterfaceType source) {
290
321
MethodElement ? _toJsonMethod (DartType type) => type.typeImplementations
291
322
.map ((dt) => dt is InterfaceType ? dt.getMethod ('toJson' ) : null )
292
323
.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));
0 commit comments