From 66b49b7345d8d8df1c730333ee7a18c8818e1db9 Mon Sep 17 00:00:00 2001 From: Guillaume Ducret Date: Wed, 30 Dec 2020 08:24:51 +0100 Subject: [PATCH] Implement file backend --- lib/src/backend/file/file_descriptor.dart | 63 ++++++++++++++++++ lib/src/backend/file/file_mirror_manager.dart | 65 +++++++++++++++++++ lib/src/backend/mirror_manager.dart | 19 ++++-- lib/src/handlers/box_mirror_handler.dart | 3 + lib/src/handlers/dynamic_mirror_handler.dart | 3 + lib/src/hive_mirror.dart | 1 + test/assets/load2.yaml | 2 + test/file_descriptors.dart | 52 +++++++++++++++ test/integration/mirror_test.dart | 15 ++++- .../file/file_mirror_manager_test.dart | 40 ++++++++++++ 10 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 lib/src/backend/file/file_descriptor.dart create mode 100644 lib/src/backend/file/file_mirror_manager.dart create mode 100644 test/assets/load2.yaml create mode 100644 test/file_descriptors.dart create mode 100644 test/unit/backend/file/file_mirror_manager_test.dart diff --git a/lib/src/backend/file/file_descriptor.dart b/lib/src/backend/file/file_descriptor.dart new file mode 100644 index 0000000..67bd751 --- /dev/null +++ b/lib/src/backend/file/file_descriptor.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:async/async.dart'; + +abstract class FileDescriptorInterface { + Stream> open(String eTag); + String get etag; + dynamic decode(String line); + dynamic decodeKey(String line); +} + +class FileDescriptor implements FileDescriptorInterface { + final File _file; + final Uri _uri; + final Decode _decode; + final DecodeKey _decodeKey; + String _eTag; + + FileDescriptor.file(File file, {Decode decode, DecodeKey decodeKey}) + : _file = file, + _uri = null, + _decode = decode, + _decodeKey = decodeKey; + + FileDescriptor.uri(Uri uri, {Decode decode, DecodeKey decodeKey}) + : _file = null, + _uri = uri, + _decode = decode, + _decodeKey = decodeKey; + + Stream> open(String eTag) { + if (_file != null) { + return LazyStream(() async { + _eTag = (await _file.lastModified()).millisecondsSinceEpoch.toString(); + return _file.openRead(); + }); + } + + if (_uri != null) { + return LazyStream(() async { + final request = await HttpClient().getUrl(_uri); + request.headers.set(HttpHeaders.ifNoneMatchHeader, eTag); + + final response = await request.close(); + _eTag = response.headers.value(HttpHeaders.etagHeader); + return response; + }); + } + + throw StateError('Invalid FileDescriptor'); + } + + String get etag { + if (_eTag != null) return _eTag; + throw StateError('etag must be called after open()'); + } + + dynamic decode(String line) => _decode(line); + dynamic decodeKey(String line) => _decodeKey(line); +} + +typedef dynamic Decode(String line); +typedef dynamic DecodeKey(String line); diff --git a/lib/src/backend/file/file_mirror_manager.dart b/lib/src/backend/file/file_mirror_manager.dart new file mode 100644 index 0000000..dcb992d --- /dev/null +++ b/lib/src/backend/file/file_mirror_manager.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; + +import '../../handlers/dynamic_mirror_handler.dart'; +import '../../handlers/handler_holder.dart'; +import '../../hive_mirror.dart'; +import '../../metadata.dart'; +import '../mirror_manager.dart'; +import 'file_descriptor.dart'; + +class FileMirrorManager implements MirrorManager { + final MirrorHandlerHolder _handler; + final Metadata _metadata; + + FileMirrorManager(MirrorHandler handler, Metadata metadata) + : _handler = MirrorHandlerHolder(handler), + _metadata = metadata; + + static FileMirrorManager withHandler( + MirrorHandler handler, Metadata metadata) { + return FileMirrorManager(DynamicMirrorHandler(handler), metadata); + } + + Future mirror(dynamic fileDescriptor) => loadFile(fileDescriptor); + + Future loadFile(FileDescriptorInterface fileDescriptor) async { + final etag = _metadata.get(metaEtag); + final fileData = fileDescriptor.open(etag); + + if (fileDescriptor.etag != etag) { + final lines = fileData.transform(Utf8Decoder()).transform(LineSplitter()); + + try { + await _applyLines(fileDescriptor.etag, await lines.toList(), + fileDescriptor.decode, fileDescriptor.decodeKey); + } finally { + await _handler.dispose(); + } + } + } + + Future _applyLines(String etag, Iterable lines, Decode decode, + DecodeKey decodeKey) async { + MapEntry decodeLine(String line) { + final key = decodeKey(line); + if (key != null) { + final object = decode(line); + return MapEntry(key, object); + } + return null; + } + + final putEntries = + Map.fromEntries(lines.map(decodeLine).where((e) => e != null)); + + await (await _handler.use()).clear(); + + if (putEntries.isNotEmpty) { + await (await _handler.use()).putAll(putEntries); + } + + await _metadata.put(metaEtag, etag); + } + + static const metaEtag = 'etag'; +} diff --git a/lib/src/backend/mirror_manager.dart b/lib/src/backend/mirror_manager.dart index 4b47aef..78c482e 100644 --- a/lib/src/backend/mirror_manager.dart +++ b/lib/src/backend/mirror_manager.dart @@ -1,19 +1,26 @@ -import 'package:hive_mirror/src/handlers/handler_holder.dart'; - import '../hive_mirror.dart'; import '../metadata.dart'; +import 'file/file_descriptor.dart'; +import 'file/file_mirror_manager.dart'; import 'git/git_mirror_manager.dart'; import 'git/git_patch.dart'; abstract class MirrorManager { Future mirror(dynamic source); - factory MirrorManager.fromSource(dynamic source, - {MirrorHandler handler, Metadata metadata}) { + factory MirrorManager.fromSource( + dynamic source, { + MirrorHandler handler, + Metadata metadata, + }) { if (source is GitPatchInterface) { return GitMirrorManager(handler, metadata); } - throw UnsupportedError('''source $source is not supported. - Use one of the supported sources [GitPatch]'''); + if (source is FileDescriptorInterface) { + return FileMirrorManager(handler, metadata); + } + throw ArgumentError('''source $source is not supported. + Use one of the supported sources + [GitPatchInterface, FileDescriptorInterface]'''); } } diff --git a/lib/src/handlers/box_mirror_handler.dart b/lib/src/handlers/box_mirror_handler.dart index 3cdc397..4efd613 100644 --- a/lib/src/handlers/box_mirror_handler.dart +++ b/lib/src/handlers/box_mirror_handler.dart @@ -23,6 +23,9 @@ class BoxMirrorHandler implements MirrorHandler { @override Future deleteAll(Iterable keys) => _box.deleteAll(keys); + @override + Future clear() => _box.clear(); + @override Future dispose() => _box.close(); } diff --git a/lib/src/handlers/dynamic_mirror_handler.dart b/lib/src/handlers/dynamic_mirror_handler.dart index a3af9a8..e3779ac 100644 --- a/lib/src/handlers/dynamic_mirror_handler.dart +++ b/lib/src/handlers/dynamic_mirror_handler.dart @@ -20,6 +20,9 @@ class DynamicMirrorHandler implements MirrorHandler { @override Future deleteAll(Iterable keys) => _delegate.deleteAll(keys); + @override + Future clear() => _delegate.clear(); + @override Future dispose() => _delegate.dispose(); } diff --git a/lib/src/hive_mirror.dart b/lib/src/hive_mirror.dart index 25a6d74..8cd5a04 100644 --- a/lib/src/hive_mirror.dart +++ b/lib/src/hive_mirror.dart @@ -11,5 +11,6 @@ abstract class MirrorHandler { Future init(); Future putAll(Map entries); Future deleteAll(Iterable keys); + Future clear(); Future dispose(); } diff --git a/test/assets/load2.yaml b/test/assets/load2.yaml new file mode 100644 index 0000000..6fc96be --- /dev/null +++ b/test/assets/load2.yaml @@ -0,0 +1,2 @@ +key1: value1 +key2: value2 diff --git a/test/file_descriptors.dart b/test/file_descriptors.dart new file mode 100644 index 0000000..7f2e3a6 --- /dev/null +++ b/test/file_descriptors.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:hive_mirror/src/backend/file/file_descriptor.dart'; + +import 'type.dart'; + +abstract class TestFileDescriptorBase implements FileDescriptorInterface { + @override + final etag; + + final String _filePath; + final Decode _decode; + final DecodeKey _decodeKey; + + TestFileDescriptorBase._( + this.etag, this._filePath, this._decode, this._decodeKey); + + @override + Stream> open(String _) => File(_filePath).openRead(); + + @override + dynamic decode(String line) => _decode(line); + + @override + dynamic decodeKey(String line) => _decodeKey(line); +} + +class Load2FileDescriptor extends TestFileDescriptorBase { + Load2FileDescriptor.primitive() + : super._(etagValue, filePath, _decodePrimitive, _decodeKey); + Load2FileDescriptor.testType() + : super._(etagValue, filePath, _decodeTestType, _decodeKey); + + static const filePath = 'test/assets/load2.yaml'; + static const etagValue = 'etag_value'; + static const loadMap = {'key1': 'value1', 'key2': 'value2'}; +} + +String _decodePrimitive(String line) { + final tupple = line.split(':'); + return tupple[1].trim(); +} + +TestType _decodeTestType(String line) { + final tupple = line.split(':'); + return TestType(tupple[0].trim(), tupple[1].trim()); +} + +String _decodeKey(String line) { + final tupple = line.split(':'); + return tupple[0].trim(); +} diff --git a/test/integration/mirror_test.dart b/test/integration/mirror_test.dart index bf1e1d1..249b8f1 100644 --- a/test/integration/mirror_test.dart +++ b/test/integration/mirror_test.dart @@ -2,11 +2,12 @@ import 'package:hive/hive.dart'; import 'package:hive_mirror/hive_mirror.dart'; import 'package:test/test.dart'; +import '../file_descriptors.dart'; import '../patches.dart'; void main() { group('Mirror', () { - test('box', () async { + test('patch', () async { HiveMirror.init('.hive'); await Hive.deleteBoxFromDisk('box'); @@ -17,5 +18,17 @@ void main() { expect(box.get('key1'), equals('value1')); expect(box.get('key2'), equals('value2')); }); + + test('file', () async { + HiveMirror.init('.hive'); + await Hive.deleteBoxFromDisk('box'); + + final source = Load2FileDescriptor.primitive(); + await HiveMirror.mirror(source, BoxMirrorHandler('box')); + + final box = await Hive.openBox('box'); + expect(box.get('key1'), equals('value1')); + expect(box.get('key2'), equals('value2')); + }); }); } diff --git a/test/unit/backend/file/file_mirror_manager_test.dart b/test/unit/backend/file/file_mirror_manager_test.dart new file mode 100644 index 0000000..2688d3b --- /dev/null +++ b/test/unit/backend/file/file_mirror_manager_test.dart @@ -0,0 +1,40 @@ +import 'package:hive_mirror/src/backend/file/file_mirror_manager.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import '../../../file_descriptors.dart'; +import '../../mocks.dart'; + +const metaETag = FileMirrorManager.metaEtag; + +void main() { + group('FileMirrorManager load()', () { + test('file with new etag', () async { + final handler = MirrorHandlerMock(); + final metadata = MetadataMock(); + final manager = FileMirrorManager.withHandler(handler, metadata); + + await manager.loadFile(Load2FileDescriptor.primitive()); + + verify(metadata.get(metaETag)); + verify(handler.clear()); + verify(handler.putAll(argThat(equals(Load2FileDescriptor.loadMap)))); + verify(metadata.put(metaETag, Load2FileDescriptor.etagValue)); + }); + + test('file with previous etag', () async { + final handler = MirrorHandlerMock(); + final metadata = MetadataMock(); + final manager = FileMirrorManager.withHandler(handler, metadata); + + when(metadata.get(metaETag)).thenReturn(Load2FileDescriptor.etagValue); + + await manager.loadFile(Load2FileDescriptor.primitive()); + + verify(metadata.get(metaETag)); + verifyNever(handler.clear()); + verifyNever(handler.putAll(any)); + verifyNever(metadata.put(any, any)); + }); + }); +}