generateForAnnotatedElement method

  1. @override
Future<String> generateForAnnotatedElement(
  1. Element element,
  2. ConstantReader annotation,
  3. 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')}
}
''';
}