276 lines
9.9 KiB
Dart
276 lines
9.9 KiB
Dart
|
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<Serializable> {
|
||
|
const JsonModelGenerator();
|
||
|
|
||
|
@override
|
||
|
Future<String> 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<String, DartType> fields = {};
|
||
|
Map<String, String> 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<String, ExpressionBuilder> 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<Map<String, ExpressionBuilder>>({}, (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;
|
||
|
}
|
||
|
}
|