diff --git a/hive/CHANGELOG.md b/hive/CHANGELOG.md index 586c7665..fb5e2b21 100644 --- a/hive/CHANGELOG.md +++ b/hive/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.20.0 + +- Adds `HiveConverter` for custom field conversion during TypeAdapter generation (similar to `json_serializable`'s `JsonConverter`) +- Adds `converters` parameter to `GenerateAdapters` + ## 2.19.3 - IsolatedHive: Handles stale send ports on hot restart diff --git a/hive/lib/hive.dart b/hive/lib/hive.dart index e18697ab..25f25541 100644 --- a/hive/lib/hive.dart +++ b/hive/lib/hive.dart @@ -11,6 +11,7 @@ export 'src/box_collection/box_collection_stub.dart' export 'src/object/hive_object.dart' show HiveObject, HiveObjectMixin; export 'src/annotations/generate_adapters.dart'; +export 'src/annotations/hive_converter.dart'; export 'src/annotations/hive_field.dart'; export 'src/annotations/hive_type.dart'; export 'src/binary/binary_reader.dart'; diff --git a/hive/lib/src/annotations/generate_adapters.dart b/hive/lib/src/annotations/generate_adapters.dart index 26f8ccc4..c4e5ed1a 100644 --- a/hive/lib/src/annotations/generate_adapters.dart +++ b/hive/lib/src/annotations/generate_adapters.dart @@ -1,3 +1,4 @@ +import 'package:hive_ce/src/annotations/hive_converter.dart'; import 'package:meta/meta.dart'; /// Annotation to generate TypeAdapters for the given [specs] @@ -9,6 +10,7 @@ class GenerateAdapters { this.specs, { this.firstTypeId = 0, this.reservedTypeIds = const {}, + this.converters = const [], }); // coverage:ignore-end @@ -22,6 +24,12 @@ class GenerateAdapters { /// /// These type ids will be skipped during generation final Set reservedTypeIds; + + /// A list of [HiveConverter]s to apply when generating adapters for [specs] + /// + /// A converter is selected by matching its type parameter [T] against each + /// field type. See [HiveConverter] for details. + final List converters; } /// Configuration that specifies the generation of a TypeAdapter diff --git a/hive/lib/src/annotations/hive_converter.dart b/hive/lib/src/annotations/hive_converter.dart new file mode 100644 index 00000000..bb7ffeca --- /dev/null +++ b/hive/lib/src/annotations/hive_converter.dart @@ -0,0 +1,42 @@ +import 'package:meta/meta.dart'; + +/// Implement this class to provide custom converters for a specific [Type]. +/// +/// [T] is the data type you'd like to convert to and from. +/// +/// [S] is the type of the value stored in Hive. It must be a type Hive can +/// write natively (such as [String], [int], [List], [Set], or [Map]) or a type +/// with a registered [TypeAdapter]. +/// +/// Pass converter instances to [GenerateAdapters.converters]: +/// +/// ```dart +/// class UriConverter implements HiveConverter { +/// const UriConverter(); +/// +/// @override +/// Uri fromHive(String hive) => Uri.parse(hive); +/// +/// @override +/// String toHive(Uri object) => object.toString(); +/// } +/// +/// @GenerateAdapters( +/// [AdapterSpec()], +/// converters: [UriConverter()], +/// ) +/// class Website { +/// final Uri url; +/// } +/// ``` +@immutable +abstract class HiveConverter { + /// Constructor + const HiveConverter(); + + /// Convert a value read from Hive into [T]. + T fromHive(S hive); + + /// Convert a [T] value into a value Hive can write. + S toHive(T object); +} diff --git a/hive/pubspec.yaml b/hive/pubspec.yaml index f26c47a9..472ec6e0 100644 --- a/hive/pubspec.yaml +++ b/hive/pubspec.yaml @@ -1,6 +1,6 @@ name: hive_ce description: Hive Community Edition - A spiritual continuation of Hive v2 -version: 2.19.3 +version: 2.20.0 homepage: https://github.com/IO-Design-Team/hive_ce/tree/main/hive topics: diff --git a/hive_generator/CHANGELOG.md b/hive_generator/CHANGELOG.md index 515dd0af..e24ef438 100644 --- a/hive_generator/CHANGELOG.md +++ b/hive_generator/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.12.0 + +- Adds support for `HiveConverter` via `GenerateAdapters.converters` (similar to `json_serializable`'s `JsonConverter`) +- Supports generic converters with inferred type arguments (e.g. `WrappedListConverter`) + ## 1.11.3 - Upgrades `analyzer` to `14.0.0` diff --git a/hive_generator/example/lib/hive/hive_adapters.dart b/hive_generator/example/lib/hive/hive_adapters.dart index 51079626..f4396d96 100644 --- a/hive_generator/example/lib/hive/hive_adapters.dart +++ b/hive_generator/example/lib/hive/hive_adapters.dart @@ -1,15 +1,31 @@ import 'package:hive_ce/hive_ce.dart'; import 'package:meta/meta.dart'; -@GenerateAdapters([ - AdapterSpec(), - AdapterSpec(), - AdapterSpec(), - AdapterSpec(), - AdapterSpec(), -], firstTypeId: 50) +@GenerateAdapters( + [ + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + ], + firstTypeId: 50, + converters: [UriConverter()], +) part 'hive_adapters.g.dart'; +/// Example converter matching json_serializable's JsonConverter pattern +class UriConverter implements HiveConverter { + const UriConverter(); + + @override + Uri fromHive(String hive) => Uri.parse(hive); + + @override + String toHive(Uri object) => object.toString(); +} + @immutable class ClassSpec1 { final int value; @@ -41,3 +57,11 @@ enum EnumSpec { EnumSpec get getter => EnumSpec.value2; } + +@immutable +class ClassSpec5 { + final Uri url; + final Uri? optionalUrl; + + const ClassSpec5(this.url, this.optionalUrl); +} diff --git a/hive_generator/example/lib/hive/hive_adapters.g.dart b/hive_generator/example/lib/hive/hive_adapters.g.dart index 040c2730..fb6fbb21 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.dart +++ b/hive_generator/example/lib/hive/hive_adapters.g.dart @@ -157,6 +157,49 @@ class ClassSpec3Adapter extends TypeAdapter { typeId == other.typeId; } +class ClassSpec5Adapter extends TypeAdapter { + @override + final typeId = 55; + + @override + ClassSpec5 read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ClassSpec5( + const UriConverter().fromHive(fields[0] as String), + fields[1] == null + ? null + : const UriConverter().fromHive(fields[1] as String), + ); + } + + @override + void write(BinaryWriter writer, ClassSpec5 obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(const UriConverter().toHive(obj.url)) + ..writeByte(1) + ..write( + obj.optionalUrl == null + ? null + : const UriConverter().toHive(obj.optionalUrl as Uri), + ); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ClassSpec5Adapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + class ClassSpec4Adapter extends TypeAdapter { @override final typeId = 54; diff --git a/hive_generator/example/lib/hive/hive_adapters.g.yaml b/hive_generator/example/lib/hive/hive_adapters.g.yaml index 21b64bc5..e0c391d8 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.yaml +++ b/hive_generator/example/lib/hive/hive_adapters.g.yaml @@ -1,7 +1,7 @@ # Generated by Hive CE # Manual modifications may be necessary for certain migrations # Check in to version control -nextTypeId: 55 +nextTypeId: 56 types: ClassSpec1: typeId: 50 @@ -39,6 +39,14 @@ types: fields: value: index: 0 + ClassSpec5: + typeId: 55 + nextIndex: 2 + fields: + url: + index: 0 + optionalUrl: + index: 1 ClassSpec4: typeId: 54 nextIndex: 0 diff --git a/hive_generator/example/lib/hive/hive_registrar.g.dart b/hive_generator/example/lib/hive/hive_registrar.g.dart index 2d9a86a4..63b7fd04 100644 --- a/hive_generator/example/lib/hive/hive_registrar.g.dart +++ b/hive_generator/example/lib/hive/hive_registrar.g.dart @@ -16,6 +16,7 @@ extension HiveRegistrar on HiveInterface { registerAdapter(ClassSpec2Adapter()); registerAdapter(ClassSpec3Adapter()); registerAdapter(ClassSpec4Adapter()); + registerAdapter(ClassSpec5Adapter()); registerAdapter(ConstructorDefaultsAdapter()); registerAdapter(EmptyClassAdapter()); registerAdapter(Enum1Adapter()); @@ -34,6 +35,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface { registerAdapter(ClassSpec2Adapter()); registerAdapter(ClassSpec3Adapter()); registerAdapter(ClassSpec4Adapter()); + registerAdapter(ClassSpec5Adapter()); registerAdapter(ConstructorDefaultsAdapter()); registerAdapter(EmptyClassAdapter()); registerAdapter(Enum1Adapter()); diff --git a/hive_generator/lib/src/adapter_builder/adapter_builder.dart b/hive_generator/lib/src/adapter_builder/adapter_builder.dart index ac1c0271..096597bb 100644 --- a/hive_generator/lib/src/adapter_builder/adapter_builder.dart +++ b/hive_generator/lib/src/adapter_builder/adapter_builder.dart @@ -49,12 +49,16 @@ abstract class AdapterBuilder { /// TODO: Document this! final List setters; + /// Converters from [GenerateAdapters.converters] + final List converters; + /// TODO: Document this! const AdapterBuilder( this.cls, - this.getters, [ + this.getters, { this.setters = const [], - ]); + this.converters = const [], + }); /// TODO: Document this! String buildRead(); diff --git a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart index 10cd15b3..c6faf898 100644 --- a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart +++ b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart @@ -12,6 +12,7 @@ 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/hive_converter_helper.dart'; import 'package:hive_ce_generator/src/helper/type_helper.dart'; /// TODO: Document this! @@ -19,9 +20,10 @@ class ClassAdapterBuilder extends AdapterBuilder { /// TODO: Document this! const ClassAdapterBuilder( super.cls, - super.getters, + super.getters, { super.setters, - ); + super.converters, + }); /// [TypeChecker] for [HiveList]. final hiveListChecker = @@ -121,6 +123,13 @@ class ClassAdapterBuilder extends AdapterBuilder { } String _cast(DartType type, String variable) { + final converter = _converterFor(type); + if (converter != null) { + final call = + '${converter.access}.fromHive($variable as ${converter.hiveType})'; + return _nullSafe(type, variable, call); + } + final suffix = _suffixFromType(type); if (hiveListChecker.isAssignableFromType(type)) { return '($variable as HiveList$suffix)$suffix.castHiveList()'; @@ -155,7 +164,8 @@ class ClassAdapterBuilder extends AdapterBuilder { final paramType = type as ParameterizedType; final arg = paramType.typeArguments.first; final suffix = _accessorSuffixFromType(type); - if (isMapOrIterable(arg) && !isUint8List(arg)) { + if (isMapOrIterable(arg) && !isUint8List(arg) || + _converterFor(arg) != null) { var cast = ''; // Using assignable because Set? is not exactly Set if (setChecker.isAssignableFromType(type)) { @@ -176,7 +186,10 @@ class ClassAdapterBuilder extends AdapterBuilder { final arg1 = paramType.typeArguments[0]; final arg2 = paramType.typeArguments[1]; final suffix = _accessorSuffixFromType(type); - if (isMapOrIterable(arg1) || isMapOrIterable(arg2)) { + if (isMapOrIterable(arg1) || + isMapOrIterable(arg2) || + _converterFor(arg1) != null || + _converterFor(arg2) != null) { return '$suffix.map((dynamic k, dynamic v)=>' 'MapEntry(${_cast(arg1, 'k')},${_cast(arg2, 'v')}))'; } else { @@ -195,12 +208,58 @@ class ClassAdapterBuilder extends AdapterBuilder { for (final field in getters) { code.writeln(''' ..writeByte(${field.index}) - ..write(obj.${field.name})'''); + ..write(${_writeValue(field.type, 'obj.${field.name}')})'''); } code.writeln(';'); return code.toString(); } + + String _writeValue(DartType type, String expression) { + final converter = _converterFor(type); + if (converter != null) { + final nullable = type.nullabilitySuffix == NullabilitySuffix.question; + final value = + nullable ? '$expression as ${converter.fieldType}' : expression; + return _nullSafe( + type, + expression, + '${converter.access}.toHive($value)', + ); + } + + if (setChecker.isAssignableFromType(type) || + (iterableChecker.isAssignableFromType(type) && !isUint8List(type))) { + final arg = (type as ParameterizedType).typeArguments.first; + final inner = _writeValue(arg, 'e'); + if (inner == 'e') return expression; + final suffix = _accessorSuffixFromType(type); + final mapped = '$expression$suffix.map((e) => $inner)'; + return setChecker.isAssignableFromType(type) + ? '$mapped.toSet()' + : '$mapped.toList()'; + } + + if (mapChecker.isAssignableFromType(type)) { + final args = (type as ParameterizedType).typeArguments; + final key = _writeValue(args[0], 'k'); + final value = _writeValue(args[1], 'v'); + if (key == 'k' && value == 'v') return expression; + final suffix = _accessorSuffixFromType(type); + return '$expression$suffix.map((dynamic k, dynamic v) => ' + 'MapEntry($key, $value))'; + } + + return expression; + } + + String _nullSafe(DartType type, String expression, String call) { + if (type.nullabilitySuffix != NullabilitySuffix.question) return call; + return '$expression == null ? null : $call'; + } + + HiveConverterMatch? _converterFor(DartType type) => + findHiveConverter(type, converters); } /// Suffix to use when accessing a field in [type]. diff --git a/hive_generator/lib/src/generator/adapters_generator.dart b/hive_generator/lib/src/generator/adapters_generator.dart index b21405ba..c2f70e92 100644 --- a/hive_generator/lib/src/generator/adapters_generator.dart +++ b/hive_generator/lib/src/generator/adapters_generator.dart @@ -96,6 +96,7 @@ class AdaptersGenerator extends GeneratorForAnnotation { typeId: schemaType.typeId, schema: schemaType, ignoredFields: spec.ignoredFields, + converters: revived.converters, ); content.write(result.content); diff --git a/hive_generator/lib/src/generator/type_adapter_generator.dart b/hive_generator/lib/src/generator/type_adapter_generator.dart index f75f6437..4a7a61fc 100644 --- a/hive_generator/lib/src/generator/type_adapter_generator.dart +++ b/hive_generator/lib/src/generator/type_adapter_generator.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:hive_ce/hive_ce.dart'; @@ -38,6 +39,7 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { String? adapterName, HiveSchemaType? schema, Set ignoredFields = const {}, + List converters = const [], }) { final cls = getClass(element); final getAccessorsResult = getAccessors( @@ -57,7 +59,12 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { adapterName ??= generateAdapterName(cls.displayName); final builder = cls.thisType.isEnum ? EnumAdapterBuilder(cls, getters) - : ClassAdapterBuilder(cls, getters, setters); + : ClassAdapterBuilder( + cls, + getters, + setters: setters, + converters: converters, + ); final content = ''' class $adapterName extends TypeAdapter<${cls.displayName}> { diff --git a/hive_generator/lib/src/helper/hive_converter_helper.dart b/hive_generator/lib/src/helper/hive_converter_helper.dart new file mode 100644 index 00000000..cfc02e38 --- /dev/null +++ b/hive_generator/lib/src/helper/hive_converter_helper.dart @@ -0,0 +1,133 @@ +import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; +import 'package:source_gen/source_gen.dart'; + +/// A [HiveConverter] matched to a field type for code generation +class HiveConverterMatch { + /// Converter instance expression, e.g. `const UriConverter()` + final String access; + + /// Stored Hive type display string, e.g. `String` + final String hiveType; + + /// Field type display string, e.g. `Uri` + final String fieldType; + + /// Constructor + const HiveConverterMatch({ + required this.access, + required this.hiveType, + required this.fieldType, + }); +} + +const _checker = TypeChecker.typeNamedLiterally( + 'HiveConverter', + inPackage: 'hive_ce', +); + +/// Find a converter for [type] in [converters] from [GenerateAdapters] +HiveConverterMatch? findHiveConverter( + DartType type, + List converters, +) { + HiveConverterMatch? result; + for (final converter in converters) { + final match = _match(type, converter); + if (match == null) continue; + if (result != null) { + throw InvalidGenerationSourceError( + 'Found more than one matching converter for ' + '`${type.getDisplayString()}`.', + ); + } + result = match; + } + return result; +} + +HiveConverterMatch? _match(DartType target, DartObject object) { + final objectType = object.type; + if (objectType is! InterfaceType) return null; + + final element = objectType.element; + if (element is! ClassElement) return null; + + final hiveConverter = element.allSupertypes + .where((t) => _checker.isExactly(t.element)) + .singleOrNull; + if (hiveConverter == null) return null; + + final converted = hiveConverter.typeArguments[0]; + final stored = hiveConverter.typeArguments[1]; + final nonNullTarget = _nonNull(target); + final name = element.name; + if (name == null) return null; + + if (converted == nonNullTarget) { + return HiveConverterMatch( + access: 'const $name()', + hiveType: stored.getDisplayString(), + fieldType: converted.getDisplayString(), + ); + } + + // e.g. `IListConverter` for `IList` + if (converted is! InterfaceType || + nonNullTarget is! InterfaceType || + converted.element != nonNullTarget.element || + element.typeParameters.isEmpty || + converted.typeArguments.length != element.typeParameters.length) { + return null; + } + + for (var i = 0; i < element.typeParameters.length; i++) { + final arg = converted.typeArguments[i]; + if (arg is! TypeParameterType || arg.element != element.typeParameters[i]) { + return null; + } + } + + final args = + nonNullTarget.typeArguments.map((t) => t.getDisplayString()).join(', '); + final bindings = { + for (var i = 0; i < element.typeParameters.length; i++) + element.typeParameters[i]: nonNullTarget.typeArguments[i], + }; + + return HiveConverterMatch( + access: '$name<$args>()', + hiveType: _substitute(stored, bindings).getDisplayString(), + fieldType: nonNullTarget.getDisplayString(), + ); +} + +DartType _nonNull(DartType type) { + if (type.nullabilitySuffix == NullabilitySuffix.none) return type; + if (type is InterfaceType) { + return type.element.instantiate( + typeArguments: type.typeArguments, + nullabilitySuffix: NullabilitySuffix.none, + ); + } + return type; +} + +DartType _substitute( + DartType type, + Map bindings, +) { + if (type is TypeParameterType) return bindings[type.element] ?? type; + if (type is InterfaceType && type.typeArguments.isNotEmpty) { + return type.element.instantiate( + typeArguments: [ + for (final arg in type.typeArguments) _substitute(arg, bindings), + ], + nullabilitySuffix: type.nullabilitySuffix, + ); + } + return type; +} diff --git a/hive_generator/lib/src/model/revived_generate_adapter.dart b/hive_generator/lib/src/model/revived_generate_adapter.dart index 008e74b8..b0742438 100644 --- a/hive_generator/lib/src/model/revived_generate_adapter.dart +++ b/hive_generator/lib/src/model/revived_generate_adapter.dart @@ -15,6 +15,9 @@ class RevivedGenerateAdapters { /// The reserved type ids final Set reservedTypeIds; + /// Converters shared by all specs + final List converters; + /// Revive a GenerateAdapters annotation RevivedGenerateAdapters(ConstantReader annotation) : specs = annotation @@ -28,7 +31,8 @@ class RevivedGenerateAdapters { .setValue .map((e) => e.toIntValue()) .whereType() - .toSet(); + .toSet(), + converters = _readConverters(annotation); } /// A revived adapter spec @@ -41,7 +45,10 @@ class RevivedAdapterSpec { final Set ignoredFields; /// Constructor - const RevivedAdapterSpec({required this.type, required this.ignoredFields}); + const RevivedAdapterSpec({ + required this.type, + required this.ignoredFields, + }); /// Create a [RevivedAdapterSpec] from a [DartObject] factory RevivedAdapterSpec.fromObject(DartObject object) { @@ -58,3 +65,9 @@ class RevivedAdapterSpec { return RevivedAdapterSpec(type: type, ignoredFields: ignoredFields); } } + +List _readConverters(ConstantReader reader) { + final converters = reader.read('converters'); + if (converters.isNull) return const []; + return converters.listValue; +} diff --git a/hive_generator/pubspec.yaml b/hive_generator/pubspec.yaml index 88430a87..3773cc20 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.11.3 +version: 1.12.0 homepage: https://github.com/IO-Design-Team/hive_ce/tree/main/hive_generator environment: diff --git a/hive_generator/test/hive_converter_test.dart b/hive_generator/test/hive_converter_test.dart new file mode 100644 index 00000000..0b141973 --- /dev/null +++ b/hive_generator/test/hive_converter_test.dart @@ -0,0 +1,156 @@ +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +const directives = ''' +import 'package:hive_ce/hive_ce.dart'; +part 'hive_adapters.g.dart'; +'''; + +const uriConverter = ''' +class UriConverter implements HiveConverter { + const UriConverter(); + + @override + Uri fromHive(String hive) => Uri.parse(hive); + + @override + String toHive(Uri object) => object.toString(); +} +'''; + +void main() { + group('HiveConverter', () { + test('applies converter from GenerateAdapters.converters', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$uriConverter + +@GenerateAdapters( + [AdapterSpec()], + converters: [UriConverter()], +) +class Website { + const Website(this.url); + + final Uri url; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'const UriConverter().fromHive(fields[0] as String)', + 'const UriConverter().toHive(obj.url)', + ]), + }, + ); + }); + + test('applies generic converter with inferred type arguments', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +class WrappedList { + const WrappedList(this.values); + final List values; +} + +class WrappedListConverter + implements HiveConverter, List> { + const WrappedListConverter(); + + @override + WrappedList fromHive(List hive) => WrappedList(hive); + + @override + List toHive(WrappedList object) => object.values; +} + +@GenerateAdapters( + [AdapterSpec()], + converters: [WrappedListConverter()], +) +class Box { + const Box(this.items); + + final WrappedList items; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'WrappedListConverter().fromHive(fields[0] as List)', + 'WrappedListConverter().toHive(obj.items)', + ]), + }, + ); + }); + + test('applies converter inside nested collections', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$uriConverter + +@GenerateAdapters( + [AdapterSpec()], + converters: [UriConverter()], +) +class LinkList { + const LinkList(this.urls); + + final List urls; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'const UriConverter().fromHive(e as String)', + 'const UriConverter().toHive(e)', + ]), + }, + ); + }); + + test('handles nullable fields with non-nullable converter', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$uriConverter + +@GenerateAdapters( + [AdapterSpec()], + converters: [UriConverter()], +) +class Website { + const Website(this.url); + + final Uri? url; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'fields[0] == null', + 'const UriConverter().fromHive(fields[0] as String)', + 'obj.url == null', + 'const UriConverter().toHive(obj.url as Uri)', + ]), + }, + ); + }); + }); +} diff --git a/hive_generator/test/test_utils.dart b/hive_generator/test/test_utils.dart index 9e9c88c2..00ce53a5 100644 --- a/hive_generator/test/test_utils.dart +++ b/hive_generator/test/test_utils.dart @@ -9,11 +9,21 @@ const schemaComment = HiveSchema.comment; const fileExists = true; const fileDoesNotExist = false; +/// Match generated file content that contains all of [snippets] +class ContainsAll { + /// The snippets that must appear in the file + final List snippets; + + /// Constructor + const ContainsAll(this.snippets); +} + /// Expect the given input generates the given output /// /// About [output] -/// - A [String] value will check if the file exists and contains the given +/// - A [String] value will check if the file exists and equals the given /// content +/// - A [ContainsAll] value will check that the file contains all snippets /// - [fileExists] will check if the file exists /// - [fileDoesNotExist] will check if the file does not exist /// @@ -44,12 +54,27 @@ void expectGeneration({ } return; } else { - expect(result.exitCode, 0); + expect( + result.exitCode, + 0, + reason: result.stdout.toString(), + ); } for (final MapEntry(:key, :value) in output.entries) { final file = File(path.join(projectRoot, key)); - expect(file.existsSync(), value == true || value is String); + expect( + file.existsSync(), + value == true || value is String || value is ContainsAll, + ); + + if (value is ContainsAll) { + final content = file.readAsStringSync(); + for (final snippet in value.snippets) { + expect(content, contains(snippet)); + } + continue; + } if (value is! String) continue; expect(file.readAsStringSync(), value);