Skip to content

Commit

Permalink
feature(SabyAppleStorage): add item version feature
Browse files Browse the repository at this point in the history
  • Loading branch information
0xWOF committed May 24, 2024
1 parent f157ba0 commit 5e033af
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 56 deletions.
29 changes: 16 additions & 13 deletions Source/AppleStorage/Implement/CoreDataArrayStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import SabySafe
import SabySize
import SabyJSON

private let STORAGE_VERSION = "Version1"

public final class CoreDataArrayStorage<Value: Codable & KeyIdentifiable>: ArrayStorage {
typealias Context = NSManagedObjectContext
typealias Item = SabyCoreDataArrayStorageItem
typealias Item = SabyCoreDataArrayStorageItemVersion1

let entity: NSEntityDescription

Expand All @@ -24,15 +26,15 @@ public final class CoreDataArrayStorage<Value: Codable & KeyIdentifiable>: Array
let encoder = JSONEncoder.acceptingNonConfirmingFloat()
let decoder = JSONDecoder.acceptingNonConfirmingFloat()

public init(directoryURL: URL, fileName: String, migration: @escaping () -> Promise<Void, Error>) {
public init(directoryURL: URL, storageName: String, migration: @escaping () -> Promise<Void, Error>) {
let schema = SabyCoreDataArrayStorageSchema()

self.entity = schema.entity

self.contextLoad = {
Context.load(
directoryURL: directoryURL,
fileName: fileName,
storageName: storageName,
migration: migration,
model: schema.model
)
Expand All @@ -50,7 +52,7 @@ extension CoreDataArrayStorage {
let request = self.createAnyRequest(key: key)
try context.executeDelete(request)

let item = SabyCoreDataArrayStorageItem(
let item = SabyCoreDataArrayStorageItemVersion1(
entity: self.entity,
insertInto: context
)
Expand All @@ -70,7 +72,7 @@ extension CoreDataArrayStorage {
try context.executeDelete(request)

for value in values {
let item = SabyCoreDataArrayStorageItem(
let item = SabyCoreDataArrayStorageItemVersion1(
entity: self.entity,
insertInto: context
)
Expand Down Expand Up @@ -195,7 +197,7 @@ extension CoreDataArrayStorage {
}

fileprivate func createSizeRequest() -> NSFetchRequest<NSDictionary> {
let byteExpression = NSExpression(forKeyPath: \SabyCoreDataArrayStorageItem.byte)
let byteExpression = NSExpression(forKeyPath: \SabyCoreDataArrayStorageItemVersion1.byte)
let sumExpression = NSExpression(forFunction: "sum:", arguments: [byteExpression])
let sumDescription = NSExpressionDescription()
sumDescription.expression = sumExpression
Expand Down Expand Up @@ -335,7 +337,7 @@ extension NSManagedObjectContext {
extension NSManagedObjectContext {
static func load(
directoryURL: URL,
fileName: String,
storageName: String,
migration: @escaping () -> Promise<Void, Error>,
model: NSManagedObjectModel
) -> Promise<NSManagedObjectContext, Error> {
Expand All @@ -353,10 +355,10 @@ extension NSManagedObjectContext {
)
}

let url = directoryURL.appendingPathComponent(fileName)
let url = directoryURL.appendingPathComponent(storageName).appendingPathExtension(STORAGE_VERSION)

let container = NSPersistentContainer(
name: fileName,
name: storageName,
managedObjectModel: model
)
let storeDescription = NSPersistentStoreDescription(url: url)
Expand All @@ -380,8 +382,9 @@ public enum CoreDataArrayStorageError: Error {
case requestResultNotFound
}

@objc(SabyCoreDataArrayStorageItem)
final class SabyCoreDataArrayStorageItem: NSManagedObject {
// Must not be modified. Write ItemVersion2 and write migration logic instead.
@objc(SabyCoreDataArrayStorageItemVersion1)
final class SabyCoreDataArrayStorageItemVersion1: NSManagedObject {
@NSManaged var key: UUID
@NSManaged var data: Data
@NSManaged var date: Date
Expand Down Expand Up @@ -426,8 +429,8 @@ final class SabyCoreDataArrayStorageSchema {
}

let itemEntity = NSEntityDescription()
itemEntity.name = String(describing: SabyCoreDataArrayStorageItem.self)
itemEntity.managedObjectClassName = String(describing: SabyCoreDataArrayStorageItem.self)
itemEntity.name = String(describing: SabyCoreDataArrayStorageItemVersion1.self)
itemEntity.managedObjectClassName = String(describing: SabyCoreDataArrayStorageItemVersion1.self)
itemEntity.properties = [
keyAttribute,
dataAttribute,
Expand Down
21 changes: 12 additions & 9 deletions Source/AppleStorage/Implement/FileArrayStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import SabyConcurrency
import SabySize
import SabyJSON

private let STORAGE_VERSION = "Version1"

public final class FileArrayStorage<Value: Codable & KeyIdentifiable>: ArrayStorage {
typealias Context = FileArrayStorageContext
typealias Item = FileArrayStorageItem
typealias Item = FileArrayStorageItemVersion1

let fileLock = Lock()

Expand All @@ -21,11 +23,11 @@ public final class FileArrayStorage<Value: Codable & KeyIdentifiable>: ArrayStor

let encoder = JSONEncoder.acceptingNonConfirmingFloat()

public init(directoryURL: URL, fileName: String, migration: @escaping () -> Promise<Void, Error>) {
public init(directoryURL: URL, storageName: String, migration: @escaping () -> Promise<Void, Error>) {
self.contextLoad = {
Context.load(
directoryURL: directoryURL,
fileName: fileName,
storageName: storageName,
migration: migration
)
}
Expand Down Expand Up @@ -191,19 +193,19 @@ extension FileArrayStorage {

struct FileArrayStorageContext<Value: Codable> {
let url: URL
let items: Atomic<[FileArrayStorageItem<Value>]>
let items: Atomic<[FileArrayStorageItemVersion1<Value>]>

private init(
url: URL,
items: Atomic<[FileArrayStorageItem<Value>]>
items: Atomic<[FileArrayStorageItemVersion1<Value>]>
) {
self.url = url
self.items = items
}

static func load(
directoryURL: URL,
fileName: String,
storageName: String,
migration: @escaping () -> Promise<Void, Error>
) -> Promise<FileArrayStorageContext, Error> {
migration().then {
Expand All @@ -221,7 +223,7 @@ struct FileArrayStorageContext<Value: Codable> {
)
}

let url = directoryURL.appendingPathComponent(fileName)
let url = directoryURL.appendingPathComponent(storageName).appendingPathExtension(STORAGE_VERSION)
if !fileManager.fileExists(atPath: url.path) {
return FileArrayStorageContext(
url: url,
Expand All @@ -232,7 +234,7 @@ struct FileArrayStorageContext<Value: Codable> {
guard
let data = try? Data(contentsOf: url),
let items = try? decoder.decode(
[FileArrayStorageItem<Value>].self,
[FileArrayStorageItemVersion1<Value>].self,
from: data
)
else {
Expand All @@ -254,7 +256,8 @@ public enum FileArrayStorageError: Error {
case libraryDirectoryNotFound
}

struct FileArrayStorageItem<Value: Codable>: Codable {
// Must not be modified. Write ItemVersion2 and write migration logic instead.
struct FileArrayStorageItemVersion1<Value: Codable>: Codable {
let key: UUID
let value: Value
let date: Date
Expand Down
19 changes: 12 additions & 7 deletions Source/AppleStorage/Implement/FileDictionaryStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Foundation
import SabyConcurrency
import SabyJSON

private let STORAGE_VERSION = "Version1"

public final class FileDictionaryStorage<
Key: Hashable & Codable,
Value: Codable
Expand All @@ -22,11 +24,11 @@ public final class FileDictionaryStorage<

let encoder = JSONEncoder.acceptingNonConfirmingFloat()

public init(directoryURL: URL, fileName: String, migration: @escaping () -> Promise<Void, Error>) {
public init(directoryURL: URL, storageName: String, migration: @escaping () -> Promise<Void, Error>) {
self.contextLoad = {
Context.load(
directoryURL: directoryURL,
fileName: fileName,
storageName: storageName,
migration: migration
)
}
Expand Down Expand Up @@ -107,16 +109,16 @@ extension FileDictionaryStorage {

struct FileDictionaryStorageContext<Key: Hashable & Codable, Value: Codable> {
let url: URL
let values: Atomic<[Key: Value]>
let values: Atomic<FileDictionaryStorageItemVersion1<Key, Value>>

private init(url: URL, values: Atomic<[Key: Value]>) {
private init(url: URL, values: Atomic<FileDictionaryStorageItemVersion1<Key, Value>>) {
self.url = url
self.values = values
}

static func load(
directoryURL: URL,
fileName: String,
storageName: String,
migration: @escaping () -> Promise<Void, Error>
) -> Promise<FileDictionaryStorageContext, Error> {
migration().then {
Expand All @@ -134,7 +136,7 @@ struct FileDictionaryStorageContext<Key: Hashable & Codable, Value: Codable> {
)
}

let url = directoryURL.appendingPathComponent(fileName)
let url = directoryURL.appendingPathComponent(storageName).appendingPathExtension(STORAGE_VERSION)
if !fileManager.fileExists(atPath: url.path) {
return FileDictionaryStorageContext(
url: url,
Expand All @@ -145,7 +147,7 @@ struct FileDictionaryStorageContext<Key: Hashable & Codable, Value: Codable> {
guard
let data = try? Data(contentsOf: url),
let values = try? decoder.decode(
[Key: Value].self,
FileDictionaryStorageItemVersion1<Key, Value>.self,
from: data
)
else {
Expand All @@ -162,3 +164,6 @@ struct FileDictionaryStorageContext<Key: Hashable & Codable, Value: Codable> {
}
}
}

// Must not be modified. Write ItemVersion2 and write migration logic instead.
typealias FileDictionaryStorageItemVersion1<Key: Hashable & Codable, Value: Codable> = [Key: Value]
19 changes: 12 additions & 7 deletions Source/AppleStorage/Implement/FileValueStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import SabySafe
import SabyConcurrency
import SabyJSON

private let STORAGE_VERSION = "Version1"

public final class FileValueStorage<Value: Codable>: ValueStorage {
typealias Context = FileValueStorageContext

Expand All @@ -20,11 +22,11 @@ public final class FileValueStorage<Value: Codable>: ValueStorage {

let encoder = JSONEncoder.acceptingNonConfirmingFloat()

public init(directoryURL: URL, fileName: String, migration: @escaping () -> Promise<Void, Error>) {
public init(directoryURL: URL, storageName: String, migration: @escaping () -> Promise<Void, Error>) {
self.contextLoad = {
Context.load(
directoryURL: directoryURL,
fileName: fileName,
storageName: storageName,
migration: migration
)
}
Expand Down Expand Up @@ -91,16 +93,16 @@ extension FileValueStorage {

struct FileValueStorageContext<Value: Codable> {
let url: URL
let value: Atomic<Value?>
let value: Atomic<FileValueStorageItemVersion1<Value>?>

private init(url: URL, value: Atomic<Value?>) {
private init(url: URL, value: Atomic<FileValueStorageItemVersion1<Value>?>) {
self.url = url
self.value = value
}

static func load(
directoryURL: URL,
fileName: String,
storageName: String,
migration: @escaping () -> Promise<Void, Error>
) -> Promise<FileValueStorageContext, Error> {
migration().then {
Expand All @@ -118,7 +120,7 @@ struct FileValueStorageContext<Value: Codable> {
)
}

let url = directoryURL.appendingPathComponent(fileName)
let url = directoryURL.appendingPathComponent(storageName).appendingPathExtension(STORAGE_VERSION)
if !fileManager.fileExists(atPath: url.path) {
return FileValueStorageContext(
url: url,
Expand All @@ -129,7 +131,7 @@ struct FileValueStorageContext<Value: Codable> {
guard
let data = try? Data(contentsOf: url),
let value = try? decoder.decode(
Value?.self,
FileValueStorageItemVersion1<Value>?.self,
from: data
)
else {
Expand All @@ -146,3 +148,6 @@ struct FileValueStorageContext<Value: Codable> {
}
}
}

// Must not be modified. Write ItemVersion2 and write migration logic instead.
typealias FileValueStorageItemVersion1<Value: Codable> = Value
14 changes: 7 additions & 7 deletions Source/AppleStorage/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import Foundation
import SabyConcurrency

public protocol Storage {
/// ``init(directoryURL:fileName:migration:)``
/// ``init(directoryURL:storageName:migration:)``
/// execute migration and then create or load preference from
/// `{Directory url}/{File name}` path.
init(directoryURL: URL, fileName: String, migration: @escaping () -> Promise<Void, Error>)
/// `{Directory url}/{Storage name}/{Version name}` path.
init(directoryURL: URL, storageName: String, migration: @escaping () -> Promise<Void, Error>)
}

extension Storage {
/// ``init(directoryName:fileName:)`` create or load storage from
/// `{Directory url}/{File name}` path.
public init(directoryURL: URL, fileName: String) {
self.init(directoryURL: directoryURL, fileName: fileName, migration: { .resolved(()) })
/// ``init(directoryName:storageName:)`` create or load storage from
/// `{Directory url}/{Storage name}/{Version name}` path.
public init(directoryURL: URL, storageName: String) {
self.init(directoryURL: directoryURL, storageName: storageName, migration: { .resolved(()) })
}
}

Expand Down
2 changes: 1 addition & 1 deletion Test/AppleStorage/Implement/CoreDataArrayStorageTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CoreDataArrayStorageTest: XCTestCase {
var encoder: JSONEncoder!

override func setUpWithError() throws {
storage = CoreDataArrayStorage(directoryURL: FileManager.default.temporaryDirectory, fileName: "\(UUID())")
storage = CoreDataArrayStorage(directoryURL: FileManager.default.temporaryDirectory, storageName: "\(UUID())")
encoder = JSONEncoder()
}

Expand Down
8 changes: 4 additions & 4 deletions Test/AppleStorage/Implement/FileArrayStorageTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class FileArrayStorageTest: XCTestCase {
fileprivate var storage: FileArrayStorage<DummyItem>!
fileprivate var encoder: JSONEncoder!
fileprivate let directoryURL = FileManager.default.temporaryDirectory
fileprivate var fileName: String!
fileprivate var storageName: String!

private struct TestItemGroup {
private static let testCount = 100
Expand All @@ -37,16 +37,16 @@ final class FileArrayStorageTest: XCTestCase {
}

override func setUpWithError() throws {
fileName = UUID().uuidString
storageName = UUID().uuidString
storage = FileArrayStorage<DummyItem>(
directoryURL: directoryURL,
fileName: fileName
storageName: storageName
)
encoder = JSONEncoder()
}

override func tearDownWithError() throws {
let fileURL = directoryURL.appendingPathComponent(fileName)
let fileURL = directoryURL.appendingPathComponent(storageName)

if FileManager.default.fileExists(atPath: fileURL.absoluteString) {
try FileManager.default.removeItem(at: fileURL)
Expand Down
Loading

0 comments on commit 5e033af

Please sign in to comment.