platform/angel_serialize_generator/lib/model.dart

222 lines
7.4 KiB
Dart
Raw Normal View History

2018-02-27 19:42:06 +00:00
part of angel_serialize_generator;
2018-02-28 00:36:53 +00:00
class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
final bool autoIdAndDateFields;
const JsonModelGenerator({this.autoIdAndDateFields: true});
2018-02-28 00:36:53 +00:00
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
2018-02-28 00:36:53 +00:00
if (element.kind != ElementKind.CLASS)
throw 'Only classes can be annotated with a @Serializable() annotation.';
var ctx = await buildContext(element, annotation, buildStep,
await buildStep.resolver, true, autoIdAndDateFields != false);
2018-02-28 00:36:53 +00:00
2018-03-02 21:23:00 +00:00
var lib = new Library((b) {
2018-02-28 01:49:14 +00:00
generateClass(ctx, b, annotation);
2018-02-28 00:36:53 +00:00
});
var buf = lib.accept(new DartEmitter());
return buf.toString();
}
/// Generate an extended model class.
2018-02-28 01:49:14 +00:00
void generateClass(
2018-03-02 21:23:00 +00:00
BuildContext ctx, LibraryBuilder file, ConstantReader annotation) {
2018-02-28 00:36:53 +00:00
file.body.add(new Class((clazz) {
2018-06-27 05:36:57 +00:00
clazz..name = ctx.modelClassNameRecase.pascalCase;
if (shouldBeConstant(ctx)) {
clazz.implements.add(new Reference(ctx.originalClassName));
} else {
clazz.extend = new Reference(ctx.originalClassName);
}
2018-02-28 00:36:53 +00:00
2018-05-15 19:33:57 +00:00
//if (ctx.importsPackageMeta)
// clazz.annotations.add(new CodeExpression(new Code('immutable')));
2018-02-28 00:36:53 +00:00
for (var field in ctx.fields) {
clazz.fields.add(new Field((b) {
b
..name = field.name
..modifier = FieldModifier.final$
2018-03-02 21:23:00 +00:00
..annotations.add(new CodeExpression(new Code('override')))
2018-02-28 00:36:53 +00:00
..type = convertTypeReference(field.type);
}));
}
generateConstructor(ctx, clazz, file);
generateCopyWithMethod(ctx, clazz, file);
2018-05-13 18:02:47 +00:00
generateEqualsOperator(ctx, clazz, file);
2018-02-28 01:49:14 +00:00
// Generate toJson() method if necessary
var serializers = annotation.peek('serializers')?.listValue ?? [];
if (serializers.any((o) => o.toIntValue() == Serializers.json)) {
clazz.methods.add(new Method((method) {
method
..name = 'toJson'
..returns = new Reference('Map<String, dynamic>')
..body = new Code('return ${clazz.name}Serializer.toMap(this);');
}));
}
2018-02-28 00:36:53 +00:00
}));
}
2018-06-27 05:36:57 +00:00
bool shouldBeConstant(BuildContext ctx) {
// Check if all fields are without a getter
return !isAssignableToModel(ctx.clazz.type) &&
2018-06-27 05:51:21 +00:00
ctx.clazz.fields.every((f) =>
f.getter?.isAbstract != false && f.setter?.isAbstract != false);
2018-06-27 05:36:57 +00:00
}
2018-02-28 00:36:53 +00:00
/// Generate a constructor with named parameters.
void generateConstructor(
2018-03-02 21:23:00 +00:00
BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) {
2018-02-28 00:36:53 +00:00
clazz.constructors.add(new Constructor((constructor) {
2018-05-13 17:23:40 +00:00
// Add all `super` params
2018-06-27 05:36:57 +00:00
constructor.constant = ctx.clazz.unnamedConstructor?.isConst == true ||
shouldBeConstant(ctx);
2018-05-15 19:01:13 +00:00
for (var param in ctx.constructorParameters) {
constructor.requiredParameters.add(new Parameter((b) => b
..name = param.name
..type = convertTypeReference(param.type)));
}
for (var field in ctx.fields) {
2018-06-27 05:45:46 +00:00
if (!shouldBeConstant(ctx) && isListOrMapType(field.type)) {
2018-05-15 19:01:13 +00:00
String typeName = const TypeChecker.fromRuntime(List)
.isAssignableFromType(field.type)
? 'List'
: 'Map';
var defaultValue = typeName == 'List' ? '[]' : '{}';
constructor.initializers.add(new Code('''
this.${field.name} =
new $typeName.unmodifiable(${field.name} ?? $defaultValue)'''));
2018-05-13 17:23:40 +00:00
}
2018-05-15 19:01:13 +00:00
}
2018-05-13 17:23:40 +00:00
2018-02-28 00:36:53 +00:00
for (var field in ctx.fields) {
constructor.optionalParameters.add(new Parameter((b) {
b
2018-06-27 05:45:46 +00:00
..toThis = shouldBeConstant(ctx)
2018-02-28 00:36:53 +00:00
..name = field.name
2018-05-15 19:01:13 +00:00
..named = true;
if (!isListOrMapType(field.type))
b.toThis = true;
else {
b.type = convertTypeReference(field.type);
}
if (ctx.requiredFields.containsKey(field.name)) {
b.annotations.add(new CodeExpression(new Code('required')));
}
2018-02-28 00:36:53 +00:00
}));
}
2018-06-27 05:45:46 +00:00
if (ctx.constructorParameters.isNotEmpty) {
if (!shouldBeConstant(ctx) ||
ctx.clazz.unnamedConstructor?.isConst == true)
constructor.initializers.add(new Code(
'super(${ctx.constructorParameters.map((p) => p.name).join(
',')})'));
}
2018-02-28 00:36:53 +00:00
}));
}
/// Generate a `copyWith` method.
void generateCopyWithMethod(
2018-03-02 21:23:00 +00:00
BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) {
2018-02-28 00:36:53 +00:00
clazz.methods.add(new Method((method) {
method
..name = 'copyWith'
..returns = ctx.modelClassType;
2018-05-13 18:02:47 +00:00
2018-05-13 17:23:40 +00:00
// Add all `super` params
if (ctx.constructorParameters.isNotEmpty) {
for (var param in ctx.constructorParameters) {
method.requiredParameters.add(new Parameter((b) => b
..name = param.name
..type = convertTypeReference(param.type)));
}
}
2018-02-28 00:36:53 +00:00
var buf = new StringBuffer('return new ${ctx.modelClassName}(');
int i = 0;
2018-05-13 17:23:40 +00:00
for (var param in ctx.constructorParameters) {
if (i++ > 0) buf.write(', ');
buf.write(param.name);
}
2018-02-28 00:36:53 +00:00
// Add named parameters
for (var field in ctx.fields) {
method.optionalParameters.add(new Parameter((b) {
b
..name = field.name
..named = true
..type = convertTypeReference(field.type);
}));
if (i++ > 0) buf.write(', ');
buf.write('${field.name}: ${field.name} ?? this.${field.name}');
}
buf.write(');');
method.body = new Code(buf.toString());
}));
}
2018-05-13 18:02:47 +00:00
static String generateEquality(DartType type, [bool nullable = false]) {
2018-06-27 05:36:57 +00:00
//if (type is! InterfaceType) return 'const DefaultEquality()';
2018-05-13 18:02:47 +00:00
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]);
2018-05-15 19:01:13 +00:00
return 'const MapEquality<${it.typeArguments[0].name}, ${it
.typeArguments[1].name}>(keys: $keq, values: $veq)';
2018-05-13 18:02:47 +00:00
} else
2018-05-15 19:01:13 +00:00
return 'const MapEquality()<${it.typeArguments[0].name}, ${it
.typeArguments[1].name}>';
2018-05-13 18:02:47 +00:00
}
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('&&')};');
}));
}
2018-02-28 00:36:53 +00:00
}