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.

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 repository for $className. Please use @DataRepository 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 repository 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}LocalAdapter._k${className}RelationshipMetas['${rel['key']}']
    as RelationshipMeta<${rel['type']}>;
return meta.clone(parent: this is RelationshipMeta ? this as RelationshipMeta : null);
}
'''
  };

  // serialization-related

  var fromJson = annotation.read('fromJson').isNull
      ? ''
      : annotation.read('fromJson').stringValue;
  var toJson = annotation.read('toJson').isNull
      ? ''
      : annotation.read('toJson').stringValue;

  if (fromJson.isEmpty) {
    final hasFromJson =
        classElement.constructors.any((c) => c.name == 'fromJson');
    fromJson = hasFromJson
        ? '$className.fromJson(map)'
        : '_\$${className}FromJson(map)';
  }

  if (toJson.isEmpty) {
    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');
    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(withNullability: false);

    // add finders
    for (final field in mixinMethods) {
      final hasFinderAnnotation =
          TypeChecker.fromRuntime(DataFinder).hasAnnotationOfExact(field);
      if (hasFinderAnnotation) {
        finders.add(field.name);
      }
    }

    return displayName;
  }).toSet();

  final localMixins = annotation.read('localAdapters').listValue.map((obj) {
    final mixinType = obj.toTypeValue() as ParameterizedType;
    final mixinMethods = <MethodElement>[];
    String displayName;

    final args = mixinType.typeArguments;

    if (args.length > 1) {
      throw UnsupportedError(
          'LocalAdapter `$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(withNullability: false);
    return displayName;
  }).toSet();

  final mixinShortcuts = mixins.map((mixin) {
    final mixinB = mixin.replaceAll(RegExp('<.*?>'), '').decapitalize();
    return '$mixin get $mixinB => remoteAdapter as $mixin;';
  }).join('\n');

  if (mixins.isEmpty) {
    mixins.add('NothingMixin');
  }

  final typeIdReader = annotation.read('typeId');
  final typeId = typeIdReader.isNull ? null : typeIdReader.intValue;

  // template

  return '''
// ignore_for_file: non_constant_identifier_names, duplicate_ignore

mixin \$${className}LocalAdapter on LocalAdapter<$className> {
static final Map<String, RelationshipMeta> _k${className}RelationshipMetas =
  $relationshipMeta;

@override
Map<String, RelationshipMeta> get relationshipMetas => _k${className}RelationshipMetas;

@override
$className deserialize(map) {
  map = transformDeserialize(map);
  return $fromJson;
}

@override
Map<String, dynamic> serialize(model, {bool withRelationships = true}) {
  final map = $toJson;
  return transformSerialize(map, withRelationships: withRelationships);
}
}

final _${classNameLower}Finders = <String, dynamic>{
${finders.map((f) => '''  '$f': (_) => _.$f,''').join('\n')}
};

// ignore: must_be_immutable
class \$${className}HiveLocalAdapter = HiveLocalAdapter<$className> with \$${className}LocalAdapter${localMixins.map((m) => ', $m').join('')};

class \$${className}RemoteAdapter = RemoteAdapter<$className> with ${mixins.join(', ')};

final internal${classNameLower.capitalize()}RemoteAdapterProvider =
  Provider<RemoteAdapter<$className>>(
      (ref) => \$${className}RemoteAdapter(\$${className}HiveLocalAdapter(ref${typeId != null ? ', typeId: $typeId' : ''}), InternalHolder(_${classNameLower}Finders)));

final ${classNameLower}RepositoryProvider =
  Provider<Repository<$className>>((ref) => Repository<$className>(ref));

extension ${className}DataRepositoryX on Repository<$className> {
$mixinShortcuts
}

extension ${className}RelationshipGraphNodeX on RelationshipGraphNode<$className> {
${relationshipGraphNodeExtension.join('\n')}
}
''';
}