Refactor Note class

The loading/saving/deleting of the Note class is now inbuilt. It's not
offloaded to a NoteRepo. We're always going to be storing them on disk,
for now. It's also way easier to manage them this way.
This commit is contained in:
Vishesh Handa
2019-09-26 16:42:52 +02:00
parent ef2ad7466f
commit 6f8a149d57
3 changed files with 113 additions and 99 deletions

View File

@ -1,19 +1,32 @@
import 'dart:io';
import 'package:journal/storage/serializers.dart';
import 'package:journal/datetime_utils.dart';
enum NoteLoadState {
None,
Loading,
Loaded,
NotExists,
}
class Note implements Comparable<Note> {
String filePath;
String filePath = "";
DateTime created;
String body;
String body = "";
Map<String, dynamic> extraProperties = <String, dynamic>{};
var _loadState = NoteLoadState.None;
var _serializer = MarkdownYAMLSerializer();
Note({this.created, this.body, this.filePath, this.extraProperties}) {
if (created == null) {
created = DateTime(0, 0, 0, 0, 0, 0, 0, 0);
}
if (extraProperties == null) {
extraProperties = <String, dynamic>{};
}
// FIXME: Make it an ordered Map
Map<String, dynamic> props = {};
Note({this.created, this.body, this.filePath, this.props}) {
created = created ?? DateTime(0, 0, 0, 0, 0, 0, 0, 0);
props = props ?? <String, dynamic>{};
body = body ?? "";
assert(filePath != null);
}
bool hasValidDate() {
@ -21,18 +34,27 @@ class Note implements Comparable<Note> {
return created.year > 10;
}
factory Note.fromJson(Map<String, dynamic> json) {
String filePath = "";
if (json.containsKey("filePath")) {
filePath = json["filePath"].toString();
json.remove("filePath");
Future<NoteLoadState> load() async {
if (_loadState == NoteLoadState.Loading) {
return _loadState;
}
DateTime created;
if (json.containsKey("created")) {
var createdStr = json['created'].toString();
final file = File(filePath);
if (!file.existsSync()) {
_loadState = NoteLoadState.NotExists;
return _loadState;
}
final string = await file.readAsString();
var noteData = _serializer.decode(string);
body = noteData.body;
props = noteData.props;
if (props.containsKey("created")) {
var createdStr = props['created'].toString();
try {
created = DateTime.parse(json['created']).toLocal();
created = DateTime.parse(props['created']).toLocal();
} catch (ex) {
// Ignore it
}
@ -46,46 +68,37 @@ class Note implements Comparable<Note> {
created = DateTime.parse(createdStr);
}
}
json.remove("created");
}
if (created == null) {
created = DateTime(0, 0, 0, 0, 0, 0, 0, 0);
}
String body = "";
if (json.containsKey("body")) {
body = json['body'];
json.remove("body");
}
return Note(
filePath: filePath,
created: created,
body: body,
extraProperties: json,
);
_loadState = NoteLoadState.Loaded;
return _loadState;
}
Map<String, dynamic> toJson() {
var json = Map<String, dynamic>.from(extraProperties);
var createdStr = toIso8601WithTimezone(created);
if (!createdStr.startsWith("00")) {
json['created'] = createdStr;
// FIXME: What about error handling?
Future<void> save() async {
if (hasValidDate()) {
props['created'] = toIso8601WithTimezone(created);
}
json['body'] = body;
json['filePath'] = filePath;
return json;
var file = File(filePath);
var contents = _serializer.encode(NoteData(body, props));
await file.writeAsString(contents);
}
// FIXME: What about error handling?
Future<void> remove() async {
var file = File(filePath);
await file.delete();
}
// FIXME: Can't this part be auto-generated?
@override
int get hashCode =>
filePath.hashCode ^
created.hashCode ^
body.hashCode ^
extraProperties.hashCode;
filePath.hashCode ^ created.hashCode ^ body.hashCode ^ props.hashCode;
@override
bool operator ==(Object other) =>
@ -95,7 +108,7 @@ class Note implements Comparable<Note> {
filePath == other.filePath &&
body == other.body &&
created == other.created &&
_equalMaps(extraProperties, other.extraProperties);
_equalMaps(props, other.props);
static bool _equalMaps(Map a, Map b) {
if (a.length != b.length) return false;
@ -105,7 +118,7 @@ class Note implements Comparable<Note> {
@override
String toString() {
return 'Note{filePath: $filePath, body: $body, created: $created, extraProperties: $extraProperties}';
return 'Note{filePath: $filePath, body: $body, created: $created, props: $props}';
}
@override

View File

@ -1,52 +0,0 @@
import 'dart:io';
import 'package:journal/note.dart';
import 'package:journal/storage/file_storage.dart';
import 'package:journal/storage/serializers.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
DateTime nowWithoutMicro() {
var dt = DateTime.now();
return DateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
}
void main() {
group('FileStorage', () {
var notes = [
Note(filePath: "1.md", body: "test", created: nowWithoutMicro()),
Note(filePath: "2.md", body: "test2", created: nowWithoutMicro()),
];
Directory tempDir;
FileStorage storage;
setUpAll(() async {
tempDir = await Directory.systemTemp.createTemp('__storage_test__');
storage = FileStorage(
baseDirectory: tempDir.path,
noteSerializer: JsonNoteSerializer(),
);
});
tearDownAll(() async {
tempDir.deleteSync(recursive: true);
});
test('Should persist Notes to disk', () async {
var dir = await storage.saveNotes(notes);
expect(dir.listSync(recursive: true).length, 2);
expect(File(p.join(dir.path, "1.md")).existsSync(), isTrue);
expect(File(p.join(dir.path, "2.md")).existsSync(), isTrue);
});
test('Should load Notes from disk', () async {
var loadedNotes = await storage.listNotes();
loadedNotes.sort();
notes.sort();
expect(loadedNotes, notes);
});
});
}

View File

@ -0,0 +1,53 @@
import 'dart:io';
import 'package:journal/note.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
void main() {
group('NoteStorage', () {
var notes = <Note>[];
String n1Path;
String n2Path;
Directory tempDir;
setUpAll(() async {
tempDir = await Directory.systemTemp.createTemp('__storage_test__');
var dt = DateTime(2019, 12, 2, 5, 4, 2);
n1Path = p.join(tempDir.path, "1.md");
n2Path = p.join(tempDir.path, "2.md");
notes = <Note>[
Note(filePath: n1Path, body: "test", created: dt),
Note(filePath: n2Path, body: "test2", created: dt),
];
});
tearDownAll(() async {
tempDir.deleteSync(recursive: true);
});
test('Should persist and load Notes from disk', () async {
await Future.forEach(notes, (Note note) async {
await note.save();
});
expect(tempDir.listSync(recursive: true).length, 2);
expect(File(n1Path).existsSync(), isTrue);
expect(File(n2Path).existsSync(), isTrue);
var loadedNotes = <Note>[];
await Future.forEach(notes, (origNote) async {
var note = Note(filePath: origNote.filePath);
var r = await note.load();
expect(r, NoteLoadState.Loaded);
loadedNotes.add(note);
});
loadedNotes.sort();
notes.sort();
expect(loadedNotes, notes);
});
});
}