Skip to content

Commit

Permalink
Support more non-String Map keys of obvious dart:core types
Browse files Browse the repository at this point in the history
Partially addresses #396
  • Loading branch information
kevmoo committed May 24, 2019
1 parent 2dceb1b commit de7cf91
Show file tree
Hide file tree
Showing 15 changed files with 589 additions and 32 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
45 changes: 38 additions & 7 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 @@ -28,15 +29,17 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {

_checkSafeKeyType(expression, keyType);

final toFromString = _forType(keyType);

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

if (closureArg == subFieldValue && _keyParam == subKeyValue) {
return expression;
}

final optionalQuestion = context.nullable ? '?' : '';

return '$expression$optionalQuestion'
'.map(($_keyParam, $closureArg) => MapEntry($subKeyValue, $subFieldValue))';
}
Expand All @@ -56,9 +59,9 @@ class MapHelper extends TypeHelper<TypeHelperContextWithConfig> {
_checkSafeKeyType(expression, keyArg);

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

if (!isEnumKey) {
if (!isKeyStringable) {
if (valueArgIsAny) {
if (context.config.anyMap) {
if (_isObjectOrDynamic(keyArg)) {
Expand Down Expand Up @@ -90,30 +93,58 @@ 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');

final _instances = {
bigIntString,
dateTimeString,
_intString,
uriString,
};

Iterable<String> get _allowedTypeNames => const [
'Object',
'dynamic',
'enum',
'String'
].followedBy(_instances.map((i) => i.coreTypeName));

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

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

bool _isStringKeyable(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);
_isStringKeyable(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(', ')}.');
}
}
28 changes: 10 additions & 18 deletions json_serializable/lib/src/type_helpers/to_from_string.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,12 @@ import 'package:source_gen/source_gen.dart';

import '../type_helper.dart';

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

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

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

final dateTimeString =
ToFromStringHelper('DateTime.parse', 'toIso8601String()', 'DateTime');

final uriString = ToFromStringHelper('Uri.parse', 'toString()', 'Uri');

/// Package-internal helper that unifies implementations of [Type]s that convert
/// trivially to-from [String].
Expand All @@ -40,9 +29,12 @@ 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 de7cf91

Please sign in to comment.