Skip to content

Add any_map support #158

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 16, 2018
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
6 changes: 6 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.5.4

* Added `any_map` to configuration. Allows `fromJson` code to
support dynamic `Map` instances that are not explicitly
`Map<String, dynaimc>`.

## 0.5.3

* Require the latest version of `package:analyzer` - `v0.32.0`.
Expand Down
3 changes: 2 additions & 1 deletion json_serializable/lib/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Builder jsonSerializable(BuilderOptions options) {

var builder = jsonPartBuilder(
header: optionsMap.remove('header') as String,
useWrappers: optionsMap.remove('use_wrappers') as bool);
useWrappers: optionsMap.remove('use_wrappers') as bool,
anyMap: optionsMap.remove('any_map') as bool);

if (optionsMap.isNotEmpty) {
if (log == null) {
Expand Down
4 changes: 3 additions & 1 deletion json_serializable/lib/src/generator_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,11 @@ class _GeneratorHelper {
});

if (_annotation.createFactory) {
var mapType = _generator.anyMap ? 'Map' : 'Map<String, dynamic>';

_buffer.writeln();
_buffer.writeln('${_element.name} '
'${_prefix}FromJson(Map<String, dynamic> json) =>');
'${_prefix}FromJson($mapType json) =>');

String deserializeFun(String paramOrFieldName,
{ParameterElement ctorParam}) =>
Expand Down
11 changes: 4 additions & 7 deletions json_serializable/lib/src/json_part_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ import 'json_serializable_generator.dart';
/// If `null`, the content of [defaultFileHeader] is used.
/// If [header] is an empty `String` no header is added.
///
/// If [useWrappers] is `true`, wrappers are used to minimize the number of
/// [Map] and [List] instances created during serialization. This will
/// increase the code size, but it may improve runtime performance, especially
/// for large object graphs.
Builder jsonPartBuilder({String header, bool useWrappers: false}) {
useWrappers ??= false;
/// For details on [useWrappers] and [anyMap], see [JsonSerializableGenerator].
Builder jsonPartBuilder(
{String header, bool useWrappers: false, bool anyMap: false}) {
return new PartBuilder([
new JsonSerializableGenerator(useWrappers: useWrappers),
new JsonSerializableGenerator(useWrappers: useWrappers, anyMap: anyMap),
const JsonLiteralGenerator()
], header: header);
}
25 changes: 18 additions & 7 deletions json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,29 @@ class JsonSerializableGenerator
const ConvertHelper()
].followedBy(_typeHelpers).followedBy(_coreHelpers);

/// If `true`, wrappers are used to minimize the number of
/// [Map] and [List] instances created during serialization. This will
/// increase the code size, but it may improve runtime performance, especially
/// for large object graphs.
final bool useWrappers;

/// If `true`, [Map] types are not assumed to be
/// [Map<String, dynamic>] – the default for JSON support in `dart:convert`.
/// This results in more code being generated, but allows [Map] types returned
/// from other sources, such as `package:yaml`.
/// *Note: in many cases the key values are still assumed to be [String]*.
final bool anyMap;

/// Creates an instance of [JsonSerializableGenerator].
///
/// If [typeHelpers] is not provided, two built-in helpers are used:
/// [JsonHelper] and [DateTimeHelper].
///
/// If [useWrappers] is `true`, wrappers are used to minimize the number of
/// [Map] and [List] instances created during serialization. This will
/// increase the code size, but it may improve runtime performance, especially
/// for large object graphs.
const JsonSerializableGenerator(
{List<TypeHelper> typeHelpers, bool useWrappers: false})
{List<TypeHelper> typeHelpers,
bool useWrappers: false,
bool anyMap: false})
: this.useWrappers = useWrappers ?? false,
this.anyMap = anyMap ?? false,
this._typeHelpers = typeHelpers ?? _defaultHelpers;

