311 lines
10 KiB
Dart
311 lines
10 KiB
Dart
part of angel3_serialize_generator;
|
|
|
|
class JsonModelGenerator extends GeneratorForAnnotation<Serializable> {
|
|
const JsonModelGenerator();
|
|
|
|
@override
|
|
Future<String> generateForAnnotatedElement(
|
|
Element element, ConstantReader annotation, BuildStep buildStep) async {
|
|
if (element.kind != ElementKind.CLASS) {
|
|
throw 'Only classes can be annotated with a @Serializable() annotation.';
|
|
}
|
|
|
|
var ctx = await buildContext(element as ClassElement, annotation, buildStep,
|
|
buildStep.resolver, true);
|
|
|
|
if (ctx == null) {
|
|
log.severe('Invalid builder context');
|
|
throw 'Invalid builder context';
|
|
}
|
|
|
|
var lib = Library((b) {
|
|
generateClass(ctx, b, annotation);
|
|
});
|
|
|
|
var buf = lib.accept(DartEmitter(useNullSafetySyntax: true));
|
|
return buf.toString();
|
|
}
|
|
|
|
/// Generate an extended model class.
|
|
void generateClass(
|
|
BuildContext ctx, LibraryBuilder file, ConstantReader annotation) {
|
|
file.body.add(Class((clazz) {
|
|
//log.fine('Generate Class: ${ctx.modelClassNameRecase.pascalCase}');
|
|
clazz
|
|
..name = ctx.modelClassNameRecase.pascalCase
|
|
..annotations.add(refer('generatedSerializable'));
|
|
|
|
for (var ann in ctx.includeAnnotations) {
|
|
clazz.annotations.add(convertObject(ann));
|
|
}
|
|
|
|
if (shouldBeConstant(ctx)) {
|
|
clazz.implements.add(Reference(ctx.originalClassName));
|
|
} else {
|
|
clazz.extend = Reference(ctx.originalClassName);
|
|
}
|
|
|
|
//if (ctx.importsPackageMeta)
|
|
// clazz.annotations.add(CodeExpression(Code('immutable')));
|
|
|
|
// Generate the fields for the class
|
|
for (var field in ctx.fields) {
|
|
//log.fine('Generate Field: ${field.name}');
|
|
|
|
clazz.fields.add(Field((b) {
|
|
b
|
|
..name = field.name
|
|
//..modifier = FieldModifier.final$
|
|
//..annotations.add(CodeExpression(Code('override')))
|
|
..annotations.add(refer('override'))
|
|
..type = convertTypeReference(field.type);
|
|
|
|
// Fields should only be forced-final if the original field has no setter.
|
|
//log.fine('Final: ${field.isFinal}');
|
|
//if (field.setter == null && field is! ShimFieldImpl) {
|
|
//if (field.isFinal) {
|
|
// b.modifier = FieldModifier.final$;
|
|
//}
|
|
|
|
for (var el in [field.getter, field]) {
|
|
//if (el?.documentationComment != null) {
|
|
b.docs.addAll(el?.documentationComment?.split('\n') ?? []);
|
|
//}
|
|
}
|
|
}));
|
|
}
|
|
|
|
generateConstructor(ctx, clazz, file);
|
|
generateCopyWithMethod(ctx, clazz, file);
|
|
generateEqualsOperator(ctx, clazz, file);
|
|
generateHashCode(ctx, clazz);
|
|
generateToString(ctx, clazz);
|
|
|
|
// Generate toJson() method if necessary
|
|
var serializers = annotation.peek('serializers')?.listValue ?? [];
|
|
|
|
if (serializers.any((o) => o.toIntValue() == Serializers.json)) {
|
|
clazz.methods.add(Method((method) {
|
|
method
|
|
..name = 'toJson'
|
|
..returns = Reference('Map<String, dynamic>')
|
|
..body = Code('return ${clazz.name}Serializer.toMap(this);');
|
|
}));
|
|
}
|
|
}));
|
|
}
|
|
|
|
bool shouldBeConstant(BuildContext ctx) {
|
|
// Check if all fields are without a getter
|
|
return !isAssignableToModel(ctx.clazz.thisType) &&
|
|
ctx.clazz.fields.every((f) =>
|
|
f.getter?.isAbstract != false && f.setter?.isAbstract != false);
|
|
}
|
|
|
|
/// Generate a constructor with named parameters.
|
|
void generateConstructor(
|
|
BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) {
|
|
clazz.constructors.add(Constructor((constructor) {
|
|
// Add all `super` params
|
|
//constructor.constant = (ctx.clazz.unnamedConstructor?.isConst == true ||
|
|
// shouldBeConstant(ctx)) &&
|
|
// ctx.fields.every((f) {
|
|
// return f.setter == null && f is! ShimFieldImpl;
|
|
// });
|
|
|
|
for (var param in ctx.constructorParameters) {
|
|
//log.fine('Contructor Parameter: ${param.name}');
|
|
constructor.requiredParameters.add(Parameter((b) => b
|
|
..name = param.name
|
|
..type = convertTypeReference(param.type)));
|
|
}
|
|
|
|
for (var field in ctx.fields) {
|
|
if (!shouldBeConstant(ctx) && isListOrMapType(field.type)) {
|
|
var typeName = const TypeChecker.fromRuntime(List)
|
|
.isAssignableFromType(field.type)
|
|
? 'List'
|
|
: 'Map';
|
|
String? defaultValue = typeName == 'List' ? '[]' : '{}';
|
|
var existingDefault = ctx.defaults[field.name];
|
|
|
|
if (existingDefault != null) {
|
|
defaultValue = dartObjectToString(existingDefault);
|
|
}
|
|
|
|
constructor.initializers.add(Code('''
|
|
this.${field.name} =
|
|
$typeName.unmodifiable(${field.name} ?? $defaultValue)'''));
|
|
}
|
|
}
|
|
|
|
// Generate the parameters for the constructor
|
|
for (var field in ctx.fields) {
|
|
//log.fine('Contructor Field: ${field.name}');
|
|
|
|
constructor.optionalParameters.add(Parameter((b) {
|
|
b
|
|
..toThis = shouldBeConstant(ctx)
|
|
..name = field.name
|
|
..named = true;
|
|
|
|
var existingDefault = ctx.defaults[field.name];
|
|
|
|
if (existingDefault != null) {
|
|
b.defaultTo = Code(dartObjectToString(existingDefault)!);
|
|
}
|
|
|
|
if (!isListOrMapType(field.type)) {
|
|
b.toThis = true;
|
|
} else if (!b.toThis) {
|
|
b.type = convertTypeReference(field.type);
|
|
}
|
|
|
|
if (ctx.requiredFields.containsKey(field.name) &&
|
|
b.defaultTo == null ||
|
|
(field.type.nullabilitySuffix != NullabilitySuffix.question)) {
|
|
//b.annotations.add(CodeExpression(Code('required')));
|
|
b.required = true;
|
|
}
|
|
}));
|
|
}
|
|
|
|
if (ctx.constructorParameters.isNotEmpty) {
|
|
if (!shouldBeConstant(ctx) ||
|
|
ctx.clazz.unnamedConstructor?.isConst == true) {
|
|
constructor.initializers.add(Code(
|
|
'super(${ctx.constructorParameters.map((p) => p.name).join(',')})'));
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
/// Generate a `copyWith` method.
|
|
void generateCopyWithMethod(
|
|
BuildContext ctx, ClassBuilder clazz, LibraryBuilder file) {
|
|
clazz.methods.add(Method((method) {
|
|
method
|
|
..name = 'copyWith'
|
|
..returns = ctx.modelClassType;
|
|
|
|
// Add all `super` params
|
|
if (ctx.constructorParameters.isNotEmpty) {
|
|
for (var param in ctx.constructorParameters) {
|
|
method.requiredParameters.add(Parameter((b) => b
|
|
..name = param.name
|
|
..type = convertTypeReference(param.type)));
|
|
}
|
|
}
|
|
|
|
var buf = StringBuffer('return ${ctx.modelClassName}(');
|
|
var i = 0;
|
|
for (var param in ctx.constructorParameters) {
|
|
if (i++ > 0) buf.write(', ');
|
|
buf.write(param.name);
|
|
}
|
|
|
|
// Add named parameters
|
|
for (var field in ctx.fields) {
|
|
method.optionalParameters.add(Parameter((b) {
|
|
b
|
|
..name = field.name
|
|
..named = true
|
|
..type = convertTypeReference(field.type, forceNullable: true);
|
|
}));
|
|
|
|
if (i++ > 0) buf.write(', ');
|
|
buf.write('${field.name}: ${field.name} ?? this.${field.name}');
|
|
}
|
|
|
|
buf.write(');');
|
|
method.body = Code(buf.toString());
|
|
}));
|
|
}
|
|
|
|
static String? generateEquality(DartType type, [bool nullable = false]) {
|
|
if (type is InterfaceType) {
|
|
if (const TypeChecker.fromRuntime(List).isAssignableFromType(type)) {
|
|
if (type.typeArguments.length == 1) {
|
|
var eq = generateEquality(type.typeArguments[0]);
|
|
return 'ListEquality<${type.typeArguments[0].element!.name}>($eq)';
|
|
} else {
|
|
return 'ListEquality()';
|
|
}
|
|
} else if (const TypeChecker.fromRuntime(Map)
|
|
.isAssignableFromType(type)) {
|
|
if (type.typeArguments.length == 2) {
|
|
var keq = generateEquality(type.typeArguments[0]),
|
|
veq = generateEquality(type.typeArguments[1]);
|
|
return 'MapEquality<${type.typeArguments[0].element!.name}, ${type.typeArguments[1].element!.name}>(keys: $keq, values: $veq)';
|
|
} else {
|
|
return 'MapEquality()';
|
|
}
|
|
}
|
|
|
|
return nullable ? null : 'DefaultEquality<${type.element.name}>()';
|
|
} else {
|
|
return 'DefaultEquality()';
|
|
}
|
|
}
|
|
|
|
static String Function(String, String) generateComparator(DartType type) {
|
|
if (type is! InterfaceType || type.element.displayName == 'dynamic') {
|
|
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 generateHashCode(BuildContext? ctx, ClassBuilder clazz) {
|
|
clazz.methods.add(Method((method) {
|
|
method
|
|
..name = 'hashCode'
|
|
..type = MethodType.getter
|
|
..returns = refer('int')
|
|
..annotations.add(refer('override'))
|
|
..body = refer('hashObjects')
|
|
.call([literalList(ctx!.fields.map((f) => refer(f.name)))])
|
|
.returned
|
|
.statement;
|
|
}));
|
|
}
|
|
|
|
void generateToString(BuildContext? ctx, ClassBuilder clazz) {
|
|
clazz.methods.add(Method((b) {
|
|
b
|
|
..name = 'toString'
|
|
..returns = refer('String')
|
|
..annotations.add(refer('override'))
|
|
..body = Block((b) {
|
|
var buf = StringBuffer('\'${ctx!.modelClassName}(');
|
|
var i = 0;
|
|
for (var field in ctx.fields) {
|
|
if (i++ > 0) buf.write(', ');
|
|
buf.write('${field.name}=\$${field.name}');
|
|
}
|
|
buf.write(')\'');
|
|
b.addExpression(CodeExpression(Code(buf.toString())).returned);
|
|
});
|
|
}));
|
|
}
|
|
|
|
void generateEqualsOperator(
|
|
BuildContext? ctx, ClassBuilder clazz, LibraryBuilder file) {
|
|
clazz.methods.add(Method((method) {
|
|
method
|
|
..name = 'operator =='
|
|
..annotations.add(refer('override'))
|
|
..returns = Reference('bool')
|
|
..requiredParameters.add(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 = Code('return ${buf.join('&&')};');
|
|
}));
|
|
}
|
|
}
|