Skip to content

Support generating a custom map and using non-copying wrappers for serialization #48

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 3 commits into from
Nov 23, 2017
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
7 changes: 7 additions & 0 deletions json_annotation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.2.2

* Added a helper class – `$JsonMapWrapper` – and helper functions – `$wrapMap`,
`$wrapMapHandleNull`, `$wrapList`, and `$wrapListHandleNull` – to support
the `useWrappers` option added to `JsonSerializableGenerator` in `v0.3.0` of
`package:json_serializable`.

## 0.2.1

* `JsonSerializable` class annotation
Expand Down
54 changes: 54 additions & 0 deletions json_annotation/lib/src/json_serializable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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:collection';

class JsonSerializable {
// TODO(kevmoo): document these fields
final bool createFactory;
Expand Down Expand Up @@ -68,3 +70,55 @@ class JsonKey {
/// Only required when the default behavior is not desired.
const JsonKey({this.name, this.nullable, this.includeIfNull});
}

// TODO(kevmoo): Add documentation
abstract class $JsonMapWrapper extends UnmodifiableMapBase<String, dynamic> {}

Map<String, dynamic> $wrapMap<K, V>(
Map<K, V> source, dynamic converter(V key)) =>
new _MappingMap(source, converter);

Map<String, dynamic> $wrapMapHandleNull<K, V>(
Map<K, V> source, dynamic converter(V key)) =>
source == null ? null : new _MappingMap(source, converter);

List<dynamic> $wrapList<T>(List<T> source, dynamic converter(T key)) =>
new _MappingList(source, converter);

List<dynamic> $wrapListHandleNull<T>(
List<T> source, dynamic converter(T key)) =>
source == null ? null : new _MappingList(source, converter);

typedef dynamic _Convert<S>(S value);

class _MappingList<S> extends ListBase<dynamic> {
final List<S> _source;
final _Convert<S> _converter;

_MappingList(this._source, this._converter);

@override
dynamic operator [](int index) => _converter(_source[index]);

@override
operator []=(int index, dynamic value) => throw new UnsupportedError('');

@override
int get length => _source.length;

@override
set length(int value) => throw new UnsupportedError('');
}

class _MappingMap<K, V> extends UnmodifiableMapBase<String, dynamic> {
final Map<K, V> _source;
final _Convert<V> _converter;

_MappingMap(this._source, this._converter);

@override
Iterable<String> get keys => _source.keys.map((k) => k as String);

@override
dynamic operator [](Object key) => _converter(_source[key]);
}
2 changes: 1 addition & 1 deletion json_annotation/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: json_annotation
version: 0.2.1
version: 0.2.2-dev
description: Annotations for the json_serializable package
homepage: https://github.com/dart-lang/json_serializable
author: Dart Team <misc@dartlang.org>
Expand Down
12 changes: 12 additions & 0 deletions json_serializable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
* **BREAKING** The deprecated `annotations.dart` library has been removed.
Use `package:json_annotation` instead.

* **BREAKING** The arguments to `TypeHelper` `serialize` and `deserialize` have
changed.
* `SerializeContext` and `DeserializeContext` (new classes) are now passed
instead of the `TypeHelperGenerator` typedef (which has been deleted).

* `JsonSerializableGenerator` now supports an optional `useWrappers` argument
when generates and uses wrapper classes to (hopefully) improve the speed and
memory usage of serialization – at the cost of more code.

**NOTE**: `useWrappers` is not guaranteed to improve the performance of
serialization. Benchmarking is recommended.

* Make `null` field handling smarter. If a field is classified as not
`nullable`, then use this knowledge when generating serialization code – even
if `includeIfNull` is `false`.
Expand Down
125 changes: 114 additions & 11 deletions json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,26 @@ class JsonSerializableGenerator

final List<TypeHelper> _typeHelpers;

final bool useWrappers;

/// Creates an instance of [JsonSerializableGenerator].
///
/// If [typeHelpers] is not provided, two built-in helpers are used:
/// [JsonHelper] and [DateTimeHelper].
const JsonSerializableGenerator({List<TypeHelper> typeHelpers})
: this._typeHelpers = typeHelpers ?? _defaultHelpers;
const JsonSerializableGenerator(
{List<TypeHelper> typeHelpers, bool useWrappers: false})
: this.useWrappers = useWrappers ?? false,
this._typeHelpers = typeHelpers ?? _defaultHelpers;

/// Creates an instance of [JsonSerializableGenerator].
///
/// [typeHelpers] provides a set of [TypeHelper] that will be used along with
/// the built-in helpers: [JsonHelper] and [DateTimeHelper].
factory JsonSerializableGenerator.withDefaultHelpers(
Iterable<TypeHelper> typeHelpers) =>
Iterable<TypeHelper> typeHelpers,
{bool useWrappers: false}) =>
new JsonSerializableGenerator(
useWrappers: useWrappers,
typeHelpers: new List.unmodifiable(
[typeHelpers, _defaultHelpers].expand((e) => e)));

