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 classType = element.name;
  final classTypePlural = element.name?.pluralize();
  final typeLowerCased = DataHelpers.getType(classType);
  ClassElement classElement;

  try {
    classElement = element as ClassElement;
  } catch (e) {
    throw UnsupportedError(
        "Can't generate repository for $classType. Please use @DataRepository on a class.");
  }

  void _checkIsFinal(final ClassElement? element, String? name) {
    if (element != null) {
      if (name != null && element.getSetter(name) != null) {
        throw UnsupportedError(
            "Can't generate repository for $classType. The `$name` field MUST be final");
      }
      _checkIsFinal(element.supertype?.element, name);
    }
  }

  _checkIsFinal(classElement, 'id');

  for (final field in relationshipFields(classElement)) {
    _checkIsFinal(classElement, field.name);
  }

  // relationship-related

  final relationships = relationshipFields(classElement)
      .fold<Set<Map<String, String?>>>({}, (result, field) {
    final relationshipClassElement = field.typeElement;

    // define inverse

    final relationshipAnnotation = TypeChecker.fromRuntime(DataRelationship)
        .firstAnnotationOfExact(field, throwOnUnresolved: false);

    var inverse =
        relationshipAnnotation?.getField('inverse')?.toStringValue();

    if (inverse == null) {
      final possibleInverseElements =
          relationshipFields(relationshipClassElement).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 $classType: ${possibleInverseElements.map((e) => e.name).join(', ')}

Please specify the correct inverse in the $classType 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

    final jsonKeyAnnotation = TypeChecker.fromRuntime(JsonKey)
        .firstAnnotationOfExact(field, throwOnUnresolved: false);

    final keyName = jsonKeyAnnotation?.getField('name')?.toStringValue();

    result.add({
      'key': keyName ?? field.name,
      'name': field.name,
      'inverse': inverse,
      'kind': field.type.element?.name,
      'type': DataHelpers.getType(relationshipClassElement.name),
    });

    return result;
  }).toList();

  final relationshipsFor = {
    for (final rel in relationships)
      '\'${rel['key']}\'': {
        '\'name\'': '\'${rel['name']}\'',
        if (rel['inverse'] != null) '\'inverse\'': '\'${rel['inverse']}\'',
        '\'type\'': '\'${rel['type']}\'',
        '\'kind\'': '\'${rel['kind']}\'',
        '\'instance\'': 'model?.' + rel['name']!,
      }
  };

  // serialization-related

  final hasFromJson =
      classElement.constructors.any((c) => c.name == 'fromJson');
  final fromJson = hasFromJson
      ? '$classType.fromJson(map)'
      : '_\$${classType}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()' : '_\$${classType}ToJson(model)';

  // additional adapters

  final remoteAdapterTypeChecker = TypeChecker.fromRuntime(RemoteAdapter);

  final mixins = annotation.read('adapters').listValue.map((obj) {
    final mixinType = obj.toTypeValue();
    final mixinMethods = <MethodElement>[];
    String? displayName;

    if (mixinType is ParameterizedType) {
      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');
      }

      if (!remoteAdapterTypeChecker.isAssignableFromType(mixinType)) {
        throw UnsupportedError(
            'Adapter `$mixinType` MUST have a constraint `on` RemoteAdapter<$classType>');
      }

      final instantiatedMixinType = (mixinType.element as ClassElement)
          .instantiate(
              typeArguments: [if (args.isNotEmpty) classElement.thisType],
              nullabilitySuffix: NullabilitySuffix.none);
      mixinMethods.addAll(instantiatedMixinType.methods);
      displayName =
          instantiatedMixinType.getDisplayString(withNullability: false);
    }

    return displayName;
  }).toSet();

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

  // template

  return '''
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member, non_constant_identifier_names

mixin \$${classType}LocalAdapter on LocalAdapter<$classType> {
@override
Map<String, Map<String, Object?>> relationshipsFor([$classType? model]) =>
  $relationshipsFor;

@override
$classType deserialize(map) {
  for (final key in relationshipsFor().keys) {
    map[key] = {
      '_': [map[key], !map.containsKey(key)],
    };
  }
  return $fromJson;
}

@override
Map<String, dynamic> serialize(model) => $toJson;
}

// ignore: must_be_immutable
class \$${classType}HiveLocalAdapter = HiveLocalAdapter<$classType> with \$${classType}LocalAdapter;

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

//

final ${typeLowerCased}LocalAdapterProvider = Provider<LocalAdapter<$classType>>(
  (ref) => \$${classType}HiveLocalAdapter(ref));

final ${typeLowerCased}RemoteAdapterProvider =
  Provider<RemoteAdapter<$classType>>(
      (ref) => \$${classType}RemoteAdapter(ref.read(${typeLowerCased}LocalAdapterProvider)));

final ${typeLowerCased}RepositoryProvider =
  Provider<Repository<$classType>>((ref) => Repository<$classType>(ref));

final _watch${classType == classTypePlural ? 'One' : ''}$classType =
  StateNotifierProvider.autoDispose.family<DataStateNotifier<$classType?>, DataState<$classType?>, WatchArgs<$classType>>(
      (ref, args) {
return ref.read(${typeLowerCased}RepositoryProvider).watchOne(args.id, remote: args.remote, params: args.params, headers: args.headers, alsoWatch: args.alsoWatch);
});

AutoDisposeStateNotifierProvider<DataStateNotifier<$classType?>, DataState<$classType?>> watch${classType == classTypePlural ? 'One' : ''}$classType(dynamic id,
  {bool? remote, Map<String, dynamic>? params, Map<String, String>? headers, AlsoWatch<$classType>? alsoWatch}) {
return _watch${classType == classTypePlural ? 'One' : ''}$classType(WatchArgs(id: id, remote: remote, params: params, headers: headers, alsoWatch: alsoWatch));
}

final _watch$classTypePlural =
  StateNotifierProvider.autoDispose.family<DataStateNotifier<List<$classType>>, DataState<List<$classType>>, WatchArgs<$classType>>(
      (ref, args) {
ref.maintainState = false;
return ref.read(${typeLowerCased}RepositoryProvider).watchAll(remote: args.remote, params: args.params, headers: args.headers, filterLocal: args.filterLocal, syncLocal: args.syncLocal);
});

AutoDisposeStateNotifierProvider<DataStateNotifier<List<$classType>>, DataState<List<$classType>>> watch$classTypePlural(
  {bool? remote, Map<String, dynamic>? params, Map<String, String>? headers, bool Function($classType)? filterLocal, bool? syncLocal}) {
return _watch$classTypePlural(WatchArgs(remote: remote, params: params, headers: headers, filterLocal: filterLocal, syncLocal: syncLocal));
}

extension ${classType}X on $classType {
/// Initializes "fresh" models (i.e. manually instantiated) to use
/// [save], [delete] and so on.
///
/// Can be obtained via `context.read`, `ref.read`, `container.read`
$classType init(Reader read, {bool save = true}) {
  final repository = internalLocatorFn(${typeLowerCased}RepositoryProvider, read);
  final updatedModel = repository.remoteAdapter.initializeModel(this, save: save);
  return save ? updatedModel : this;
}
}
''';
}