generateForAnnotatedElement method
- Element element,
- ConstantReader annotation,
- BuildStep buildStep
Implement to return source code to generate for element
.
This method is invoked based on finding elements annotated with an
instance of T
. The annotation
is provided as a ConstantReader
.
Supported return values include a single String or multiple String instances within an Iterable or Stream. It is also valid to return a Future of String, Iterable, or Stream. When multiple values are returned through an iterable or stream they will be deduplicated. Typically each value will be an independent unit of code and the deduplication prevents re-defining the same member multiple times. For example if multiple annotated elements may need a specific utility method available it can be output for each one, and the single deduplicated definition can be shared.
Implementations should return null
when no content is generated. Empty
or whitespace-only String instances are also ignored.
Implementation
@override
Future<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
final className = element.name!;
final classNameLower = DataHelpers.internalTypeFor(className);
ClassElement classElement;
try {
classElement = element as ClassElement;
} catch (e) {
throw UnsupportedError(
"Can't generate adapter for $className. Please use @DataAdapter on a class.");
}
final annot = TypeChecker.fromRuntime(JsonSerializable);
var fieldRename = annot
.firstAnnotationOfExact(classElement, throwOnUnresolved: false)
?.getField('fieldRename');
if (fieldRename == null && classElement.freezedConstructor != null) {
fieldRename = annot
.firstAnnotationOfExact(classElement.freezedConstructor!,
throwOnUnresolved: false)
?.getField('fieldRename');
}
void _checkIsFinal(final InterfaceElement? element, String? name) {
if (element != null) {
if (name != null &&
element.getSetter(name) != null &&
!element.getField(name)!.isLate) {
throw UnsupportedError(
"Can't generate adapter for $className. The `$name` field MUST be final");
}
_checkIsFinal(element.supertype?.element, name);
}
}
_checkIsFinal(classElement, 'id');
for (final field in classElement.relationshipFields) {
_checkIsFinal(classElement, field.name);
}
// relationship-related
final relationships = classElement.relationshipFields
.fold<Set<Map<String, String?>>>({}, (result, field) {
final relationshipClassElement = field.typeElement;
final relationshipAnnotation = TypeChecker.fromRuntime(DataRelationship)
.firstAnnotationOfExact(field, throwOnUnresolved: false);
final jsonKeyAnnotation = TypeChecker.fromRuntime(JsonKey)
.firstAnnotationOfExact(field, throwOnUnresolved: false);
final jsonKeyIgnored =
jsonKeyAnnotation?.getField('ignore')?.toBoolValue() ?? false;
if (jsonKeyIgnored) {
throw UnsupportedError('''
@JsonKey(ignore: true) is not allowed in Flutter Data relationships.
Please use @DataRelationship(serialized: false) to prevent it from
serializing and deserializing.
''');
}
// try again with @DataRelationship
final serialize =
relationshipAnnotation?.getField('serialize')?.toBoolValue() ?? true;
// define inverse
var inverse =
relationshipAnnotation?.getField('inverse')?.toStringValue();
if (inverse == null) {
final possibleInverseElements =
relationshipClassElement.relationshipFields.where((elem) {
return (elem.type as ParameterizedType)
.typeArguments
.single
.element ==
classElement;
});
if (possibleInverseElements.length > 1) {
throw UnsupportedError('''
Too many possible inverses for relationship `${field.name}`
of type $className: ${possibleInverseElements.map((e) => e.name).join(', ')}
Please specify the correct inverse in the $className class, for example:
@DataRelationship(inverse: '${possibleInverseElements.first.name}')
final BelongsTo<${relationshipClassElement.name}> ${field.name};
and execute a code generation build again.
''');
} else if (possibleInverseElements.length == 1) {
inverse = possibleInverseElements.single.name;
}
}
// prepare metadata
// try to guess correct key name in json_serializable
var keyName = jsonKeyAnnotation?.getField('name')?.toStringValue();
if (keyName == null && fieldRename != null) {
final fieldCase = fieldRename.getField('_name')?.toStringValue();
switch (fieldCase) {
case 'kebab':
keyName = field.name.kebab;
break;
case 'snake':
keyName = field.name.snake;
break;
case 'pascal':
keyName = field.name.pascal;
break;
case 'none':
keyName = field.name;
break;
default:
}
}
keyName ??= field.name;
result.add({
'key': keyName,
'name': field.name,
'inverseName': inverse,
'kind': field.type.element?.name,
'type': relationshipClassElement.name,
if (!serialize) 'serialize': 'false',
});
return result;
}).toList();
final relationshipMeta = {
for (final rel in relationships)
'\'${rel['key']}\'': '''RelationshipMeta<${rel['type']}>(
name: '${rel['name']}',
${rel['inverseName'] != null ? 'inverseName: \'${rel['inverseName']}\',' : ''}
type: '${DataHelpers.internalTypeFor(rel['type']!)}',
kind: '${rel['kind']}',
${rel['serialize'] != null ? 'serialize: ${rel['serialize']},' : ''}
instance: (_) => (_ as $className).${rel['name']},
)''',
};
final relationshipGraphNodeExtension = {
for (final rel in relationships)
'''
RelationshipGraphNode<${rel['type']}> get ${rel['name']} {
final meta = _\$${className}Adapter._k${className}RelationshipMetas['${rel['key']}']
as RelationshipMeta<${rel['type']}>;
return meta.clone(parent: this is RelationshipMeta ? this as RelationshipMeta : null);
}
'''
};
// serialization-related
final hasFromJson =
classElement.constructors.any((c) => c.name == 'fromJson');
final fromJson = hasFromJson
? '$className.fromJson(map)'
: '_\$${className}FromJson(map)';
final methods = [
...classElement.methods,
...classElement.interfaces.map((i) => i.methods).expand((i) => i),
...classElement.mixins.map((i) => i.methods).expand((i) => i)
];
final hasToJson = methods.any((c) => c.name == 'toJson');
final toJson =
hasToJson ? 'model.toJson()' : '_\$${className}ToJson(model)';
// additional adapters
final finders = <String>[];
final mixins = annotation.read('adapters').listValue.map((obj) {
final mixinType = obj.toTypeValue() as ParameterizedType;
final mixinMethods = <MethodElement>[];
String displayName;
final args = mixinType.typeArguments;
if (args.length > 1) {
throw UnsupportedError(
'Adapter `$mixinType` MUST have at most one type argument (T extends DataModel<T>) is supported for $mixinType');
}
final instantiatedMixinType = (mixinType.element as MixinElement)
.instantiate(
typeArguments: [if (args.isNotEmpty) classElement.thisType],
nullabilitySuffix: NullabilitySuffix.none);
mixinMethods.addAll(instantiatedMixinType.methods);
displayName = instantiatedMixinType.getDisplayString();
// add finders
for (final field in mixinMethods) {
final hasFinderAnnotation =
TypeChecker.fromRuntime(DataFinder).hasAnnotationOfExact(field);
if (hasFinderAnnotation) {
finders.add(field.name);
}
}
return displayName;
}).toSet();
final mixinShortcuts = mixins.map((mixin) {
final mixinB = mixin.replaceAll(RegExp('<.*?>'), '').decapitalize();
return '$mixin get $mixinB => this as $mixin;';
}).join('\n');
if (mixins.isEmpty) {
mixins.add('NothingMixin');
}
// template
return '''
// ignore_for_file: non_constant_identifier_names, duplicate_ignore
mixin _\$${className}Adapter on Adapter<$className> {
static final Map<String, RelationshipMeta> _k${className}RelationshipMetas =
$relationshipMeta;
@override
Map<String, RelationshipMeta> get relationshipMetas => _k${className}RelationshipMetas;
@override
$className deserializeLocal(map, {String? key}) {
map = transformDeserialize(map);
return internalWrapStopInit(() => $fromJson, key: key);
}
@override
Map<String, dynamic> serializeLocal(model, {bool withRelationships = true}) {
final map = $toJson;
return transformSerialize(map, withRelationships: withRelationships);
}
}
final _${classNameLower}Finders = <String, dynamic>{
${finders.map((f) => ''' '$f': (_) => _.$f,''').join('\n')}
};
class \$${className}Adapter = Adapter<$className> with _\$${className}Adapter, ${mixins.join(', ')};
final ${classNameLower}AdapterProvider =
Provider<Adapter<$className>>(
(ref) => \$${className}Adapter(ref, InternalHolder(_${classNameLower}Finders)));
extension ${className}AdapterX on Adapter<$className> {
$mixinShortcuts
}
extension ${className}RelationshipGraphNodeX on RelationshipGraphNode<$className> {
${relationshipGraphNodeExtension.join('\n')}
}
''';
}