diff --git a/lib/src/backend/file/file_descriptor.dart b/lib/src/backend/file/file_descriptor.dart new file mode 100644 index 0000000..5cbd687 --- /dev/null +++ b/lib/src/backend/file/file_descriptor.dart @@ -0,0 +1,67 @@ +// Copyright 2020 Guillaume Ducret. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +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..dce623b --- /dev/null +++ b/lib/src/backend/file/file_mirror_manager.dart @@ -0,0 +1,70 @@ +// Copyright 2020 Guillaume Ducret. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +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 as FileDescriptorInterface); + + 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 dynamic key = decodeKey(line); + if (key != null) { + final dynamic 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 5ade753..394778f 100644 --- a/lib/src/backend/mirror_manager.dart +++ b/lib/src/backend/mirror_manager.dart @@ -4,18 +4,27 @@ 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 4b92714..61dd404 100644 --- a/lib/src/handlers/box_mirror_handler.dart +++ b/lib/src/handlers/box_mirror_handler.dart @@ -27,6 +27,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 9326f96..9c3212b 100644 --- a/lib/src/handlers/dynamic_mirror_handler.dart +++ b/lib/src/handlers/dynamic_mirror_handler.dart @@ -24,6 +24,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 d0dffd7..ea3ee2e 100644 --- a/lib/src/hive_mirror.dart +++ b/lib/src/hive_mirror.dart @@ -15,5 +15,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..6032c18 --- /dev/null +++ b/test/file_descriptors.dart @@ -0,0 +1,56 @@ +// Copyright 2020 Guillaume Ducret. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +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 2eb5b19..73734a3 100644 --- a/test/integration/mirror_test.dart +++ b/test/integration/mirror_test.dart @@ -6,11 +6,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'); @@ -21,5 +22,18 @@ void main() { expect(box.get('key1'), equals('value1')); expect(box.get('key2'), equals('value2')); }); + + test('file', () async { + HiveMirror.init('.hive'); + await Hive.deleteBoxFromDisk('box'); + await Hive.deleteBoxFromDisk('.hive_mirror_metadata'); + + 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..ab7db4f --- /dev/null +++ b/test/unit/backend/file/file_mirror_manager_test.dart @@ -0,0 +1,44 @@ +// Copyright 2020 Guillaume Ducret. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +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)); + }); + }); +}