generateForAnnotatedElement method

  1. @override
dynamic 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
dynamic generateForAnnotatedElement(
  Element element,
  ConstantReader annotation,
  BuildStep buildStep,
) {
  if (element is! ClassElement) {
    throw "@QuickDatabase targets must be a class (${element.name})";
  }

  final dbClassName = "\$${element.name}";
  final dbModelMap = annotation.read("models").mapValue.entries;

  if (dbModelMap.isEmpty) {
    throw "${element.name}, annotated with @QuickDatabase, must specify at least one model type";
  }

  final intStoreModelMixinChecker = TypeChecker.typeNamed(IntStoreModel);
  final stringStoreModelMixinChecker = TypeChecker.typeNamed(
    StringStoreModel,
  );
  final mapTypeChecker = TypeChecker.typeNamed(Map);
  final models = <_ModelData>[];

  for (final entry in dbModelMap) {
    final modelTableName = entry.key!.toStringValue()!;
    final modelElement = entry.value!.toTypeValue()!.element!;

    // Ensure model is a class type
    if (modelElement is! ClassElement) {
      throw "${modelElement.name} is not a class";
    }

    // Ensure model implements the [StoreModel] mixin
    final implementsIntStoreModel = modelElement.mixins.any((mixinType) {
      return intStoreModelMixinChecker.isAssignableFrom(mixinType.element);
    });

    final implementsStringStoreModel = modelElement.mixins.any((mixinType) {
      return stringStoreModelMixinChecker.isAssignableFrom(mixinType.element);
    });

    if (!implementsIntStoreModel && !implementsStringStoreModel) {
      throw "${modelElement.name} must mixin IntStoreModel or StringStoreModel";
    } else if (implementsIntStoreModel && implementsStringStoreModel) {
      throw "${modelElement.name} must mixin only one of IntStoreModel or StringStoreModel";
    }

    // Ensure model has a factory [fromMap] method which accepts a Map object
    final fromMapConstructorElement = modelElement.getNamedConstructor(
      "fromMap",
    );

    final fromMapParameters = fromMapConstructorElement?.formalParameters;

    if (fromMapConstructorElement?.isFactory != true ||
        fromMapParameters!.length != 1 ||
        !mapTypeChecker.isAssignableFromType(fromMapParameters.first.type)) {
      throw "${modelElement.name} must have a 'fromMap' factory constructor which accepts a single Map object";
    }

    models.add(
      _ModelData(
        modelElement.name!,
        modelTableName,
        implementsIntStoreModel ? _IdType.int : _IdType.string,
      ),
    );
  }

  final output = <String>[];

  output.add("""
typedef LocationGetter = Future<File> Function();

class $dbClassName {
${_generateModelFieldDeclarations(models)}

$dbClassName._(\$DatabaseWrapper wrapper):
  ${_generateModelConstructorAssignments(models)};

/// Create an instance of the database backed by a file stored in the
/// file returned by [getLocation]
static Future<$dbClassName> createInstance(LocationGetter getLocation) async {
  final dbPath = await getLocation().then((file) => file.path);
  final sembastDb = await databaseFactoryIo.openDatabase(dbPath);
  return $dbClassName._(\$DatabaseWrapper(sembastDb));
}
}

${_generateModelStoreClassDeclarations(models)}
  """);

  return output.join("\n");
}