Skip to content

Commit

Permalink
Implement file backend
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDucret committed Jan 5, 2021
1 parent 8568bdf commit 66b49b7
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 7 deletions.
63 changes: 63 additions & 0 deletions lib/src/backend/file/file_descriptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:io';

import 'package:async/async.dart';

abstract class FileDescriptorInterface {
Stream<List<int>> 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<List<int>> 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);
65 changes: 65 additions & 0 deletions lib/src/backend/file/file_mirror_manager.dart
Original file line number Diff line number Diff line change
@@ -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<T>(
MirrorHandler<T> handler, Metadata metadata) {
return FileMirrorManager(DynamicMirrorHandler<T>(handler), metadata);
}

Future<void> mirror(dynamic fileDescriptor) => loadFile(fileDescriptor);

Future<void> 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<void> _applyLines(String etag, Iterable<String> 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';
}
19 changes: 13 additions & 6 deletions lib/src/backend/mirror_manager.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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]''');
}
}
3 changes: 3 additions & 0 deletions lib/src/handlers/box_mirror_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class BoxMirrorHandler<T> implements MirrorHandler<T> {
@override
Future<void> deleteAll(Iterable keys) => _box.deleteAll(keys);

@override
Future<void> clear() => _box.clear();

@override
Future<void> dispose() => _box.close();
}
3 changes: 3 additions & 0 deletions lib/src/handlers/dynamic_mirror_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class DynamicMirrorHandler<T> implements MirrorHandler {
@override
Future<void> deleteAll(Iterable keys) => _delegate.deleteAll(keys);

@override
Future<void> clear() => _delegate.clear();

@override
Future<void> dispose() => _delegate.dispose();
}
1 change: 1 addition & 0 deletions lib/src/hive_mirror.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ abstract class MirrorHandler<T> {
Future<void> init();
Future<void> putAll(Map<dynamic, T> entries);
Future<void> deleteAll(Iterable<dynamic> keys);
Future<void> clear();
Future<void> dispose();
}
2 changes: 2 additions & 0 deletions test/assets/load2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
key1: value1
key2: value2
52 changes: 52 additions & 0 deletions test/file_descriptors.dart
Original file line number Diff line number Diff line change
@@ -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<List<int>> 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();
}
15 changes: 14 additions & 1 deletion test/integration/mirror_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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<String>('box');
expect(box.get('key1'), equals('value1'));
expect(box.get('key2'), equals('value2'));
});
});
}
40 changes: 40 additions & 0 deletions test/unit/backend/file/file_mirror_manager_test.dart
Original file line number Diff line number Diff line change
@@ -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));
});
});
}

0 comments on commit 66b49b7

Please sign in to comment.