diff --git a/Sources/Fridge/BSONConverter.swift b/Sources/Fridge/BSONConverter.swift index 9594268..c7cd719 100644 --- a/Sources/Fridge/BSONConverter.swift +++ b/Sources/Fridge/BSONConverter.swift @@ -8,21 +8,53 @@ import Foundation import BSONCoder +/// Internal helper struct that wraps generic array +fileprivate struct WrappedObject: Codable, Identifiable { + var id = BSONObjectID.init() + var array: [T] + + init(arrayObject: [T]) { + array = arrayObject + } +} + /// Utility class providing write/read BSON functionality final class BSONConverter { - let _rawFilePath: URL + private let _rawFilePath: URL init(compartment: FridgeCompartment) { - _rawFilePath = compartment.filePath + _rawFilePath = compartment.objectPath } - /// Writes given object to a compartment storage + /// Writes given object to a local system storage func write(object: T) throws { - // declare BSONEncoder - let rawBSONData = try BSONEncoder().encode(object).toData() //will throw further + var rawData: Data + + do { + rawData = try BSONEncoder().encode(object).toData() + } catch let err { + print(" Error occured. Reason:\n\(err)") + throw err + } - // now flush the data to a file - try rawBSONData.write(to: _rawFilePath) + // flush the data to a file + try rawData.write(to: _rawFilePath) + } + + /// Writes given array of objects to a local system storage + func write(objects: [T]) throws { + var rawData: Data + let wrappedObject = WrappedObject.init(arrayObject: objects) + + do { + rawData = try BSONEncoder().encode(wrappedObject).toData() + } catch let err { + print(" Error occured. Reason:\n\(err)") + throw err + } + + // flush the data to a file + try rawData.write(to: _rawFilePath) } /// Reads object from compartment data storage and returns Foundation counterpart @@ -33,4 +65,13 @@ final class BSONConverter { let realObject = try BSONDecoder().decode(T.self, from: rawBSONData) return realObject } + + /// Reads array of objects from compartment data storage and returns Foundation counterpart + func read() throws -> [T] { + // get raw data from the storage + let rawBSONData = try Data(contentsOf: _rawFilePath) + + let wrappedObject = try BSONDecoder().decode(WrappedObject.self, from: rawBSONData) + return wrappedObject.array + } } diff --git a/Sources/Fridge/Freezer.swift b/Sources/Fridge/Freezer.swift index 6b55ade..8ae2e3d 100644 --- a/Sources/Fridge/Freezer.swift +++ b/Sources/Fridge/Freezer.swift @@ -31,12 +31,12 @@ import Foundation // MARK: - final internal class Freezer { /// Freezes an object into Fridge persistant storage. Any new object will overwrite previously stored object - func freeze(object: T, identifier: String) throws { //async ? + func freeze(object: T, identifier: String) throws { do { // 1. initialize fridge compartment for given key let comp = FridgeCompartment(key: identifier) - // 2. initialize Streamer with produced compartment + // 2. initialize BSON writer from given compartment let writer = BSONConverter(compartment: comp) // 3. perform stream write of given object @@ -46,13 +46,29 @@ final internal class Freezer { } } + /// Freezes an object into Fridge persistant storage. Any new object will overwrite previously stored object + func freeze(objects: [T], identifier: String) throws { + do { + // 1. initialize fridge compartment for given key + let comp = FridgeCompartment(key: identifier) + + // 2. initialize BSON writer from given compartment + let writer = BSONConverter(compartment: comp) + + // 3. perform stream write of array of objects + try writer.write(objects: objects) + } catch { + throw FreezingErrors.dataStoringError + } + } + /// Unfreezes an object from Fridge persistant storage. func unfreeze(identifier: String) throws -> T { do { // 1. setup compartment let comp = FridgeCompartment(key: identifier) - // 2. initialize Streamer with created compartment + // 2. initialize BSON reader from given compartment let reader = BSONConverter(compartment: comp) // 3. perform stream read @@ -63,6 +79,23 @@ final internal class Freezer { } } + /// Unfreezes an array of objects from Fridge persistant storage. + func unfreeze(identifier: String) throws -> [T] { + do { + // 1. setup compartment + let comp = FridgeCompartment(key: identifier) + + // 2. initialize BSON reader from given compartment + let reader = BSONConverter(compartment: comp) + + // 3. perform stream read + let storedObject: [T] = try reader.read() + return storedObject + } catch { + throw FreezingErrors.dataReadingError + } + } + /// Returns `true` if a given object has been frozen previously. func isAlreadyFrozen(identifier: String) -> Bool { FridgeCompartment(key: identifier).alreadyExist diff --git a/Sources/Fridge/FridgeCompartment.swift b/Sources/Fridge/FridgeCompartment.swift index 2a660c0..f8ca031 100644 --- a/Sources/Fridge/FridgeCompartment.swift +++ b/Sources/Fridge/FridgeCompartment.swift @@ -40,25 +40,26 @@ internal struct FridgeCompartment { } /// Returns `URL` based file path of this compartment - var filePath: URL { - //get documents directory + var objectPath: URL { + // TODO: Alter between DocumentsDirectory and CacheDirectory later guard let documentDirectoryURL = _fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { fatalError(" Unable to compute DocumentsDirectory path") } - - let finalName = "\(key)\(BLOB_EXTENSION)" - let finalURL = documentDirectoryURL.appendingPathComponent(finalName) - return finalURL + return documentDirectoryURL.appendingPathComponent(storageName) + } + + /// Returns the compartment name formatted by key and static identifier + private var storageName: String { + key + BLOB_EXTENSION } /// Returns `true` if raw data already exists at this compartment, `false` otherwise var alreadyExist: Bool { - _fileManager.fileExists(atPath: filePath.path) + _fileManager.fileExists(atPath: objectPath.path) } - /// Removes compartment from persistent storage func remove() { - try? _fileManager.removeItem(atPath: filePath.path) + try? _fileManager.removeItem(atPath: objectPath.path) } } diff --git a/Tests/FridgeTests/FreezerTests.swift b/Tests/FridgeTests/FreezerTests.swift index 3129379..25078fc 100644 --- a/Tests/FridgeTests/FreezerTests.swift +++ b/Tests/FridgeTests/FreezerTests.swift @@ -48,17 +48,21 @@ fileprivate struct FridgeTestObject: Codable { string_field = "Some f🧊ncy string" int_field = Int.max dict_field = InnerTestObject() - arr_field = [100, 200, 300, Int.random(in: Int.min...Int.max)] - data_field = Data(repeating: 0xAE, count: 0xABCDEF) + arr_field = [0xABCDEF, 0xCAB0CAB, 0xFADE, 0xEFCAD] + data_field = Data(repeating: 0xAE, count: 0xE1FE1) url_field = URL(fileURLWithPath: "someFilePathOfAMockObject") } } fileprivate struct InnerTestObject: Codable { - var field1: Float = 1_234_567.890_001 - var field2: Double = Double.pi - var field3: Set = Set([1,2,3]) - var field4: String? = nil + var field1: String? = nil + var field2: Float = 1_234_567.890_001 + var field3: Double = Double.pi + var field4: Date = Date.init() + var field5: Bool = !false + + var field6: Set = Set([1,2,3]) + var field7: Array = Array.init() } extension FridgeTestObject: Equatable { @@ -66,24 +70,27 @@ extension FridgeTestObject: Equatable { let equality = (lhs.string_field == rhs.string_field) && (lhs.int_field == rhs.int_field) && - (lhs.arr_field == rhs.arr_field) + (lhs.arr_field == rhs.arr_field) && + (lhs.data_field == rhs.data_field) && + (lhs.url_field == rhs.url_field) return equality } } +// !! LET THE HUNT BEGIN !! 🕵️‍♂️🥷 final class FreezerTests: XCTestCase { // SHARED TESTING OBJECT let freezer = Freezer() /// Tests weather an object can be saved without throwing error - func testBasicFreezing(){ - let testData1 = FridgeTestObject() - XCTAssertNoThrow(try freezer.freeze(object: testData1, identifier: FridgeTestObject.IDENTIFIER)) + func testObjectFreezing(){ + let testData = FridgeTestObject() + XCTAssertNoThrow(try freezer.freeze(object: testData, identifier: FridgeTestObject.IDENTIFIER)) } /// Tests weather an object can be loaded without throwing error - func testBasicUnfreezing() { + func testObjectUnfreezing() { //freeze an object first let frozenObject = FridgeTestObject() @@ -106,15 +113,37 @@ final class FreezerTests: XCTestCase { } /// Tests if array can be stored -// func testFreezingAnArray() throws { -// XCTAssertFalse(freezer.isAlreadyFrozen(identifier: "array.test")) -// -// let freezingArray = [1,2,3,4,5,6,7,8] -// let objectArray: Array = [FridgeTestObject(), FridgeTestObject()] -// try freezer.freeze(object: freezingArray, identifier: "array.test") -// try freezer.freeze(object: objectArray, identifier: "array-object.test") -// -// let unpackedObject: [Int] = try freezer.unfreeze(identifier: "array.test") -// XCTAssert(unpackedObject[0] == freezingArray[0]) -// } + func testArrayFreezing() throws { + XCTAssertFalse(freezer.isAlreadyFrozen(identifier: "array.test")) + + let foundationArray = [1,2,3,4,5,6,7,8] + let customStructArray: Array = [FridgeTestObject(), FridgeTestObject()] + XCTAssertNoThrow(try freezer.freeze(objects: foundationArray, identifier: "foundation.array")) + XCTAssertNoThrow(try freezer.freeze(objects: customStructArray, identifier: "array-object.test")) + + // make sure it actually throws when passed incorrectly + XCTAssertThrowsError(try freezer.freeze(object: foundationArray, identifier: "wrong.method.array")) + XCTAssertThrowsError(try freezer.freeze(object: customStructArray, identifier: "wrong.method.custom.array")) + } + + /// Tests if array can be read from the storage + func testArrayUnFreezing() throws { + let expectedFoundationArray = [1,2,3,4,5,6,7,8] + let expectedStructArray: Array = [FridgeTestObject(), FridgeTestObject()] + var failureMessage: String + + do { + // check foundation + failureMessage = "Foundation array issue" + let unfrozenFoundation: Array = try freezer.unfreeze(identifier: "foundation.array") + XCTAssert(unfrozenFoundation == expectedFoundationArray) + + failureMessage = "Array of custom struct issue" + let unfrozenCustomArray: Array = try freezer.unfreeze(identifier: "array-object.test") + XCTAssert(unfrozenCustomArray[0] == expectedStructArray[0]) +// XCTAssert(unfrozenCustomArray[0].dict_field == expectedStructArray[0].dict_field) + } catch { + XCTFail(failureMessage) + } + } }