Expand Down Expand Up @@ -125,6 +131,7 @@ class JsonSerializableGenerator

if (classAnnotation.createToJson) {
var mixClassName = '${prefix}SerializerMixin';
var helpClassName = '${prefix}JsonMapWrapper';

//
// Generate the mixin class
Expand All @@ -138,26 +145,96 @@ class JsonSerializableGenerator
buffer.writeln(' ${field.type} get ${field.name};');
}

buffer.writeln(' Map<String, dynamic> toJson() ');
buffer.write(' Map<String, dynamic> toJson() ');

var writeNaive =
fieldsList.every((e) => _writeJsonValueNaive(e, classAnnotation));

if (writeNaive) {
// write simple `toJson` method that includes all keys...
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
if (useWrappers) {
buffer.writeln('=> new $helpClassName(this);');
} else {
// At least one field should be excluded if null
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
if (writeNaive) {
// write simple `toJson` method that includes all keys...
_writeToJsonSimple(buffer, fields.values, classAnnotation.nullable);
} else {
// At least one field should be excluded if null
_writeToJsonWithNullChecks(buffer, fields.values, classAnnotation);
}
}

// end of the mixin class
buffer.writeln('}');

if (useWrappers) {
_writeWrapper(
buffer, helpClassName, mixClassName, classAnnotation, fields);
}
}

return buffer.toString();
}

void _writeWrapper(
StringBuffer buffer,
String helpClassName,
String mixClassName,
JsonSerializable classAnnotation,
Map<String, FieldElement> fields) {
buffer.writeln();
// TODO(kevmoo): write JsonMapWrapper if annotation lib is prefix-imported
buffer.writeln('''class $helpClassName extends \$JsonMapWrapper {
final $mixClassName _v;
$helpClassName(this._v);
''');

if (fields.values.every((e) => _writeJsonValueNaive(e, classAnnotation))) {
// TODO(kevmoo): consider just doing one code path – if it's fast
// enough
var jsonKeys = fields.values.map(_safeNameAccess).join(', ');

// TODO(kevmoo): maybe put this in a static field instead?
// const lists have unfortunate overhead
buffer.writeln(''' @override
Iterable<String> get keys => const [${jsonKeys}];
''');
} else {
// At least one field should be excluded if null
buffer.writeln('@override\nIterable<String> get keys sync* {');

for (var field in fields.values) {
var nullCheck = !_writeJsonValueNaive(field, classAnnotation);
if (nullCheck) {
buffer.writeln('if (_v.${field.name} != null) {');
}
buffer.writeln('yield ${_safeNameAccess(field)};');
if (nullCheck) {
buffer.writeln('}');
}
}

buffer.writeln('}\n');
}

buffer.writeln('''@override
dynamic operator [](Object key) {
if (key is String) {
switch(key) {
''');

for (var field in fields.values) {
var valueAccess = '_v.${field.name}';
buffer.write('''case ${_safeNameAccess(field)}:
return ${_serializeField(field, classAnnotation.nullable, accessOverride: valueAccess)};''');
}

buffer.writeln('''
}}
return null;
}''');

buffer.writeln('}');
}

