Skip to content

Support more non-String Map keys of obvious dart:core types #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>
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.

Loading