Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CAT-92] DatabaseClient 모듈 추가 #14

Merged
merged 16 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public enum Core: String, Modulable {
case APIClient
case KeychainClient
case UserNotificationClient
case DatabaseClient
}
3 changes: 2 additions & 1 deletion Projects/Core/APIClient/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ let project: Project = .makeTMABasedProject(
],
dependencies: [
.interface: [
.dependency(rootModule: Shared.self)
.dependency(rootModule: Shared.self),
.dependency(module: Core.KeychainClient, target: .interface)
devMinseok marked this conversation as resolved.
Show resolved Hide resolved
]
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// DatabaseClientInterface.swift
// DatabaseClient
//
// Created by devMinseok on 7/27/24.
//

import Foundation

import RealmSwift
import Dependencies
import DependenciesMacros

@DependencyClient
public struct DatabaseClient {
public var initialize: @Sendable (Realm.Configuration) async throws -> Void
/// Use create<T: Persistable>(object:).
public var create: @Sendable (any Persistable) async throws -> Void
/// Use read<T: Persistable>(_ type:).
public var read: @Sendable (Object.Type) async throws -> [Object]
/// Use read<T: Persistable>(_ type:, predicateFormat:, args:).
public var readWithFilter: @Sendable (Object.Type, String, Any) async throws -> [Object]
/// Use update<T: Persistable>(_ object:)
public var update: @Sendable (any Persistable) async throws -> Void
/// Use update<T: Object>(_ type:, value:).
public var updateWithValue: @Sendable (Object.Type, Any) async throws -> Void
/// Use delete<T: Object>(_ object:).
public var delete: @Sendable (Object) async throws -> Void
/// Use delete<T: Object>(_ object:).
public var deleteTable: @Sendable (Object.Type) async throws -> Void
/// Use delete<T: Object>(_ type:).
public var deleteAllTable: @Sendable () async throws -> Void

public func create<T: Persistable>(object: T) async throws -> Void {

Check warning on line 34 in Projects/Core/DatabaseClient/Interface/DatabaseClientInterface.swift

View workflow job for this annotation

GitHub Actions / Run Swiftlint

Returning Void in a function declaration is redundant (redundant_void_return)
try await create(object)
}

public func read<T: Persistable>(_ type: T.Type) async throws -> [T] {
let results = try await self.read(type.ManagedObject)
return await Self.realmActor?.convertToPersistable(type: type, objects: results) ?? []
}

public func read<T: Persistable>(_ type: T.Type, predicateFormat: String, args: Any) async throws -> [T] {
let results = try await self.readWithFilter(type.ManagedObject, predicateFormat, args)
return await Self.realmActor?.convertToPersistable(type: type, objects: results) ?? []
}

public func update<T: Persistable>(_ object: T) async throws {
try await self.update(object)
}

public func update<T: Object>(_ type: T.Type, value: Any) async throws {
try await self.updateWithValue(type, value)
}

public func delete<T: Object>(_ object: T) async throws {
try await self.delete(object)
}

public func delete<T: Object>(_ type: T.Type) async throws {
try await self.deleteTable(type)
}


// MARK: - Actor for realm

public static var realmActor: RealmActor?

public actor RealmActor {
public var realm: Realm!

Check warning on line 70 in Projects/Core/DatabaseClient/Interface/DatabaseClientInterface.swift

View workflow job for this annotation

GitHub Actions / Run Swiftlint

Implicitly unwrapped optionals should be avoided when possible (implicitly_unwrapped_optional)

public init(configuration: Realm.Configuration) async throws {
realm = try await Realm(configuration: configuration, actor: self)
}

func convertToPersistable<T: Persistable>(type: T.Type, objects: [Object]) -> [T] {
return objects.compactMap { type.init(managedObject: $0 as! T.ManagedObject) }

Check warning on line 77 in Projects/Core/DatabaseClient/Interface/DatabaseClientInterface.swift

View workflow job for this annotation

GitHub Actions / Run Swiftlint

Force casts should be avoided (force_cast)
}
}
}


extension DatabaseClient: TestDependencyKey {
public static let previewValue = Self()
public static let testValue = Self()
}
19 changes: 19 additions & 0 deletions Projects/Core/DatabaseClient/Interface/Persistable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Persistable.swift
// DatabaseClientInterface
//
// Created by devMinseok on 8/3/24.
// Copyright © 2024 PomoNyang. All rights reserved.
//

import Foundation

import RealmSwift

public protocol Persistable where Self: Decodable {
associatedtype ManagedObject: Object

var id: UUID { get set }
init(managedObject: ManagedObject)
func managedObject() -> ManagedObject
}
29 changes: 29 additions & 0 deletions Projects/Core/DatabaseClient/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// DatabaseClientTesting.swift
// DatabaseClientManifests
//
// Created by devMinseok on 7/27/24.
//

import ProjectDescription
import ProjectDescriptionHelpers

@_spi(Core)
@_spi(Shared)
import DependencyPlugin

let project: Project = .makeTMABasedProject(
module: Core.DatabaseClient,
scripts: [],
targets: [
.sources,
.interface,
.tests,
.testing
],
devMinseok marked this conversation as resolved.
Show resolved Hide resolved
dependencies: [
.interface: [
.dependency(rootModule: Shared.self)
]
]
)
138 changes: 138 additions & 0 deletions Projects/Core/DatabaseClient/Sources/DatabaseClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// DatabaseClient.swift
// DatabaseClient
//
// Created by devMinseok on 7/27/24.
//

import Foundation

import DatabaseClientInterface

import RealmSwift
import Dependencies

extension DatabaseClient: DependencyKey {
public static let liveValue: DatabaseClient = .live()

public static func live() -> DatabaseClient {
devMinseok marked this conversation as resolved.
Show resolved Hide resolved
return .init(
initialize: { configuration in
if realmActor == nil {
realmActor = try await RealmActor(configuration: configuration)
}
},
create: { object in
if let realmActor {
try await realmActor.create(object.managedObject())
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
},
read: { type in
if let realmActor {
let results = await realmActor.read(type)
return results
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
},
readWithFilter: { type, predicateFormat, args in
if let realmActor {
let results = await realmActor.read(type, predicateFormat: predicateFormat, args: args)
return results
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
},
update: { object in
if let realmActor {
try await realmActor.update(object.managedObject())
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
},
updateWithValue: { object, value in
if let realmActor {
try await realmActor.update(object, value: value)
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
},
delete: { object in
if let realmActor {
try await realmActor.delete(object)
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
},
deleteTable: { objectType in
if let realmActor {
try await realmActor.delete(objectType)
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
},
deleteAllTable: {
if let realmActor {
try await realmActor.deleteAll()
} else {
throw(NSError(domain: "Realm is not initialized", code: 0))
}
}
)
}
}

extension DatabaseClient.RealmActor {
public func create<T: Object>(_ object: T) async throws {
try await realm.asyncWrite {
realm.add(object, update: .error)
}
}

public func read<T: Object>(_ object: T.Type) -> [T] {
let results = realm.objects(object)
return results.map { $0 }
}

public func read<T: Object>(_ object: T.Type, predicateFormat: String, args: Any...) -> [T] {
let results = realm.objects(object)
return results.filter(predicateFormat, args).map { $0 }
}

public func update<T: Object>(_ object: T) async throws {
try await realm.asyncWrite {
realm.add(object, update: .modified)
}
}

public func update<T: Object>(_ object: T.Type, value: Any) async throws {
try await realm.asyncWrite {
realm.create(
object,
value: value,
update: .modified
)
}
}

public func delete<T: Object>(_ object: T) async throws {
try await realm.asyncWrite {
realm.delete(object)
}
}

public func delete<T: Object>(_ object: T.Type) async throws {
let objects = realm.objects(object)
try await realm.asyncWrite {
realm.delete(objects)
}
}

public func deleteAll() async throws {
try await realm.asyncWrite {
realm.deleteAll()
}
}
}
42 changes: 42 additions & 0 deletions Projects/Core/DatabaseClient/Testing/DatabaseClientTesting.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// DatabaseClientTesting.swift
// DatabaseClient
//
// Created by devMinseok on 7/27/24.
//

import Foundation

import DatabaseClientInterface

import RealmSwift

public class TestObject: Object {
@Persisted(primaryKey: true) public var id: UUID
@Persisted public var test: Int
}

public struct Test: Persistable {
public var id: UUID
public var test: Int

public init(
id: UUID = UUID(),
test: Int
) {
self.id = id
self.test = test
}

public init(managedObject: TestObject) {
self.id = managedObject.id
self.test = managedObject.test
}

public func managedObject() -> TestObject {
let object = TestObject()
object.id = id
object.test = test
return object
}
}
56 changes: 56 additions & 0 deletions Projects/Core/DatabaseClient/Tests/DatabaseClientTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

Check warning on line 1 in Projects/Core/DatabaseClient/Tests/DatabaseClientTests.swift

View workflow job for this annotation

GitHub Actions / Run Swiftlint

Files should not contain leading whitespace (leading_whitespace)
// DatabaseClientTests.swift
// DatabaseClient
//
// Created by devMinseok on 7/27/24.
//

import XCTest

import DatabaseClient
import DatabaseClientInterface
import DatabaseClientTesting

import RealmSwift
import Dependencies

final class DatabaseClientTests: XCTestCase {
@Dependency(DatabaseClient.self) var databaseClient

override func setUp() async throws {
try await withDependencies {
$0[DatabaseClient.self] = DatabaseClient.live()
} operation: {
let inmemoryConfig = Realm.Configuration(inMemoryIdentifier: "DatabaseClientTests")
try await databaseClient.initialize(inmemoryConfig)
}
try await super.setUp()
}

func testCRUD() async throws {
try await withDependencies {
$0[DatabaseClient.self] = DatabaseClient.live()
} operation: {
// create
let initialNumber = 1
var testModel = Test(test: initialNumber)
try await databaseClient.create(object: testModel)

// read
let results1 = try await databaseClient.read(Test.self)
XCTAssertEqual(results1[0].test, initialNumber)
let updateNumber = 2
testModel.test = 2

// update
try await databaseClient.update(testModel)
let results2 = try await databaseClient.read(Test.self)
XCTAssertEqual(results2[0].test, updateNumber)

// delete
try await databaseClient.deleteAllTable()
let results3 = try await databaseClient.read(Test.self)
XCTAssertEqual(results3.count, 0)
}
}
}
Loading
Loading