diff --git a/README.md b/README.md index 52efb4e3..c4afaa03 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ part 'book.g.dart'; abstract class _Book extends Model { String get author; + @DefaultValue('[Untitled]') String get title; String get description; diff --git a/angel_serialize_generator/build.yaml b/angel_serialize_generator/build.yaml index cdf8f649..0cd80495 100644 --- a/angel_serialize_generator/build.yaml +++ b/angel_serialize_generator/build.yaml @@ -27,6 +27,7 @@ targets: _book: sources: - "test/models/book.dart" + - "test/models/goat.dart" - "test/models/game_pad_button.dart" - "test/models/with_enum.dart" _typescript_definition: diff --git a/angel_serialize_generator/lib/angel_serialize_generator.dart b/angel_serialize_generator/lib/angel_serialize_generator.dart index 04bf794e..9e366b1f 100644 --- a/angel_serialize_generator/lib/angel_serialize_generator.dart +++ b/angel_serialize_generator/lib/angel_serialize_generator.dart @@ -2,7 +2,7 @@ library angel_serialize_generator; import 'dart:async'; import 'dart:typed_data'; - +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:angel_model/angel_model.dart'; @@ -48,6 +48,32 @@ TypeReference convertTypeReference(DartType t) { }); } +String dartObjectToString(DartObject v) { + if (v.isNull) return 'null'; + if (v.toBoolValue() != null) return v.toBoolValue().toString(); + if (v.toIntValue() != null) return v.toIntValue().toString(); + if (v.toDoubleValue() != null) return v.toDoubleValue().toString(); + if (v.toSymbolValue() != null) return '#' + v.toSymbolValue(); + if (v.toTypeValue() != null) return v.toTypeValue().name; + if (v.toListValue() != null) + return 'const [' + v.toListValue().map(dartObjectToString).join(', ') + ']'; + if (v.toMapValue() != null) { + return 'const {' + + v.toMapValue().entries.map((entry) { + var k = dartObjectToString(entry.key); + var v = dartObjectToString(entry.value); + return '$k: $v'; + }).join(', ') + + '}'; + } + if (v.toStringValue() != null) { + return literalString(v.toStringValue()) + .accept(new DartEmitter()) + .toString(); + } + throw new ArgumentError(v.toString()); +} + /// Determines if a type supports `package:angel_serialize`. bool isModelClass(DartType t) { if (t == null) return false; diff --git a/angel_serialize_generator/lib/build_context.dart b/angel_serialize_generator/lib/build_context.dart index 38e0869a..6b5c92e7 100644 --- a/angel_serialize_generator/lib/build_context.dart +++ b/angel_serialize_generator/lib/build_context.dart @@ -67,6 +67,14 @@ Future buildContext( ); } + // Check for @DefaultValue() + var defAnn = + const TypeChecker.fromRuntime(DefaultValue).firstAnnotationOf(el); + if (defAnn != null) { + var rev = new ConstantReader(defAnn).revive().positionalArguments[0]; + ctx.defaults[field.name] = rev; + } + // Check for alias Alias alias; var aliasAnn = aliasTypeChecker.firstAnnotationOf(el); diff --git a/angel_serialize_generator/lib/context.dart b/angel_serialize_generator/lib/context.dart index 6f5dba58..9c5fe0ef 100644 --- a/angel_serialize_generator/lib/context.dart +++ b/angel_serialize_generator/lib/context.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:angel_serialize/angel_serialize.dart'; import 'package:code_builder/code_builder.dart'; @@ -15,6 +16,9 @@ class BuildContext { /// A map of field names to resolved names from `@Alias()` declarations. final Map aliases = {}; + /// A map of field names to their default values. + final Map defaults = {}; + /// A map of fields that have been marked as to be excluded from serialization. final Map excluded = {}; diff --git a/angel_serialize_generator/lib/model.dart b/angel_serialize_generator/lib/model.dart index cf0a94e7..0b3d82ab 100644 --- a/angel_serialize_generator/lib/model.dart +++ b/angel_serialize_generator/lib/model.dart @@ -96,6 +96,12 @@ class JsonModelGenerator extends GeneratorForAnnotation { ? 'List' : 'Map'; var defaultValue = typeName == 'List' ? '[]' : '{}'; + var existingDefault = ctx.defaults[field.name]; + + if (existingDefault != null) { + defaultValue = dartObjectToString(existingDefault); + } + constructor.initializers.add(new Code(''' this.${field.name} = new $typeName.unmodifiable(${field.name} ?? $defaultValue)''')); @@ -109,6 +115,12 @@ class JsonModelGenerator extends GeneratorForAnnotation { ..name = field.name ..named = true; + var existingDefault = ctx.defaults[field.name]; + + if (existingDefault != null) { + b.defaultTo = new Code(dartObjectToString(existingDefault)); + } + if (!isListOrMapType(field.type)) b.toThis = true; else { diff --git a/angel_serialize_generator/lib/serialize.dart b/angel_serialize_generator/lib/serialize.dart index eaa872aa..eb10bee8 100644 --- a/angel_serialize_generator/lib/serialize.dart +++ b/angel_serialize_generator/lib/serialize.dart @@ -203,18 +203,27 @@ class SerializerGenerator extends GeneratorForAnnotation { String deserializedRepresentation = "map['$alias'] as ${typeToString(field.type)}"; + var defaultValue = 'null'; + var existingDefault = ctx.defaults[field.name]; + + if (existingDefault != null) { + defaultValue = dartObjectToString(existingDefault); + deserializedRepresentation = + '$deserializedRepresentation ?? $defaultValue'; + } + // Deserialize dates if (dateTimeTypeChecker.isAssignableFromType(field.type)) deserializedRepresentation = "map['$alias'] != null ? " "(map['$alias'] is DateTime ? (map['$alias'] as DateTime) : DateTime.parse(map['$alias'].toString()))" - " : null"; + " : $defaultValue"; // Serialize model classes via `XSerializer.toMap` else if (isModelClass(field.type)) { var rc = new ReCase(field.type.name); deserializedRepresentation = "map['$alias'] != null" " ? ${rc.pascalCase}Serializer.fromMap(map['$alias'] as Map)" - " : null"; + " : $defaultValue"; } else if (field.type is InterfaceType) { var t = field.type as InterfaceType; @@ -224,7 +233,7 @@ class SerializerGenerator extends GeneratorForAnnotation { " ? new List.unmodifiable(((map['$alias'] as Iterable)" ".where((x) => x is Map) as Iterable)" ".map(${rc.pascalCase}Serializer.fromMap))" - " : null"; + " : $defaultValue"; } else if (isMapToModelType(t)) { var rc = new ReCase(t.typeArguments[1].name); deserializedRepresentation = ''' @@ -233,7 +242,7 @@ class SerializerGenerator extends GeneratorForAnnotation { return out..[key] = ${rc.pascalCase}Serializer .fromMap(((map['$alias'] as Map)[key]) as Map); })) - : null + : $defaultValue '''; } else if (t.element.isEnum) { deserializedRepresentation = ''' @@ -243,7 +252,7 @@ class SerializerGenerator extends GeneratorForAnnotation { ( map['$alias'] is int ? ${t.name}.values[map['$alias'] as int] - : null + : $defaultValue ) '''; } else if (const TypeChecker.fromRuntime(List) @@ -254,7 +263,7 @@ class SerializerGenerator extends GeneratorForAnnotation { deserializedRepresentation = ''' map['$alias'] is Iterable ? (map['$alias'] as Iterable).cast<$arg>().toList() - : null + : $defaultValue '''; } else if (const TypeChecker.fromRuntime(Map) .isAssignableFromType(t) && @@ -266,7 +275,7 @@ class SerializerGenerator extends GeneratorForAnnotation { deserializedRepresentation = ''' map['$alias'] is Map ? (map['$alias'] as Map).cast<$key, $value>() - : null + : $defaultValue '''; } else if (const TypeChecker.fromRuntime(Uint8List) .isAssignableFromType(t)) { @@ -281,7 +290,7 @@ class SerializerGenerator extends GeneratorForAnnotation { ( map['$alias'] is String ? new Uint8List.fromList(base64.decode(map['$alias'] as String)) - : null + : $defaultValue ) ) '''; diff --git a/angel_serialize_generator/test/default_value_test.dart b/angel_serialize_generator/test/default_value_test.dart new file mode 100644 index 00000000..a37b470f --- /dev/null +++ b/angel_serialize_generator/test/default_value_test.dart @@ -0,0 +1,24 @@ +import 'package:test/test.dart'; +import 'models/goat.dart'; + +void main() { + group('constructor', () { + test('int default', () { + expect(Goat().integer, 34); + }); + + test('list default', () { + expect(Goat().list, [34, 35]); + }); + }); + + group('from map', () { + test('int default', () { + expect(GoatSerializer.fromMap({}).integer, 34); + }); + + test('list default', () { + expect(GoatSerializer.fromMap({}).list, [34, 35]); + }); + }); +} diff --git a/angel_serialize_generator/test/models/game_pad.dart b/angel_serialize_generator/test/models/game_pad.dart index 91af7ace..dc2c6a8c 100644 --- a/angel_serialize_generator/test/models/game_pad.dart +++ b/angel_serialize_generator/test/models/game_pad.dart @@ -3,7 +3,6 @@ import 'package:collection/collection.dart'; import 'game_pad_button.dart'; part 'game_pad.g.dart'; - @Serializable(autoIdAndDateFields: false) class _Gamepad { List buttons; diff --git a/angel_serialize_generator/test/models/goat.dart b/angel_serialize_generator/test/models/goat.dart new file mode 100644 index 00000000..2b1a0584 --- /dev/null +++ b/angel_serialize_generator/test/models/goat.dart @@ -0,0 +1,12 @@ +import 'package:angel_serialize/angel_serialize.dart'; +import 'package:collection/collection.dart'; +part 'goat.g.dart'; + +@Serializable(autoIdAndDateFields: false) +abstract class _Goat { + @DefaultValue(34) + int get integer; + + @DefaultValue([34, 35]) + List get list; +} diff --git a/angel_serialize_generator/test/models/goat.g.dart b/angel_serialize_generator/test/models/goat.g.dart new file mode 100644 index 00000000..db2ddfbe --- /dev/null +++ b/angel_serialize_generator/test/models/goat.g.dart @@ -0,0 +1,67 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'goat.dart'; + +// ************************************************************************** +// JsonModelGenerator +// ************************************************************************** + +@generatedSerializable +class Goat implements _Goat { + const Goat({this.integer: 34, List this.list: const [34, 35]}); + + @override + final int integer; + + @override + final List list; + + Goat copyWith({int integer, List list}) { + return new Goat(integer: integer ?? this.integer, list: list ?? this.list); + } + + bool operator ==(other) { + return other is _Goat && + other.integer == integer && + const ListEquality(const DefaultEquality()) + .equals(other.list, list); + } + + @override + int get hashCode { + return hashObjects([integer, list]); + } + + Map toJson() { + return GoatSerializer.toMap(this); + } +} + +// ************************************************************************** +// SerializerGenerator +// ************************************************************************** + +abstract class GoatSerializer { + static Goat fromMap(Map map) { + return new Goat( + integer: map['integer'] as int ?? 34, + list: map['list'] is Iterable + ? (map['list'] as Iterable).cast().toList() + : const [34, 35]); + } + + static Map toMap(_Goat model) { + if (model == null) { + return null; + } + return {'integer': model.integer, 'list': model.list}; + } +} + +abstract class GoatFields { + static const List allFields = const [integer, list]; + + static const String integer = 'integer'; + + static const String list = 'list'; +}