@@ -11,10 +11,13 @@ import 'package:source_gen/source_gen.dart';
1111import 'package:source_helper/source_helper.dart' ;
1212
1313import '../default_container.dart' ;
14+ import '../shared_checkers.dart' ;
1415import '../type_helper.dart' ;
16+ import '../unsupported_type_error.dart' ;
1517import '../utils.dart' ;
1618import 'config_types.dart' ;
1719import 'generic_factory_helper.dart' ;
20+ import 'to_from_string.dart' ;
1821
1922const _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
139146List <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) {
290321MethodElement ? _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));
0 commit comments