part of angel3_serialize_generator; class SerializerGenerator extends GeneratorForAnnotation { final bool autoSnakeCaseNames; const SerializerGenerator({this.autoSnakeCaseNames = true}); @override Future 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 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 { 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') ..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 as Map?)?..[key] = ${serializerToMap(rc, 'model.${field.name}[key]')}; })'''; } else if (type.element.isEnum) { 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(${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.isEnum) { 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 ? Uint8List.fromList((map['$alias'] as Iterable).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)}'"); })); } })); } }