Skip to content

Commit c58ac0c

Browse files
committed
✨ add enum support with validation for ItemHolder and backend operations
1 parent 0f0f135 commit c58ac0c

10 files changed

+618
-9
lines changed

packages/hyper_storage/lib/src/api/backend.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ mixin GenericStorageOperationsMixin implements StorageOperationsApi {
195195
const (List<String>) => await getStringList(key) as E?,
196196
const (Map<String, dynamic>) => await getJson(key) as E?,
197197
const (List<Map<String, dynamic>>) => await getJsonList(key) as E?,
198-
_ when enumValues != null => () {
198+
_ when enumValues != null => await () async {
199199
checkEnumType<E>(enumValues);
200-
return getEnum(key, enumValues.cast<Enum>()) as E?;
200+
return await getEnum(key, enumValues.cast<Enum>()) as E?;
201201
}(),
202202
_ => throw UnsupportedError('Type $E is not supported. If this is an enum, provide the enumValues parameter.'),
203203
};

packages/hyper_storage/lib/src/item_holder.dart

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -454,13 +454,8 @@ mixin ItemHolderMixin on BaseStorage {
454454
existing = null;
455455
}
456456
if (existing != null) {
457-
if (existing is ItemHolder<E>) {
458-
return existing;
459-
} else {
460-
throw ArgumentError(
461-
'An ItemHolder with key "$key" already exists with a different type: ${existing.runtimeType}.',
462-
);
463-
}
457+
_checkExistingMatch<E>(key, existing, hasCustomAccessors: hasCustomAccessors, enumValues: enumValues);
458+
return existing as ItemHolder<E>;
464459
}
465460

466461
final holder = ItemHolder<E>(
@@ -544,4 +539,60 @@ mixin ItemHolderMixin on BaseStorage {
544539
}
545540
_holders.clear();
546541
}
542+
543+
/// Ensures an existing cached holder is compatible with the requested configuration.
544+
///
545+
/// The method verifies that the cached holder uses the same generic type, that
546+
/// custom getter/setter accessors are either present or absent on both versions,
547+
/// and that enum configurations (when provided) match exactly. It throws when
548+
/// the caller attempts to change any of these characteristics for the same key.
549+
void _checkExistingMatch<E extends Object>(
550+
String key,
551+
ItemHolder<Object> existing, {
552+
required bool hasCustomAccessors,
553+
List<Enum>? enumValues,
554+
}) {
555+
if (existing is ItemHolder<E>) {
556+
// Validate consistency between existing and new holder configurations
557+
final existingHasCustomAccessors = existing.getter != null || existing.setter != null;
558+
if (existingHasCustomAccessors != hasCustomAccessors) {
559+
throw StateError(
560+
'Cannot change ItemHolder configuration for key "$key". '
561+
'Existing holder ${existingHasCustomAccessors ? "has" : "does not have"} custom accessors, '
562+
'but new configuration ${hasCustomAccessors ? "has" : "does not have"} custom accessors.',
563+
);
564+
}
565+
// Validate enum values consistency
566+
if (existing._enumValues != null || enumValues != null) {
567+
if (existing._enumValues == null || enumValues == null) {
568+
throw StateError(
569+
'Cannot change ItemHolder configuration for key "$key". '
570+
'Existing holder ${existing._enumValues != null ? "has" : "does not have"} enumValues, '
571+
'but new configuration ${enumValues != null ? "has" : "does not have"} enumValues.',
572+
);
573+
}
574+
// Both have enumValues, check if they match
575+
if (existing._enumValues.length != enumValues.length) {
576+
throw StateError(
577+
'Cannot change enumValues for existing ItemHolder with key "$key". '
578+
'Existing holder has ${existing._enumValues.length} values, '
579+
'but new configuration has ${enumValues.length} values.',
580+
);
581+
}
582+
// Check if all values match in order
583+
for (var i = 0; i < existing._enumValues.length; i++) {
584+
if (existing._enumValues[i] != enumValues[i]) {
585+
throw StateError(
586+
'Cannot change enumValues for existing ItemHolder with key "$key". '
587+
'EnumValues differ from existing holder.',
588+
);
589+
}
590+
}
591+
}
592+
} else {
593+
throw ArgumentError(
594+
'An ItemHolder with key "$key" already exists with a different type: ${existing.runtimeType}.',
595+
);
596+
}
597+
}
547598
}

