diff --git a/README.md b/README.md index 29764e90..7d882698 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ the time you spend writing boilerplate serialization code for your models. - [Excluding Keys](#excluding-keys) - [Required Fields](#required-fields) - [Adding Annotations to Generated Classes](#adding-annotations-to-generated-classes) + - [Custom Serializers](#custom-serializers) - [Serialization](#serializaition) - [Nesting](#nesting) - [ID and Date Fields](#id-and-dates) @@ -247,6 +248,24 @@ There are times when you need the generated class to have annotations affixed to abstract class _Foo extends Model {} ``` +## Custom Serializers +`package:angel_serialize` does not cover every known Dart data type; you can add support for your own. +Provide `serializer` and `deserializer` arguments to `@SerializableField()` as you see fit. + +They are typically used together. Note that the argument to `serializer` will always be +`dynamic`. + +```dart +DateTime _dateFromString(s) => s is String ? HttpDate.parse(s) : null; +String _dateToString(v) => v == null ? null : HttpDate.format(v); + +@Serializable(autoIdAndDateFields: false) +abstract class _HttpRequest { + @SerializableField(serializer: #_dateToString, deserializer: #_dateFromString) + DateTime date; +} +``` + # Nesting `angel_serialize` also supports a few types of nesting of `@serializable` classes: diff --git a/angel_serialize/CHANGELOG.md b/angel_serialize/CHANGELOG.md index 8ea84e3c..3769e1f0 100644 --- a/angel_serialize/CHANGELOG.md +++ b/angel_serialize/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2.2.0 +* Add `@SerializableField`. + # 2.1.0 * Export `hashObjects`. diff --git a/angel_serialize/pubspec.yaml b/angel_serialize/pubspec.yaml index b74c252a..ba75d3fa 100644 --- a/angel_serialize/pubspec.yaml +++ b/angel_serialize/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_serialize -version: 2.1.0 +version: 2.2.0 description: Static annotations powering Angel model serialization. Combine with angel_serialize_generator for flexible modeling. author: Tobe O homepage: https://github.com/angel-dart/serialize diff --git a/angel_serialize_generator/CHANGELOG.md b/angel_serialize_generator/CHANGELOG.md index 19fddc0f..d18b5129 100644 --- a/angel_serialize_generator/CHANGELOG.md +++ b/angel_serialize_generator/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.4.0 +* Introduce `@SerializableField`, and say goodbye to annotation hell. +* Support custom (de)serializers. +* Allow passing of annotations to the generated class. +* Fixted TypeScript `ref` generator. + # 2.3.0 * Add `@DefaultValue` support. diff --git a/angel_serialize_generator/build.yaml b/angel_serialize_generator/build.yaml index 0cd80495..cbe43107 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/has_map.dart" - "test/models/goat.dart" - "test/models/game_pad_button.dart" - "test/models/with_enum.dart" diff --git a/angel_serialize_generator/lib/angel_serialize_generator.dart b/angel_serialize_generator/lib/angel_serialize_generator.dart index 1f99838c..915d8d49 100644 --- a/angel_serialize_generator/lib/angel_serialize_generator.dart +++ b/angel_serialize_generator/lib/angel_serialize_generator.dart @@ -1,6 +1,7 @@ library angel_serialize_generator; import 'dart:async'; +import 'dart:mirrors'; import 'dart:typed_data'; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; diff --git a/angel_serialize_generator/lib/build_context.dart b/angel_serialize_generator/lib/build_context.dart index 45f7c2bb..cb26a0fe 100644 --- a/angel_serialize_generator/lib/build_context.dart +++ b/angel_serialize_generator/lib/build_context.dart @@ -28,6 +28,8 @@ const TypeChecker serializableTypeChecker = const TypeChecker generatedSerializableTypeChecker = const TypeChecker.fromRuntime(GeneratedSerializable); +final Map _cache = {}; + /// Create a [BuildContext]. Future buildContext( ClassElement clazz, @@ -37,6 +39,11 @@ Future buildContext( bool autoSnakeCaseNames, bool autoIdAndDateFields, {bool heedExclude: true}) async { + var id = clazz.location.components.join('-'); + if (_cache.containsKey(id)) { + return _cache[id]; + } + // Check for autoIdAndDateFields, autoSnakeCaseNames autoIdAndDateFields = annotation.peek('autoIdAndDateFields')?.boolValue ?? autoIdAndDateFields; @@ -155,6 +162,8 @@ Future buildContext( const TypeChecker.fromRuntime(Required).firstAnnotationOf(el); if (required != null) { + log.warning( + 'Using @required on fields (like ${clazz.name}.${field.name}) is now deprecated; use @SerializableField(isNullable: false) instead.'); var cr = new ConstantReader(required); var reason = cr.peek('reason')?.stringValue ?? "Missing required field '${ctx.resolveFieldName(field.name)}' on ${ctx.modelClassName}."; diff --git a/angel_serialize_generator/lib/serialize.dart b/angel_serialize_generator/lib/serialize.dart index eb10bee8..9c599760 100644 --- a/angel_serialize_generator/lib/serialize.dart +++ b/angel_serialize_generator/lib/serialize.dart @@ -96,8 +96,13 @@ class SerializerGenerator extends GeneratorForAnnotation { return '${rc.pascalCase}Serializer.toMap($value)'; } + if (ctx.fieldInfo[field.name]?.serializer != null) { + var name = MirrorSystem.getName(ctx.fieldInfo[field.name].serializer); + serializedRepresentation = '$name(model.${field.name})'; + } + // Serialize dates - if (dateTimeTypeChecker.isAssignableFromType(field.type)) + else if (dateTimeTypeChecker.isAssignableFromType(field.type)) serializedRepresentation = 'model.${field.name}?.toIso8601String()'; // Serialize model classes via `XSerializer.toMap` @@ -212,8 +217,11 @@ class SerializerGenerator extends GeneratorForAnnotation { '$deserializedRepresentation ?? $defaultValue'; } - // Deserialize dates - if (dateTimeTypeChecker.isAssignableFromType(field.type)) + if (ctx.fieldInfo[field.name]?.deserializer != null) { + var name = + MirrorSystem.getName(ctx.fieldInfo[field.name].deserializer); + deserializedRepresentation = "$name(map['$alias'])"; + } else if (dateTimeTypeChecker.isAssignableFromType(field.type)) deserializedRepresentation = "map['$alias'] != null ? " "(map['$alias'] is DateTime ? (map['$alias'] as DateTime) : DateTime.parse(map['$alias'].toString()))" " : $defaultValue"; diff --git a/angel_serialize_generator/lib/typescript.dart b/angel_serialize_generator/lib/typescript.dart index c3890d2e..af59223a 100644 --- a/angel_serialize_generator/lib/typescript.dart +++ b/angel_serialize_generator/lib/typescript.dart @@ -79,15 +79,26 @@ class TypeScriptDefinitionBuilder implements Builder { var targetPath = type.element.source.uri.toString(); if (!p.equals(sourcePath, targetPath)) { - //var relative = p.relative(targetPath, from: sourcePath); - var relative = (p.dirname(targetPath) == p.dirname(sourcePath)) - ? p.basename(targetPath) - : p.relative(targetPath, from: sourcePath); - var parent = p.dirname(relative); - var filename = - p.setExtension(p.basenameWithoutExtension(relative), '.d.ts'); - relative = p.joinAll(p.split(parent).toList()..add(filename)); - var ref = '/// '; + var relative = p.relative(targetPath, from: sourcePath); + String ref; + + if (type.element.source.uri.scheme == 'asset') { + var id = AssetId.resolve(type.element.source.uri.toString()); + if (id.package != buildStep.inputId.package) { + ref = '/// '; + } + } + + if (ref == null) { + // var relative = (p.dirname(targetPath) == p.dirname(sourcePath)) + // ? p.basename(targetPath) + // : p.relative(targetPath, from: sourcePath); + var parent = p.dirname(relative); + var filename = + p.setExtension(p.basenameWithoutExtension(relative), '.d.ts'); + relative = p.joinAll(p.split(parent).toList()..add(filename)); + ref = '/// '; + } if (!refs.contains(ref)) refs.add(ref); } diff --git a/angel_serialize_generator/pubspec.yaml b/angel_serialize_generator/pubspec.yaml index 134d9e6e..10dffe41 100644 --- a/angel_serialize_generator/pubspec.yaml +++ b/angel_serialize_generator/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_serialize_generator -version: 2.3.0 +version: 2.4.0 description: Model serialization generators, designed for use with Angel. Combine with angel_serialize for flexible modeling. author: Tobe O homepage: https://github.com/angel-dart/serialize diff --git a/angel_serialize_generator/test/models/author.d.ts b/angel_serialize_generator/test/models/author.d.ts index 54089a47..f5a987ea 100644 --- a/angel_serialize_generator/test/models/author.d.ts +++ b/angel_serialize_generator/test/models/author.d.ts @@ -1,4 +1,4 @@ -/// +/// // GENERATED CODE - DO NOT MODIFY BY HAND declare module 'angel_serialize_generator' { interface Author { diff --git a/angel_serialize_generator/test/models/author.dart b/angel_serialize_generator/test/models/author.dart index 7db71732..e66120a4 100644 --- a/angel_serialize_generator/test/models/author.dart +++ b/angel_serialize_generator/test/models/author.dart @@ -9,12 +9,13 @@ part 'author.g.dart'; @Serializable(serializers: Serializers.all) abstract class _Author extends Model { - @required + @SerializableField(isNullable: false) String get name; String get customMethod => 'hey!'; - @Required('Custom message for missing `age`') + @SerializableField( + isNullable: false, errorMessage: 'Custom message for missing `age`') int get age; List get books; @@ -35,13 +36,12 @@ abstract class _Library extends Model { @Serializable(serializers: Serializers.all) abstract class _Bookmark extends Model { - @SerializableField(exclude: true) final Book book; List get history; - @required + @SerializableField(isNullable: false) int get page; String get comment; diff --git a/angel_serialize_generator/test/models/has_map.dart b/angel_serialize_generator/test/models/has_map.dart new file mode 100644 index 00000000..3e0e2f82 --- /dev/null +++ b/angel_serialize_generator/test/models/has_map.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; +import 'package:angel_serialize/angel_serialize.dart'; +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +part 'has_map.g.dart'; + +Map _fromString(v) => json.decode(v.toString()) as Map; + +String _toString(Map v) => json.encode(v); + +@Serializable(autoIdAndDateFields: false) +abstract class _HasMap { + @SerializableField( + serializer: #_toString, deserializer: #_fromString, isNullable: false) + Map get value; +} diff --git a/angel_serialize_generator/test/models/has_map.g.dart b/angel_serialize_generator/test/models/has_map.g.dart new file mode 100644 index 00000000..8cfa4e7c --- /dev/null +++ b/angel_serialize_generator/test/models/has_map.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'has_map.dart'; + +// ************************************************************************** +// JsonModelGenerator +// ************************************************************************** + +@generatedSerializable +class HasMap implements _HasMap { + const HasMap({@required Map this.value}); + + @override + final Map value; + + HasMap copyWith({Map value}) { + return new HasMap(value: value ?? this.value); + } + + bool operator ==(other) { + return other is _HasMap && + const MapEquality( + keys: const DefaultEquality(), values: const DefaultEquality()) + .equals(other.value, value); + } + + @override + int get hashCode { + return hashObjects([value]); + } + + Map toJson() { + return HasMapSerializer.toMap(this); + } +} + +// ************************************************************************** +// SerializerGenerator +// ************************************************************************** + +abstract class HasMapSerializer { + static HasMap fromMap(Map map) { + if (map['value'] == null) { + throw new FormatException("Missing required field 'value' on HasMap."); + } + + return new HasMap(value: _fromString(map['value'])); + } + + static Map toMap(_HasMap model) { + if (model == null) { + return null; + } + if (model.value == null) { + throw new FormatException("Missing required field 'value' on HasMap."); + } + + return {'value': _toString(model.value)}; + } +} + +abstract class HasMapFields { + static const List allFields = const [value]; + + static const String value = 'value'; +} diff --git a/angel_serialize_generator/test/serializer_test.dart b/angel_serialize_generator/test/serializer_test.dart new file mode 100644 index 00000000..e530cb9f --- /dev/null +++ b/angel_serialize_generator/test/serializer_test.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; +import 'package:test/test.dart'; +import 'models/has_map.dart'; + +void main() { + var m = HasMap(value: {'foo': 'bar'}); + print(json.encode(m)); + + test('json', () { + expect(json.encode(m), r'{"value":"{\"foo\":\"bar\"}"}'); + }); + + test('decode', () { + var mm = json.decode(r'{"value":"{\"foo\":\"bar\"}"}') as Map; + var mmm = HasMapSerializer.fromMap(mm); + expect(mmm, m); + }); +}