import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; import 'package:code_builder/code_builder.dart'; import 'package:code_builder/dart/core.dart'; import 'package:recase/recase.dart'; import 'package:source_gen/src/annotation.dart'; import 'package:source_gen/source_gen.dart'; import 'angel_serialize.dart'; class JsonModelGenerator extends GeneratorForAnnotation { const JsonModelGenerator(); @override Future generateForAnnotatedElement( Element element, Serializable annotation, BuildStep buildStep) async { if (element.kind != ElementKind.CLASS) throw 'Only classes can be annotated with a @Serializable() annotation.'; var lib = generateSerializerLibrary(element); return prettyToSource(lib.buildAst()); } LibraryBuilder generateSerializerLibrary(ClassElement clazz) { var lib = new LibraryBuilder(); lib.addMember(generateBaseModelClass(clazz)); return lib; } ClassBuilder generateBaseModelClass(ClassElement clazz) { if (!clazz.name.startsWith('_')) throw 'Classes annotated with @Serializable() must have names starting with a leading underscore.'; var genClassName = clazz.name.substring(1); var genClass = new ClassBuilder(genClassName, asExtends: new TypeBuilder(clazz.name)); Map fields = {}; Map aliases = {}; // Find all fields for (var field in clazz.fields) { // Skip if annotated with @exclude var excludeAnnotation = field.metadata.firstWhere( (ann) => matchAnnotation(Exclude, ann), orElse: () => null); if (excludeAnnotation == null) { // Register the field fields[field.name] = field.type; // Search for Alias var aliasAnnotation = field.metadata.firstWhere( (ann) => matchAnnotation(Alias, ann), orElse: () => null); if (aliasAnnotation != null) { var alias = instantiateAnnotation(aliasAnnotation) as Alias; aliases[field.name] = alias.name; } } } // Now, add all fields to the base class clazz.fields.forEach((field) { genClass.addField( varField(field.name, type: new TypeBuilder(field.type.displayName)) ..addAnnotation(reference('override'))); }); // Create convenience constructor var convenienceConstructor = constructor(clazz.fields.map((field) { return thisField(named(parameter(field.name))); })); genClass.addConstructor(convenienceConstructor); // Create toJson Map toJsonFields = {}; fields.forEach((fieldName, type) { var resolvedName = aliases.containsKey(fieldName) ? aliases[fieldName] : fieldName; ExpressionBuilder value; // DateTime if (type.name == 'DateTime') { value = reference(fieldName).equals(literal(null)).ternary( literal(null), reference(fieldName).invoke('toIso8601String', [])); } // Anything else else { value = reference(fieldName); } toJsonFields[resolvedName] = value; }); var toJson = new MethodBuilder('toJson', returnType: new TypeBuilder('Map', genericTypes: [ new TypeBuilder('String'), new TypeBuilder('dynamic') ]), returns: map(toJsonFields)); genClass.addMethod(toJson); // Create factory [name].fromJson var fromJson = new ConstructorBuilder(name: 'fromJson', asFactory: true); fromJson.addPositional(parameter('data', [new TypeBuilder('Map')])); var namedParams = fields.keys.fold>({}, (out, fieldName) { var resolvedName = aliases.containsKey(fieldName) ? aliases[fieldName] : fieldName; var mapKey = reference('data')[literal(resolvedName)]; ExpressionBuilder value = mapKey; var type = fields[fieldName]; // DateTime if (type.name == 'DateTime') { // map['foo'] is DateTime ? map['foo'] : (map['foo'] is String ? DateTime.parse(map['foo']) : null) var dt = new TypeBuilder('DateTime'); value = mapKey.isInstanceOf(dt).ternary( mapKey, (mapKey.isInstanceOf(new TypeBuilder('String')).ternary( new TypeBuilder('DateTime').invoke('parse', [mapKey]), literal(null))) .parentheses()); } bool done = false; // Handle List if (type.toString().contains('List') && type is ParameterizedType) { var listType = type.typeArguments.first; if (listType.element is ClassElement) { var genericClass = listType.element as ClassElement; String fromJsonClassName; bool hasFromJson = genericClass.constructors.any((c) => c.name == 'fromJson'); if (hasFromJson) { fromJsonClassName = genericClass.displayName; } else { // If it has a serializable annotation, act accordingly. if (genericClass.metadata .any((ann) => matchAnnotation(Serializable, ann))) { fromJsonClassName = genericClass.displayName.substring(1); hasFromJson = true; } } // Check for fromJson if (hasFromJson) { var outputType = new TypeBuilder(fromJsonClassName); var x = reference('x'); value = mapKey.isInstanceOf(lib$core.List).ternary( mapKey.invoke('map', [ new MethodBuilder.closure( returns: x.equals(literal(null)).ternary( literal(null), (x.isInstanceOf(outputType).ternary( x, outputType.newInstance([reference('x')], constructor: 'fromJson'))) .parentheses())) ..addPositional(parameter('x')) ]).invoke('toList', []), literal(null)); done = true; } } } // Check for fromJson if (!done && type.element is ClassElement) { String fromJsonClassName; var genericClass = type.element as ClassElement; bool hasFromJson = genericClass.constructors.any((c) => c.name == 'fromJson'); if (hasFromJson) { fromJsonClassName = type.displayName; } else { // If it has a serializable annotation, act accordingly. if (genericClass.metadata .any((ann) => matchAnnotation(Serializable, ann))) { fromJsonClassName = type.displayName.substring(1); hasFromJson = true; } } // Check for fromJson if (hasFromJson) { var outputType = new TypeBuilder(fromJsonClassName); value = mapKey.equals(literal(null)).ternary( literal(null), (mapKey.isInstanceOf(outputType).ternary( mapKey, outputType .newInstance([mapKey], constructor: 'fromJson'))) .parentheses()); } } // Handle Map... if (!done && type.toString().contains('Map') && type is ParameterizedType && type.typeArguments.length >= 2) { var targetType = type.typeArguments[1]; if (targetType.element is ClassElement) { String fromJsonClassName; var genericClass = targetType.element as ClassElement; bool hasFromJson = genericClass.constructors.any((c) => c.name == 'fromJson'); if (hasFromJson) { fromJsonClassName = targetType.displayName; } else { // If it has a serializable annotation, act accordingly. if (genericClass.metadata .any((ann) => matchAnnotation(Serializable, ann))) { fromJsonClassName = targetType.displayName.substring(1); hasFromJson = true; } } // Check for fromJson if (hasFromJson) { var outputType = new TypeBuilder(fromJsonClassName); var v = mapKey[reference('k')]; value = mapKey.isInstanceOf(lib$core.Map).ternary( mapKey.property('keys').invoke('fold', [ map({}), new MethodBuilder.closure() ..addStatements([ v .equals(literal(null)) .ternary( literal(null), (v.isInstanceOf(outputType).ternary( v, outputType.newInstance([v], constructor: 'fromJson'))) .parentheses()) .asAssign(reference('out')[reference('k')]), reference('out').asReturn() ]) ..addPositional(parameter('out')) ..addPositional(parameter('k')) ]), literal(null)); } else { value = mapKey .isInstanceOf(lib$core.Map) .ternary(mapKey, literal(null)); } } } return out..[fieldName] = value; }); fromJson.addStatement(new TypeBuilder(genClassName) .newInstance([], named: namedParams).asReturn()); genClass.addConstructor(fromJson); // Create `parse` to just forward var parseMethod = new MethodBuilder('parse', returnType: new TypeBuilder(genClassName), returns: new TypeBuilder(genClassName) .newInstance([reference('map')], constructor: 'fromJson')); parseMethod.addPositional(parameter('map', [new TypeBuilder('Map')])); genClass.addMethod(parseMethod, asStatic: true); return genClass; } }