packages/hyper_storage/test/hyper_storage_container_test.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:hyper_storage/hyper_storage.dart';
22
import 'package:test/test.dart';
33

4+
enum ContainerTestEnum { foo, bar, baz }
5+
46
void main() {
57
group('HyperStorageContainer', () {
68
late InMemoryBackend backend;
@@ -89,6 +91,37 @@ void main() {
8991
});
9092
});
9193

94+
group('Enum operations', () {
95+
test('setEnum and getEnum', () async {
96+
await container.setEnum('status', ContainerTestEnum.bar);
97+
final result = await container.getEnum('status', ContainerTestEnum.values);
98+
expect(result, ContainerTestEnum.bar);
99+
});
100+
101+
test('getEnum returns null when key missing', () async {
102+
final result = await container.getEnum('missing', ContainerTestEnum.values);
103+
expect(result, isNull);
104+
});
105+
106+
test('getEnum returns null when stored value mismatches', () async {
107+
await backend.setString('test___status', 'unknown');
108+
final result = await container.getEnum('status', ContainerTestEnum.values);
109+
expect(result, isNull);
110+
});
111+
112+
test('generic get retrieves enum with values', () async {
113+
await container.setEnum('status', ContainerTestEnum.foo);
114+
final result = await container.get<ContainerTestEnum>('status', enumValues: ContainerTestEnum.values);
115+
expect(result, ContainerTestEnum.foo);
116+
});
117+
118+
test('generic set stores enum name', () async {
119+
await container.set('status', ContainerTestEnum.baz);
120+
final stored = await backend.getString('test___status');
121+
expect(stored, 'baz');
122+
});
123+
});
124+
92125
group('StringList operations', () {
93126
test('setStringList and getStringList', () async {
94127
final list = ['a', 'b', 'c'];
@@ -533,6 +566,38 @@ void main() {
533566
await Future.delayed(Duration(milliseconds: 50));
534567
await doubleSub.cancel();
535568
expect(doubleValues, contains(3.14));
569+
570+
// Test with enum
571+
await container.setEnum('enumKey', ContainerTestEnum.bar);
572+
final enumStream = container.stream<ContainerTestEnum>(
573+
'enumKey',
574+
enumValues: ContainerTestEnum.values,
575+
);
576+
final enumValues = <ContainerTestEnum?>[];
577+
final enumSub = enumStream.listen(enumValues.add);
578+
await Future.delayed(Duration(milliseconds: 50));
579+
await enumSub.cancel();
580+
expect(enumValues, contains(ContainerTestEnum.bar));
581+
});
582+
583+
test('stream() updates enum values', () async {
584+
await container.setEnum('enumKey', ContainerTestEnum.foo);
585+
586+
final stream = container.stream<ContainerTestEnum>(
587+
'enumKey',
588+
enumValues: ContainerTestEnum.values,
589+
);
590+
final values = <ContainerTestEnum?>[];
591+
592+
final subscription = stream.listen(values.add);
593+
await Future.delayed(Duration(milliseconds: 50));
594+
595+
await container.setEnum('enumKey', ContainerTestEnum.baz);
596+
await Future.delayed(Duration(milliseconds: 50));
597+
598+
await subscription.cancel();
599+
600+
expect(values, containsAll([ContainerTestEnum.foo, ContainerTestEnum.baz]));
536601
});
537602

538603
test('stream() cleans up listener on cancellation', () async {

packages/hyper_storage/test/hyper_storage_test.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'package:test/test.dart';
33

44
import 'helpers/test_helpers.dart';
55

6+
enum StorageTestEnum { pending, success, failed }
7+
68
void main() {
79
group('HyperStorage', () {
810
tearDown(() async {
@@ -322,6 +324,36 @@ void main() {
322324
expect(retrieved, jsonList);
323325
});
324326

327+
test('setEnum and getEnum', () async {
328+
final storage = HyperStorage.instance;
329+
await storage.setEnum('status', StorageTestEnum.success);
330+
final result = await storage.getEnum('status', StorageTestEnum.values);
331+
expect(result, StorageTestEnum.success);
332+
});
333+
334+
test('getEnum returns null when stored name mismatches', () async {
335+
final storage = HyperStorage.instance;
336+
await storage.backend.setString('status', 'unknown');
337+
final result = await storage.getEnum('status', StorageTestEnum.values);
338+
expect(result, isNull);
339+
});
340+
341+
test('get<StorageTestEnum> retrieves enum when values provided', () async {
342+
final storage = HyperStorage.instance;
343+
await storage.setEnum('status', StorageTestEnum.pending);
344+
final result = await storage.get<StorageTestEnum>('status', enumValues: StorageTestEnum.values);
345+
expect(result, StorageTestEnum.pending);
346+
});
347+
348+
test('get<StorageTestEnum> throws when enum values missing', () async {
349+
final storage = HyperStorage.instance;
350+
await storage.backend.setString('status', 'failed');
351+
expect(
352+
() => storage.get<StorageTestEnum>('status'),
353+
throwsUnsupportedError,
354+
);
355+
});
356+
325357
test('removeAll deletes multiple keys', () async {
326358
final storage = HyperStorage.instance;
327359
await storage.setString('key1', 'value1');
@@ -512,6 +544,20 @@ void main() {
512544
);
513545
});
514546

547+
test('validates keys for enum operations', () async {
548+
final storage = HyperStorage.instance;
549+
550+
expect(
551+
() => storage.setEnum('', StorageTestEnum.failed),
552+
throwsArgumentError,
553+
);
554+
555+
expect(
556+
() => storage.getEnum(' ', StorageTestEnum.values),
557+
throwsArgumentError,
558+
);
559+
});
560+
515561
test('accepts valid keys', () async {
516562
final storage = HyperStorage.instance;
517563

@@ -880,6 +926,39 @@ void main() {
880926
await Future.delayed(Duration(milliseconds: 50));
881927
await doubleSub.cancel();
882928
expect(doubleValues, contains(3.14));
929+
930+
// Test with enum
931+
await storage.setEnum('enumKey', StorageTestEnum.success);
932+
final enumStream = storage.stream<StorageTestEnum>(
933+
'enumKey',
934+
enumValues: StorageTestEnum.values,
935+
);
936+
final enumValues = <StorageTestEnum?>[];
937+
final enumSub = enumStream.listen(enumValues.add);
938+
await Future.delayed(Duration(milliseconds: 50));
939+
await enumSub.cancel();
940+
expect(enumValues, contains(StorageTestEnum.success));
941+
});
942+
943+
test('stream() updates enum values', () async {
944+
final storage = HyperStorage.instance;
945+
await storage.setEnum('enumKey', StorageTestEnum.pending);
946+
947+
final stream = storage.stream<StorageTestEnum>(
948+
'enumKey',
949+
enumValues: StorageTestEnum.values,
950+
);
951+
final values = <StorageTestEnum?>[];
952+
953+
final subscription = stream.listen(values.add);
954+
await Future.delayed(Duration(milliseconds: 50));
955+
956+
await storage.setEnum('enumKey', StorageTestEnum.failed);
957+
await Future.delayed(Duration(milliseconds: 50));
958+
959+
await subscription.cancel();
960+
961+
expect(values, containsAll([StorageTestEnum.pending, StorageTestEnum.failed]));
883962
});
884963

885964
test('stream() works with DateTime', () async {

packages/hyper_storage/test/in_memory_backend_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:hyper_storage/hyper_storage.dart';
22
import 'package:test/test.dart';
33

4+
enum MemoryTestEnum { pending, completed, cancelled }
5+
46
void main() {
57
group('InMemoryBackend', () {
68
late InMemoryBackend backend;
@@ -130,6 +132,26 @@ void main() {
130132
});
131133
});
132134

135+
group('Enum operations', () {
136+
test('setEnum and getEnum', () async {
137+
await backend.setEnum('status', MemoryTestEnum.completed);
138+
final result = await backend.getEnum('status', MemoryTestEnum.values);
139+
expect(result, MemoryTestEnum.completed);
140+
});
141+
142+
test('getEnum returns null for unknown value', () async {
143+
await backend.setString('status', 'unknown');
144+
final result = await backend.getEnum('status', MemoryTestEnum.values);
145+
expect(result, isNull);
146+
});
147+
148+
test('generic get retrieves enum when values provided', () async {
149+
await backend.setEnum('status', MemoryTestEnum.pending);
150+
final result = await backend.get<MemoryTestEnum>('status', enumValues: MemoryTestEnum.values);
151+
expect(result, MemoryTestEnum.pending);
152+
});
153+
});
154+
133155
group('StringList operations', () {
134156
test('setStringList and getStringList', () async {
135157
final list = ['a', 'b', 'c'];

0 commit comments

Comments
 (0)