/// Creates an instance of [JsonSerializableGenerator].
Expand All @@ -64,9 +73,11 @@ class JsonSerializableGenerator
/// the built-in helpers: [JsonHelper] and [DateTimeHelper].
factory JsonSerializableGenerator.withDefaultHelpers(
Iterable<TypeHelper> typeHelpers,
{bool useWrappers: false}) =>
{bool useWrappers: false,
bool anyMap: false}) =>
new JsonSerializableGenerator(
useWrappers: useWrappers,
anyMap: anyMap,
typeHelpers: new List.unmodifiable(
[typeHelpers, _defaultHelpers].expand((e) => e)));

Expand Down
24 changes: 24 additions & 0 deletions json_serializable/lib/src/shared_checkers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ const simpleJsonTypeChecker = const TypeChecker.any(const [
const TypeChecker.fromRuntime(num)
]);

String asStatement(DartType type) {
if (type.isDynamic || type.isObject) {
return '';
}

if (coreIterableTypeChecker.isAssignableFromType(type)) {
var itemType = coreIterableGenericType(type);
if (itemType.isDynamic || itemType.isObject) {
return ' as List';
}
}

if (coreMapTypeChecker.isAssignableFromType(type)) {
var args = typeArgumentsOf(type, coreMapTypeChecker);
assert(args.length == 2);

if (args.every((dt) => dt.isDynamic || dt.isObject)) {
return ' as Map';
}
}

return ' as $type';
}

