diff --git a/README.md b/README.md index 73012fbd..eb611ef2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ dependencies: angel_serialize: ^2.0.0 dev_dependencies: angel_serialize_generator: ^2.0.0 - build_runner: ^0.7.0 + build_runner: ^0.8.0 ``` With the recent updates to `package:build_runner`, you can build models in @@ -41,7 +41,7 @@ class to have it serialized, and a serializable model class's name should also s with a leading underscore. In addition, you may consider using an `abstract` class to ensure immutability -of models.s +of models. Rather you writing the public class, `angel_serialize` does it for you. This means that the main class can have its constructors automatically generated, in addition into serialization functions. @@ -85,13 +85,15 @@ myFunction() { var map = BookSerializer.toMap(warAndPeace); // Also deserialize from Maps - var book = BookSerialize.fromMap(map); + var book = BookSerializer.fromMap(map); print(book.title); // 'War and Peace' // For compatibility with `JSON.encode`, a `toJson` method // is included that forwards to `BookSerializer.toMap`: expect(book.toJson(), map); + // Generated classes act as value types, and thus can be compared. + expect(BookSerializer.fromMap(map), equals(warAndPeace)); } ``` diff --git a/angel_serialize_generator/CHANGELOG.md b/angel_serialize_generator/CHANGELOG.md index 77144763..0574301a 100644 --- a/angel_serialize_generator/CHANGELOG.md +++ b/angel_serialize_generator/CHANGELOG.md @@ -1,6 +1,9 @@ # 2.0.6 * Support for using `abstract` to create immutable model classes. * Add support for custom constructor parameters. +* Closed [#21](https://github.com/angel-dart/serialize/issues/21) - better naming +of `Map` types. +* Added overridden `==` operators. # 2.0.5 * Deserialization now supports un-serialized `DateTime`. diff --git a/angel_serialize_generator/lib/model.dart b/angel_serialize_generator/lib/model.dart index dcf54dad..60e9957a 100644 --- a/angel_serialize_generator/lib/model.dart +++ b/angel_serialize_generator/lib/model.dart @@ -42,6 +42,7 @@ class JsonModelGenerator extends GeneratorForAnnotation { generateConstructor(ctx, clazz, file); generateCopyWithMethod(ctx, clazz, file); + generateEqualsOperator(ctx, clazz, file); // Generate toJson() method if necessary var serializers = annotation.peek('serializers')?.listValue ?? []; @@ -91,7 +92,7 @@ class JsonModelGenerator extends GeneratorForAnnotation { method ..name = 'copyWith' ..returns = ctx.modelClassType; - + // Add all `super` params if (ctx.constructorParameters.isNotEmpty) { for (var param in ctx.constructorParameters) { @@ -126,4 +127,50 @@ class JsonModelGenerator extends GeneratorForAnnotation { method.body = new Code(buf.toString()); })); } + + static String generateEquality(DartType type, [bool nullable = false]) { + //if (type is! InterfaceType) return 'const DefaultEquality()'; + var it = type as InterfaceType; + if (const TypeChecker.fromRuntime(List).isAssignableFromType(type)) { + if (it.typeParameters.length == 1) { + var eq = generateEquality(it.typeArguments[0]); + return 'const ListEquality<${it.typeArguments[0].name}>($eq)'; + } else + return 'const ListEquality<${it.typeArguments[0].name}>()'; + } else if (const TypeChecker.fromRuntime(Map).isAssignableFromType(type)) { + if (it.typeParameters.length == 2) { + var keq = generateEquality(it.typeArguments[0]), + veq = generateEquality(it.typeArguments[1]); + return 'const MapEquality<${it.typeArguments[0].name}, ${it.typeArguments[1].name}>(keys: $keq, values: $veq)'; + } else + return 'const MapEquality()<${it.typeArguments[0].name}, ${it.typeArguments[1].name}>'; + } + + return nullable ? null : 'const DefaultEquality<${type.name}>()'; + } + + static String Function(String, String) generateComparator(DartType type) { + if (type is! InterfaceType) return (a, b) => '$a == $b'; + var eq = generateEquality(type, true); + if (eq == null) return (a, b) => '$a == $b'; + return (a, b) => '$eq.equals($a, $b)'; + } + + void generateEqualsOperator( + BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) { + clazz.methods.add(new Method((method) { + method + ..name = 'operator ==' + ..returns = new Reference('bool') + ..requiredParameters.add(new Parameter((b) => b.name = 'other')); + + var buf = ['other is ${ctx.originalClassName}']; + + buf.addAll(ctx.fields.map((f) { + return generateComparator(f.type)('other.${f.name}', f.name); + })); + + method.body = new Code('return ${buf.join('&&')};'); + })); + } } diff --git a/angel_serialize_generator/lib/typescript.dart b/angel_serialize_generator/lib/typescript.dart index b173e65e..5a7ed47e 100644 --- a/angel_serialize_generator/lib/typescript.dart +++ b/angel_serialize_generator/lib/typescript.dart @@ -12,8 +12,8 @@ class TypeScriptDefinitionBuilder implements Builder { }; } - Future compileToTypeScriptType(String fieldName, InterfaceType type, - List ext, BuildStep buildStep) async { + Future compileToTypeScriptType(BuildContext ctx, String fieldName, + InterfaceType type, List ext, BuildStep buildStep) async { String typeScriptType = 'any'; var types = const { @@ -30,16 +30,16 @@ class TypeScriptDefinitionBuilder implements Builder { if (isListModelType(type)) { var arg = await compileToTypeScriptType( - fieldName, type.typeArguments[0], ext, buildStep); + ctx, fieldName, type.typeArguments[0], ext, buildStep); typeScriptType = '$arg[]'; - } else if (const TypeChecker.fromRuntime(List).isAssignableFromType(type) && + } else if (const TypeChecker.fromRuntime(Map).isAssignableFromType(type) && type.typeArguments.length == 2) { var key = await compileToTypeScriptType( - fieldName, type.typeArguments[0], ext, buildStep); + ctx, fieldName, type.typeArguments[0], ext, buildStep); var value = await compileToTypeScriptType( - fieldName, type.typeArguments[1], ext, buildStep); - var modelType = type.typeArguments[1]; - var ctx = await buildContext( + ctx, fieldName, type.typeArguments[1], ext, buildStep); + //var modelType = type.typeArguments[1]; + /*var innerCtx = await buildContext( modelType.element, new ConstantReader( serializableTypeChecker.firstAnnotationOf(modelType.element)), @@ -47,7 +47,7 @@ class TypeScriptDefinitionBuilder implements Builder { buildStep.resolver, autoSnakeCaseNames, true, - ); + );*/ typeScriptType = ctx.modelClassNameRecase.pascalCase + new ReCase(fieldName).pascalCase; @@ -63,7 +63,7 @@ class TypeScriptDefinitionBuilder implements Builder { typeScriptType = 'any[]'; else { var arg = await compileToTypeScriptType( - fieldName, type.typeArguments[0], ext, buildStep); + ctx, fieldName, type.typeArguments[0], ext, buildStep); typeScriptType = '$arg[]'; } } else if (isModelClass(type)) { @@ -148,7 +148,7 @@ class TypeScriptDefinitionBuilder implements Builder { var alias = ctx.resolveFieldName(field.name); var typeScriptType = await compileToTypeScriptType( - field.name, field.type, ext, buildStep); + ctx, field.name, field.type, ext, buildStep); // foo: string; buf.writeln('$alias?: $typeScriptType;'); diff --git a/angel_serialize_generator/test/book_test.dart b/angel_serialize_generator/test/book_test.dart index 5f288145..cde4d824 100644 --- a/angel_serialize_generator/test/book_test.dart +++ b/angel_serialize_generator/test/book_test.dart @@ -85,6 +85,12 @@ main() { expect(BookFields.camelCaseString, 'camelCase'); }); + test('equals', () { + expect(jkRowling.copyWith(), jkRowling); + expect(deathlyHallows.copyWith(), deathlyHallows); + expect(library.copyWith(), library); + }); + group('deserialization', () { test('deserialization sets proper fields', () { var book = BookSerializer.fromMap(deathlyHallowsMap); diff --git a/angel_serialize_generator/test/models/author.d.ts b/angel_serialize_generator/test/models/author.d.ts index 653333b3..b0979f27 100644 --- a/angel_serialize_generator/test/models/author.d.ts +++ b/angel_serialize_generator/test/models/author.d.ts @@ -1,7 +1,18 @@ // GENERATED CODE - DO NOT MODIFY BY HAND interface Library { id?: string; - collection?: any; + collection?: LibraryCollection; + created_at?: any; + updated_at?: any; +} +interface LibraryCollection { + [key: string]: Book; +} +interface Bookmark { + id?: string; + history?: number[]; + page?: number; + comment?: string; created_at?: any; updated_at?: any; } diff --git a/angel_serialize_generator/test/models/author.dart b/angel_serialize_generator/test/models/author.dart index ed100971..3e0cc868 100644 --- a/angel_serialize_generator/test/models/author.dart +++ b/angel_serialize_generator/test/models/author.dart @@ -2,6 +2,7 @@ library angel_serialize.test.models.author; import 'package:angel_framework/common.dart'; import 'package:angel_serialize/angel_serialize.dart'; +import 'package:collection/collection.dart'; import 'book.dart'; part 'author.g.dart'; @@ -27,13 +28,14 @@ abstract class _Library extends Model { Map get collection; } -@serializable +@Serializable(serializers: Serializers.all) abstract class _Bookmark extends Model { @exclude final Book book; + List get history; int get page; String get comment; - + _Bookmark(this.book); } diff --git a/angel_serialize_generator/test/models/author.g.dart b/angel_serialize_generator/test/models/author.g.dart index bf65832a..24dcf596 100644 --- a/angel_serialize_generator/test/models/author.g.dart +++ b/angel_serialize_generator/test/models/author.g.dart @@ -67,6 +67,20 @@ class Author extends _Author { updatedAt: updatedAt ?? this.updatedAt); } + bool operator ==(other) { + return other is _Author && + other.id == id && + other.name == name && + other.age == age && + const ListEquality(const DefaultEquality()) + .equals(other.books, books) && + other.newestBook == newestBook && + other.secret == secret && + other.obscured == obscured && + other.createdAt == createdAt && + other.updatedAt == updatedAt; + } + Map toJson() { return AuthorSerializer.toMap(this); } @@ -99,6 +113,17 @@ class Library extends _Library { updatedAt: updatedAt ?? this.updatedAt); } + bool operator ==(other) { + return other is _Library && + other.id == id && + const MapEquality( + keys: const DefaultEquality(), + values: const DefaultEquality()) + .equals(other.collection, collection) && + other.createdAt == createdAt && + other.updatedAt == updatedAt; + } + Map toJson() { return LibrarySerializer.toMap(this); } @@ -106,12 +131,20 @@ class Library extends _Library { class Bookmark extends _Bookmark { Bookmark(Book book, - {this.id, this.page, this.comment, this.createdAt, this.updatedAt}) + {this.id, + this.history, + this.page, + this.comment, + this.createdAt, + this.updatedAt}) : super(book); @override final String id; + @override + final List history; + @override final int page; @@ -126,18 +159,31 @@ class Bookmark extends _Bookmark { Bookmark copyWith(Book book, {String id, + List history, int page, String comment, DateTime createdAt, DateTime updatedAt}) { return new Bookmark(book, id: id ?? this.id, + history: history ?? this.history, page: page ?? this.page, comment: comment ?? this.comment, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt); } + bool operator ==(other) { + return other is _Bookmark && + other.id == id && + const ListEquality(const DefaultEquality()) + .equals(other.history, history) && + other.page == page && + other.comment == comment && + other.createdAt == createdAt && + other.updatedAt == updatedAt; + } + Map toJson() { return BookmarkSerializer.toMap(this); } diff --git a/angel_serialize_generator/test/models/author.serializer.g.dart b/angel_serialize_generator/test/models/author.serializer.g.dart index 66f9d976..2aadeddf 100644 --- a/angel_serialize_generator/test/models/author.serializer.g.dart +++ b/angel_serialize_generator/test/models/author.serializer.g.dart @@ -112,6 +112,7 @@ abstract class BookmarkSerializer { static Bookmark fromMap(Map map, Book book) { return new Bookmark(book, id: map['id'], + history: map['history'], page: map['page'], comment: map['comment'], createdAt: map['created_at'] != null @@ -129,6 +130,7 @@ abstract class BookmarkSerializer { static Map toMap(Bookmark model) { return { 'id': model.id, + 'history': model.history, 'page': model.page, 'comment': model.comment, 'created_at': model.createdAt?.toIso8601String(), @@ -140,6 +142,8 @@ abstract class BookmarkSerializer { abstract class BookmarkFields { static const String id = 'id'; + static const String history = 'history'; + static const String page = 'page'; static const String comment = 'comment'; diff --git a/angel_serialize_generator/test/models/book.dart b/angel_serialize_generator/test/models/book.dart index c1b5b8fe..a79b07cd 100644 --- a/angel_serialize_generator/test/models/book.dart +++ b/angel_serialize_generator/test/models/book.dart @@ -2,6 +2,7 @@ library angel_serialize.test.models.book; import 'package:angel_framework/common.dart'; import 'package:angel_serialize/angel_serialize.dart'; +import 'package:collection/collection.dart'; part 'book.g.dart'; part 'book.serializer.g.dart'; diff --git a/angel_serialize_generator/test/models/book.g.dart b/angel_serialize_generator/test/models/book.g.dart index 1ea39166..5c5bfbe9 100644 --- a/angel_serialize_generator/test/models/book.g.dart +++ b/angel_serialize_generator/test/models/book.g.dart @@ -67,6 +67,20 @@ class Book extends _Book { updatedAt: updatedAt ?? this.updatedAt); } + bool operator ==(other) { + return other is _Book && + other.id == id && + other.author == author && + other.title == title && + other.description == description && + other.pageCount == pageCount && + const ListEquality(const DefaultEquality()) + .equals(other.notModels, notModels) && + other.camelCaseString == camelCaseString && + other.createdAt == createdAt && + other.updatedAt == updatedAt; + } + Map toJson() { return BookSerializer.toMap(this); }