platform/angel_serialize_generator/lib/serialize.dart

332 lines
11 KiB
Dart
Raw Normal View History

part of angel_serialize_generator;
class SerializerGenerator extends GeneratorForAnnotation<Serializable> {
final bool autoSnakeCaseNames;
const SerializerGenerator({this.autoSnakeCaseNames: true});
@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,
await buildStep.resolver, true, autoSnakeCaseNames != false);
var serializers = annotation.peek('serializers')?.listValue ?? [];
if (serializers.isEmpty) return null;
// Check if any serializer is recognized
if (!serializers.any((s) => Serializers.all.contains(s.toIntValue()))) {
return null;
}
2018-03-02 21:23:00 +00:00
var lib = new Library((b) {
2018-02-28 01:10:57 +00:00
generateClass(serializers.map((s) => s.toIntValue()).toList(), ctx, b);
2018-03-09 12:39:21 +00:00
generateFieldsClass(ctx, b);
});
var buf = lib.accept(new DartEmitter());
return buf.toString();
}
/// Generate a serializer class.
2018-02-28 01:10:57 +00:00
void generateClass(
2018-03-02 21:23:00 +00:00
List<int> serializers, BuildContext ctx, LibraryBuilder file) {
file.body.add(new Class((clazz) {
clazz
..name = '${ctx.modelClassNameRecase.pascalCase}Serializer'
..abstract = true;
2018-02-28 01:10:57 +00:00
if (serializers.contains(Serializers.map)) {
2018-02-28 02:10:43 +00:00
generateFromMapMethod(clazz, ctx, file);
2018-02-28 01:10:57 +00:00
}
if (serializers.contains(Serializers.map) ||
serializers.contains(Serializers.json)) {
generateToMapMethod(clazz, ctx, file);
}
}));
}
void generateToMapMethod(
2018-03-02 21:23:00 +00:00
ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) {
2018-02-28 01:10:57 +00:00
clazz.methods.add(new Method((method) {
method
2018-02-28 01:41:19 +00:00
..static = true
2018-02-28 01:10:57 +00:00
..name = 'toMap'
2018-02-28 01:41:19 +00:00
..returns = new Reference('Map<String, dynamic>')
..requiredParameters.add(new Parameter((b) {
b
..name = 'model'
2018-12-08 22:29:06 +00:00
..type = refer(ctx.originalClassName);
2018-02-28 01:41:19 +00:00
}));
2018-02-28 01:10:57 +00:00
2018-05-15 19:33:57 +00:00
var buf = new StringBuffer();
ctx.requiredFields.forEach((key, msg) {
if (ctx.excluded[key]?.canSerialize == false) return;
buf.writeln('''
if (model.$key == null) {
throw new FormatException("$msg");
}
''');
});
buf.writeln('return {');
2018-02-28 01:10:57 +00:00
int i = 0;
// Add named parameters
for (var field in ctx.fields) {
// Skip excluded fields
if (ctx.excluded[field.name]?.canSerialize == false) continue;
2018-02-28 01:10:57 +00:00
var alias = ctx.resolveFieldName(field.name);
if (i++ > 0) buf.write(', ');
2018-02-28 01:41:19 +00:00
String serializedRepresentation = 'model.${field.name}';
2018-02-28 01:10:57 +00:00
2018-06-28 01:58:42 +00:00
String serializerToMap(ReCase rc, String value) {
2018-12-08 22:29:06 +00:00
// if (rc.pascalCase == ctx.modelClassName) {
// return '($value)?.toJson()';
// }
2018-06-28 01:58:42 +00:00
return '${rc.pascalCase}Serializer.toMap($value)';
}
2018-02-28 01:18:48 +00:00
// Serialize dates
2018-02-28 01:10:57 +00:00
if (dateTimeTypeChecker.isAssignableFromType(field.type))
2018-02-28 02:10:43 +00:00
serializedRepresentation = 'model.${field.name}?.toIso8601String()';
2018-02-28 01:10:57 +00:00
2018-02-28 01:18:48 +00:00
// Serialize model classes via `XSerializer.toMap`
else if (isModelClass(field.type)) {
2018-02-28 01:20:42 +00:00
var rc = new ReCase(field.type.name);
2018-02-28 01:41:19 +00:00
serializedRepresentation =
2018-06-28 01:58:42 +00:00
'${serializerToMap(rc, 'model.${field.name}')}';
2018-02-28 02:10:43 +00:00
} else if (field.type is InterfaceType) {
2018-02-28 01:41:19 +00:00
var t = field.type as InterfaceType;
if (isListOfModelType(t)) {
2018-12-08 22:29:06 +00:00
var name = t.typeArguments[0].name;
if (name.startsWith('_')) name = name.substring(1);
var rc = new ReCase(name);
var m = serializerToMap(rc, 'm');
2018-07-11 13:06:03 +00:00
serializedRepresentation = '''
model.${field.name}
2018-12-08 22:29:06 +00:00
?.map((m) => $m)
2018-07-11 13:06:03 +00:00
?.toList()''';
2018-02-28 02:10:43 +00:00
} else if (isMapToModelType(t)) {
2018-02-28 01:43:43 +00:00
var rc = new ReCase(t.typeArguments[1].name);
2018-02-28 02:10:43 +00:00
serializedRepresentation =
'''model.${field.name}.keys?.fold({}, (map, key) {
2018-07-11 13:06:03 +00:00
return map..[key] =
${serializerToMap(rc, 'model.${field.name}[key]')};
2018-02-28 01:43:43 +00:00
})''';
2018-06-27 05:36:57 +00:00
} else if (t.element.isEnum) {
serializedRepresentation = '''
model.${field.name} == null ?
null
: ${t.name}.values.indexOf(model.${field.name})
''';
2018-12-08 20:53:49 +00:00
} else if (const TypeChecker.fromRuntime(Uint8List)
.isAssignableFromType(t)) {
serializedRepresentation = '''
model.${field.name} == null ?
null
: base64.encode(model.${field.name})
''';
2018-02-28 01:41:19 +00:00
}
2018-02-28 01:18:48 +00:00
}
2018-02-28 01:10:57 +00:00
buf.write("'$alias': $serializedRepresentation");
}
buf.write('};');
2018-06-28 01:58:42 +00:00
method.body = new Block.of([
new Code('if (model == null) { return null; }'),
new Code(buf.toString()),
]);
}));
}
2018-02-28 02:10:43 +00:00
void generateFromMapMethod(
2018-03-02 21:23:00 +00:00
ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) {
2018-02-28 02:10:43 +00:00
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')),
);
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-05-15 19:33:57 +00:00
var buf = new StringBuffer();
ctx.requiredFields.forEach((key, msg) {
if (ctx.excluded[key]?.canDeserialize == false) return;
var name = ctx.resolveFieldName(key);
buf.writeln('''
if (map['$name'] == null) {
throw new FormatException("$msg");
}
''');
});
buf.writeln('return new ${ctx.modelClassName}(');
2018-02-28 02:10:43 +00:00
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 02:10:43 +00:00
for (var field in ctx.fields) {
if (ctx.excluded[field.name]?.canDeserialize == false) continue;
2018-02-28 02:10:43 +00:00
var alias = ctx.resolveFieldName(field.name);
if (i++ > 0) buf.write(', ');
String deserializedRepresentation =
"map['$alias'] as ${typeToString(field.type)}";
2018-02-28 02:10:43 +00:00
// Deserialize dates
if (dateTimeTypeChecker.isAssignableFromType(field.type))
deserializedRepresentation = "map['$alias'] != null ? "
"(map['$alias'] is DateTime ? (map['$alias'] as DateTime) : DateTime.parse(map['$alias'].toString()))"
" : null";
2018-02-28 02:10:43 +00:00
// Serialize model classes via `XSerializer.toMap`
else if (isModelClass(field.type)) {
var rc = new ReCase(field.type.name);
deserializedRepresentation = "map['$alias'] != null"
2018-07-11 13:06:03 +00:00
" ? ${rc.pascalCase}Serializer.fromMap(map['$alias'] as Map)"
2018-02-28 02:10:43 +00:00
" : null";
} else if (field.type is InterfaceType) {
var t = field.type as InterfaceType;
if (isListOfModelType(t)) {
2018-02-28 02:10:43 +00:00
var rc = new ReCase(t.typeArguments[0].name);
deserializedRepresentation = "map['$alias'] is Iterable"
2018-06-27 05:36:57 +00:00
" ? new List.unmodifiable(((map['$alias'] as Iterable)"
".where((x) => x is Map) as Iterable<Map>)"
".map(${rc.pascalCase}Serializer.fromMap))"
2018-02-28 02:10:43 +00:00
" : null";
} else if (isMapToModelType(t)) {
var rc = new ReCase(t.typeArguments[1].name);
deserializedRepresentation = '''
map['$alias'] is Map
2018-06-23 04:52:46 +00:00
? new Map.unmodifiable((map['$alias'] as Map).keys.fold({}, (out, key) {
2018-06-27 05:36:57 +00:00
return out..[key] = ${rc.pascalCase}Serializer
2018-06-29 15:30:47 +00:00
.fromMap(((map['$alias'] as Map)[key]) as Map);
2018-05-15 19:01:13 +00:00
}))
2018-02-28 02:10:43 +00:00
: null
''';
2018-06-27 05:36:57 +00:00
} else if (t.element.isEnum) {
deserializedRepresentation = '''
map['$alias'] is ${t.name}
2018-07-11 13:06:03 +00:00
? (map['$alias'] as ${t.name})
2018-06-27 05:36:57 +00:00
:
(
map['$alias'] is int
2018-07-11 13:06:03 +00:00
? ${t.name}.values[map['$alias'] as int]
2018-06-27 05:36:57 +00:00
: null
)
''';
2018-12-08 20:53:49 +00:00
} else if (const TypeChecker.fromRuntime(List)
.isAssignableFromType(t) &&
t.typeArguments.length == 1) {
var arg = convertTypeReference(t.typeArguments[0])
.accept(new DartEmitter());
deserializedRepresentation = '''
map['$alias'] is Iterable
? (map['$alias'] as Iterable).cast<$arg>().toList()
: null
''';
} else if (const TypeChecker.fromRuntime(Map)
.isAssignableFromType(t) &&
t.typeArguments.length == 2) {
var key = convertTypeReference(t.typeArguments[0])
.accept(new DartEmitter());
var value = convertTypeReference(t.typeArguments[1])
.accept(new DartEmitter());
deserializedRepresentation = '''
map['$alias'] is Map
? (map['$alias'] as Map).cast<$key, $value>()
: null
''';
} else if (const TypeChecker.fromRuntime(Uint8List)
.isAssignableFromType(t)) {
deserializedRepresentation = '''
map['$alias'] is Uint8List
? (map['$alias'] as Uint8List)
:
(
map['$alias'] is Iterable<int>
? new Uint8List.fromList((map['$alias'] as Iterable<int>).toList())
:
(
map['$alias'] is String
? new Uint8List.fromList(base64.decode(map['$alias'] as String))
: null
)
)
''';
2018-02-28 02:10:43 +00:00
}
}
buf.write('${field.name}: $deserializedRepresentation');
}
buf.write(');');
method.body = new Code(buf.toString());
}));
}
2018-03-09 12:39:21 +00:00
void generateFieldsClass(BuildContext ctx, LibraryBuilder file) {
file.body.add(new Class((clazz) {
clazz
..abstract = true
..name = '${ctx.modelClassNameRecase.pascalCase}Fields';
clazz.fields.add(new Field((b) {
b
..static = true
..modifier = FieldModifier.constant
..type = new TypeReference((b) => b
..symbol = 'List'
..types.add(refer('String')))
..name = 'allFields'
..assignment = literalConstList(
ctx.fields.map((f) => refer(f.name)).toList(),
refer('String'))
.code;
}));
2018-03-09 12:39:21 +00:00
for (var field in ctx.fields) {
clazz.fields.add(new Field((b) {
b
..static = true
..modifier = FieldModifier.constant
..type = new Reference('String')
..name = field.name
..assignment = new Code("'${ctx.resolveFieldName(field.name)}'");
}));
}
}));
}
}