Skip to content

Commit

Permalink
Support more non-String Map keys of obvious dart:core types (#493)
Browse files Browse the repository at this point in the history
Partially addresses #396
  • Loading branch information
kevmoo committed May 29, 2019
1 parent 4d33182 commit a192130
Show file tree
Hide file tree
Showing 15 changed files with 594 additions and 20 deletions.
4 changes: 4 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.1.0

- Support `Map` keys of type `int`, `BigInt`, `DateTime`, and `Uri`.

## 3.0.0

This release is entirely **BREAKING** changes. It removes underused features
Expand Down
50 changes: 44 additions & 6 deletions json_serializable/lib/src/type_helpers/map_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../constants.dart';
import '../shared_checkers.dart';
import '../type_helper.dart';
import '../utils.dart';
import 'to_from_string.dart';

const _keyParam = 'k';

Expand All @@ -29,7 +30,9 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
_checkSafeKeyType(expression, keyType);

final subFieldValue = context.serialize(valueType, closureArg);
final subKeyValue = context.serialize(keyType, _keyParam);
final subKeyValue =
_forType(keyType)?.serialize(keyType, _keyParam, false) ??
context.serialize(keyType, _keyParam);

if (closureArg == subFieldValue && _keyParam == subKeyValue) {
return expression;
Expand All @@ -56,9 +59,9 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
_checkSafeKeyType(expression, keyArg);

final valueArgIsAny = _isObjectOrDynamic(valueArg);
final isEnumKey = isEnum(keyArg);
final isKeyStringable = _isKeyStringable(keyArg);

if (!isEnumKey) {
if (!isKeyStringable) {
if (valueArgIsAny) {
if (context.config.anyMap) {
if (_isObjectOrDynamic(keyArg)) {
Expand Down Expand Up @@ -90,30 +93,65 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
context.config.anyMap ? 'as Map' : 'as Map<String, dynamic>';

String keyUsage;
if (isEnumKey) {
if (isEnum(keyArg)) {
keyUsage = context.deserialize(keyArg, _keyParam).toString();
} else if (context.config.anyMap && !_isObjectOrDynamic(keyArg)) {
keyUsage = '$_keyParam as String';
} else {
keyUsage = _keyParam;
}

final toFromString = _forType(keyArg);
if (toFromString != null) {
keyUsage = toFromString.deserialize(keyArg, keyUsage, false, true);
}

return '($expression $mapCast)$optionalQuestion.map('
'($_keyParam, $closureArg) => MapEntry($keyUsage, $itemSubVal),)';
}
}

final _intString = ToFromStringHelper('int.parse', 'toString()', 'int');

/// [ToFromStringHelper] instances representing non-String types that can
/// be used as [Map] keys.
final _instances = [
bigIntString,
dateTimeString,
_intString,
uriString,
];

ToFromStringHelper _forType(DartType type) =>
_instances.singleWhere((i) => i.matches(type), orElse: () => null);

bool _isObjectOrDynamic(DartType type) => type.isObject || type.isDynamic;

/// Returns `true` if [keyType] can be automatically converted to/from String –
/// and is therefor usable as a key in a [Map].
bool _isKeyStringable(DartType keyType) =>
isEnum(keyType) || _instances.any((inst) => inst.matches(keyType));

void _checkSafeKeyType(String expression, DartType keyArg) {
// We're not going to handle converting key types at the moment
// So the only safe types for key are dynamic/Object/String/enum
final safeKey = _isObjectOrDynamic(keyArg) ||
coreStringTypeChecker.isExactlyType(keyArg) ||
isEnum(keyArg);
_isKeyStringable(keyArg);

if (!safeKey) {
throw UnsupportedTypeError(keyArg, expression,
'Map keys must be of type `String`, enum, `Object` or `dynamic`.');
'Map keys must be one of: ${_allowedTypeNames.join(', ')}.');
}
}

/// The names of types that can be used as [Map] keys.
///
/// Used in [_checkSafeKeyType] to provide a helpful error with unsupported
/// types.
Iterable<String> get _allowedTypeNames => const [
'Object',
'dynamic',
'enum',
'String',
].followedBy(_instances.map((i) => i.coreTypeName));
16 changes: 9 additions & 7 deletions json_serializable/lib/src/type_helpers/to_from_string.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@ import 'package:source_gen/source_gen.dart';

import '../type_helper.dart';

const bigIntString = ToFromStringHelper(
final bigIntString = ToFromStringHelper(
'BigInt.parse',
'toString()',
TypeChecker.fromUrl('dart:core#BigInt'),
'BigInt',
);

const dateTimeString = ToFromStringHelper(
final dateTimeString = ToFromStringHelper(
'DateTime.parse',
'toIso8601String()',
TypeChecker.fromUrl('dart:core#DateTime'),
'DateTime',
);

const uriString = ToFromStringHelper(
final uriString = ToFromStringHelper(
'Uri.parse',
'toString()',
TypeChecker.fromUrl('dart:core#Uri'),
'Uri',
);

/// Package-internal helper that unifies implementations of [Type]s that convert
Expand All @@ -40,9 +40,11 @@ class ToFromStringHelper {
///
/// Examples: `toString()` for a function or `stringValue` for a property.
final String _toString;
final String coreTypeName;
final TypeChecker _checker;

const ToFromStringHelper(this._parse, this._toString, this._checker);
ToFromStringHelper(this._parse, this._toString, this.coreTypeName)
: _checker = TypeChecker.fromUrl('dart:core#$coreTypeName');

bool matches(DartType type) => _checker.isExactlyType(type);

Expand Down
2 changes: 1 addition & 1 deletion json_serializable/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: json_serializable
version: 3.0.0
version: 3.1.0-dev
author: Dart Team <misc@dartlang.org>
description: >-
Automatically generate code for converting to and from JSON by annotating
Expand Down
13 changes: 13 additions & 0 deletions json_serializable/test/integration/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,17 @@ void main() {
expect(() => Numbers.fromJson(value), throwsCastError);
});
});

test('MapKeyVariety', () {
final instance = MapKeyVariety()
..bigIntMap = {BigInt.from(1): 1}
..dateTimeIntMap = {DateTime.parse('2018-01-01'): 2}
..intIntMap = {3: 3}
..uriIntMap = {Uri.parse('https://example.com'): 4};

final roundTrip =
roundTripObject(instance, (j) => MapKeyVariety.fromJson(j));

expect(roundTrip, instance);
});
}
24 changes: 24 additions & 0 deletions json_serializable/test/integration/json_test_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import 'dart:collection';

import 'package:json_annotation/json_annotation.dart';

import 'json_test_common.dart';

part 'json_test_example.g.dart';
Expand Down Expand Up @@ -146,3 +147,26 @@ class Numbers {
deepEquals(duration, other.duration) &&
deepEquals(date, other.date);
}

@JsonSerializable()
class MapKeyVariety {
Map<int, int> intIntMap;
Map<Uri, int> uriIntMap;
Map<DateTime, int> dateTimeIntMap;
Map<BigInt, int> bigIntMap;

MapKeyVariety();

factory MapKeyVariety.fromJson(Map<String, dynamic> json) =>
_$MapKeyVarietyFromJson(json);

Map<String, dynamic> toJson() => _$MapKeyVarietyToJson(this);

@override
bool operator ==(Object other) =>
other is MapKeyVariety &&
deepEquals(other.intIntMap, intIntMap) &&
deepEquals(other.uriIntMap, uriIntMap) &&
deepEquals(other.dateTimeIntMap, dateTimeIntMap) &&
deepEquals(other.bigIntMap, bigIntMap);
}
25 changes: 25 additions & 0 deletions json_serializable/test/integration/json_test_example.g.dart

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

0 comments on commit a192130

Please sign in to comment.