diff --git a/datastore/memoryds.nim b/datastore/memoryds.nim new file mode 100644 index 0000000..c4d735e --- /dev/null +++ b/datastore/memoryds.nim @@ -0,0 +1,96 @@ +import std/tables + +import pkg/chronos +import pkg/questionable +import pkg/questionable/results +import pkg/upraises + +import ./key +import ./query +import ./datastore + +export key, query + +push: {.upraises: [].} + +type + MemoryStore* = object + store*: Datastore + key*: Key + + MemoryDatastore* = ref object of Datastore + stores*: Table[Key, MemoryStore] + +method has*( + self: MemoryDatastore, + key: Key): Future[?!bool] {.async.} = + + without mounted =? self.dispatch(key): + return failure "No mounted datastore found" + + return (await mounted.store.store.has(mounted.relative)) + +method delete*( + self: MemoryDatastore, + key: Key): Future[?!void] {.async.} = + + without mounted =? self.dispatch(key), error: + return failure(error) + + return (await mounted.store.store.delete(mounted.relative)) + +method delete*( + self: MemoryDatastore, + keys: seq[Key]): Future[?!void] {.async.} = + + for key in keys: + if err =? (await self.delete(key)).errorOption: + return failure err + + return success() + +method get*( + self: MemoryDatastore, + key: Key): Future[?!seq[byte]] {.async.} = + + without mounted =? self.dispatch(key), error: + return failure(error) + + return await mounted.store.store.get(mounted.relative) + +method put*( + self: MemoryDatastore, + key: Key, + data: seq[byte]): Future[?!void] {.async.} = + + without mounted =? self.dispatch(key), error: + return failure(error) + + return (await mounted.store.store.put(mounted.relative, data)) + +method put*( + self: MemoryDatastore, + batch: seq[BatchEntry]): Future[?!void] {.async.} = + + for entry in batch: + if err =? (await self.put(entry.key, entry.data)).errorOption: + return failure err + + return success() + +method close*(self: MemoryDatastore): Future[?!void] {.async.} = + for s in self.stores.values: + discard await s.store.close() + + # TODO: how to handle failed close? + return success() + +func new*( + T: type MemoryDatastore, + stores: Table[Key, Datastore] = initTable[Key, Datastore]()): ?!T = + + var self = T() + for (k, v) in stores.pairs: + self.stores[?k.path] = MemoryStore(store: v, key: k) + + success self diff --git a/tests/datastore/testmemoryds.nim b/tests/datastore/testmemoryds.nim new file mode 100644 index 0000000..626d98d --- /dev/null +++ b/tests/datastore/testmemoryds.nim @@ -0,0 +1,138 @@ +import std/options +import std/sequtils +import std/os +from std/algorithm import sort, reversed + +import pkg/asynctest/unittest2 +import pkg/chronos +import pkg/stew/results +import pkg/stew/byteutils + +import pkg/datastore/fsds + +import ./dscommontests +import ./querycommontests + +suite "Test Basic MemoryDatastore": + let + path = currentSourcePath() # get this file's name + basePath = "tests_data" + basePathAbs = path.parentDir / basePath + key = Key.init("/a/b").tryGet() + bytes = "some bytes".toBytes + otherBytes = "some other bytes".toBytes + + var + fsStore: MemoryDatastore + + setupAll: + removeDir(basePathAbs) + require(not dirExists(basePathAbs)) + createDir(basePathAbs) + + fsStore = MemoryDatastore.new(root = basePathAbs, depth = 3).tryGet() + + teardownAll: + removeDir(basePathAbs) + require(not dirExists(basePathAbs)) + + basicStoreTests(fsStore, key, bytes, otherBytes) + +suite "Test Misc MemoryDatastore": + let + path = currentSourcePath() # get this file's name + basePath = "tests_data" + basePathAbs = path.parentDir / basePath + bytes = "some bytes".toBytes + + setup: + removeDir(basePathAbs) + require(not dirExists(basePathAbs)) + createDir(basePathAbs) + + teardown: + removeDir(basePathAbs) + require(not dirExists(basePathAbs)) + + test "Test validDepth()": + let + fs = MemoryDatastore.new(root = "/", depth = 3).tryGet() + invalid = Key.init("/a/b/c/d").tryGet() + valid = Key.init("/a/b/c").tryGet() + + check: + not fs.validDepth(invalid) + fs.validDepth(valid) + + test "Test invalid key (path) depth": + let + fs = MemoryDatastore.new(root = basePathAbs, depth = 3).tryGet() + key = Key.init("/a/b/c/d").tryGet() + + check: + (await fs.put(key, bytes)).isErr + (await fs.get(key)).isErr + (await fs.delete(key)).isErr + (await fs.has(key)).isErr + + test "Test valid key (path) depth": + let + fs = MemoryDatastore.new(root = basePathAbs, depth = 3).tryGet() + key = Key.init("/a/b/c").tryGet() + + check: + (await fs.put(key, bytes)).isOk + (await fs.get(key)).isOk + (await fs.delete(key)).isOk + (await fs.has(key)).isOk + + test "Test key cannot write outside of root": + let + fs = MemoryDatastore.new(root = basePathAbs, depth = 3).tryGet() + key = Key.init("/a/../../c").tryGet() + + check: + (await fs.put(key, bytes)).isErr + (await fs.get(key)).isErr + (await fs.delete(key)).isErr + (await fs.has(key)).isErr + + test "Test key cannot convert to invalid path": + let + fs = MemoryDatastore.new(root = basePathAbs).tryGet() + + for c in invalidFilenameChars: + if c == ':': continue + if c == '/': continue + + let + key = Key.init("/" & c).tryGet() + + check: + (await fs.put(key, bytes)).isErr + (await fs.get(key)).isErr + (await fs.delete(key)).isErr + (await fs.has(key)).isErr + +suite "Test Query": + let + path = currentSourcePath() # get this file's name + basePath = "tests_data" + basePathAbs = path.parentDir / basePath + + var + ds: MemoryDatastore + + setup: + removeDir(basePathAbs) + require(not dirExists(basePathAbs)) + createDir(basePathAbs) + + ds = MemoryDatastore.new(root = basePathAbs, depth = 5).tryGet() + + teardown: + + removeDir(basePathAbs) + require(not dirExists(basePathAbs)) + + queryTests(ds, false)