1.0.0-alpha

This commit is contained in:
thosakwe 2017-06-20 18:13:04 -04:00
parent 328224fc10
commit 1ec4b89707
9 changed files with 220 additions and 149 deletions

View file

@ -68,15 +68,8 @@ part 'book.g.dart';
@serializable
abstract class _Book extends Model {
@override
String id;
String author, title, description;
@Alias('page_count')
int pageCount;
@override
DateTime createdAt, updatedAt;
}
```
@ -129,15 +122,15 @@ class Book extends _Book {
title: data['title'],
description: data['description'],
pageCount: data['page_count'],
createdAt: data['createdAt'] is DateTime
? data['createdAt']
: (data['createdAt'] is String
? DateTime.parse(data['createdAt'])
createdAt: data['created_at'] is DateTime
? data['created_at']
: (data['created_at'] is String
? DateTime.parse(data['created_at'])
: null),
updatedAt: data['updatedAt'] is DateTime
? data['updatedAt']
: (data['updatedAt'] is String
? DateTime.parse(data['updatedAt'])
updatedAt: data['updated_at'] is DateTime
? data['updated_at']
: (data['updated_at'] is String
? DateTime.parse(data['updated_at'])
: null));
}
@ -147,8 +140,8 @@ class Book extends _Book {
'title': title,
'description': description,
'page_count': pageCount,
'createdAt': createdAt == null ? null : createdAt.toIso8601String(),
'updatedAt': updatedAt == null ? null : updatedAt.toIso8601String()
'created_at': createdAt == null ? null : createdAt.toIso8601String(),
'updated_at': updatedAt == null ? null : updatedAt.toIso8601String()
};
static Book parse(Map map) => new Book.fromJson(map);
@ -160,6 +153,9 @@ Whereas Dart fields conventionally are camelCased, most database columns
tend to be snake_cased. This is not a problem, because we can define an alias
for a field.
By default `angel_serialize` will transform keys into snake case. Use `Alias` to
provide a custom name, or pass `autoSnakeCaseNames`: `false` to the builder;
```dart
@serializable
abstract class _Spy extends Model {
@ -169,8 +165,10 @@ abstract class _Spy extends Model {
/// it will use 'agency_id'.
///
/// Hooray!
@Alias('agency_id')
String agencyId;
@Alias('foo')
String someOtherField;
}
```
@ -200,10 +198,15 @@ Be sure to use the underscored name of a child class (ex. `_Book`):
@serializable
abstract class _Author extends Model {
List<_Book> books;
@Alias('newest_book')
_Book newestBook;
Map<String, _Book> booksByIsbn;
}
```
```
The caveat here is that nested classes must be written in the same file. `source_gen`
otherwise will not be able to resolve the nested type.
# ID and Dates
This package will automatically generate `id`, `createdAt`, and `updatedAt` fields for you,
in the style of an Angel `Model`. To disable this, set `autoIdAndDates` to `false` in the
builder constructor.

79
lib/build_context.dart Normal file
View file

@ -0,0 +1,79 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:build/build.dart';
import 'package:path/path.dart' as p;
import 'package:recase/recase.dart';
import 'package:source_gen/src/annotation.dart';
import 'src/find_annotation.dart';
import 'context.dart';
// TODO: Should add id, createdAt, updatedAt...
BuildContext buildContext(
ClassElement clazz,
Serializable annotation,
BuildStep buildStep,
Resolver resolver,
bool autoSnakeCaseNames,
bool autoIdAndDateFields,
{bool heedExclude: true}) {
var ctx = new BuildContext(annotation,
originalClassName: clazz.name,
sourceFilename: p.basename(buildStep.inputId.path));
var lib = resolver.getLibrary(buildStep.inputId);
List<String> fieldNames = [];
for (var field in clazz.fields) {
if (field.getter != null && field.setter != null) {
fieldNames.add(field.name);
// Skip if annotated with @exclude
var excludeAnnotation = field.metadata.firstWhere(
(ann) => matchAnnotation(Exclude, ann),
orElse: () => null);
if (excludeAnnotation != null) continue;
// Check for alias
var alias = findAnnotation<Alias>(field, Alias);
if (alias?.name?.isNotEmpty == true) {
ctx.aliases[field.name] = alias.name;
} else if (autoSnakeCaseNames != false) {
ctx.aliases[field.name] = new ReCase(field.name).snakeCase;
}
ctx.fields.add(field);
}
}
if (autoIdAndDateFields != false) {
if (!fieldNames.contains('id')) {
var idField = new _ShimField('id', lib.context.typeProvider.stringType);
ctx.fields.insert(0, idField);
ctx.shimmed['id'] = true;
}
DartType dateTime;
['createdAt', 'updatedAt'].forEach((key) {
if (!fieldNames.contains(key)) {
if (dateTime == null) {
var coreLib = resolver.libraries.singleWhere((lib) => lib.isDartCore);
var dt = coreLib.getType('DateTime');
dateTime = dt.type;
}
var field = new _ShimField(key, dateTime);
ctx.aliases[key] = new ReCase(key).snakeCase;
ctx.fields.add(field);
ctx.shimmed[key] = true;
}
});
}
return ctx;
}
class _ShimField extends FieldElementImpl {
@override
final DartType type;
_ShimField(String name, this.type) : super(name, -1);
}

View file

@ -4,70 +4,57 @@ import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:code_builder/dart/core.dart';
import 'package:recase/recase.dart';
import 'package:source_gen/src/annotation.dart';
import 'package:source_gen/source_gen.dart';
import 'angel_serialize.dart';
import 'build_context.dart';
import 'context.dart';
class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
const JsonModelGenerator();
final bool autoSnakeCaseNames;
final bool autoIdAndDateFields;
const JsonModelGenerator(
{this.autoSnakeCaseNames: true, this.autoIdAndDateFields: true});
@override
Future<String> generateForAnnotatedElement(
Element element, Serializable annotation, BuildStep buildStep) async {
if (element.kind != ElementKind.CLASS)
throw 'Only classes can be annotated with a @Serializable() annotation.';
var lib = generateSerializerLibrary(element);
var ctx = buildContext(
element,
annotation,
buildStep,
await buildStep.resolver,
autoSnakeCaseNames != false,
autoIdAndDateFields != false);
var lib = generateSerializerLibrary(ctx);
return prettyToSource(lib.buildAst());
}
LibraryBuilder generateSerializerLibrary(ClassElement clazz) {
LibraryBuilder generateSerializerLibrary(BuildContext ctx) {
var lib = new LibraryBuilder();
lib.addMember(generateBaseModelClass(clazz));
lib.addMember(generateBaseModelClass(ctx));
return lib;
}
ClassBuilder generateBaseModelClass(ClassElement clazz) {
if (!clazz.name.startsWith('_'))
ClassBuilder generateBaseModelClass(BuildContext ctx) {
if (!ctx.originalClassName.startsWith('_'))
throw 'Classes annotated with @Serializable() must have names starting with a leading underscore.';
var genClassName = clazz.name.substring(1);
var genClass =
new ClassBuilder(genClassName, asExtends: new TypeBuilder(clazz.name));
Map<String, DartType> fields = {};
Map<String, String> aliases = {};
// Find all fields
for (var field in clazz.fields) {
// Skip if annotated with @exclude
var excludeAnnotation = field.metadata.firstWhere(
(ann) => matchAnnotation(Exclude, ann),
orElse: () => null);
if (excludeAnnotation == null) {
// Register the field
fields[field.name] = field.type;
// Search for Alias
var aliasAnnotation = field.metadata.firstWhere(
(ann) => matchAnnotation(Alias, ann),
orElse: () => null);
if (aliasAnnotation != null) {
var alias = instantiateAnnotation(aliasAnnotation) as Alias;
aliases[field.name] = alias.name;
}
}
}
var genClassName = ctx.modelClassName;
var genClass = new ClassBuilder(genClassName,
asExtends: new TypeBuilder(ctx.originalClassName));
// Now, add all fields to the base class
clazz.fields.forEach((field) {
ctx.fields.forEach((field) {
genClass.addField(
varField(field.name, type: new TypeBuilder(field.type.displayName))
..addAnnotation(reference('override')));
});
// Create convenience constructor
var convenienceConstructor = constructor(clazz.fields.map((field) {
var convenienceConstructor = constructor(ctx.fields.map((field) {
return thisField(named(parameter(field.name)));
}));
genClass.addConstructor(convenienceConstructor);
@ -75,20 +62,19 @@ class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
// Create toJson
Map<String, ExpressionBuilder> toJsonFields = {};
fields.forEach((fieldName, type) {
var resolvedName =
aliases.containsKey(fieldName) ? aliases[fieldName] : fieldName;
ctx.fields.forEach((field) {
var resolvedName = ctx.resolveFieldName(field.name);
ExpressionBuilder value;
// DateTime
if (type.name == 'DateTime') {
value = reference(fieldName).equals(literal(null)).ternary(
literal(null), reference(fieldName).invoke('toIso8601String', []));
if (field.type.name == 'DateTime') {
value = reference(field.name).equals(literal(null)).ternary(
literal(null), reference(field.name).invoke('toIso8601String', []));
}
// Anything else
else {
value = reference(fieldName);
value = reference(field.name);
}
toJsonFields[resolvedName] = value;
@ -106,12 +92,11 @@ class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
var fromJson = new ConstructorBuilder(name: 'fromJson', asFactory: true);
fromJson.addPositional(parameter('data', [new TypeBuilder('Map')]));
var namedParams =
fields.keys.fold<Map<String, ExpressionBuilder>>({}, (out, fieldName) {
var resolvedName =
aliases.containsKey(fieldName) ? aliases[fieldName] : fieldName;
ctx.fields.fold<Map<String, ExpressionBuilder>>({}, (out, field) {
var resolvedName = ctx.resolveFieldName(field.name);
var mapKey = reference('data')[literal(resolvedName)];
ExpressionBuilder value = mapKey;
var type = fields[fieldName];
var type = field.type;
// DateTime
if (type.name == 'DateTime') {
@ -256,7 +241,7 @@ class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
}
}
return out..[fieldName] = value;
return out..[field.name] = value;
});
fromJson.addStatement(new TypeBuilder(genClassName)
.newInstance([], named: namedParams).asReturn());

24
lib/context.dart Normal file
View file

@ -0,0 +1,24 @@
import 'package:analyzer/dart/element/element.dart';
import 'angel_serialize.dart';
class BuildContext {
final Map<String, String> aliases = {};
final Map<String, bool> shimmed = {};
final String originalClassName, sourceFilename;
// Todo: We can use analyzer to copy straight from Model class
final List<FieldElement> fields = [];
final Serializable annotation;
String primaryKeyName = 'id';
BuildContext(this.annotation, {this.originalClassName, this.sourceFilename});
String get modelClassName => originalClassName.startsWith('_')
? originalClassName.substring(1)
: originalClassName;
String get queryClassName => modelClassName + 'Query';
String get whereClassName => queryClassName + 'Where';
String resolveFieldName(String name) =>
aliases.containsKey(name) ? aliases[name] : name;
}

View file

@ -0,0 +1,8 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:source_gen/src/annotation.dart';
T findAnnotation<T>(FieldElement field, Type outType) {
var first = field.metadata
.firstWhere((ann) => matchAnnotation(outType, ann), orElse: () => null);
return first == null ? null : instantiateAnnotation(first);
}

View file

@ -1,5 +1,5 @@
name: angel_serialize
version: 0.0.0
version: 1.0.0-alpha
description: Model serialization generators.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/serialize

View file

@ -36,8 +36,8 @@ main() {
expect(
serializedDeathlyHallows['description'], deathlyHallows.description);
expect(serializedDeathlyHallows['page_count'], deathlyHallows.pageCount);
expect(serializedDeathlyHallows['createdAt'], isNull);
expect(serializedDeathlyHallows['updatedAt'],
expect(serializedDeathlyHallows['created_at'], isNull);
expect(serializedDeathlyHallows['updated_at'],
deathlyHallows.updatedAt.toIso8601String());
});

View file

@ -6,45 +6,21 @@ part 'book.g.dart';
@serializable
abstract class _Book extends Model {
@override
String id;
String author, title, description;
@Alias('page_count')
int pageCount;
@override
DateTime createdAt, updatedAt;
}
@serializable
abstract class _Author extends Model {
@override
String id;
String name;
int age;
@override
DateTime createdAt, updatedAt;
List<_Book> books;
@Alias('newest_book')
_Book newestBook;
@exclude
String secret;
}
@serializable
abstract class _Library extends Model {
@override
String id;
@override
DateTime createdAt, updatedAt;
Map<String, _Book> collection;
}

View file

@ -45,15 +45,15 @@ class Book extends _Book {
title: data['title'],
description: data['description'],
pageCount: data['page_count'],
createdAt: data['createdAt'] is DateTime
? data['createdAt']
: (data['createdAt'] is String
? DateTime.parse(data['createdAt'])
createdAt: data['created_at'] is DateTime
? data['created_at']
: (data['created_at'] is String
? DateTime.parse(data['created_at'])
: null),
updatedAt: data['updatedAt'] is DateTime
? data['updatedAt']
: (data['updatedAt'] is String
? DateTime.parse(data['updatedAt'])
updatedAt: data['updated_at'] is DateTime
? data['updated_at']
: (data['updated_at'] is String
? DateTime.parse(data['updated_at'])
: null));
}
@ -63,8 +63,8 @@ class Book extends _Book {
'title': title,
'description': description,
'page_count': pageCount,
'createdAt': createdAt == null ? null : createdAt.toIso8601String(),
'updatedAt': updatedAt == null ? null : updatedAt.toIso8601String()
'created_at': createdAt == null ? null : createdAt.toIso8601String(),
'updated_at': updatedAt == null ? null : updatedAt.toIso8601String()
};
static Book parse(Map map) => new Book.fromJson(map);
@ -85,12 +85,6 @@ class Author extends _Author {
@override
int age;
@override
DateTime createdAt;
@override
DateTime updatedAt;
@override
List<_Book> books;
@ -98,33 +92,25 @@ class Author extends _Author {
_Book newestBook;
@override
String secret;
DateTime createdAt;
@override
DateTime updatedAt;
Author(
{this.id,
this.name,
this.age,
this.createdAt,
this.updatedAt,
this.books,
this.newestBook,
this.secret});
this.createdAt,
this.updatedAt});
factory Author.fromJson(Map data) {
return new Author(
id: data['id'],
name: data['name'],
age: data['age'],
createdAt: data['createdAt'] is DateTime
? data['createdAt']
: (data['createdAt'] is String
? DateTime.parse(data['createdAt'])
: null),
updatedAt: data['updatedAt'] is DateTime
? data['updatedAt']
: (data['updatedAt'] is String
? DateTime.parse(data['updatedAt'])
: null),
books: data['books'] is List
? data['books']
.map((x) =>
@ -135,17 +121,27 @@ class Author extends _Author {
? null
: (data['newest_book'] is Book
? data['newest_book']
: new Book.fromJson(data['newest_book'])));
: new Book.fromJson(data['newest_book'])),
createdAt: data['created_at'] is DateTime
? data['created_at']
: (data['created_at'] is String
? DateTime.parse(data['created_at'])
: null),
updatedAt: data['updated_at'] is DateTime
? data['updated_at']
: (data['updated_at'] is String
? DateTime.parse(data['updated_at'])
: null));
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'age': age,
'createdAt': createdAt == null ? null : createdAt.toIso8601String(),
'updatedAt': updatedAt == null ? null : updatedAt.toIso8601String(),
'books': books,
'newest_book': newestBook
'newest_book': newestBook,
'created_at': createdAt == null ? null : createdAt.toIso8601String(),
'updated_at': updatedAt == null ? null : updatedAt.toIso8601String()
};
static Author parse(Map map) => new Author.fromJson(map);
@ -160,30 +156,20 @@ class Library extends _Library {
@override
String id;
@override
Map<String, _Book> collection;
@override
DateTime createdAt;
@override
DateTime updatedAt;
@override
Map<String, _Book> collection;
Library({this.id, this.createdAt, this.updatedAt, this.collection});
Library({this.id, this.collection, this.createdAt, this.updatedAt});
factory Library.fromJson(Map data) {
return new Library(
id: data['id'],
createdAt: data['createdAt'] is DateTime
? data['createdAt']
: (data['createdAt'] is String
? DateTime.parse(data['createdAt'])
: null),
updatedAt: data['updatedAt'] is DateTime
? data['updatedAt']
: (data['updatedAt'] is String
? DateTime.parse(data['updatedAt'])
: null),
collection: data['collection'] is Map
? data['collection'].keys.fold({}, (out, k) {
out[k] = data['collection'][k] == null
@ -193,14 +179,24 @@ class Library extends _Library {
: new Book.fromJson(data['collection'][k]));
return out;
})
: null);
: null,
createdAt: data['created_at'] is DateTime
? data['created_at']
: (data['created_at'] is String
? DateTime.parse(data['created_at'])
: null),
updatedAt: data['updated_at'] is DateTime
? data['updated_at']
: (data['updated_at'] is String
? DateTime.parse(data['updated_at'])
: null));
}
Map<String, dynamic> toJson() => {
'id': id,
'createdAt': createdAt == null ? null : createdAt.toIso8601String(),
'updatedAt': updatedAt == null ? null : updatedAt.toIso8601String(),
'collection': collection
'collection': collection,
'created_at': createdAt == null ? null : createdAt.toIso8601String(),
'updated_at': updatedAt == null ? null : updatedAt.toIso8601String()
};
static Library parse(Map map) => new Library.fromJson(map);