void _writeToJsonWithNullChecks(StringBuffer buffer,
Iterable<FieldElement> fields, JsonSerializable classAnnotation) {
buffer.writeln('{');
Expand Down Expand Up @@ -352,7 +429,8 @@ void $toJsonMapHelperName(String key, dynamic value) {
/// representing the serialization of a value.
String _serialize(DartType targetType, String expression, bool nullable) =>
_allHelpers
.map((h) => h.serialize(targetType, expression, nullable, _serialize))
.map((h) =>
h.serialize(targetType, expression, nullable, _helperContext))
.firstWhere((r) => r != null,
orElse: () => throw new UnsupportedTypeError(
targetType, expression, _notSupportedWithTypeHelpersMsg));
Expand All @@ -374,10 +452,35 @@ void $toJsonMapHelperName(String key, dynamic value) {
String _deserialize(DartType targetType, String expression, bool nullable) =>
_allHelpers
.map((th) =>
th.deserialize(targetType, expression, nullable, _deserialize))
th.deserialize(targetType, expression, nullable, _helperContext))
.firstWhere((r) => r != null,
orElse: () => throw new UnsupportedTypeError(
targetType, expression, _notSupportedWithTypeHelpersMsg));

_TypeHelperContext get _helperContext => _typeHelperContextExpando[this] ??=
new _TypeHelperContext(_serialize, _deserialize, useWrappers);
}

final _typeHelperContextExpando = new Expando<_TypeHelperContext>();

typedef String _TypeHelperGenerator(
DartType fieldType, String expression, bool nullable);

class _TypeHelperContext implements SerializeContext, DeserializeContext {
final _TypeHelperGenerator _serialize, _deserialize;

@override
final bool useWrappers;

_TypeHelperContext(this._serialize, this._deserialize, this.useWrappers);

@override
String serialize(DartType fieldType, String expression, bool nullable) =>
_serialize(fieldType, expression, nullable);

@override
String deserialize(DartType fieldType, String expression, bool nullable) =>
_deserialize(fieldType, expression, nullable);
}

String _safeNameAccess(FieldElement field) {
Expand Down
18 changes: 12 additions & 6 deletions json_serializable/lib/src/type_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ List<DartType> typeArgumentsOf(DartType type, TypeChecker checker) {
return implementation?.typeArguments;
}

typedef String TypeHelperGenerator(
DartType fieldType, String expression, bool nullable);
abstract class SerializeContext {
bool get useWrappers;
String serialize(DartType fieldType, String expression, bool nullable);
}

abstract class DeserializeContext {
String deserialize(DartType fieldType, String expression, bool nullable);
}

abstract class TypeHelper {
const TypeHelper();
Expand All @@ -36,9 +42,9 @@ abstract class TypeHelper {
/// String serialize(DartType targetType, String expression) =>
/// "$expression.id";
/// ```.
// TODO(kevmoo) – document `serializeNested`
// TODO(kevmoo) – document `context`
String serialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator serializeNested);
SerializeContext context);

/// Returns Dart code that deserializes an [expression] representing a JSON
/// literal to into [targetType].
Expand All @@ -63,9 +69,9 @@ abstract class TypeHelper {
/// String deserialize(DartType targetType, String expression) =>
/// "new ${targetType.name}.fromInt($expression)";
/// ```.
// TODO(kevmoo) – document `deserializeNested`
// TODO(kevmoo) – document `context`
String deserialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator deserializeNested);
DeserializeContext context);
}

/// A [TypeChecker] for [String], [bool] and [num].
Expand Down
4 changes: 2 additions & 2 deletions json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class EnumHelper extends TypeHelper {

@override
String serialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator serializeNested) {
SerializeContext context) {
if (targetType is InterfaceType && targetType.element.isEnum) {
return commonNullPrefix(
nullable, expression, "$expression.toString().split('.')[1]");
Expand All @@ -18,7 +18,7 @@ class EnumHelper extends TypeHelper {

@override
String deserialize(DartType targetType, String expression, bool nullable,
TypeHelperGenerator deserializeNested) {
DeserializeContext context) {
if (targetType is InterfaceType && targetType.element.isEnum) {
return commonNullPrefix(
nullable,
Expand Down
Loading