DartType _getImplementationType(DartType type, TypeChecker checker) {
if (checker.isExactlyType(type)) return type;

Expand Down
4 changes: 4 additions & 0 deletions json_serializable/lib/src/type_helper_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class TypeHelperContext implements SerializeContext, DeserializeContext {
final ConvertData fromJsonData;
final ConvertData toJsonData;

// This is effectively private to TypeHelpers outside this package.
// Consider exposing it if there is interest
bool get anyMap => _generator.anyMap;

TypeHelperContext(this._generator, this.metadata, this.nullable,
this.fromJsonData, this.toJsonData);

Expand Down
26 changes: 1 addition & 25 deletions json_serializable/lib/src/type_helpers/convert_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,10 @@ class ConvertHelper extends TypeHelper {
DartType targetType, String expression, DeserializeContext context) {
var fromJsonData = (context as TypeHelperContext).fromJsonData;
if (fromJsonData != null) {
var asContent = _asContent(fromJsonData.paramType);
var asContent = asStatement(fromJsonData.paramType);
var result = '${fromJsonData.name}($expression$asContent)';
return commonNullPrefix(context.nullable, expression, result);
}
return null;
}
}

String _asContent(DartType type) {
if (type.isDynamic || type.isObject) {
return '';
}

if (coreIterableTypeChecker.isAssignableFromType(type)) {
var itemType = coreIterableGenericType(type);
if (itemType.isDynamic || itemType.isObject) {
return ' as List';
}
}

if (coreMapTypeChecker.isAssignableFromType(type)) {
var args = typeArgumentsOf(type, coreMapTypeChecker);
assert(args.length == 2);

if (args.every((dt) => dt.isDynamic || dt.isObject)) {
return ' as Map';
}
}

return ' as $type';
}
6 changes: 2 additions & 4 deletions json_serializable/lib/src/type_helpers/json_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import '../shared_checkers.dart';
import '../type_helper.dart';
import '../utils.dart';

Expand Down Expand Up @@ -40,10 +41,7 @@ class JsonHelper extends TypeHelper {
// TODO: should verify that this type is a valid JSON type...but for now...
var asCastType = fromJsonCtor.parameters.first.type;

var asCast = '';
if (!asCastType.isDynamic && !asCastType.isObject) {
asCast = ' as $asCastType';
}
var asCast = asStatement(asCastType);

// TODO: the type could be imported from a library with a prefix!
// github.com/dart-lang/json_serializable/issues/19
Expand Down
27 changes: 20 additions & 7 deletions json_serializable/lib/src/type_helpers/map_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:analyzer/dart/element/type.dart';

import '../shared_checkers.dart';
import '../type_helper.dart';
import '../type_helper_context.dart';
import '../utils.dart';

/// Name used for closure argument when generating calls to `map`.
Expand Down Expand Up @@ -67,13 +68,22 @@ class MapHelper extends TypeHelper {

_checkSafeKeyType(expression, keyArg);

// this is the trivial case. Do a runtime cast to the known type of JSON
// map values - `Map<String, dynamic>`
if (valueArg.isDynamic || valueArg.isObject) {
return '$expression as Map<String, dynamic>';
var valueArgIsAny = valueArg.isDynamic || valueArg.isObject;
var isAnyMap = context is TypeHelperContext && context.anyMap;

if (valueArgIsAny) {
if (isAnyMap) {
if (keyArg.isObject || keyArg.isDynamic) {
return '$expression as Map';
}
} else {
// this is the trivial case. Do a runtime cast to the known type of JSON
// map values - `Map<String, dynamic>`
return '$expression as Map<String, dynamic>';
}
}

if (simpleJsonTypeChecker.isAssignableFromType(valueArg)) {
if (valueArgIsAny || simpleJsonTypeChecker.isAssignableFromType(valueArg)) {
// No mapping of the values is required!

var result = 'new Map<String, $valueArg>.from($expression as Map)';
Expand All @@ -85,8 +95,11 @@ class MapHelper extends TypeHelper {

var itemSubVal = context.deserialize(valueArg, _closureArg);

var result = 'new Map<String, $valueArg>.fromIterables('
'($expression as Map<String, dynamic>).keys,'
var keyIterable = isAnyMap
? '($expression as Map).keys.cast<String>()'
: '($expression as Map<String, dynamic>).keys';

var result = 'new Map<String, $valueArg>.fromIterables($keyIterable,'
'($expression as Map).values.map(($_closureArg) => $itemSubVal))';

return commonNullPrefix(context.nullable, expression, result);
Expand Down
2 changes: 2 additions & 0 deletions json_serializable/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ dev_dependencies:
build_test: ">=0.9.0 <0.11.0"
collection: ^1.14.0
dart_style: ^1.0.0
logging: ^0.11.3+1
test: ^0.12.3
yaml: ^2.1.13
76 changes: 76 additions & 0 deletions json_serializable/test/config_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:build/build.dart';
import 'package:json_serializable/builder.dart';
import 'package:logging/logging.dart';
import 'package:source_gen/source_gen.dart';
import 'package:test/test.dart';

final _throwsCastError = throwsA(const isInstanceOf<CastError>());

void main() {
StreamSubscription sub;
final records = <LogRecord>[];

setUpAll(() {
sub = log.onRecord.listen(records.add);
});

tearDownAll(() async {
await sub.cancel();
});

setUp(records.clear);

test('empty', () async {
var builder = jsonSerializable(BuilderOptions.empty) as PartBuilder;
expect(builder, isNotNull);
expect(records, isEmpty);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that since the logs come through asynchronously you would never see them at this point, you would need to add an await null or something along those lines. Not really sure the absolute best way to deal with it honestly.

});

test('valid config', () async {
var builder =
jsonSerializable(const BuilderOptions(_validConfig)) as PartBuilder;
expect(builder, isNotNull);

expect(records, isEmpty);
});

test('unsupported configuration', () async {
var builder =
jsonSerializable(const BuilderOptions(const {'unsupported': 'config'}))
as PartBuilder;
expect(builder, isNotNull);

expect(records.single.message,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get how this is passing given my comment above...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed...

'These options were ignored: `{unsupported: config}`.');
});

group('invalid config', () {
for (var entry in _invalidConfig.entries) {
test(entry.key, () {
var config = new Map<String, dynamic>.from(_validConfig);
config[entry.key] = entry.value;

expect(() => jsonSerializable(new BuilderOptions(config)),
_throwsCastError);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth checking the actual message here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. They will all be 'String is not a bool' etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I was just wondering if you wanted to check the cast messages were correct, but its probably overkill

});
}
});
}

const _validConfig = const {
'header': 'header',
'use_wrappers': true,
'any_map': true
};

const _invalidConfig = const {
'header': true,
'use_wrappers': 42,
'any_map': 42
};
Loading