diff --git a/hive/example/pubspec.yaml b/hive/example/pubspec.yaml index c1a709bc..c1d1ba25 100644 --- a/hive/example/pubspec.yaml +++ b/hive/example/pubspec.yaml @@ -17,9 +17,4 @@ dependency_overrides: hive_ce: path: ../ hive_ce_generator: - # TODO: Revert - # path: ../../hive_generator - git: - url: https://github.com/IO-Design-Team/hive_ce - ref: 4f06756e5eee0b027f2e9d623e8136620309afcb - path: hive_generator + path: ../../hive_generator diff --git a/hive_generator/CHANGELOG.md b/hive_generator/CHANGELOG.md index 9620fb06..51223926 100644 --- a/hive_generator/CHANGELOG.md +++ b/hive_generator/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.8.0 + +- Adds support for the `GenerateAdapters` annotation. See the [hive_ce documentation](https://pub.dev/packages/hive_ce) for more information. + ## 1.7.0 - Supports named imports diff --git a/hive_generator/build.yaml b/hive_generator/build.yaml index 535563cd..8f17c84b 100644 --- a/hive_generator/build.yaml +++ b/hive_generator/build.yaml @@ -21,3 +21,17 @@ builders: build_extensions: { "$lib$": ["hive_registrar.g.dart"] } auto_apply: dependents build_to: source + hive_adapters_generator: + import: "package:hive_ce_generator/hive_generator.dart" + builder_factories: ["getAdaptersBuilder"] + build_extensions: { ".dart": ["hive_adapters_generator.g.part"] } + auto_apply: dependents + build_to: cache + applies_builders: ["source_gen|combining_builder"] + hive_schema_migrator: + import: "package:hive_ce_generator/hive_generator.dart" + builder_factories: ["getSchemaMigratorBuilder"] + build_extensions: + { "$lib$": ["hive/hive_adapters.yaml", "hive/hive_adapters.g.yaml"] } + auto_apply: none + build_to: source diff --git a/hive_generator/example/lib/hive/hive_adapters.dart b/hive_generator/example/lib/hive/hive_adapters.dart new file mode 100644 index 00000000..7d9b11c2 --- /dev/null +++ b/hive_generator/example/lib/hive/hive_adapters.dart @@ -0,0 +1,36 @@ +import 'package:hive_ce/hive.dart'; + +part 'hive_adapters.g.dart'; + +@GenerateAdapters( + [ + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + ], + firstTypeId: 50, +) +// This is for code generation +// ignore: unused_element +void _() {} + +class ClassSpec1 { + final int value; + final int value2; + + ClassSpec1(this.value, this.value2); +} + +class ClassSpec2 { + final String value; + final String value2; + + ClassSpec2(this.value, this.value2); +} + +enum EnumSpec { + value1, + value2; + + EnumSpec get getter => EnumSpec.value2; +} diff --git a/hive_generator/example/lib/hive/hive_adapters.g.dart b/hive_generator/example/lib/hive/hive_adapters.g.dart new file mode 100644 index 00000000..feb9801c --- /dev/null +++ b/hive_generator/example/lib/hive/hive_adapters.g.dart @@ -0,0 +1,120 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: unnecessary_const, require_trailing_commas, document_ignores + +part of 'hive_adapters.dart'; + +// ************************************************************************** +// AdaptersGenerator +// ************************************************************************** + +class ClassSpec1Adapter extends TypeAdapter { + @override + final int typeId = 50; + + @override + ClassSpec1 read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ClassSpec1( + (fields[0] as num).toInt(), + (fields[1] as num).toInt(), + ); + } + + @override + void write(BinaryWriter writer, ClassSpec1 obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.value) + ..writeByte(1) + ..write(obj.value2); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ClassSpec1Adapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ClassSpec2Adapter extends TypeAdapter { + @override + final int typeId = 51; + + @override + ClassSpec2 read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ClassSpec2( + fields[0] as String, + fields[1] as String, + ); + } + + @override + void write(BinaryWriter writer, ClassSpec2 obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.value) + ..writeByte(1) + ..write(obj.value2); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ClassSpec2Adapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class EnumSpecAdapter extends TypeAdapter { + @override + final int typeId = 52; + + @override + EnumSpec read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return EnumSpec.value1; + case 1: + return EnumSpec.value2; + default: + return EnumSpec.value1; + } + } + + @override + void write(BinaryWriter writer, EnumSpec obj) { + switch (obj) { + case EnumSpec.value1: + writer.writeByte(0); + case EnumSpec.value2: + writer.writeByte(1); + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EnumSpecAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/hive_generator/example/lib/hive/hive_adapters.g.yaml b/hive_generator/example/lib/hive/hive_adapters.g.yaml new file mode 100644 index 00000000..aac88d02 --- /dev/null +++ b/hive_generator/example/lib/hive/hive_adapters.g.yaml @@ -0,0 +1,29 @@ +# Generated by Hive CE +# Manual modifications may be necessary for certain migrations +# Check in to version control +nextTypeId: 53 +types: + ClassSpec1: + typeId: 50 + nextIndex: 2 + fields: + value: + index: 0 + value2: + index: 1 + ClassSpec2: + typeId: 51 + nextIndex: 2 + fields: + value: + index: 0 + value2: + index: 1 + EnumSpec: + typeId: 52 + nextIndex: 2 + fields: + value1: + index: 0 + value2: + index: 1 diff --git a/hive_generator/example/lib/hive_registrar.g.dart b/hive_generator/example/lib/hive/hive_registrar.g.dart similarity index 78% rename from hive_generator/example/lib/hive_registrar.g.dart rename to hive_generator/example/lib/hive/hive_registrar.g.dart index 7076c4ff..18485ad4 100644 --- a/hive_generator/example/lib/hive_registrar.g.dart +++ b/hive_generator/example/lib/hive/hive_registrar.g.dart @@ -5,18 +5,20 @@ // ignore_for_file: unnecessary_const, require_trailing_commas, document_ignores import 'package:hive_ce/hive.dart'; -import 'package:example/named_import.dart'; +import 'package:example/hive/hive_adapters.dart'; import 'package:example/types.dart'; extension HiveRegistrar on HiveInterface { void registerAdapters() { registerAdapter(Class1Adapter()); registerAdapter(Class2Adapter()); + registerAdapter(ClassSpec1Adapter()); + registerAdapter(ClassSpec2Adapter()); registerAdapter(ConstructorDefaultsAdapter()); registerAdapter(EmptyClassAdapter()); registerAdapter(Enum1Adapter()); + registerAdapter(EnumSpecAdapter()); registerAdapter(IterableClassAdapter()); - registerAdapter(NamedImportTypeAdapter()); registerAdapter(NamedImportsAdapter()); registerAdapter(NullableTypesAdapter()); } diff --git a/hive_generator/example/lib/named_import.dart b/hive_generator/example/lib/named_import.dart index 588bd7a9..3d3fe4fa 100644 --- a/hive_generator/example/lib/named_import.dart +++ b/hive_generator/example/lib/named_import.dart @@ -1,6 +1 @@ -import 'package:hive_ce/hive.dart'; - -part 'named_import.g.dart'; - -@HiveType(typeId: 100) class NamedImportType {} diff --git a/hive_generator/example/lib/named_import.g.dart b/hive_generator/example/lib/named_import.g.dart deleted file mode 100644 index 419eb7cc..00000000 --- a/hive_generator/example/lib/named_import.g.dart +++ /dev/null @@ -1,34 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -// ignore_for_file: unnecessary_const, require_trailing_commas, document_ignores - -part of 'named_import.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class NamedImportTypeAdapter extends TypeAdapter { - @override - final int typeId = 100; - - @override - NamedImportType read(BinaryReader reader) { - return NamedImportType(); - } - - @override - void write(BinaryWriter writer, NamedImportType obj) { - writer.writeByte(0); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is NamedImportTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/hive_generator/lib/hive_generator.dart b/hive_generator/lib/hive_generator.dart index 7f38e5bf..65745a85 100644 --- a/hive_generator/lib/hive_generator.dart +++ b/hive_generator/lib/hive_generator.dart @@ -1,7 +1,9 @@ import 'package:build/build.dart'; -import 'package:hive_ce_generator/src/registrar_builder.dart'; -import 'package:hive_ce_generator/src/registrar_intermediate_builder.dart'; -import 'package:hive_ce_generator/src/type_adapter_generator.dart'; +import 'package:hive_ce_generator/src/builder/schema_migrator_builder.dart'; +import 'package:hive_ce_generator/src/generator/adapters_generator.dart'; +import 'package:hive_ce_generator/src/builder/registrar_builder.dart'; +import 'package:hive_ce_generator/src/builder/registrar_intermediate_builder.dart'; +import 'package:hive_ce_generator/src/generator/type_adapter_generator.dart'; import 'package:source_gen/source_gen.dart'; /// Builds Hive TypeAdapters @@ -14,3 +16,11 @@ Builder getRegistrarIntermediateBuilder(BuilderOptions options) => /// Builds the HiveRegistrar extension Builder getRegistrarBuilder(BuilderOptions options) => RegistrarBuilder(); + +/// Builds Hive TypeAdapters from the GenerateAdapters annotation +Builder getAdaptersBuilder(BuilderOptions options) => + SharedPartBuilder([AdaptersGenerator()], 'hive_adapters_generator'); + +/// Builds a Hive schema from existing HiveType annotations +Builder getSchemaMigratorBuilder(BuilderOptions options) => + SchemaMigratorBuilder(); diff --git a/hive_generator/lib/src/builder.dart b/hive_generator/lib/src/adapter_builder/adapter_builder.dart similarity index 82% rename from hive_generator/lib/src/builder.dart rename to hive_generator/lib/src/adapter_builder/adapter_builder.dart index 9786c2d3..06fb3c89 100644 --- a/hive_generator/lib/src/builder.dart +++ b/hive_generator/lib/src/adapter_builder/adapter_builder.dart @@ -4,6 +4,9 @@ import 'package:analyzer/dart/element/type.dart'; /// Metadata about a field in a class adapter class AdapterField { + /// The corresponding element for this field + final PropertyAccessorElement element; + /// The index of the field /// /// Determines the order fields are read and written @@ -23,6 +26,7 @@ class AdapterField { /// Constructor AdapterField( + this.element, this.index, this.name, this.type, @@ -32,7 +36,7 @@ class AdapterField { } /// TODO: Document this! -abstract class Builder { +abstract class AdapterBuilder { /// TODO: Document this! final InterfaceElement cls; @@ -43,7 +47,11 @@ abstract class Builder { final List setters; /// TODO: Document this! - Builder(this.cls, this.getters, [this.setters = const []]); + AdapterBuilder( + this.cls, + this.getters, [ + this.setters = const [], + ]); /// TODO: Document this! String buildRead(); diff --git a/hive_generator/lib/src/class_builder.dart b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart similarity index 97% rename from hive_generator/lib/src/class_builder.dart rename to hive_generator/lib/src/adapter_builder/class_adapter_builder.dart index 4d88ce38..354cac33 100644 --- a/hive_generator/lib/src/class_builder.dart +++ b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart @@ -5,16 +5,16 @@ import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; import 'package:hive_ce/hive.dart'; -import 'package:hive_ce_generator/src/builder.dart'; +import 'package:hive_ce_generator/src/adapter_builder/adapter_builder.dart'; import 'package:hive_ce_generator/src/helper/helper.dart'; import 'package:source_gen/source_gen.dart'; import 'package:hive_ce_generator/src/helper/type_helper.dart'; /// TODO: Document this! -class ClassBuilder extends Builder { +class ClassAdapterBuilder extends AdapterBuilder { /// TODO: Document this! - ClassBuilder( + ClassAdapterBuilder( super.cls, super.getters, super.setters, diff --git a/hive_generator/lib/src/enum_builder.dart b/hive_generator/lib/src/adapter_builder/enum_adapter_builder.dart similarity index 85% rename from hive_generator/lib/src/enum_builder.dart rename to hive_generator/lib/src/adapter_builder/enum_adapter_builder.dart index fa6a8395..0aedcd50 100644 --- a/hive_generator/lib/src/enum_builder.dart +++ b/hive_generator/lib/src/adapter_builder/enum_adapter_builder.dart @@ -1,9 +1,9 @@ -import 'package:hive_ce_generator/src/builder.dart'; +import 'package:hive_ce_generator/src/adapter_builder/adapter_builder.dart'; /// TODO: Document this! -class EnumBuilder extends Builder { +class EnumAdapterBuilder extends AdapterBuilder { /// TODO: Document this! - EnumBuilder(super.cls, super.getters); + EnumAdapterBuilder(super.cls, super.getters); @override String buildRead() { diff --git a/hive_generator/lib/src/registrar_builder.dart b/hive_generator/lib/src/builder/registrar_builder.dart similarity index 70% rename from hive_generator/lib/src/registrar_builder.dart rename to hive_generator/lib/src/builder/registrar_builder.dart index 50641211..0423d9e4 100644 --- a/hive_generator/lib/src/registrar_builder.dart +++ b/hive_generator/lib/src/builder/registrar_builder.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:glob/glob.dart'; import 'package:build/build.dart'; +import 'package:hive_ce/hive.dart'; import 'dart:async'; import 'package:hive_ce_generator/src/helper/helper.dart'; @@ -20,6 +21,7 @@ class RegistrarBuilder implements Builder { Future build(BuildStep buildStep) async { final uris = []; final adapters = []; + Uri? registrarUri; await for (final input in buildStep.findAssets(Glob('**/*.hive_registrar.info'))) { final content = await buildStep.readAsString(input); @@ -27,6 +29,17 @@ class RegistrarBuilder implements Builder { final uri = data.uri; uris.add(uri.toString()); adapters.addAll(data.adapters); + if (data.registrarLocation) { + if (registrarUri != null) { + final sortedUris = + [registrarUri, uri].map((e) => e.toString()).toList()..sort(); + final urisString = sortedUris.map((e) => '- $e').join('\n'); + throw HiveError( + 'GenerateAdapters annotation found in more than one file:\n$urisString', + ); + } + registrarUri = uri; + } } // Do not create the registrar if there are no adapters @@ -79,8 +92,17 @@ extension HiveRegistrar on HiveInterface { } '''); - await buildStep.writeAsString( - buildStep.asset('lib/hive_registrar.g.dart'), + var registrarLocation = 'lib'; + if (registrarUri != null) { + final segments = registrarUri.pathSegments; + // Skip the package segment and remove the file segment + final registrarPath = segments.sublist(1, segments.length - 1).join('/'); + registrarLocation += '/$registrarPath'; + } + registrarLocation += '/hive_registrar.g.dart'; + + buildStep.forceWriteAsString( + buildStep.asset(registrarLocation), buffer.toString(), ); } diff --git a/hive_generator/lib/src/registrar_intermediate_builder.dart b/hive_generator/lib/src/builder/registrar_intermediate_builder.dart similarity index 55% rename from hive_generator/lib/src/registrar_intermediate_builder.dart rename to hive_generator/lib/src/builder/registrar_intermediate_builder.dart index 34e525ca..1d8949a2 100644 --- a/hive_generator/lib/src/registrar_intermediate_builder.dart +++ b/hive_generator/lib/src/builder/registrar_intermediate_builder.dart @@ -5,6 +5,7 @@ import 'package:build/build.dart'; import 'package:hive_ce/hive.dart'; import 'package:hive_ce_generator/src/helper/helper.dart'; import 'package:hive_ce_generator/src/model/registrar_intermediate.dart'; +import 'package:hive_ce_generator/src/model/revived_generate_adapter.dart'; import 'package:source_gen/source_gen.dart'; /// Builds intermediate data required for the registrar builder @@ -32,6 +33,32 @@ class RegistrarIntermediateBuilder implements Builder { adapters.add(adapterName); } + // If the registrar should be placed next to this file + final bool registrarLocation; + + final generateAdaptersChecker = TypeChecker.fromRuntime(GenerateAdapters); + final generateAdaptersElements = + LibraryReader(library).annotatedWith(generateAdaptersChecker); + // Read multiple annotations if they exist + final generateAdaptersAnnotations = generateAdaptersElements + .expand((e) => generateAdaptersChecker.annotationsOf(e.element)); + + if (generateAdaptersAnnotations.length > 1) { + throw HiveError( + 'Multiple GenerateAdapters annotations found in file: ${library.source.uri}', + ); + } else if (generateAdaptersElements.isNotEmpty) { + registrarLocation = true; + + final annotation = generateAdaptersElements.single.annotation; + final revived = RevivedGenerateAdapters(annotation); + for (final spec in revived.specs) { + adapters.add(generateAdapterName(spec.type.getDisplayString())); + } + } else { + registrarLocation = false; + } + if (adapters.isEmpty) return; await buildStep.writeAsString( @@ -40,6 +67,7 @@ class RegistrarIntermediateBuilder implements Builder { RegistrarIntermediate( uri: library.source.uri, adapters: adapters, + registrarLocation: registrarLocation, ), ), ); diff --git a/hive_generator/lib/src/builder/schema_migrator_builder.dart b/hive_generator/lib/src/builder/schema_migrator_builder.dart new file mode 100644 index 00000000..46bd135c --- /dev/null +++ b/hive_generator/lib/src/builder/schema_migrator_builder.dart @@ -0,0 +1,247 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:collection/collection.dart'; +import 'package:glob/glob.dart'; +import 'package:build/build.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:hive_ce_generator/src/generator/type_adapter_generator.dart'; +import 'dart:async'; + +import 'package:hive_ce_generator/src/helper/helper.dart'; +import 'package:hive_ce_generator/src/model/hive_schema.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:source_helper/source_helper.dart'; + +/// Generate a Hive schema from existing HiveType annotations +class SchemaMigratorBuilder implements Builder { + /// Exception if a field has a default value in the HiveField annotation + static String hasAnnotationDefaultValue({ + required String className, + required String fieldName, + }) => + '$className.$fieldName has a default value in the HiveField annotation.' + ' Convert it to a constructor parameter default before migrating.'; + + /// Exception if a field does not have a public setter + static String hasNoPublicSetter({ + required String className, + required String fieldName, + }) => + '$className.$fieldName does not have a public setter or corresponding constructor parameter'; + + /// Exception if a field does not have a public getter + static String hasNoPublicGetter({ + required String className, + required String fieldName, + }) => + '$className.$fieldName does not have a public getter'; + + /// Exception if a field will cause a schema mismatch + static String hasSchemaMismatch({ + required String className, + required Set accessors, + }) { + final accessorsString = accessors.join('\n- '); + return 'Accessors in $className do not have HiveField annotations' + ' but are valid accessors for the GenerateAdapters annotation.' + ' This will result in a schema mismatch.' + ' Consider moving these accessors to an extension:\n- $accessorsString'; + } + + @override + final buildExtensions = const { + r'$lib$': ['hive/hive_adapters.dart', 'hive/hive_adapters.g.yaml'], + }; + + @override + Future build(BuildStep buildStep) async { + final hiveTypes = []; + await for (final input in buildStep.findAssets(Glob('**/*.dart'))) { + if (!await buildStep.resolver.isLibrary(input)) continue; + final library = await buildStep.resolver.libraryFor(input); + final hiveTypeElements = LibraryReader(library) + .annotatedWith(TypeChecker.fromRuntime(HiveType)); + hiveTypes.addAll(hiveTypeElements); + } + + final schemaInfos = <_SchemaInfo>[]; + for (final type in hiveTypes) { + final cls = getClass(type.element); + final className = cls.displayName; + final library = type.element.library!; + final typeId = readTypeId(type.annotation); + final result = TypeAdapterGenerator.getAccessors( + typeId: typeId, + cls: cls, + library: library, + ); + + // Ensure no HiveField default values + for (final accessor in result.getters + result.setters) { + final annotationDefault = accessor.annotationDefault; + if (annotationDefault != null && !annotationDefault.isNull) { + throw InvalidGenerationSourceError( + hasAnnotationDefaultValue( + className: className, + fieldName: accessor.name, + ), + element: accessor.element, + ); + } + } + + final uri = library.source.uri; + final isEnum = cls.thisType.isEnum; + final constructor = getConstructor(cls); + final accessors = cls.accessors; + final info = _SchemaInfo( + uri: uri, + className: className, + isEnum: isEnum, + constructor: constructor, + accessors: accessors, + schema: result.schema, + ); + + // This includes any fields without HiveField annotations that would be + // included in adapters generated by the GenerateAdapters annotation + final secondPassResult = TypeAdapterGenerator.getAccessors( + typeId: typeId, + cls: cls, + library: library, + schema: info.schema, + ); + final secondPassInfo = _SchemaInfo( + uri: uri, + className: className, + isEnum: isEnum, + constructor: constructor, + accessors: accessors, + schema: secondPassResult.schema, + ); + + final firstPassFields = info.schema.fields.keys.toSet(); + final secondPassFields = secondPassInfo.schema.fields.keys.toSet(); + final accessorsWithoutAnnotations = + secondPassFields.difference(firstPassFields); + + if (accessorsWithoutAnnotations.isNotEmpty) { + throw InvalidGenerationSourceError( + hasSchemaMismatch( + className: className, + accessors: accessorsWithoutAnnotations, + ), + element: cls, + ); + } + + schemaInfos.add(info); + } + schemaInfos.sort((a, b) => a.schema.typeId.compareTo(b.schema.typeId)); + final nextTypeId = + schemaInfos.isEmpty ? 0 : schemaInfos.last.schema.typeId + 1; + + final types = { + for (final type in schemaInfos) type.className: type.schema, + }; + + final imports = schemaInfos + .map((e) => e.uri) + .toSet() // Remove duplicates + .map((e) => "import '$e';") + .sorted() // Sort alphabetically + .join('\n'); + final specs = + schemaInfos.map((e) => 'AdapterSpec<${e.className}>()').join(',\n '); + buildStep.forceWriteAsString( + buildStep.asset('lib/hive/hive_adapters.dart'), + ''' +import 'package:hive_ce/hive.dart'; +$imports + +part 'hive_adapters.g.dart'; + +@GenerateAdapters([ + $specs, +]) +// This is for code generation +// ignore: unused_element +void _() {} +''', + ); + + buildStep.forceWriteAsString( + buildStep.asset('lib/hive/hive_adapters.g.yaml'), + HiveSchema(nextTypeId: nextTypeId, types: types).toString(), + ); + } +} + +class _SchemaInfo { + final Uri uri; + final String className; + final HiveSchemaType schema; + + _SchemaInfo({ + required this.uri, + required this.className, + required bool isEnum, + required ConstructorElement constructor, + required List accessors, + required HiveSchemaType schema, + }) : schema = _sanitizeSchema( + className: className, + isEnum: isEnum, + schema: schema, + constructor: constructor, + accessors: accessors, + ); + + static HiveSchemaType _sanitizeSchema({ + required String className, + required bool isEnum, + required HiveSchemaType schema, + required ConstructorElement constructor, + required List accessors, + }) { + // Enums need no sanitization + if (isEnum) return schema; + + final sanitizedFields = {}; + for (final MapEntry(key: fieldName, value: schema) + in schema.fields.entries) { + final publicFieldName = + fieldName.startsWith('_') ? fieldName.substring(1) : fieldName; + + final isInConstructor = + constructor.parameters.any((e) => e.displayName == publicFieldName); + final publicAccessors = + accessors.where((e) => e.displayName == publicFieldName).toList(); + final hasPublicSetter = publicAccessors.any((e) => e.isSetter); + final hasPublicGetter = publicAccessors.any((e) => e.isGetter); + + if (!isInConstructor && !hasPublicSetter) { + throw InvalidGenerationSourceError( + SchemaMigratorBuilder.hasNoPublicSetter( + className: className, + fieldName: fieldName, + ), + element: constructor, + ); + } + + if (!hasPublicGetter) { + throw InvalidGenerationSourceError( + SchemaMigratorBuilder.hasNoPublicGetter( + className: className, + fieldName: fieldName, + ), + element: constructor, + ); + } + + sanitizedFields[publicFieldName] = schema; + } + + return schema.copyWith(fields: sanitizedFields); + } +} diff --git a/hive_generator/lib/src/generator/adapters_generator.dart b/hive_generator/lib/src/generator/adapters_generator.dart new file mode 100644 index 00000000..a80121cf --- /dev/null +++ b/hive_generator/lib/src/generator/adapters_generator.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:hive_ce_generator/src/helper/helper.dart'; +import 'package:hive_ce_generator/src/model/hive_schema.dart'; +import 'package:hive_ce_generator/src/model/revived_generate_adapter.dart'; +import 'package:hive_ce_generator/src/generator/type_adapter_generator.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:yaml/yaml.dart'; + +/// Builder that generates Hive adapters from a GenerateAdapters annotation +class AdaptersGenerator extends GeneratorForAnnotation { + @override + Future generateForAnnotatedElement( + Element element, + ConstantReader annotation, + BuildStep buildStep, + ) async { + final library = await buildStep.inputLibrary; + final revived = RevivedGenerateAdapters(annotation); + + final schemaAsset = buildStep.inputId.changeExtension('.g.yaml'); + final HiveSchema schema; + if (await buildStep.canRead(schemaAsset)) { + final schemaContent = await buildStep.readAsString(schemaAsset); + schema = + HiveSchema.fromJson(jsonDecode(jsonEncode(loadYaml(schemaContent)))); + } else { + schema = HiveSchema(nextTypeId: revived.firstTypeId, types: {}); + } + _validateSchema(schema); + + // Sort existing types by type ID + final existingSpecs = revived.specs + .where((spec) => schema.types.containsKey(spec.type.getDisplayString())) + .toList() + ..sort((a, b) { + final aTypeId = schema.types[a.type.getDisplayString()]!.typeId; + final bTypeId = schema.types[b.type.getDisplayString()]!.typeId; + return aTypeId.compareTo(bTypeId); + }); + + // Maintain order of new types + final newSpecs = revived.specs + .where( + (spec) => !schema.types.containsKey(spec.type.getDisplayString()), + ) + .toList(); + + var nextTypeId = schema.nextTypeId; + final newTypes = {}; + final content = StringBuffer(); + for (final spec in existingSpecs + newSpecs) { + final typeKey = spec.type.getDisplayString(); + final schemaType = schema.types[typeKey] ?? + HiveSchemaType(typeId: nextTypeId++, nextIndex: 0, fields: {}); + final result = TypeAdapterGenerator.generateTypeAdapter( + element: spec.type.element!, + library: library, + typeId: schemaType.typeId, + schema: schemaType, + ); + + content.write(result.content); + newTypes[typeKey] = result.schema; + } + + // Do not output the schema file through the buildStep since conflicting + // output handling will delete it before this generator runs + // Not the safest thing to do, but there doesn't seem to be a better way + buildStep.forceWriteAsString( + schemaAsset, + HiveSchema(nextTypeId: nextTypeId, types: newTypes).toString(), + ); + + return content.toString(); + } + + void _validateSchema(HiveSchema schema) { + void invalidSchema(String message) { + throw HiveError('Invalid schema: $message'); + } + + final typeIds = {}; + for (final type in schema.types.values) { + final typeId = type.typeId; + if (typeIds.contains(typeId)) { + invalidSchema('Duplicate type ID $typeId'); + } + typeIds.add(typeId); + + final fieldIndices = {}; + for (final field in type.fields.values) { + final index = field.index; + if (fieldIndices.contains(index)) { + invalidSchema('Duplicate field index $index for type ID $typeId'); + } + fieldIndices.add(index); + } + + final sortedIndices = fieldIndices.toList()..sort(); + final lastIndex = sortedIndices.lastOrNull ?? -1; + if (lastIndex >= type.nextIndex) { + invalidSchema('Next index is invalid for type ID $typeId'); + } + } + + final sortedTypeIds = typeIds.toList()..sort(); + final lastTypeId = sortedTypeIds.lastOrNull ?? -1; + if (lastTypeId >= schema.nextTypeId) { + invalidSchema('Next type ID is invalid'); + } + } +} diff --git a/hive_generator/lib/src/generator/type_adapter_generator.dart b/hive_generator/lib/src/generator/type_adapter_generator.dart new file mode 100644 index 00000000..84390cb5 --- /dev/null +++ b/hive_generator/lib/src/generator/type_adapter_generator.dart @@ -0,0 +1,250 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:hive_ce_generator/src/adapter_builder/adapter_builder.dart'; +import 'package:hive_ce_generator/src/adapter_builder/class_adapter_builder.dart'; +import 'package:hive_ce_generator/src/adapter_builder/enum_adapter_builder.dart'; +import 'package:hive_ce_generator/src/helper/helper.dart'; +import 'package:hive_ce_generator/src/model/hive_schema.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:source_helper/source_helper.dart'; + +/// TODO: Document this! +class TypeAdapterGenerator extends GeneratorForAnnotation { + @override + Future generateForAnnotatedElement( + Element element, + ConstantReader annotation, + BuildStep buildStep, + ) async { + final result = generateTypeAdapter( + element: element, + library: await buildStep.inputLibrary, + typeId: readTypeId(annotation), + adapterName: readAdapterName(annotation), + ); + return result.content; + } + + /// Generate a type adapter with the given information + /// + /// If this is an incremental update, pass the existing [schema] + static GenerateTypeAdapterResult generateTypeAdapter({ + required Element element, + required LibraryElement library, + required int typeId, + String? adapterName, + HiveSchemaType? schema, + }) { + final cls = getClass(element); + final getAccessorsResult = getAccessors( + typeId: typeId, + cls: cls, + library: library, + schema: schema, + ); + + final getters = getAccessorsResult.getters; + _verifyFieldIndices(getters); + + final setters = getAccessorsResult.setters; + _verifyFieldIndices(setters); + + adapterName ??= generateAdapterName(cls.name); + final builder = cls.thisType.isEnum + ? EnumAdapterBuilder(cls, getters) + : ClassAdapterBuilder(cls, getters, setters); + + final content = ''' + class $adapterName extends TypeAdapter<${cls.name}> { + @override + final int typeId = $typeId; + + @override + ${cls.name} read(BinaryReader reader) { + ${builder.buildRead()} + } + + @override + void write(BinaryWriter writer, ${cls.name} obj) { + ${builder.buildWrite()} + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is $adapterName && + runtimeType == other.runtimeType && + typeId == other.typeId; + } + '''; + + return GenerateTypeAdapterResult(content, getAccessorsResult.schema); + } + + /// TODO: Document this! + static Set _getAllAccessorNames(InterfaceElement cls) { + final isEnum = cls.thisType.isEnum; + final constructorFields = + getConstructor(cls).parameters.map((it) => it.name).toSet(); + + final accessorNames = {}; + final supertypes = cls.allSupertypes.map((it) => it.element); + for (final type in [cls, ...supertypes]) { + // Ignore Object base members + if (const TypeChecker.fromRuntime(Object).isExactly(type)) continue; + + for (final accessor in type.accessors) { + // Ignore any non-enum accessors on enums + if (isEnum && !accessor.returnType.isEnum) continue; + + // Ignore non-static fields on enums + if (isEnum && !accessor.isStatic) continue; + + // Ignore static fields on classes + if (!isEnum && accessor.isStatic) continue; + + // Ignore getters without setters on classes + if (!isEnum && + accessor.isGetter && + accessor.correspondingSetter == null && + !constructorFields.contains(accessor.name)) { + continue; + } + + // The display name does not have the trailing '=' for setters + accessorNames.add(accessor.displayName); + } + } + + return accessorNames; + } + + /// TODO: Document this! + static GetAccessorsResult getAccessors({ + required int typeId, + required InterfaceElement cls, + required LibraryElement library, + HiveSchemaType? schema, + }) { + final accessorNames = _getAllAccessorNames(cls); + + final constr = getConstructor(cls); + final parameterDefaults = { + for (final param in constr.parameters) param.name: param.defaultValueCode, + }; + + var nextIndex = schema?.nextIndex ?? 0; + final newSchemaFields = {}; + AdapterField? accessorToField(PropertyAccessorElement? element) { + if (element == null) return null; + + final annotation = + getHiveFieldAnn(element.variable2) ?? getHiveFieldAnn(element); + if (schema == null && annotation == null) return null; + + final field = element.variable2!; + final name = field.name; + final int index; + if (schema != null) { + // Only generate one id per field name + index = schema.fields[name]?.index ?? + newSchemaFields[name]?.index ?? + nextIndex++; + } else if (annotation != null) { + index = annotation.index; + + // Keep track of the next index for the migration tool + if (index >= nextIndex) nextIndex = index + 1; + } else { + // This should be impossible + throw HiveError('No index found'); + } + + newSchemaFields[name] = HiveSchemaField(index: index); + return AdapterField( + element, + index, + name, + field.type, + annotation?.defaultValue, + parameterDefaults[name], + ); + } + + final getters = []; + final setters = []; + for (final name in accessorNames) { + final getter = cls.augmented.lookUpGetter(name: name, library: library); + final getterField = accessorToField(getter); + if (getterField != null) getters.add(getterField); + + final setter = + cls.augmented.lookUpSetter(name: '$name=', library: library); + final setterField = accessorToField(setter); + if (setterField != null) setters.add(setterField); + } + + // Sort by index for deterministic output + getters.sort((a, b) => a.index.compareTo(b.index)); + setters.sort((a, b) => a.index.compareTo(b.index)); + final newSchema = HiveSchemaType( + typeId: typeId, + nextIndex: nextIndex, + fields: Map.fromEntries( + newSchemaFields.entries.toList() + ..sort((a, b) => a.value.index.compareTo(b.value.index)), + ), + ); + return GetAccessorsResult(getters, setters, newSchema); + } + + /// TODO: Document this! + static void _verifyFieldIndices(List fields) { + for (final field in fields) { + if (field.index < 0 || field.index > 255) { + throw 'Field numbers can only be in the range 0-255.'; + } + + for (final otherField in fields) { + if (otherField == field) continue; + if (otherField.index == field.index) { + throw HiveError( + 'Duplicate field number: ${field.index}. Fields "${field.name}" ' + 'and "${otherField.name}" have the same number.', + ); + } + } + } + } +} + +/// Result of [TypeAdapterGenerator.getAccessors] +class GetAccessorsResult { + /// The getters of the class + final List getters; + + /// The setters of the class + final List setters; + + /// The Hive schema generated for the class + final HiveSchemaType schema; + + /// Constructor + const GetAccessorsResult(this.getters, this.setters, this.schema); +} + +/// Result of [TypeAdapterGenerator.generateTypeAdapter] +class GenerateTypeAdapterResult { + /// The generated content + final String content; + + /// The generated schema + final HiveSchemaType schema; + + /// Constructor + const GenerateTypeAdapterResult(this.content, this.schema); +} diff --git a/hive_generator/lib/src/helper/helper.dart b/hive_generator/lib/src/helper/helper.dart index c5feebf5..8f052d8e 100644 --- a/hive_generator/lib/src/helper/helper.dart +++ b/hive_generator/lib/src/helper/helper.dart @@ -1,9 +1,12 @@ +import 'dart:io'; + import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:collection/collection.dart'; import 'package:hive_ce/hive.dart'; import 'package:source_gen/source_gen.dart'; +import 'package:path/path.dart' as path; final _hiveFieldChecker = const TypeChecker.fromRuntime(HiveField); @@ -81,4 +84,16 @@ int readTypeId(ConstantReader annotation) { extension BuildStepExtension on BuildStep { /// Create an [AssetId] for the given [path] relative to the input package AssetId asset(String path) => AssetId(inputId.package, path); + + /// Write [content] to asset [id] ignoring output restrictions + /// + /// This exists to bypass the following restrictions: + /// - `$lib$` inputs can only have fixed output locations + /// - Any files output through `buildStep.writeAsString` will be deleted + /// before the build starts + void forceWriteAsString(AssetId id, String content) { + File(path.joinAll(id.pathSegments)) + ..createSync(recursive: true) + ..writeAsStringSync(content); + } } diff --git a/hive_generator/lib/src/model/hive_schema.dart b/hive_generator/lib/src/model/hive_schema.dart new file mode 100644 index 00000000..ae65333a --- /dev/null +++ b/hive_generator/lib/src/model/hive_schema.dart @@ -0,0 +1,95 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:yaml_writer/yaml_writer.dart'; + +part 'hive_schema.g.dart'; + +/// Information about Hive adapters used to support incremental changes +@JsonSerializable() +class HiveSchema { + /// The comment placed at the top of the schema file + static const comment = ''' +# Generated by Hive CE +# Manual modifications may be necessary for certain migrations +# Check in to version control'''; + + /// The next type ID to use for future updates + final int nextTypeId; + + /// The adapter types + final Map types; + + /// Constructor + const HiveSchema({required this.nextTypeId, required this.types}); + + /// From json + factory HiveSchema.fromJson(Map json) => + _$HiveSchemaFromJson(json); + + /// To json + Map toJson() => _$HiveSchemaToJson(this); + + @override + String toString() { + final yaml = YamlWriter().write(toJson()); + return ''' +$comment +$yaml'''; + } +} + +/// Information about a Hive adapter type +@JsonSerializable() +class HiveSchemaType { + /// The adapter's type ID + final int typeId; + + /// The next field index to use for future updates + final int nextIndex; + + /// The fields in the adapter + final Map fields; + + /// Constructor + const HiveSchemaType({ + required this.typeId, + required this.nextIndex, + required this.fields, + }); + + /// From json + factory HiveSchemaType.fromJson(Map json) => + _$HiveSchemaTypeFromJson(json); + + /// To json + Map toJson() => _$HiveSchemaTypeToJson(this); + + /// Copy with + HiveSchemaType copyWith({ + int? typeId, + int? nextIndex, + Map? fields, + }) { + return HiveSchemaType( + typeId: typeId ?? this.typeId, + nextIndex: nextIndex ?? this.nextIndex, + fields: fields ?? this.fields, + ); + } +} + +/// Information about a Hive adapter field +@JsonSerializable() +class HiveSchemaField { + /// The field index + final int index; + + /// Constructor + const HiveSchemaField({required this.index}); + + /// From json + factory HiveSchemaField.fromJson(Map json) => + _$HiveSchemaFieldFromJson(json); + + /// To json + Map toJson() => _$HiveSchemaFieldToJson(this); +} diff --git a/hive_generator/lib/src/model/hive_schema.g.dart b/hive_generator/lib/src/model/hive_schema.g.dart new file mode 100644 index 00000000..2920aa52 --- /dev/null +++ b/hive_generator/lib/src/model/hive_schema.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: document_ignores, require_trailing_commas + +part of 'hive_schema.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +HiveSchema _$HiveSchemaFromJson(Map json) => HiveSchema( + nextTypeId: (json['nextTypeId'] as num).toInt(), + types: (json['types'] as Map).map( + (k, e) => + MapEntry(k, HiveSchemaType.fromJson(e as Map)), + ), + ); + +Map _$HiveSchemaToJson(HiveSchema instance) => + { + 'nextTypeId': instance.nextTypeId, + 'types': instance.types, + }; + +HiveSchemaType _$HiveSchemaTypeFromJson(Map json) => + HiveSchemaType( + typeId: (json['typeId'] as num).toInt(), + nextIndex: (json['nextIndex'] as num).toInt(), + fields: (json['fields'] as Map).map( + (k, e) => + MapEntry(k, HiveSchemaField.fromJson(e as Map)), + ), + ); + +Map _$HiveSchemaTypeToJson(HiveSchemaType instance) => + { + 'typeId': instance.typeId, + 'nextIndex': instance.nextIndex, + 'fields': instance.fields, + }; + +HiveSchemaField _$HiveSchemaFieldFromJson(Map json) => + HiveSchemaField( + index: (json['index'] as num).toInt(), + ); + +Map _$HiveSchemaFieldToJson(HiveSchemaField instance) => + { + 'index': instance.index, + }; diff --git a/hive_generator/lib/src/model/registrar_intermediate.dart b/hive_generator/lib/src/model/registrar_intermediate.dart index 1a870342..abd86dfa 100644 --- a/hive_generator/lib/src/model/registrar_intermediate.dart +++ b/hive_generator/lib/src/model/registrar_intermediate.dart @@ -11,10 +11,16 @@ class RegistrarIntermediate { /// The names of the adapters final List adapters; + /// If this is where the Hive registrar should be placed + /// + /// Only one intermediate may have this set to true + final bool registrarLocation; + /// Constructor const RegistrarIntermediate({ required this.uri, required this.adapters, + required this.registrarLocation, }); /// From json diff --git a/hive_generator/lib/src/model/registrar_intermediate.g.dart b/hive_generator/lib/src/model/registrar_intermediate.g.dart index 75edb86f..19bbf0b0 100644 --- a/hive_generator/lib/src/model/registrar_intermediate.g.dart +++ b/hive_generator/lib/src/model/registrar_intermediate.g.dart @@ -14,6 +14,7 @@ RegistrarIntermediate _$RegistrarIntermediateFromJson( uri: Uri.parse(json['uri'] as String), adapters: (json['adapters'] as List).map((e) => e as String).toList(), + registrarLocation: json['registrarLocation'] as bool, ); Map _$RegistrarIntermediateToJson( @@ -21,4 +22,5 @@ Map _$RegistrarIntermediateToJson( { 'uri': instance.uri.toString(), 'adapters': instance.adapters, + 'registrarLocation': instance.registrarLocation, }; diff --git a/hive_generator/lib/src/model/revived_generate_adapter.dart b/hive_generator/lib/src/model/revived_generate_adapter.dart new file mode 100644 index 00000000..cc47239b --- /dev/null +++ b/hive_generator/lib/src/model/revived_generate_adapter.dart @@ -0,0 +1,31 @@ +import 'package:analyzer/dart/element/type.dart'; +import 'package:source_gen/source_gen.dart'; + +/// A revived GenerateAdapters annotation +class RevivedGenerateAdapters { + /// The revived adapter specs + final List specs; + + /// The first type ID to use + final int firstTypeId; + + /// Revive a GenerateAdapters annotation + RevivedGenerateAdapters(ConstantReader annotation) + : specs = annotation + .read('specs') + .listValue + .map((spec) => spec.type as InterfaceType) + .map((type) => type.typeArguments.single) + .map((type) => RevivedAdapterSpec(type: type)) + .toList(), + firstTypeId = annotation.read('firstTypeId').intValue; +} + +/// A revived adapter spec +class RevivedAdapterSpec { + /// The type of the adapter + final DartType type; + + /// Constructor + const RevivedAdapterSpec({required this.type}); +} diff --git a/hive_generator/lib/src/type_adapter_generator.dart b/hive_generator/lib/src/type_adapter_generator.dart deleted file mode 100644 index 356787ce..00000000 --- a/hive_generator/lib/src/type_adapter_generator.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:analyzer/dart/element/element.dart'; -import 'package:build/build.dart'; -import 'package:hive_ce/hive.dart'; -import 'package:hive_ce_generator/src/builder.dart'; -import 'package:hive_ce_generator/src/class_builder.dart'; -import 'package:hive_ce_generator/src/enum_builder.dart'; -import 'package:hive_ce_generator/src/helper/helper.dart'; -import 'package:source_gen/source_gen.dart'; -import 'package:source_helper/source_helper.dart'; - -/// TODO: Document this! -class TypeAdapterGenerator extends GeneratorForAnnotation { - @override - Future generateForAnnotatedElement( - Element element, - ConstantReader annotation, - BuildStep buildStep, - ) async { - final cls = getClass(element); - final library = await buildStep.inputLibrary; - final getAccessorsResult = _getAccessors(cls: cls, library: library); - - final getters = getAccessorsResult.getters; - _verifyFieldIndices(getters); - - final setters = getAccessorsResult.setters; - _verifyFieldIndices(setters); - - final typeId = readTypeId(annotation); - - final adapterName = - readAdapterName(annotation) ?? generateAdapterName(cls.name); - final builder = cls.thisType.isEnum - ? EnumBuilder(cls, getters) - : ClassBuilder(cls, getters, setters); - - return ''' - class $adapterName extends TypeAdapter<${cls.name}> { - @override - final int typeId = $typeId; - - @override - ${cls.name} read(BinaryReader reader) { - ${builder.buildRead()} - } - - @override - void write(BinaryWriter writer, ${cls.name} obj) { - ${builder.buildWrite()} - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is $adapterName && - runtimeType == other.runtimeType && - typeId == other.typeId; - } - '''; - } - - /// TODO: Document this! - Set _getAllAccessorNames(InterfaceElement cls) { - final accessorNames = {}; - - final supertypes = cls.allSupertypes.map((it) => it.element); - for (final type in [cls, ...supertypes]) { - for (final accessor in type.accessors) { - final name = accessor.name; - if (accessor.isSetter) { - // Remove '=' from setter name - accessorNames.add(name.substring(0, name.length - 1)); - } else { - accessorNames.add(name); - } - } - } - - return accessorNames; - } - - /// TODO: Document this! - _GetAccessorsResult _getAccessors({ - required InterfaceElement cls, - required LibraryElement library, - }) { - final accessorNames = _getAllAccessorNames(cls); - - final constr = getConstructor(cls); - final parameterDefaults = { - for (final param in constr.parameters) param.name: param.defaultValueCode, - }; - - AdapterField? accessorToField(PropertyAccessorElement? element) { - if (element == null) return null; - - final annotation = - getHiveFieldAnn(element.variable2) ?? getHiveFieldAnn(element); - if (annotation == null) return null; - - final field = element.variable2!; - final name = field.name; - return AdapterField( - annotation.index, - name, - field.type, - annotation.defaultValue, - parameterDefaults[name], - ); - } - - final getters = []; - final setters = []; - for (final name in accessorNames) { - final getter = cls.augmented.lookUpGetter(name: name, library: library); - final getterField = accessorToField(getter); - if (getterField != null) getters.add(getterField); - - final setter = - cls.augmented.lookUpSetter(name: '$name=', library: library); - final setterField = accessorToField(setter); - if (setterField != null) setters.add(setterField); - } - - // Sort by index for deterministic output - getters.sort((a, b) => a.index.compareTo(b.index)); - setters.sort((a, b) => a.index.compareTo(b.index)); - return _GetAccessorsResult(getters, setters); - } - - /// TODO: Document this! - void _verifyFieldIndices(List fields) { - for (final field in fields) { - if (field.index < 0 || field.index > 255) { - throw 'Field numbers can only be in the range 0-255.'; - } - - for (final otherField in fields) { - if (otherField == field) continue; - if (otherField.index == field.index) { - throw HiveError( - 'Duplicate field number: ${field.index}. Fields "${field.name}" ' - 'and "${otherField.name}" have the same number.', - ); - } - } - } - } -} - -class _GetAccessorsResult { - final List getters; - final List setters; - - const _GetAccessorsResult(this.getters, this.setters); -} diff --git a/hive_generator/pubspec.yaml b/hive_generator/pubspec.yaml index 30c309a0..1fa41320 100644 --- a/hive_generator/pubspec.yaml +++ b/hive_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: hive_ce_generator description: Extension for Hive. Automatically generates TypeAdapters to store any class. -version: 1.7.0 +version: 1.8.0 homepage: https://github.com/IO-Design-Team/hive_ce/tree/main/hive_generator documentation: https://docs.hivedb.dev/ @@ -10,12 +10,13 @@ environment: dependencies: build: ^2.0.0 source_gen: ^1.0.0 - hive_ce: ^2.4.0 + hive_ce: ^2.8.0 analyzer: ^6.5.0 source_helper: ^1.1.0 glob: ^2.1.2 path: ^1.9.0 yaml: ^3.1.2 + yaml_writer: ^2.0.1 json_annotation: ^4.9.0 collection: ^1.18.0 diff --git a/hive_generator/test/adapters_generator_test.dart b/hive_generator/test/adapters_generator_test.dart new file mode 100644 index 00000000..7fe124be --- /dev/null +++ b/hive_generator/test/adapters_generator_test.dart @@ -0,0 +1,314 @@ +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +const directives = ''' +import 'package:hive_ce/hive.dart'; +part 'hive_adapters.g.dart';'''; + +const personSchema = ''' +$schemaComment +nextTypeId: 1 +types: + Person: + typeId: 0 + nextIndex: 2 + fields: + name: + index: 0 + age: + index: 1 +'''; + +void main() { + group('adapters_generator', () { + test('fresh', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([AdapterSpec()]) +class Person { + const Person({required this.name, required this.age}); + + final String name; + final int age; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.yaml': personSchema, + }, + ); + }); + + test('add type', () { + // Adding Person2 should result in Person2 having a typeId of 1 no matter + // the order in the annotation + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([AdapterSpec(), AdapterSpec()]) +class Person { + const Person({required this.name, required this.age}); + + final String name; + final int age; +} + +class Person2 { + const Person2({required this.name, required this.age}); + + final String name; + final int age; +} +''', + 'lib/hive/hive_adapters.g.yaml': personSchema, + }, + output: { + 'lib/hive/hive_adapters.g.yaml': ''' +$schemaComment +nextTypeId: 2 +types: + Person: + typeId: 0 + nextIndex: 2 + fields: + name: + index: 0 + age: + index: 1 + Person2: + typeId: 1 + nextIndex: 2 + fields: + name: + index: 0 + age: + index: 1 +''', + }, + ); + }); + + test('add and remove type', () { + // Adding Person2 while removing Person should result in Person2 having a + // typeId of 1 + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([AdapterSpec()]) +class Person2 { + const Person2({required this.name, required this.age}); + + final String name; + final int age; +} +''', + 'lib/hive/hive_adapters.g.yaml': personSchema, + }, + output: { + 'lib/hive/hive_adapters.g.yaml': ''' +$schemaComment +nextTypeId: 2 +types: + Person2: + typeId: 1 + nextIndex: 2 + fields: + name: + index: 0 + age: + index: 1 +''', + }, + ); + }); + + test('add field', () { + // A new field on Person should have the last index no matter the order + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([AdapterSpec()]) +class Person { + const Person({required this.balance, required this.name, required this.age}); + + final double balance; + final String name; + final int age; +} +''', + 'lib/hive/hive_adapters.g.yaml': personSchema, + }, + output: { + 'lib/hive/hive_adapters.g.yaml': ''' +$schemaComment +nextTypeId: 1 +types: + Person: + typeId: 0 + nextIndex: 3 + fields: + name: + index: 0 + age: + index: 1 + balance: + index: 2 +''', + }, + ); + }); + + test('add and remove field', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([AdapterSpec()]) +class Person { + const Person({required this.name, required this.balance}); + + final String name; + final double balance; +} +''', + 'lib/hive/hive_adapters.g.yaml': personSchema, + }, + output: { + 'lib/hive/hive_adapters.g.yaml': ''' +$schemaComment +nextTypeId: 1 +types: + Person: + typeId: 0 + nextIndex: 3 + fields: + name: + index: 0 + balance: + index: 2 +''', + }, + ); + }); + + group('validates schema', () { + test('with invalid next type ID', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([]) +void _() {} +''', + 'lib/hive/hive_adapters.g.yaml': ''' +nextTypeId: 0 +types: + Person: + typeId: 0 + nextIndex: 0 + fields: {} +''', + }, + throws: 'Invalid schema: Next type ID is invalid', + ); + }); + + test('with invalid next field index', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([]) +void _() {} +''', + 'lib/hive/hive_adapters.g.yaml': ''' +nextTypeId: 1 +types: + Person: + typeId: 0 + nextIndex: 0 + fields: + name: + index: 0 +''', + }, + throws: 'Invalid schema: Next index is invalid for type ID 0', + ); + }); + + test('with duplicate type ID', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([]) +void _() {} +''', + 'lib/hive/hive_adapters.g.yaml': ''' +nextTypeId: 1 +types: + Person: + typeId: 0 + nextIndex: 0 + fields: {} + Person2: + typeId: 0 + nextIndex: 0 + fields: {} +''', + }, + throws: 'Invalid schema: Duplicate type ID 0', + ); + }); + + test('with duplicate field index', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +$directives + +@GenerateAdapters([]) +void _() {} +''', + 'lib/hive/hive_adapters.g.yaml': ''' +nextTypeId: 1 +types: + Person: + typeId: 0 + nextIndex: 0 + fields: + name: + index: 0 + age: + index: 0 +''', + }, + throws: 'Invalid schema: Duplicate field index 0 for type ID 0', + ); + }); + }); + }); +} diff --git a/hive_generator/test/registrar_builder_test.dart b/hive_generator/test/registrar_builder_test.dart index 54de6482..a86e76fb 100644 --- a/hive_generator/test/registrar_builder_test.dart +++ b/hive_generator/test/registrar_builder_test.dart @@ -7,7 +7,7 @@ void main() { test('outputs to lib folder by default', () { expectGeneration( input: { - 'pubspec.yaml': pubspec, + ...pubspec, 'lib/nested/type.dart': ''' import 'package:hive_ce/hive.dart'; part 'type.g.dart'; @@ -25,12 +25,103 @@ class Type {} test('does not output with no types', () { expectGeneration( input: { - 'pubspec.yaml': pubspec, + ...pubspec, + 'lib/hive_adapters.dart': ''' +import 'package:hive_ce/hive.dart'; +part 'hive_adapters.g.dart'; + +@GenerateAdapters([]) +void _() {} +''', }, output: { 'lib/hive_registrar.g.dart': fileDoesNotExist, }, ); }); + + test('outputs next to GenerateAdapters annotation', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive/hive_adapters.dart': ''' +import 'package:hive_ce/hive.dart'; +part 'hive_adapters.g.dart'; + +@GenerateAdapters([AdapterSpec()]) +class Type {} +''', + }, + output: { + 'lib/hive/hive_registrar.g.dart': fileExists, + }, + ); + }); + + test('fails with multiple GenerateAdapters in same file', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive_adapters.dart': ''' +import 'package:hive_ce/hive.dart'; +part 'hive_adapters.g.dart'; + +@GenerateAdapters([AdapterSpec()]) +class Type {} + +@GenerateAdapters([AdapterSpec()]) +class Type2 {} +''', + }, + throws: + 'Multiple GenerateAdapters annotations found in file: package:hive_ce_generator_test/hive_adapters.dart', + ); + }); + + test('fails with multiple GenerateAdapters on same element', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive_adapters.dart': ''' +import 'package:hive_ce/hive.dart'; +part 'hive_adapters.g.dart'; + +@GenerateAdapters([AdapterSpec()]) +@GenerateAdapters([AdapterSpec()]) +class Type {} + +class Type2 {} +''', + }, + throws: + 'Multiple GenerateAdapters annotations found in file: package:hive_ce_generator_test/hive_adapters.dart', + ); + }); + + test('fails with multiple GenerateAdapters files', () { + expectGeneration( + input: { + ...pubspec, + 'lib/hive_adapters.dart': ''' +import 'package:hive_ce/hive.dart'; +part 'hive_adapters.g.dart'; + +@GenerateAdapters([AdapterSpec()]) +class Type {} +''', + 'lib/hive_adapters_2.dart': ''' +import 'package:hive_ce/hive.dart'; +part 'hive_adapters_2.g.dart'; + +@GenerateAdapters([AdapterSpec()]) +class Type2 {} +''', + }, + throws: ''' +GenerateAdapters annotation found in more than one file: +- package:hive_ce_generator_test/hive_adapters.dart +- package:hive_ce_generator_test/hive_adapters_2.dart''', + ); + }); }); } diff --git a/hive_generator/test/schema_migrator_test.dart b/hive_generator/test/schema_migrator_test.dart new file mode 100644 index 00000000..00a437b8 --- /dev/null +++ b/hive_generator/test/schema_migrator_test.dart @@ -0,0 +1,231 @@ +import 'package:hive_ce_generator/src/builder/schema_migrator_builder.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +const buildYaml = { + 'build.yaml': r''' +targets: + $default: + builders: + hive_ce_generator|hive_schema_migrator: + enabled: true +''', +}; + +const adapters = { + 'lib/adapters.dart': ''' +import 'package:hive_ce/hive.dart'; + +@HiveType(typeId: 1) +class Class1 {} + +@HiveType(typeId: 0) +class Class2 { + const Class2(this.lastName, this.firstName); + + @HiveField(1) + final String lastName; + + @HiveField(0) + final String firstName; +} +''', + 'lib/adapters_2.dart': ''' +import 'package:hive_ce/hive.dart'; + +@HiveType(typeId: 3) +class Class3 { + Class3({required double value}): _value = value; + + @HiveField(0) + double _value; + + double get value => _value; +} + +@HiveType(typeId: 4) +enum Enum { + @HiveField(0) + a, + + @HiveField(1) + b, + + @HiveField(2) + c, +} +''', +}; + +void main() { + group('schema_migrator', () { + test('does not run if not enabled', () { + expectGeneration( + input: { + ...pubspec, + ...adapters, + }, + output: { + 'lib/hive_schema.g.yaml': fileDoesNotExist, + }, + ); + }); + + test('generates schema', () { + expectGeneration( + input: { + ...pubspec, + ...buildYaml, + ...adapters, + }, + output: { + 'lib/hive/hive_adapters.dart': ''' +import 'package:hive_ce/hive.dart'; +import 'package:hive_ce_generator_test/adapters.dart'; +import 'package:hive_ce_generator_test/adapters_2.dart'; + +part 'hive_adapters.g.dart'; + +@GenerateAdapters([ + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), +]) +// This is for code generation +// ignore: unused_element +void _() {} +''', + 'lib/hive/hive_adapters.g.yaml': ''' +$schemaComment +nextTypeId: 5 +types: + Class2: + typeId: 0 + nextIndex: 2 + fields: + firstName: + index: 0 + lastName: + index: 1 + Class1: + typeId: 1 + nextIndex: 0 + fields: {} + Class3: + typeId: 3 + nextIndex: 1 + fields: + value: + index: 0 + Enum: + typeId: 4 + nextIndex: 3 + fields: + a: + index: 0 + b: + index: 1 + c: + index: 2 +''', + }, + ); + }); + + test('throws with default value', () { + expectGeneration( + input: { + ...pubspec, + ...buildYaml, + 'lib/adapters.dart': ''' +import 'package:hive_ce/hive.dart'; + +@HiveType(typeId: 0) +class Class { + @HiveField(0, defaultValue: 42) + int? value; +} +''', + }, + throws: SchemaMigratorBuilder.hasAnnotationDefaultValue( + className: 'Class', + fieldName: 'value', + ), + ); + }); + + test('throws with no public setter', () { + expectGeneration( + input: { + ...pubspec, + ...buildYaml, + 'lib/adapters.dart': ''' +import 'package:hive_ce/hive.dart'; + +@HiveType(typeId: 0) +class Class { + @HiveField(0) + int? _value; + + int? get value => _value; +} +''', + }, + throws: SchemaMigratorBuilder.hasNoPublicSetter( + className: 'Class', + fieldName: '_value', + ), + ); + }); + + test('throws with no public getter', () { + expectGeneration( + input: { + ...pubspec, + ...buildYaml, + 'lib/adapters.dart': ''' +import 'package:hive_ce/hive.dart'; + +@HiveType(typeId: 0) +class Class { + Class({required int value}): _value = value; + + @HiveField(0) + int? _value; +} +''', + }, + throws: SchemaMigratorBuilder.hasNoPublicGetter( + className: 'Class', + fieldName: '_value', + ), + ); + }); + + test('throws with schema mismatch', () { + expectGeneration( + input: { + ...pubspec, + ...buildYaml, + 'lib/adapters.dart': ''' +import 'package:hive_ce/hive.dart'; + +@HiveType(typeId: 0) +class Class { + @HiveField(0) + int? value; + + int? value2; +} +''', + }, + throws: SchemaMigratorBuilder.hasSchemaMismatch( + className: 'Class', + accessors: {'value2'}, + ), + ); + }); + }); +} diff --git a/hive_generator/test/test_utils.dart b/hive_generator/test/test_utils.dart index 398ccbbf..76b43827 100644 --- a/hive_generator/test/test_utils.dart +++ b/hive_generator/test/test_utils.dart @@ -1,8 +1,11 @@ import 'dart:io'; +import 'package:hive_ce_generator/src/model/hive_schema.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; +const schemaComment = HiveSchema.comment; + const fileExists = true; const fileDoesNotExist = false; @@ -58,11 +61,12 @@ String createTestProject(Map project) { return directory.path; } -String get pubspec { +Map get pubspec { final hivePath = path.absolute(path.current, '..', 'hive'); final hiveGeneratorPath = path.absolute(path.current); - return ''' + return { + 'pubspec.yaml': ''' name: hive_ce_generator_test environment: @@ -80,5 +84,6 @@ dependency_overrides: path: $hivePath hive_ce_generator: path: $hiveGeneratorPath -'''; +''', + }; }