497 lines
17 KiB
Dart
497 lines
17 KiB
Dart
part of angel3_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 {
|
|
log.fine('Running SerializerGenerator');
|
|
|
|
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, !autoSnakeCaseNames);
|
|
|
|
if (ctx == null) {
|
|
log.severe('Invalid builder context');
|
|
throw 'Invalid builder context';
|
|
}
|
|
|
|
var serializers = annotation.peek('serializers')?.listValue ?? [];
|
|
|
|
if (serializers.isEmpty) {
|
|
log.severe("No Serializers");
|
|
return null;
|
|
}
|
|
|
|
// Check if any serializer is recognized
|
|
if (!serializers.any((s) => Serializers.all.contains(s.toIntValue()))) {
|
|
log.severe("No recognizable Serializers");
|
|
return null;
|
|
}
|
|
|
|
var lib = Library((b) {
|
|
generateClass(
|
|
serializers.map((s) => s.toIntValue() ?? 0).toList(), ctx, b);
|
|
generateFieldsClass(ctx, b);
|
|
});
|
|
|
|
var buf = lib.accept(DartEmitter(useNullSafetySyntax: true));
|
|
return buf.toString();
|
|
}
|
|
|
|
/// Generate a serializer class.
|
|
void generateClass(
|
|
List<int> serializers, BuildContext ctx, LibraryBuilder file) {
|
|
log.fine('Generate serializer class');
|
|
|
|
// Generate canonical codecs, etc.
|
|
var pascal = ctx.modelClassNameRecase.pascalCase.replaceAll('?', '');
|
|
var camel = ctx.modelClassNameRecase.camelCase.replaceAll('?', '');
|
|
|
|
log.fine('Generating ${pascal}Serializer');
|
|
|
|
if (ctx.constructorParameters.isEmpty) {
|
|
log.fine("Constructor is empty");
|
|
|
|
file.body.add(Code('''
|
|
const ${pascal}Serializer ${camel}Serializer = ${pascal}Serializer();
|
|
|
|
class ${pascal}Encoder extends Converter<$pascal, Map> {
|
|
const ${pascal}Encoder();
|
|
|
|
@override
|
|
Map convert($pascal model) => ${pascal}Serializer.toMap(model);
|
|
}
|
|
|
|
class ${pascal}Decoder extends Converter<Map, $pascal> {
|
|
const ${pascal}Decoder();
|
|
|
|
@override
|
|
$pascal convert(Map map) => ${pascal}Serializer.fromMap(map);
|
|
}
|
|
'''));
|
|
}
|
|
|
|
file.body.add(Class((clazz) {
|
|
clazz.name = '${pascal}Serializer';
|
|
if (ctx.constructorParameters.isEmpty) {
|
|
clazz.extend = TypeReference((b) => b
|
|
..symbol = 'Codec'
|
|
..types.addAll([ctx.modelClassType, refer('Map')]));
|
|
|
|
// Add constructor, Codec impl, etc.
|
|
clazz.constructors.add(Constructor((b) => b..constant = true));
|
|
clazz.methods.add(Method((b) => b
|
|
..name = 'encoder'
|
|
..returns = refer('${pascal}Encoder')
|
|
..type = MethodType.getter
|
|
..annotations.add(refer('override'))
|
|
..body = refer('${pascal}Encoder').constInstance([]).code));
|
|
clazz.methods.add(Method((b) => b
|
|
..name = 'decoder'
|
|
..returns = refer('${pascal}Decoder')
|
|
..type = MethodType.getter
|
|
..annotations.add(refer('override'))
|
|
..body = refer('${pascal}Decoder').constInstance([]).code));
|
|
} else {
|
|
clazz.abstract = true;
|
|
}
|
|
|
|
if (serializers.contains(Serializers.map)) {
|
|
generateFromMapMethod(clazz, ctx, file);
|
|
}
|
|
|
|
if (serializers.contains(Serializers.map) ||
|
|
serializers.contains(Serializers.json)) {
|
|
generateToMapMethod(clazz, ctx, file);
|
|
}
|
|
}));
|
|
}
|
|
|
|
// Generate toMapMethod
|
|
void generateToMapMethod(
|
|
ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) {
|
|
var originalClassName = ctx.originalClassName;
|
|
if (originalClassName == null) {
|
|
log.warning('Unable to generate toMap(), classname is null');
|
|
return;
|
|
}
|
|
clazz.methods.add(Method((method) {
|
|
method
|
|
..static = true
|
|
..name = 'toMap'
|
|
..returns = Reference('Map<String, dynamic>')
|
|
..requiredParameters.add(Parameter((b) {
|
|
b
|
|
..name = 'model'
|
|
..type = TypeReference((b) => b
|
|
..symbol = originalClassName
|
|
..isNullable = true);
|
|
}));
|
|
|
|
var buf = StringBuffer();
|
|
|
|
/*
|
|
ctx.requiredFields.forEach((key, msg) {
|
|
if (ctx.excluded[key]?.canSerialize == false) return;
|
|
buf.writeln('''
|
|
if (model.$key == null) {
|
|
throw FormatException("$msg");
|
|
}
|
|
''');
|
|
});
|
|
*/
|
|
|
|
buf.writeln('return {');
|
|
var i = 0;
|
|
|
|
// Add named parameters
|
|
for (var field in ctx.fields) {
|
|
var type = ctx.resolveSerializedFieldType(field.name);
|
|
|
|
// Skip excluded fields
|
|
if (ctx.excluded[field.name]?.canSerialize == false) continue;
|
|
|
|
var alias = ctx.resolveFieldName(field.name);
|
|
|
|
if (i++ > 0) buf.write(', ');
|
|
|
|
var serializedRepresentation = 'model.${field.name}';
|
|
|
|
String serializerToMap(ReCase rc, String value) {
|
|
// if (rc.pascalCase == ctx.modelClassName) {
|
|
// return '($value)?.toJson()';
|
|
// }
|
|
return '${rc.pascalCase.replaceAll('?', '')}Serializer.toMap($value)';
|
|
}
|
|
|
|
var fieldNameSerializer = ctx.fieldInfo[field.name]?.serializer;
|
|
if (fieldNameSerializer != null) {
|
|
var name = MirrorSystem.getName(fieldNameSerializer);
|
|
serializedRepresentation = '$name(model.${field.name})';
|
|
}
|
|
|
|
// Serialize dates
|
|
else if (dateTimeTypeChecker.isAssignableFromType(type)) {
|
|
var question =
|
|
field.type.nullabilitySuffix == NullabilitySuffix.question
|
|
? "?"
|
|
: "";
|
|
serializedRepresentation =
|
|
'model.${field.name}$question.toIso8601String()';
|
|
}
|
|
|
|
// Serialize model classes via `XSerializer.toMap`
|
|
else if (isModelClass(type)) {
|
|
var rc = ReCase(type.getDisplayString(withNullability: true));
|
|
serializedRepresentation = serializerToMap(rc, 'model.${field.name}');
|
|
} else if (type is InterfaceType) {
|
|
if (isListOfModelType(type)) {
|
|
var name =
|
|
type.typeArguments[0].getDisplayString(withNullability: true);
|
|
if (name.startsWith('_')) name = name.substring(1);
|
|
var rc = ReCase(name);
|
|
var m = serializerToMap(rc, 'm');
|
|
|
|
var question =
|
|
(field.type.nullabilitySuffix == NullabilitySuffix.question)
|
|
? '?'
|
|
: '';
|
|
serializedRepresentation =
|
|
'model.${field.name}$question.map((m) => $m).toList()';
|
|
log.fine('serializedRepresentation => $serializedRepresentation');
|
|
} else if (isMapToModelType(type)) {
|
|
var rc = ReCase(
|
|
type.typeArguments[1].getDisplayString(withNullability: true));
|
|
serializedRepresentation =
|
|
'''model.${field.name}.keys.fold({}, (map, key) {
|
|
return map..[key] =
|
|
${serializerToMap(rc, 'model.${field.name}[key]')};
|
|
})''';
|
|
} else if (type.element is Enum) {
|
|
var convert =
|
|
(field.type.nullabilitySuffix == NullabilitySuffix.question)
|
|
? '!'
|
|
: '';
|
|
|
|
serializedRepresentation = '''
|
|
model.${field.name} != null ?
|
|
${type.getDisplayString(withNullability: false)}.values.indexOf(model.${field.name}$convert)
|
|
: null
|
|
''';
|
|
} else if (const TypeChecker.fromRuntime(Uint8List)
|
|
.isAssignableFromType(type)) {
|
|
var convert =
|
|
(field.type.nullabilitySuffix == NullabilitySuffix.question)
|
|
? '!'
|
|
: '';
|
|
|
|
serializedRepresentation = '''
|
|
model.${field.name} != null ?
|
|
base64.encode(model.${field.name}$convert)
|
|
: null
|
|
''';
|
|
}
|
|
}
|
|
|
|
buf.write("'$alias': $serializedRepresentation");
|
|
}
|
|
|
|
buf.write('};');
|
|
method.body = Block.of([
|
|
Code(
|
|
'if (model == null) { throw FormatException("Required field [model] cannot be null"); }'),
|
|
Code(buf.toString()),
|
|
]);
|
|
}));
|
|
}
|
|
|
|
// Generate fromMapMethod
|
|
void generateFromMapMethod(
|
|
ClassBuilder clazz, BuildContext ctx, LibraryBuilder file) {
|
|
clazz.methods.add(Method((method) {
|
|
method
|
|
..static = true
|
|
..name = 'fromMap'
|
|
..returns = ctx.modelClassType
|
|
..requiredParameters.add(
|
|
Parameter((b) => b
|
|
..name = 'map'
|
|
..type = Reference('Map')),
|
|
);
|
|
|
|
// 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();
|
|
|
|
// Required Fields
|
|
ctx.requiredFields.forEach((key, msg) {
|
|
if (ctx.excluded[key]?.canDeserialize == false) return;
|
|
var name = ctx.resolveFieldName(key);
|
|
if (msg.contains("'")) {
|
|
buf.writeln('''
|
|
if (map['$name'] == null) {
|
|
throw FormatException("$msg");
|
|
}
|
|
''');
|
|
} else {
|
|
buf.writeln('''
|
|
if (map['$name'] == null) {
|
|
throw FormatException('$msg');
|
|
}
|
|
''');
|
|
}
|
|
});
|
|
|
|
buf.writeln('return ${ctx.modelClassName}(');
|
|
var i = 0;
|
|
|
|
// Parameters in the constructor
|
|
for (var param in ctx.constructorParameters) {
|
|
if (i++ > 0) buf.write(', ');
|
|
buf.write(param.name);
|
|
}
|
|
|
|
// Fields
|
|
for (var field in ctx.fields) {
|
|
var type = ctx.resolveSerializedFieldType(field.name);
|
|
|
|
if (ctx.excluded[field.name]?.canDeserialize == false) continue;
|
|
|
|
var alias = ctx.resolveFieldName(field.name);
|
|
|
|
if (i++ > 0) buf.write(', ');
|
|
|
|
var deserializedRepresentation =
|
|
"map['$alias'] as ${typeToString(type)}";
|
|
|
|
if (type.nullabilitySuffix == NullabilitySuffix.question) {
|
|
deserializedRepresentation += '?';
|
|
}
|
|
|
|
var defaultValue = 'null';
|
|
var existingDefault = ctx.defaults[field.name];
|
|
|
|
if (existingDefault != null) {
|
|
var d = dartObjectToString(existingDefault);
|
|
if (d != null) {
|
|
defaultValue = d;
|
|
|
|
if (!deserializedRepresentation.endsWith("?")) {
|
|
deserializedRepresentation += "?";
|
|
}
|
|
}
|
|
deserializedRepresentation =
|
|
'$deserializedRepresentation ?? $defaultValue';
|
|
}
|
|
|
|
var fieldNameDeserializer = ctx.fieldInfo[field.name]?.deserializer;
|
|
if (fieldNameDeserializer != null) {
|
|
var name = MirrorSystem.getName(fieldNameDeserializer);
|
|
deserializedRepresentation = "$name(map['$alias'])";
|
|
} else if (dateTimeTypeChecker.isAssignableFromType(type)) {
|
|
if (field.type.nullabilitySuffix != NullabilitySuffix.question) {
|
|
if (defaultValue.toLowerCase() == 'null') {
|
|
defaultValue = 'DateTime.parse("1970-01-01 00:00:00")';
|
|
} else {
|
|
defaultValue = 'DateTime.parse("$defaultValue")';
|
|
}
|
|
}
|
|
deserializedRepresentation = "map['$alias'] != null ? "
|
|
"(map['$alias'] is DateTime ? (map['$alias'] as DateTime) : DateTime.parse(map['$alias'].toString()))"
|
|
' : $defaultValue';
|
|
}
|
|
|
|
// Serialize model classes via `XSerializer.toMap`
|
|
else if (isModelClass(type)) {
|
|
var rc = ReCase(type.getDisplayString(withNullability: true));
|
|
deserializedRepresentation = "map['$alias'] != null"
|
|
" ? ${rc.pascalCase.replaceAll('?', '')}Serializer.fromMap(map['$alias'] as Map)"
|
|
' : $defaultValue';
|
|
} else if (type is InterfaceType) {
|
|
if (isListOfModelType(type)) {
|
|
if (defaultValue == 'null') {
|
|
defaultValue = '[]';
|
|
}
|
|
var rc = ReCase(
|
|
type.typeArguments[0].getDisplayString(withNullability: true));
|
|
|
|
deserializedRepresentation = "map['$alias'] is Iterable"
|
|
" ? List.unmodifiable(((map['$alias'] as Iterable)"
|
|
'.whereType<Map>())'
|
|
'.map(${rc.pascalCase.replaceAll('?', '')}Serializer.fromMap))'
|
|
' : $defaultValue';
|
|
} else if (isMapToModelType(type)) {
|
|
// TODO: This requires refractoring
|
|
if (defaultValue == 'null') {
|
|
defaultValue = '{}';
|
|
}
|
|
|
|
var rc = ReCase(
|
|
type.typeArguments[1].getDisplayString(withNullability: true));
|
|
deserializedRepresentation = '''
|
|
map['$alias'] is Map
|
|
? Map.unmodifiable((map['$alias'] as Map).keys.fold({}, (out, key) {
|
|
return out..[key] = ${rc.pascalCase.replaceAll('?', '')}Serializer
|
|
.fromMap(((map['$alias'] as Map)[key]) as Map);
|
|
}))
|
|
: $defaultValue
|
|
''';
|
|
} else if (type.element is Enum) {
|
|
deserializedRepresentation = '''
|
|
map['$alias'] is ${type.getDisplayString(withNullability: true)}
|
|
? (map['$alias'] as ${type.getDisplayString(withNullability: true)}) ?? $defaultValue
|
|
:
|
|
(
|
|
map['$alias'] is int
|
|
? ${type.getDisplayString(withNullability: true)}.values[map['$alias'] as int]
|
|
: $defaultValue
|
|
)
|
|
''';
|
|
|
|
//log.warning('Code => $deserializedRepresentation');
|
|
} else if (const TypeChecker.fromRuntime(List)
|
|
.isAssignableFromType(type) &&
|
|
type.typeArguments.length == 1) {
|
|
if (defaultValue == 'null') {
|
|
defaultValue = '[]';
|
|
}
|
|
var arg = convertTypeReference(type.typeArguments[0])
|
|
.accept(DartEmitter(useNullSafetySyntax: true));
|
|
deserializedRepresentation = '''
|
|
map['$alias'] is Iterable
|
|
? (map['$alias'] as Iterable).cast<$arg>().toList()
|
|
: $defaultValue
|
|
''';
|
|
} else if (const TypeChecker.fromRuntime(Map)
|
|
.isAssignableFromType(type) &&
|
|
type.typeArguments.length == 2) {
|
|
var key = convertTypeReference(type.typeArguments[0])
|
|
.accept(DartEmitter(useNullSafetySyntax: true));
|
|
var value = convertTypeReference(type.typeArguments[1])
|
|
.accept(DartEmitter(useNullSafetySyntax: true));
|
|
|
|
if (defaultValue == 'null') {
|
|
defaultValue = '{}';
|
|
}
|
|
deserializedRepresentation = '''
|
|
map['$alias'] is Map
|
|
? (map['$alias'] as Map).cast<$key, $value>()
|
|
: $defaultValue
|
|
''';
|
|
} else if (const TypeChecker.fromRuntime(Uint8List)
|
|
.isAssignableFromType(type)) {
|
|
deserializedRepresentation = '''
|
|
map['$alias'] is Uint8List
|
|
? (map['$alias'] as Uint8List)
|
|
:
|
|
(
|
|
map['$alias'] is Iterable<int>
|
|
? Uint8List.fromList((map['$alias'] as Iterable<int>).toList())
|
|
:
|
|
(
|
|
map['$alias'] is String
|
|
? Uint8List.fromList(base64.decode(map['$alias'] as String))
|
|
: $defaultValue
|
|
)
|
|
)
|
|
''';
|
|
}
|
|
}
|
|
|
|
buf.write('${field.name}: $deserializedRepresentation');
|
|
}
|
|
|
|
buf.write(');');
|
|
method.body = Code(buf.toString());
|
|
}));
|
|
}
|
|
|
|
void generateFieldsClass(BuildContext ctx, LibraryBuilder file) {
|
|
//log.fine('Generate serializer fields');
|
|
|
|
file.body.add(Class((clazz) {
|
|
clazz
|
|
..abstract = true
|
|
..name = '${ctx.modelClassNameRecase.pascalCase}Fields';
|
|
|
|
clazz.fields.add(Field((b) {
|
|
b
|
|
..static = true
|
|
..modifier = FieldModifier.constant
|
|
..type = TypeReference((b) => b
|
|
..symbol = 'List'
|
|
..types.add(refer('String')))
|
|
..name = 'allFields'
|
|
..assignment = literalConstList(
|
|
ctx.fields.map((f) => refer(f.name)).toList(),
|
|
refer('String'))
|
|
.code;
|
|
}));
|
|
|
|
for (var field in ctx.fields) {
|
|
clazz.fields.add(Field((b) {
|
|
b
|
|
..static = true
|
|
..modifier = FieldModifier.constant
|
|
..type = Reference('String')
|
|
..name = field.name
|
|
..assignment = Code("'${ctx.resolveFieldName(field.name)}'");
|
|
}));
|
|
}
|
|
}));
|
|
}
|
|
}
|