diff --git a/angel_serialize_generator/lib/angel_serialize_generator.dart b/angel_serialize_generator/lib/angel_serialize_generator.dart index 11d26be0..3ea115d1 100644 --- a/angel_serialize_generator/lib/angel_serialize_generator.dart +++ b/angel_serialize_generator/lib/angel_serialize_generator.dart @@ -24,6 +24,7 @@ TypeReference convertTypeReference(DartType t) { }); } +/// Determines if a type supports `package:angel_serialize`. bool isModelClass(DartType t) { if (t == null) return false; @@ -35,4 +36,11 @@ bool isModelClass(DartType t) { } else { return false; } +} + +/// Determines if a [DartType] is a `Map` with the second type argument being a `Model`. +bool isMapToModelType(InterfaceType t) { + return t.name == 'Map' && + t.typeArguments.length == 2 && + isModelClass(t.typeArguments[1]); } \ No newline at end of file diff --git a/angel_serialize_generator/lib/serialize.dart b/angel_serialize_generator/lib/serialize.dart index 16a1ba0e..17f2e000 100644 --- a/angel_serialize_generator/lib/serialize.dart +++ b/angel_serialize_generator/lib/serialize.dart @@ -40,13 +40,12 @@ class SerializerGenerator extends GeneratorForAnnotation { ..abstract = true; if (serializers.contains(Serializers.map)) { - // TODO: Generate fromMap + generateFromMapMethod(clazz, ctx, file); } if (serializers.contains(Serializers.map) || serializers.contains(Serializers.json)) { generateToMapMethod(clazz, ctx, file); - // TODO: Generate toJson } })); } @@ -80,27 +79,26 @@ class SerializerGenerator extends GeneratorForAnnotation { // Serialize dates if (dateTimeTypeChecker.isAssignableFromType(field.type)) - serializedRepresentation = 'model.${field.name}.toIso8601String()'; + serializedRepresentation = 'model.${field.name}?.toIso8601String()'; // Serialize model classes via `XSerializer.toMap` else if (isModelClass(field.type)) { var rc = new ReCase(field.type.name); serializedRepresentation = '${rc.pascalCase}Serializer.toMap(model.${field.name})'; - } - - else if (field.type is InterfaceType) { + } else if (field.type is InterfaceType) { var t = field.type as InterfaceType; if (t.name == 'List' && t.typeArguments.length == 1) { var rc = new ReCase(t.typeArguments[0].name); - serializedRepresentation = 'model.${field.name}.map(${rc.pascalCase}Serializer.toMap).toList()'; - } - - else if (t.name == 'Map' && t.typeArguments.length == 2 && isModelClass(t.typeArguments[1])) { + serializedRepresentation = 'model.${field.name}?.map(${rc + .pascalCase}Serializer.toMap)?.toList()'; + } else if (isMapToModelType(t)) { var rc = new ReCase(t.typeArguments[1].name); - serializedRepresentation = '''model.${field.name}.keys.fold({}, (map, key) { - return map..[key] = ${rc.pascalCase}Serializer.toMap(model.${field.name}[key]); + serializedRepresentation = + '''model.${field.name}.keys?.fold({}, (map, key) { + return map..[key] = ${rc.pascalCase}Serializer.toMap(model.${field + .name}[key]); })'''; } } @@ -112,4 +110,77 @@ class SerializerGenerator extends GeneratorForAnnotation { method.body = new Code(buf.toString()); })); } + + void generateFromMapMethod( + ClassBuilder clazz, BuildContext ctx, FileBuilder file) { + clazz.methods.add(new Method((method) { + method + ..static = true + ..name = 'fromMap' + ..returns = ctx.modelClassType + ..requiredParameters.add( + new Parameter((b) => b + ..name = 'map' + ..type = new Reference('Map')), + ); + + var buf = new StringBuffer('return new ${ctx.modelClassName}('); + int i = 0; + + // Add named parameters + for (var field in ctx.fields) { + if (ctx.excluded[field.name] == true) continue; + + var alias = ctx.resolveFieldName(field.name); + method.optionalParameters.add(new Parameter((b) { + b + ..name = field.name + ..named = true + ..type = convertTypeReference(field.type); + })); + + if (i++ > 0) buf.write(', '); + + String deserializedRepresentation = "map['$alias']"; + + // Deserialize dates + if (dateTimeTypeChecker.isAssignableFromType(field.type)) + deserializedRepresentation = + "map['$alias'] != null ? DateTime.parse(map['$alias']) : null"; + + // 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'])" + " : null"; + } else if (field.type is InterfaceType) { + var t = field.type as InterfaceType; + + if (t.name == 'List' && t.typeArguments.length == 1) { + var rc = new ReCase(t.typeArguments[0].name); + deserializedRepresentation = "map['$alias'] is Iterable" + " ? map['$alias'].map(${rc + .pascalCase}Serializer.fromMap).toList()" + " : null"; + } else if (isMapToModelType(t)) { + var rc = new ReCase(t.typeArguments[1].name); + deserializedRepresentation = ''' + map['$alias'] is Map + ? map['$alias'].keys.fold({}, (out, key) { + return out..[key] = ${rc + .pascalCase}Serializer.fromMap(map['$alias'][key]); + }) + : null + '''; + } + } + + buf.write('${field.name}: $deserializedRepresentation'); + } + + buf.write(');'); + method.body = new Code(buf.toString()); + })); + } } diff --git a/angel_serialize_generator/test/book_test.dart b/angel_serialize_generator/test/book_test.dart index 13f0b4f9..f8d19a84 100644 --- a/angel_serialize_generator/test/book_test.dart +++ b/angel_serialize_generator/test/book_test.dart @@ -66,7 +66,7 @@ main() { group('deserialization', () { test('deserialization sets proper fields', () { - var book = new Book.fromJson(deathlyHallowsMap); + var book = BookSerializer.fromMap(deathlyHallowsMap); expect(book.id, deathlyHallows.id); expect(book.author, deathlyHallows.author); expect(book.description, deathlyHallows.description); @@ -76,7 +76,7 @@ main() { }); group('nested @serializable', () { - var author = new Author.fromJson(serializedJkRowling); + var author = AuthorSerializer.fromMap(serializedJkRowling); test('nested @serializable class is deserialized', () { var newestBook = author.newestBook; @@ -98,7 +98,7 @@ main() { }); test('map with @serializable class as second key is deserialized', () { - var lib = new Library.fromJson(serializedLibrary); + var lib = LibrarySerializer.fromMap(serializedLibrary); expect(lib.collection, allOf(isNotEmpty, hasLength(1))); expect(lib.collection.keys.first, deathlyHallowsIsbn); var book = lib.collection[deathlyHallowsIsbn]; diff --git a/angel_serialize_generator/test/models/author.serializer.g.dart b/angel_serialize_generator/test/models/author.serializer.g.dart index 405355e3..6ac9bf7f 100644 --- a/angel_serialize_generator/test/models/author.serializer.g.dart +++ b/angel_serialize_generator/test/models/author.serializer.g.dart @@ -7,28 +7,75 @@ part of angel_serialize.test.models.author; // ************************************************************************** abstract class AuthorSerializer { + static Author fromMap(Map map, + {String id, + String name, + int age, + List books, + Book newestBook, + DateTime createdAt, + DateTime updatedAt}) { + return new Author( + id: map['id'], + name: map['name'], + age: map['age'], + books: map['books'] is Iterable + ? map['books'].map(BookSerializer.fromMap).toList() + : null, + newestBook: map['newest_book'] != null + ? BookSerializer.fromMap(map['newest_book']) + : null, + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + updatedAt: map['updated_at'] != null + ? DateTime.parse(map['updated_at']) + : null); + } + static Map toMap(Author model) { return { 'id': model.id, 'name': model.name, 'age': model.age, - 'books': model.books.map(BookSerializer.toMap).toList(), + 'books': model.books?.map(BookSerializer.toMap)?.toList(), 'newest_book': BookSerializer.toMap(model.newestBook), - 'created_at': model.createdAt.toIso8601String(), - 'updated_at': model.updatedAt.toIso8601String() + 'created_at': model.createdAt?.toIso8601String(), + 'updated_at': model.updatedAt?.toIso8601String() }; } } abstract class LibrarySerializer { + static Library fromMap(Map map, + {String id, + Map collection, + DateTime createdAt, + DateTime updatedAt}) { + return new Library( + id: map['id'], + collection: map['collection'] is Map + ? map['collection'].keys.fold({}, (out, key) { + return out + ..[key] = BookSerializer.fromMap(map['collection'][key]); + }) + : null, + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + updatedAt: map['updated_at'] != null + ? DateTime.parse(map['updated_at']) + : null); + } + static Map toMap(Library model) { return { 'id': model.id, - 'collection': model.collection.keys.fold({}, (map, key) { + 'collection': model.collection.keys?.fold({}, (map, key) { return map..[key] = BookSerializer.toMap(model.collection[key]); }), - 'created_at': model.createdAt.toIso8601String(), - 'updated_at': model.updatedAt.toIso8601String() + 'created_at': model.createdAt?.toIso8601String(), + 'updated_at': model.updatedAt?.toIso8601String() }; } } diff --git a/angel_serialize_generator/test/models/book.serializer.g.dart b/angel_serialize_generator/test/models/book.serializer.g.dart index 830254b2..7ab5ab17 100644 --- a/angel_serialize_generator/test/models/book.serializer.g.dart +++ b/angel_serialize_generator/test/models/book.serializer.g.dart @@ -7,6 +7,28 @@ part of angel_serialize.test.models.book; // ************************************************************************** abstract class BookSerializer { + static Book fromMap(Map map, + {String id, + String author, + String title, + String description, + int pageCount, + DateTime createdAt, + DateTime updatedAt}) { + return new Book( + id: map['id'], + author: map['author'], + title: map['title'], + description: map['description'], + pageCount: map['page_count'], + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + updatedAt: map['updated_at'] != null + ? DateTime.parse(map['updated_at']) + : null); + } + static Map toMap(Book model) { return { 'id': model.id, @@ -14,8 +36,8 @@ abstract class BookSerializer { 'title': model.title, 'description': model.description, 'page_count': model.pageCount, - 'created_at': model.createdAt.toIso8601String(), - 'updated_at': model.updatedAt.toIso8601String() + 'created_at': model.createdAt?.toIso8601String(), + 'updated_at': model.updatedAt?.toIso8601String() }; } }