From 97532fcc3f42000fb9aaaf3a9de57ea57b21101c Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sat, 4 Nov 2023 22:00:17 +0330 Subject: [PATCH 01/16] Add .gitignore file - added the .gitignore file, The file copied from 'https://cocoacasts.com/a-gitignore-for-swift-projects' url. --- .gitignore | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fae5995 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +.DS_Store +*.swp +*~.nib +DerivedData/ +build/ +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 +xcuserdata +!xcshareddata +!xcschemes +*.moved-aside +/Pods +/Carthage + +.swiftpm +/node_modules +/tmp + +.env +.env.* + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output \ No newline at end of file From a095d265f44b8832aca968c088d2347c5bb365fa Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sat, 4 Nov 2023 22:09:47 +0330 Subject: [PATCH 02/16] Adjust Minimum Deployment Target to iOS 14.0 --- TransferList.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 05bc24e..e02ab27 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -294,6 +294,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -321,6 +322,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", From 5287d1094e71faeddc4e2764973e061227c3e8a5 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sat, 4 Nov 2023 22:37:30 +0330 Subject: [PATCH 03/16] Add Entities: Person, Card, CardTransferCount, and PersonBankAccount - Added the `Person` entity to store personal information including name and avatar. - Added the `Card` entity to hold card number data. - Added the `CardTransferCount` entity for tracking transfer counts. - Added the `PersonBankAccount` entity to combine and manage these entities. --- Domain/.gitignore | 9 ++++++ Domain/Package.swift | 28 +++++++++++++++++++ Domain/README.md | 3 ++ Domain/Sources/Domain/Entities/Card.swift | 14 ++++++++++ .../Domain/Entities/CardTransferCount.swift | 14 ++++++++++ Domain/Sources/Domain/Entities/Person.swift | 15 ++++++++++ .../Domain/Entities/PersonBankAccount.swift | 20 +++++++++++++ TransferList.xcodeproj/project.pbxproj | 2 ++ .../xcschemes/xcschememanagement.plist | 2 +- 9 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 Domain/.gitignore create mode 100644 Domain/Package.swift create mode 100644 Domain/README.md create mode 100644 Domain/Sources/Domain/Entities/Card.swift create mode 100644 Domain/Sources/Domain/Entities/CardTransferCount.swift create mode 100644 Domain/Sources/Domain/Entities/Person.swift create mode 100644 Domain/Sources/Domain/Entities/PersonBankAccount.swift diff --git a/Domain/.gitignore b/Domain/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Domain/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Domain/Package.swift b/Domain/Package.swift new file mode 100644 index 0000000..f1e1726 --- /dev/null +++ b/Domain/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.8 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Domain", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Domain", + targets: ["Domain"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Domain", + dependencies: []), + .testTarget( + name: "DomainTests", + dependencies: ["Domain"]), + ] +) diff --git a/Domain/README.md b/Domain/README.md new file mode 100644 index 0000000..eb40e6c --- /dev/null +++ b/Domain/README.md @@ -0,0 +1,3 @@ +# Domain + +A description of this package. diff --git a/Domain/Sources/Domain/Entities/Card.swift b/Domain/Sources/Domain/Entities/Card.swift new file mode 100644 index 0000000..e8e6ba0 --- /dev/null +++ b/Domain/Sources/Domain/Entities/Card.swift @@ -0,0 +1,14 @@ +// +// Card.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation + +public struct Card { + + public var cardNumber: String? + public var cardType: String? +} diff --git a/Domain/Sources/Domain/Entities/CardTransferCount.swift b/Domain/Sources/Domain/Entities/CardTransferCount.swift new file mode 100644 index 0000000..3fee572 --- /dev/null +++ b/Domain/Sources/Domain/Entities/CardTransferCount.swift @@ -0,0 +1,14 @@ +// +// CardTransferCount.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation + +public struct CardTransferCount { + + public var numberOfTransfers: Int? + public var totalTransfer: Int? +} diff --git a/Domain/Sources/Domain/Entities/Person.swift b/Domain/Sources/Domain/Entities/Person.swift new file mode 100644 index 0000000..6beadef --- /dev/null +++ b/Domain/Sources/Domain/Entities/Person.swift @@ -0,0 +1,15 @@ +// +// Person.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation + +public struct Person { + + public var name: String? + public var emial: String? + public var avatar: String? +} diff --git a/Domain/Sources/Domain/Entities/PersonBankAccount.swift b/Domain/Sources/Domain/Entities/PersonBankAccount.swift new file mode 100644 index 0000000..0c8bed9 --- /dev/null +++ b/Domain/Sources/Domain/Entities/PersonBankAccount.swift @@ -0,0 +1,20 @@ +// +// PersonBankAccount.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation + +public struct PersonBankAccount: Identifiable { + + public var id: String { + card?.cardNumber ?? UUID().uuidString + } + public var person: Person? + public var card: Card? + public var cardTransferCount: CardTransferCount? + public var note: String? + public var lastDateTransfer: Date? +} diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index e02ab27..843dbf2 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ DEBE4ACB2AF6C3B200A58501 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DEBE4ACE2AF6C3B200A58501 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; DEBE4AD02AF6C3B200A58501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DEBE4AD62AF6C8F600A58501 /* Domain */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Domain; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -40,6 +41,7 @@ DEBE4AB62AF6C3B000A58501 = { isa = PBXGroup; children = ( + DEBE4AD62AF6C8F600A58501 /* Domain */, DEBE4AC12AF6C3B000A58501 /* TransferList */, DEBE4AC02AF6C3B000A58501 /* Products */, ); diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 7a6bac3..097b2a4 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 0 + 1 From 7c735fc609db28ff4f79e97b745cd2871334d4f5 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 01:03:03 +0330 Subject: [PATCH 04/16] Add Repository, UseCase, and AnyPublisher Extension - Added the `PersonBankAccountError` enum for error handling. - Added the `PersonBankAccountRepository` for communication between the Domain and Data layers. - Added the `PersonBankAccountUseCase` for interactions with the Presentation Layer. - Added an extension to `AnyPublisher` for integrating async/await with the Combine framework. --- Domain/Package.swift | 1 + Domain/Sources/Domain/Entities/Card.swift | 5 + .../Domain/Entities/CardTransferCount.swift | 5 + Domain/Sources/Domain/Entities/Person.swift | 6 + .../Domain/Entities/PersonBankAccount.swift | 19 ++ .../Domain/Error/PersonBankAccountError.swift | 32 ++ .../Domain/Extension/AnyPublisher+async.swift | 54 ++++ .../PersonBankAccountRepository.swift | 17 + .../UseCases/PersonBankAccountUseCase.swift | 19 ++ .../PersonBankAccountUseCaseImpl.swift | 110 +++++++ .../DomainTests/UseCasesTests/MockError.swift | 13 + .../UseCasesTests/MockRepository.swift | 81 +++++ .../PersonBankAccountUseCaseTests.swift | 291 ++++++++++++++++++ .../xcschemes/xcschememanagement.plist | 2 +- 14 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 Domain/Sources/Domain/Error/PersonBankAccountError.swift create mode 100644 Domain/Sources/Domain/Extension/AnyPublisher+async.swift create mode 100644 Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift create mode 100644 Domain/Sources/Domain/UseCases/PersonBankAccountUseCase.swift create mode 100644 Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift create mode 100644 Domain/Tests/DomainTests/UseCasesTests/MockError.swift create mode 100644 Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift create mode 100644 Domain/Tests/DomainTests/UseCasesTests/PersonBankAccountUseCaseTests.swift diff --git a/Domain/Package.swift b/Domain/Package.swift index f1e1726..ce3a19f 100644 --- a/Domain/Package.swift +++ b/Domain/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "Domain", + platforms: [.iOS(.v15)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/Domain/Sources/Domain/Entities/Card.swift b/Domain/Sources/Domain/Entities/Card.swift index e8e6ba0..03fa565 100644 --- a/Domain/Sources/Domain/Entities/Card.swift +++ b/Domain/Sources/Domain/Entities/Card.swift @@ -11,4 +11,9 @@ public struct Card { public var cardNumber: String? public var cardType: String? + + public init(cardNumber: String?, cardType: String?) { + self.cardNumber = cardNumber + self.cardType = cardType + } } diff --git a/Domain/Sources/Domain/Entities/CardTransferCount.swift b/Domain/Sources/Domain/Entities/CardTransferCount.swift index 3fee572..9e24070 100644 --- a/Domain/Sources/Domain/Entities/CardTransferCount.swift +++ b/Domain/Sources/Domain/Entities/CardTransferCount.swift @@ -11,4 +11,9 @@ public struct CardTransferCount { public var numberOfTransfers: Int? public var totalTransfer: Int? + + public init(numberOfTransfers: Int?, totalTransfer: Int?) { + self.numberOfTransfers = numberOfTransfers + self.totalTransfer = totalTransfer + } } diff --git a/Domain/Sources/Domain/Entities/Person.swift b/Domain/Sources/Domain/Entities/Person.swift index 6beadef..0d27034 100644 --- a/Domain/Sources/Domain/Entities/Person.swift +++ b/Domain/Sources/Domain/Entities/Person.swift @@ -12,4 +12,10 @@ public struct Person { public var name: String? public var emial: String? public var avatar: String? + + public init(name: String?, emial: String?, avatar: String?) { + self.name = name + self.emial = emial + self.avatar = avatar + } } diff --git a/Domain/Sources/Domain/Entities/PersonBankAccount.swift b/Domain/Sources/Domain/Entities/PersonBankAccount.swift index 0c8bed9..017dafc 100644 --- a/Domain/Sources/Domain/Entities/PersonBankAccount.swift +++ b/Domain/Sources/Domain/Entities/PersonBankAccount.swift @@ -17,4 +17,23 @@ public struct PersonBankAccount: Identifiable { public var cardTransferCount: CardTransferCount? public var note: String? public var lastDateTransfer: Date? + public private(set) var isFavorite: Bool = false + var indexAtList: Int = 0 + + public init(person: Person?, card: Card?, cardTransferCount: CardTransferCount?, + note: String?, lastDateTransfer: Date?) { + self.person = person + self.card = card + self.cardTransferCount = cardTransferCount + self.note = note + self.lastDateTransfer = lastDateTransfer + } + + public mutating func update(favoriteStatus isFavorite: Bool) { + self.isFavorite = isFavorite + } + + mutating func update(indexAtList index: Int) { + self.indexAtList = index + } } diff --git a/Domain/Sources/Domain/Error/PersonBankAccountError.swift b/Domain/Sources/Domain/Error/PersonBankAccountError.swift new file mode 100644 index 0000000..e38ad5f --- /dev/null +++ b/Domain/Sources/Domain/Error/PersonBankAccountError.swift @@ -0,0 +1,32 @@ +// +// PersonBankAccountError.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation + +public enum PersonBankAccountError: Error { + + case cannotSavePersonAccountToFavorites + case cannotRemovePersonAccountFromFavorites + case cannotFetchPersonAccounts + case cannotFetchFavoritePersonAccounts + + var errorDescription: String? { + switch self { + case .cannotSavePersonAccountToFavorites: + return "Failed to save person accounts to favorites" + + case .cannotRemovePersonAccountFromFavorites: + return "Failed to remove person account from favorites" + + case .cannotFetchPersonAccounts: + return "Failed to fetch person accounts" + + case .cannotFetchFavoritePersonAccounts: + return "Failed to fetch favorite person accounts" + } + } +} diff --git a/Domain/Sources/Domain/Extension/AnyPublisher+async.swift b/Domain/Sources/Domain/Extension/AnyPublisher+async.swift new file mode 100644 index 0000000..5abc22f --- /dev/null +++ b/Domain/Sources/Domain/Extension/AnyPublisher+async.swift @@ -0,0 +1,54 @@ +// +// AnyPublisher+async.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Combine + +extension AnyPublisher { + struct Subscriber { + fileprivate let send: (Output) -> Void + fileprivate let complete: (Subscribers.Completion) -> Void + + func send(_ value: Output) { + self.send(value) + } + func send(completion: Subscribers.Completion) { + self.complete(completion) + } + } + + init(_ closure: (Subscriber) -> AnyCancellable) { + let subject = PassthroughSubject() + + let subscriber = Subscriber( + send: subject.send, + complete: subject.send(completion:) + ) + let cancel = closure(subscriber) + + self = subject + .handleEvents(receiveCancel: cancel.cancel) + .eraseToAnyPublisher() + } +} + +extension AnyPublisher { + init(taskPriority: TaskPriority? = nil, asyncFunc: @escaping () async throws -> Output) { + self.init { subscriber in + let task = Task(priority: taskPriority) { + do { + subscriber.send(try await asyncFunc()) + subscriber.send(completion: .finished) + } catch { + subscriber.send(completion: .failure(error as! Failure)) + } + } + return AnyCancellable { + task.cancel() + } + } + } +} diff --git a/Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift b/Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift new file mode 100644 index 0000000..44b9f89 --- /dev/null +++ b/Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift @@ -0,0 +1,17 @@ +// +// PersonBankAccountRepository.swift.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation + +public protocol PersonBankAccountRepository { + + func fetchPersonAccounts(withOffest offset: Int) async throws -> [PersonBankAccount] + func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] + func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount + func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount + func updatefavoriteStatusForPersonAccount(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount +} diff --git a/Domain/Sources/Domain/UseCases/PersonBankAccountUseCase.swift b/Domain/Sources/Domain/UseCases/PersonBankAccountUseCase.swift new file mode 100644 index 0000000..6582a0b --- /dev/null +++ b/Domain/Sources/Domain/UseCases/PersonBankAccountUseCase.swift @@ -0,0 +1,19 @@ +// +// PersonBankAccountUseCase.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation +import Combine + +public protocol PersonBankAccountUseCase { + + func fetchPersonAccounts(withOffest offset: Int) -> AnyPublisher<[PersonBankAccount], PersonBankAccountError> + func fetchFavoritePersonAccounts() -> AnyPublisher<[PersonBankAccount], PersonBankAccountError> + func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) + -> AnyPublisher + func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) + -> AnyPublisher +} diff --git a/Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift b/Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift new file mode 100644 index 0000000..7cbf183 --- /dev/null +++ b/Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift @@ -0,0 +1,110 @@ +// +// PersonBankAccountUseCaseImpl.swift +// +// +// Created by Hessam Mahdiabadi on 11/4/23. +// + +import Foundation +import Combine + +public class PersonBankAccountUseCaseImpl: PersonBankAccountUseCase { + + private var repository: PersonBankAccountRepository + + public init(repository: PersonBankAccountRepository) { + self.repository = repository + } + + public func fetchPersonAccounts(withOffest offset: Int) + -> AnyPublisher<[PersonBankAccount], PersonBankAccountError> { + return AnyPublisher { [weak self] in + guard let self else { throw PersonBankAccountError.cannotFetchPersonAccounts } + + do { + let accounts = try await self.repository.fetchPersonAccounts(withOffest: offset) + let newAccount = await self.updateFavoriteStatus(forPersonBankAccounts: accounts) + return newAccount + } catch { + throw PersonBankAccountError.cannotFetchPersonAccounts + } + } + } + + public func fetchFavoritePersonAccounts() + -> AnyPublisher<[PersonBankAccount], PersonBankAccountError> { + return AnyPublisher { [weak self] in + guard let self else { throw PersonBankAccountError.cannotFetchFavoritePersonAccounts } + + do { + return try await self.repository.fetchFavoritePersonAccounts() + } catch { + throw PersonBankAccountError.cannotFetchFavoritePersonAccounts + } + } + } + + public func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) + -> AnyPublisher { + return AnyPublisher { [weak self] in + guard let self else { throw PersonBankAccountError.cannotSavePersonAccountToFavorites } + + do { + return try await self.repository.savePersonAccountToFavorites(personBankAccount) + } catch { + throw PersonBankAccountError.cannotSavePersonAccountToFavorites + } + } + } + + public func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) + -> AnyPublisher { + return AnyPublisher { [weak self] in + guard let self else { throw PersonBankAccountError.cannotRemovePersonAccountFromFavorites } + + do { + return try await self.repository.removePersonAccountFromFavorites(personBankAccount) + } catch { + throw PersonBankAccountError.cannotRemovePersonAccountFromFavorites + } + } + } + + private func updateFavoriteStatus(forPersonBankAccounts accounts: [PersonBankAccount]) async + -> [PersonBankAccount] { + return await withTaskGroup(of: PersonBankAccount.self, + returning: [PersonBankAccount].self) { [weak self] taskGroup in + guard let self else { return accounts } + + var updatedFavoriteStatusAccounts = accounts + + let totalSize = accounts.count + let batchSize = totalSize < 4 ? totalSize : 4 + + func createUpdateFavoriteStatusTask(atIndex index: Int) { + taskGroup.addTask { + var updatedAccount = await self.repository + .updatefavoriteStatusForPersonAccount(accounts[index]) + updatedAccount.update(indexAtList: index) + return updatedAccount + } + } + + for index in 0 ..< batchSize { + createUpdateFavoriteStatusTask(atIndex: index) + } + + var index = batchSize + + for await account in taskGroup { + updatedFavoriteStatusAccounts[account.indexAtList].update(favoriteStatus: account.isFavorite) + + if index < totalSize { + createUpdateFavoriteStatusTask(atIndex: index) + index += 1 + } + } + return updatedFavoriteStatusAccounts + } + } +} diff --git a/Domain/Tests/DomainTests/UseCasesTests/MockError.swift b/Domain/Tests/DomainTests/UseCasesTests/MockError.swift new file mode 100644 index 0000000..f49a95a --- /dev/null +++ b/Domain/Tests/DomainTests/UseCasesTests/MockError.swift @@ -0,0 +1,13 @@ +// +// MockError.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +enum MockError: Error { + + case mockCase +} diff --git a/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift b/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift new file mode 100644 index 0000000..f073348 --- /dev/null +++ b/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift @@ -0,0 +1,81 @@ +// +// MockRepository.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +@testable import Domain + +class MockPersonBankAccountRepository: PersonBankAccountRepository { + + func createMockPersonAccount() -> PersonBankAccount { + let person = Person(name: "hessam", emial: "h.mahdi", avatar: nil) + let card = Card(cardNumber: "123", cardType: "master") + let cardCount = CardTransferCount(numberOfTransfers: 12, totalTransfer: 12) + let note = "note" + return .init(person: person, card: card, cardTransferCount: cardCount, + note: note, lastDateTransfer: nil) + } + + func fetchPersonAccounts(withOffest offset: Int) async throws -> [Domain.PersonBankAccount] { + throw MockError.mockCase + } + + func fetchFavoritePersonAccounts() async throws -> [Domain.PersonBankAccount] { + throw MockError.mockCase + } + + func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + throw MockError.mockCase + } + + func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + throw MockError.mockCase + } + + func updatefavoriteStatusForPersonAccount(_ personBankAccount: Domain.PersonBankAccount) async -> Domain.PersonBankAccount { + return personBankAccount + } +} + +class MockSuccessEmptyFetchAccountRepository: MockPersonBankAccountRepository { + override func fetchPersonAccounts(withOffest offset: Int) async throws -> [PersonBankAccount] { + return [] + } +} + +class MockSuccessFetchAccountRepository: MockPersonBankAccountRepository { + + override func fetchPersonAccounts(withOffest offset: Int) async throws -> [PersonBankAccount] { + return [createMockPersonAccount()] + } + + override func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] { + var person = createMockPersonAccount() + person.update(favoriteStatus: true) + return [person] + } +} + +class MockSuccessSaveOrRemoveAccountToFavoritesRepository: MockPersonBankAccountRepository { + + var person: PersonBankAccount! + + override func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + person = personBankAccount + person.update(favoriteStatus: true) + return person + } + + override func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + person = personBankAccount + person.update(favoriteStatus: false) + return person + } + + override func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] { + return [person] + } +} diff --git a/Domain/Tests/DomainTests/UseCasesTests/PersonBankAccountUseCaseTests.swift b/Domain/Tests/DomainTests/UseCasesTests/PersonBankAccountUseCaseTests.swift new file mode 100644 index 0000000..44f79fb --- /dev/null +++ b/Domain/Tests/DomainTests/UseCasesTests/PersonBankAccountUseCaseTests.swift @@ -0,0 +1,291 @@ +// +// PersonBankAccountUseCaseTests.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import XCTest +import Combine +@testable import Domain + +final class PersonBankAccountUseCaseTests: XCTestCase { + + var cancellables: Set! + var useCase: PersonBankAccountUseCase! + + override func setUp() { + cancellables = [] + } + + override func tearDown() { + useCase = nil + cancellables.removeAll() + } + + + func testSuccessFetchPersonAccounts() { + // given + let repository = MockSuccessFetchAccountRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "fetch accounts") + + var accounts: [PersonBankAccount]? + + // when + useCase.fetchPersonAccounts(withOffest: 1) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in + }, receiveValue: { values in + accounts = values + expectation.fulfill() + }) + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertNotNil(accounts) + XCTAssertEqual(accounts?.count, 1) + } + + func testSuccessEmptyFetchPersonAccounts() { + // given + let repository = MockSuccessEmptyFetchAccountRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "fetch accounts") + + var accounts: [PersonBankAccount]? + + // when + useCase.fetchPersonAccounts(withOffest: 1) + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in + }, receiveValue: { values in + accounts = values + expectation.fulfill() + }) + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertNotNil(accounts) + XCTAssertTrue(accounts?.isEmpty ?? false) + } + + func testFailFetchPersonAccounts() { + + // given + let repository = MockPersonBankAccountRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "fetch accounts") + + var error: PersonBankAccountError? + + // when + useCase.fetchPersonAccounts(withOffest: 1) + .sink { completion in + switch completion { + case .failure(let perEror): error = perEror + case .finished: break + } + expectation.fulfill() + } receiveValue: { _ in } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertNotNil(error) + XCTAssertEqual(error, .cannotFetchPersonAccounts) + } + + func testSuccessFetchFavoritePersonAccounts() { + // given + let repository = MockSuccessFetchAccountRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "fetch accounts") + + var accounts: [PersonBankAccount]? + + // when + useCase.fetchFavoritePersonAccounts() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in + }, receiveValue: { values in + accounts = values + expectation.fulfill() + }) + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertNotNil(accounts) + XCTAssertEqual(accounts?.count, 1) + XCTAssertEqual(accounts?.first?.isFavorite, true) + } + + func testFailFetchFavoritePersonAccounts() { + + // given + let repository = MockPersonBankAccountRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "fetch accounts") + + var error: PersonBankAccountError? + + // when + useCase.fetchFavoritePersonAccounts() + .sink { completion in + switch completion { + case .failure(let perEror): error = perEror + case .finished: break + } + expectation.fulfill() + } receiveValue: { _ in } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertNotNil(error) + XCTAssertEqual(error, .cannotFetchFavoritePersonAccounts) + } + + func testSuccessSavePersonAccount() { + // given + let repository = MockSuccessSaveOrRemoveAccountToFavoritesRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "save account") + + let mockAccount = repository.createMockPersonAccount() + var accounts: [PersonBankAccount]? + + // when + useCase.savePersonAccountToFavorites(mockAccount) + .sink { _ in } receiveValue: { _ in }.store(in: &cancellables) + + + useCase.fetchFavoritePersonAccounts() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in + }, receiveValue: { values in + accounts = values + expectation.fulfill() + }) + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertEqual(accounts?.count, 1) + XCTAssertEqual(accounts?.first?.person?.name, mockAccount.person?.name) + XCTAssertEqual(accounts?.first?.isFavorite, true) + } + + func testSuccessRemovePersonAccount() { + // given + let repository = MockSuccessSaveOrRemoveAccountToFavoritesRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "remove account") + + let mockAccount = repository.createMockPersonAccount() + var accounts: [PersonBankAccount]? + + func runFetchFavorite() { + useCase.fetchFavoritePersonAccounts() + .receive(on: DispatchQueue.main) + .sink(receiveCompletion: { _ in + }, receiveValue: { values in + accounts = values + expectation.fulfill() + }) + .store(in: &cancellables) + } + + func runRemove() { + useCase.removePersonAccountFromFavorites(mockAccount) + .sink { _ in + runFetchFavorite() + } receiveValue: { _ in }.store(in: &cancellables) + } + + // when + useCase.savePersonAccountToFavorites(mockAccount) + .sink { _ in + runRemove() + } receiveValue: { _ in }.store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertEqual(accounts?.count, 1) + XCTAssertEqual(accounts?.first?.person?.name, mockAccount.person?.name) + XCTAssertEqual(accounts?.first?.isFavorite, false) + } + + func testFailSavePersonAccount() { + // given + let repository = MockSuccessFetchAccountRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "save account") + + let mockAccount = repository.createMockPersonAccount() + var error: PersonBankAccountError? + + // when + useCase.savePersonAccountToFavorites(mockAccount) + .sink { completion in + switch completion { + case .failure(let perEror): error = perEror + case .finished: break + } + expectation.fulfill() + } receiveValue: { _ in } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertNotNil(error) + XCTAssertEqual(error, .cannotSavePersonAccountToFavorites) + } + + func testFailRemovePersonAccount() { + // given + let repository = MockSuccessFetchAccountRepository() + useCase = PersonBankAccountUseCaseImpl(repository: repository) + + let expectation = XCTestExpectation(description: "remove account") + + let mockAccount = repository.createMockPersonAccount() + var error: PersonBankAccountError? + + // when + useCase.removePersonAccountFromFavorites(mockAccount) + .sink { completion in + switch completion { + case .failure(let perEror): error = perEror + case .finished: break + } + expectation.fulfill() + } receiveValue: { _ in } + .store(in: &cancellables) + + wait(for: [expectation], timeout: 2.0) + + // then + XCTAssertNotNil(error) + XCTAssertEqual(error, .cannotRemovePersonAccountFromFavorites) + } +} diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 097b2a4..7a6bac3 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 1 + 0 From f3ecb9757d6aec7072be48f9e6e40586357bec9e Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 01:32:58 +0330 Subject: [PATCH 05/16] Refine AnyPublisher Extension to Constrain Error Type, Add GitHub Workflow for Domain Layer, Including Linting and Testing - Enhanced the AnyPublisher extension to include a constraint on the error type, ensuring it aligns with the PersonBankAccountError type and avoiding forced casting within the extension. - Added a GitHub workflow for the Domain package to run linting and tests on every pull request. - Added support for macOS 10.15 and iOS 14 within this package. --- .github/workflows/Domain.yml | 27 +++++++++++++++++++ Domain/Package.swift | 2 +- .../Domain/Error/PersonBankAccountError.swift | 3 +++ .../Domain/Extension/AnyPublisher+async.swift | 7 ++--- .../xcschemes/xcschememanagement.plist | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/Domain.yml diff --git a/.github/workflows/Domain.yml b/.github/workflows/Domain.yml new file mode 100644 index 0000000..a0ffcab --- /dev/null +++ b/.github/workflows/Domain.yml @@ -0,0 +1,27 @@ +name: Domain Layer + +on: + pull_request: + branches: + - '*' + - '*/*' + +jobs: + build: + runs-on: macos-13 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up SwiftLint + run: brew install swiftlint + + - name: Lint code + run: swiftlint + + - name: run unit test + run: | + cd Domain/Sources/ + swift build + swift test \ No newline at end of file diff --git a/Domain/Package.swift b/Domain/Package.swift index ce3a19f..5191856 100644 --- a/Domain/Package.swift +++ b/Domain/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "Domain", - platforms: [.iOS(.v15)], + platforms: [.iOS(.v14), .macOS(.v10_15)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/Domain/Sources/Domain/Error/PersonBankAccountError.swift b/Domain/Sources/Domain/Error/PersonBankAccountError.swift index e38ad5f..a69adb7 100644 --- a/Domain/Sources/Domain/Error/PersonBankAccountError.swift +++ b/Domain/Sources/Domain/Error/PersonBankAccountError.swift @@ -13,6 +13,7 @@ public enum PersonBankAccountError: Error { case cannotRemovePersonAccountFromFavorites case cannotFetchPersonAccounts case cannotFetchFavoritePersonAccounts + case unexpectedError var errorDescription: String? { switch self { @@ -27,6 +28,8 @@ public enum PersonBankAccountError: Error { case .cannotFetchFavoritePersonAccounts: return "Failed to fetch favorite person accounts" + + case .unexpectedError: return "Unexpected error" } } } diff --git a/Domain/Sources/Domain/Extension/AnyPublisher+async.swift b/Domain/Sources/Domain/Extension/AnyPublisher+async.swift index 5abc22f..a1fc26e 100644 --- a/Domain/Sources/Domain/Extension/AnyPublisher+async.swift +++ b/Domain/Sources/Domain/Extension/AnyPublisher+async.swift @@ -7,7 +7,7 @@ import Combine -extension AnyPublisher { +extension AnyPublisher where Failure == PersonBankAccountError { struct Subscriber { fileprivate let send: (Output) -> Void fileprivate let complete: (Subscribers.Completion) -> Void @@ -35,7 +35,7 @@ extension AnyPublisher { } } -extension AnyPublisher { +extension AnyPublisher where Failure == PersonBankAccountError { init(taskPriority: TaskPriority? = nil, asyncFunc: @escaping () async throws -> Output) { self.init { subscriber in let task = Task(priority: taskPriority) { @@ -43,7 +43,8 @@ extension AnyPublisher { subscriber.send(try await asyncFunc()) subscriber.send(completion: .finished) } catch { - subscriber.send(completion: .failure(error as! Failure)) + let personError = error as? PersonBankAccountError ?? .unexpectedError + subscriber.send(completion: .failure(personError)) } } return AnyCancellable { diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 7a6bac3..097b2a4 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 0 + 1 From adacf5fb0b39424c911edfe8c4bf382488d33b91 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 02:54:30 +0330 Subject: [PATCH 06/16] Add API Components: Protocol, Router, DTO Model, and Alamofire Library - Added an API protocol to facilitate network access used by repositories. - Added the API Router for creating endpoints. - Added DTO models for converting JSON data from the server. - Added the Alamofire third-party library for simplified API calls. --- Data/.gitignore | 9 ++ Data/Package.swift | 31 +++++ Data/README.md | 3 + Data/Sources/Data/Network/Api.swift | 13 ++ Data/Sources/Data/Network/ApiImpl.swift | 66 +++++++++ Data/Sources/Data/Network/ApiRouter.swift | 64 +++++++++ Data/Sources/Data/Network/CardDTO.swift | 14 ++ .../Data/Network/CardTransferCountDTO.swift | 14 ++ Data/Sources/Data/Network/NetworkError.swift | 21 +++ .../Data/Network/PersonBankAccountDTO.swift | 17 +++ Data/Sources/Data/Network/PersonDTO.swift | 15 ++ .../NetworkTests/ApiRouterTests.swift | 46 +++++++ .../DataTests/NetworkTests/ApiTests.swift | 129 ++++++++++++++++++ .../NetworkTests/Mock/ApiMockResponse.swift | 24 ++++ .../Mock/BadResponseMockURLProtocol.swift | 72 ++++++++++ .../Mock/OfflineServerMockURLProtocol.swift | 54 ++++++++ .../Mock/ResponseMockURLProtocol.swift | 103 ++++++++++++++ Domain/Sources/Domain/Entities/Person.swift | 6 +- TransferList.xcodeproj/project.pbxproj | 2 + .../xcshareddata/swiftpm/Package.resolved | 14 ++ .../xcschemes/xcschememanagement.plist | 2 +- 21 files changed, 715 insertions(+), 4 deletions(-) create mode 100644 Data/.gitignore create mode 100644 Data/Package.swift create mode 100644 Data/README.md create mode 100644 Data/Sources/Data/Network/Api.swift create mode 100644 Data/Sources/Data/Network/ApiImpl.swift create mode 100644 Data/Sources/Data/Network/ApiRouter.swift create mode 100644 Data/Sources/Data/Network/CardDTO.swift create mode 100644 Data/Sources/Data/Network/CardTransferCountDTO.swift create mode 100644 Data/Sources/Data/Network/NetworkError.swift create mode 100644 Data/Sources/Data/Network/PersonBankAccountDTO.swift create mode 100644 Data/Sources/Data/Network/PersonDTO.swift create mode 100644 Data/Tests/DataTests/NetworkTests/ApiRouterTests.swift create mode 100644 Data/Tests/DataTests/NetworkTests/ApiTests.swift create mode 100644 Data/Tests/DataTests/NetworkTests/Mock/ApiMockResponse.swift create mode 100644 Data/Tests/DataTests/NetworkTests/Mock/BadResponseMockURLProtocol.swift create mode 100644 Data/Tests/DataTests/NetworkTests/Mock/OfflineServerMockURLProtocol.swift create mode 100644 Data/Tests/DataTests/NetworkTests/Mock/ResponseMockURLProtocol.swift create mode 100644 TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Data/.gitignore b/Data/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Data/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Data/Package.swift b/Data/Package.swift new file mode 100644 index 0000000..bcdb3c0 --- /dev/null +++ b/Data/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.8 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Data", + platforms: [.iOS(.v14), .macOS(.v10_15)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Data", + targets: ["Data"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + .package(path: "../Domain"), + .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.8.1")) + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Data", + dependencies: ["Alamofire"]), + .testTarget( + name: "DataTests", + dependencies: ["Data"]), + ] +) diff --git a/Data/README.md b/Data/README.md new file mode 100644 index 0000000..9f2dd4d --- /dev/null +++ b/Data/README.md @@ -0,0 +1,3 @@ +# Data + +A description of this package. diff --git a/Data/Sources/Data/Network/Api.swift b/Data/Sources/Data/Network/Api.swift new file mode 100644 index 0000000..b3cd02b --- /dev/null +++ b/Data/Sources/Data/Network/Api.swift @@ -0,0 +1,13 @@ +// +// Api.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +protocol Api { + + func callApi(route: ApiRouter, decodeType type: T.Type) async throws -> T +} diff --git a/Data/Sources/Data/Network/ApiImpl.swift b/Data/Sources/Data/Network/ApiImpl.swift new file mode 100644 index 0000000..144d971 --- /dev/null +++ b/Data/Sources/Data/Network/ApiImpl.swift @@ -0,0 +1,66 @@ +// +// ApiImpl.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Alamofire + +final public class ApiImpl: Api { + + private var sessionManager: Session + private var decoder: JSONDecoder! + +#if DEBUG + public init(configuration: URLSessionConfiguration) { + sessionManager = Session(configuration: configuration) + setupDecoder() + } +#endif + + public init() { + sessionManager = Session() + setupDecoder() + } + + private func setupDecoder() { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + + decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(dateFormatter) + } + + public func callApi(route: ApiRouter, decodeType type: T.Type) async throws -> T { + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self else { + continuation.resume(throwing: NetworkError.cannotConnectToServer) + return + } + + sessionManager.request(route) + .validate(statusCode: 200 ..< 300) + .responseData { [weak self] responseData in + guard let self else { + continuation.resume(throwing: NetworkError.cannotParseJson) + return + } + + switch responseData.result { + case .success(let data): + do { + let retVal = try decoder.decode(type, from: data) + continuation.resume(returning: retVal) + } catch { + continuation.resume(throwing: NetworkError.cannotParseJson) + } + + case .failure: + continuation.resume(throwing: NetworkError.cannotConnectToServer) + } + } + } + } +} diff --git a/Data/Sources/Data/Network/ApiRouter.swift b/Data/Sources/Data/Network/ApiRouter.swift new file mode 100644 index 0000000..2dc0aa8 --- /dev/null +++ b/Data/Sources/Data/Network/ApiRouter.swift @@ -0,0 +1,64 @@ +// +// ApiRouter.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Alamofire + +public enum ApiRouter: URLRequestConvertible { + + public typealias Params = [String: Any] + + case transferList(offset: Int) + + public func asURLRequest() throws -> URLRequest { + + let httpMethod = getHttpMethod() + let url = createURL() + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = httpMethod.rawValue + urlRequest.timeoutInterval = 20.0 + urlRequest.cachePolicy = .reloadIgnoringLocalCacheData + + let encoding: ParameterEncoding = { + switch httpMethod { + default: + return URLEncoding.queryString + } + }() + + return try encoding.encode(urlRequest, with: self.getParams()) + } +} + +public extension ApiRouter { + + func getHttpMethod() -> HTTPMethod { + switch self { + case .transferList: + return .get + } + } + + func getParams() -> Params? { + return nil + } + + var urlPath: String { + switch self { + case .transferList(let offset): + return "/transfer-list/\(offset)" + } + } + + func createURL() -> URL { + var component = URLComponents() + component.scheme = "https" + component.host = "191da1ac-768c-4c6a-80ad-b533beafec25.mock.pstmn.io" + component.path = urlPath + return component.url! + } +} diff --git a/Data/Sources/Data/Network/CardDTO.swift b/Data/Sources/Data/Network/CardDTO.swift new file mode 100644 index 0000000..9805228 --- /dev/null +++ b/Data/Sources/Data/Network/CardDTO.swift @@ -0,0 +1,14 @@ +// +// CardDTO.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +struct CardDTO: Decodable { + + var card_number: String? + var card_type: String? +} diff --git a/Data/Sources/Data/Network/CardTransferCountDTO.swift b/Data/Sources/Data/Network/CardTransferCountDTO.swift new file mode 100644 index 0000000..d7b26f0 --- /dev/null +++ b/Data/Sources/Data/Network/CardTransferCountDTO.swift @@ -0,0 +1,14 @@ +// +// CardTransferCountDTO.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +struct CardTransferCountDTO: Decodable { + + var number_of_transfers: Int? + var total_transfer: Int? +} diff --git a/Data/Sources/Data/Network/NetworkError.swift b/Data/Sources/Data/Network/NetworkError.swift new file mode 100644 index 0000000..a0b4fde --- /dev/null +++ b/Data/Sources/Data/Network/NetworkError.swift @@ -0,0 +1,21 @@ +// +// File.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +enum NetworkError: Error, LocalizedError { + + case cannotConnectToServer + case cannotParseJson + + var errorDescription: String? { + switch self { + case .cannotConnectToServer: return "You seem to be offline!" + case .cannotParseJson: return "Unexpected error" + } + } +} diff --git a/Data/Sources/Data/Network/PersonBankAccountDTO.swift b/Data/Sources/Data/Network/PersonBankAccountDTO.swift new file mode 100644 index 0000000..3b2a675 --- /dev/null +++ b/Data/Sources/Data/Network/PersonBankAccountDTO.swift @@ -0,0 +1,17 @@ +// +// PersonBankAccountDTO.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +public struct PersonBankAccountDTO: Decodable { + + var person: PersonDTO? + var card: CardDTO? + var more_info: CardTransferCountDTO? + var note: String? + var last_transfer: Date? +} diff --git a/Data/Sources/Data/Network/PersonDTO.swift b/Data/Sources/Data/Network/PersonDTO.swift new file mode 100644 index 0000000..b4fdb30 --- /dev/null +++ b/Data/Sources/Data/Network/PersonDTO.swift @@ -0,0 +1,15 @@ +// +// PersonDTO.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +struct PersonDTO: Decodable { + + var full_name: String? + var email: String? + var avatar: String? +} diff --git a/Data/Tests/DataTests/NetworkTests/ApiRouterTests.swift b/Data/Tests/DataTests/NetworkTests/ApiRouterTests.swift new file mode 100644 index 0000000..66db7f2 --- /dev/null +++ b/Data/Tests/DataTests/NetworkTests/ApiRouterTests.swift @@ -0,0 +1,46 @@ +// +// ApiRouterTests.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import XCTest +@testable import Data + +final class ApiRouterTests: XCTestCase { + + func testGetHttpMethod() { + // given + let route = ApiRouter.transferList(offset: 1) + + // when + let httpMethod = route.getHttpMethod() + + // then + XCTAssertEqual(httpMethod, .get) + XCTAssertNotEqual(httpMethod, .post) + } + + func testPath() { + // given + let route = ApiRouter.transferList(offset: 1) + + // when + let path = route.urlPath + + // then + XCTAssertEqual(path, "/transfer-list/1") + } + + func testCreateURL() { + // given + let route = ApiRouter.transferList(offset: 1) + + // when + let url = route.createURL() + + // then + XCTAssertEqual(url.absoluteString, "https://191da1ac-768c-4c6a-80ad-b533beafec25.mock.pstmn.io/transfer-list/1") + } +} diff --git a/Data/Tests/DataTests/NetworkTests/ApiTests.swift b/Data/Tests/DataTests/NetworkTests/ApiTests.swift new file mode 100644 index 0000000..0553216 --- /dev/null +++ b/Data/Tests/DataTests/NetworkTests/ApiTests.swift @@ -0,0 +1,129 @@ +// +// File.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import XCTest +@testable import Data + +final class ApiTests: XCTestCase { + + private var api: Api! + + override func tearDown() { + api = nil + } + + func testRealMediaListApiCall() async { + + // given + api = ApiImpl() + do { + + // when + let accounts = try await api.callApi(route: .transferList(offset: 1), + decodeType: [PersonBankAccountDTO].self) + + // then + XCTAssertEqual(accounts.count, 10) + XCTAssertEqual(accounts.first?.person?.full_name, "Jemimah Sprott") + XCTAssertEqual(accounts.first?.person?.email, nil) + XCTAssertEqual(accounts.first?.card?.card_number, "5602217292772382") + + } catch { + // then + XCTAssertNil(error as? NetworkError) + } + } + + func testSuccessMockResponse() async { + + // given + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [ResponseMockURLProtocol.self] + let api = ApiImpl(configuration: configuration) + + do { + // when + let accounts = try await api.callApi(route: .transferList(offset: 10), + decodeType: [PersonBankAccountDTO].self) + + // then + XCTAssertEqual(accounts.count, 2) + XCTAssertEqual(accounts.first?.person?.full_name, "Jemimah Sprott") + XCTAssertEqual(accounts.first?.person?.email, nil) + XCTAssertEqual(accounts.first?.card?.card_number, "5602217292772382") + + } catch { + + // then + XCTAssertNil(error) + } + } + + func testSuccessEmptyMockResponse() async { + + // given + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [ResponseMockURLProtocol.self] + let api = ApiImpl(configuration: configuration) + + do { + // when + let accounts = try await api.callApi(route: .transferList(offset: 11), + decodeType: [PersonBankAccountDTO].self) + + // then + XCTAssertTrue(accounts.isEmpty) + + } catch { + + // then + XCTAssertNil(error) + } + } + + func testOfflineApiCall() async { + // given + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [OfflineServerMockURLProtocol.self] + let api = ApiImpl(configuration: configuration) + + do { + // when + let _ = try await api.callApi(route: .transferList(offset: 10), + decodeType: [PersonBankAccountDTO].self) + + } catch { + + // then + let networkError = error as? NetworkError + XCTAssertNotNil(networkError) + XCTAssertEqual(networkError, .cannotConnectToServer) + } + } + + func testBadResponseApiCall() async { + + // given + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [BadResponseMockURLProtocol.self] + let api = ApiImpl(configuration: configuration) + + do { + + // when + let _ = try await api.callApi(route: .transferList(offset: 10), + decodeType: [PersonBankAccountDTO].self) + + } catch { + + // then + let networkError = error as? NetworkError + XCTAssertNotNil(networkError) + XCTAssertEqual(networkError, .cannotParseJson) + } + } +} diff --git a/Data/Tests/DataTests/NetworkTests/Mock/ApiMockResponse.swift b/Data/Tests/DataTests/NetworkTests/Mock/ApiMockResponse.swift new file mode 100644 index 0000000..66c6872 --- /dev/null +++ b/Data/Tests/DataTests/NetworkTests/Mock/ApiMockResponse.swift @@ -0,0 +1,24 @@ +// +// ApiMockResponse.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +struct ApiMockResponse: Hashable, Equatable { + + var url: URL + var data: Data? + var httpResponse: HTTPURLResponse? + var error: Error? + + func hash(into hasher: inout Hasher) { + hasher.combine(url) + } + + static func == (lhs: ApiMockResponse, rhs: ApiMockResponse) -> Bool { + lhs.hashValue == rhs.hashValue + } +} diff --git a/Data/Tests/DataTests/NetworkTests/Mock/BadResponseMockURLProtocol.swift b/Data/Tests/DataTests/NetworkTests/Mock/BadResponseMockURLProtocol.swift new file mode 100644 index 0000000..953d91f --- /dev/null +++ b/Data/Tests/DataTests/NetworkTests/Mock/BadResponseMockURLProtocol.swift @@ -0,0 +1,72 @@ +// +// BadResponseMockURLProtocol.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +class BadResponseMockURLProtocol: URLProtocol { + + private static var mockResponses = createMockResponse() + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + if let url = request.url { + if let apiResponse = BadResponseMockURLProtocol.mockResponses[url] { + if let response = apiResponse.httpResponse { + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + } + if let data = apiResponse.data { + client?.urlProtocol(self, didLoad: data) + } + if let error = apiResponse.error { + client?.urlProtocol(self, didFailWithError: error) + } + } + client?.urlProtocolDidFinishLoading(self) + } + } + + override func stopLoading() {} + + private static func createMockResponse() -> [URL: ApiMockResponse] { + + let strUrl = "https://191da1ac-768c-4c6a-80ad-b533beafec25.mock.pstmn.io/transfer-list/10" + let url = URL(string: strUrl)! + let data = #""" +{ + "person": { + "full_name": "Jemimah Sprott", + "email": null, + "avatar": "https://www.dropbox.com/s/64y9lcnca22p1jx/avatar1.png?dl=1" + }, + "card": { + "card_number": "5602217292772382", + "card_type": "bankcard" + }, + "last_transfer": "2022-08-31T15:24:16Z", + "note": null, + "more_info": { + "number_of_transfers": 74, + "total_transfer": 83111687 + } + } +"""#.data(using: .utf8) + let httpResponse = HTTPURLResponse(url: url, statusCode: 200, + httpVersion: nil, headerFields: nil) + let response = ApiMockResponse(url: url, data: data, + httpResponse: httpResponse, + error: nil) + + return [url: response] + } +} diff --git a/Data/Tests/DataTests/NetworkTests/Mock/OfflineServerMockURLProtocol.swift b/Data/Tests/DataTests/NetworkTests/Mock/OfflineServerMockURLProtocol.swift new file mode 100644 index 0000000..3c52cdb --- /dev/null +++ b/Data/Tests/DataTests/NetworkTests/Mock/OfflineServerMockURLProtocol.swift @@ -0,0 +1,54 @@ +// +// OfflineServerMockURLProtocol.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +class OfflineServerMockURLProtocol: URLProtocol { + + private static var mockResponses = createMockResponse() + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + if let url = request.url { + if let apiResponse = OfflineServerMockURLProtocol.mockResponses[url] { + if let response = apiResponse.httpResponse { + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + } + if let data = apiResponse.data { + client?.urlProtocol(self, didLoad: data) + } + if let error = apiResponse.error { + client?.urlProtocol(self, didFailWithError: error) + } + } + client?.urlProtocolDidFinishLoading(self) + } + } + + override func stopLoading() {} + + private static func createMockResponse() -> [URL: ApiMockResponse] { + + let strUrl = "https://191da1ac-768c-4c6a-80ad-b533beafec25.mock.pstmn.io/transfer-list/10" + let url = URL(string: strUrl)! + let data = "offline server".data(using: .utf8) + let httpResponse = HTTPURLResponse(url: url, statusCode: -1, + httpVersion: nil, headerFields: nil) + let response = ApiMockResponse(url: url, data: data, + httpResponse: httpResponse, + error: URLError(URLError.Code.cannotConnectToHost)) + + return [url: response] + } +} diff --git a/Data/Tests/DataTests/NetworkTests/Mock/ResponseMockURLProtocol.swift b/Data/Tests/DataTests/NetworkTests/Mock/ResponseMockURLProtocol.swift new file mode 100644 index 0000000..fedb270 --- /dev/null +++ b/Data/Tests/DataTests/NetworkTests/Mock/ResponseMockURLProtocol.swift @@ -0,0 +1,103 @@ +// +// ResponseMockURLProtocol.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +class ResponseMockURLProtocol: URLProtocol { + + private static var mockResponses = createMockResponse() + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + if let url = request.url { + if let apiResponse = ResponseMockURLProtocol.mockResponses[url] { + if let response = apiResponse.httpResponse { + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + } + if let data = apiResponse.data { + client?.urlProtocol(self, didLoad: data) + } + if let error = apiResponse.error { + client?.urlProtocol(self, didFailWithError: error) + } + } + client?.urlProtocolDidFinishLoading(self) + } + } + + override func stopLoading() {} + + private static func createMockResponse() -> [URL: ApiMockResponse] { + + let strUrl = "https://191da1ac-768c-4c6a-80ad-b533beafec25.mock.pstmn.io/transfer-list/10" + let url = URL(string: strUrl)! + let data = #""" +[ + { + "person": { + "full_name": "Jemimah Sprott", + "email": null, + "avatar": "https://www.dropbox.com/s/64y9lcnca22p1jx/avatar1.png?dl=1" + }, + "card": { + "card_number": "5602217292772382", + "card_type": "bankcard" + }, + "last_transfer": "2022-08-31T15:24:16Z", + "note": null, + "more_info": { + "number_of_transfers": 74, + "total_transfer": 83111687 + } + }, + { + "person": { + "full_name": "Bondy Lathleiff", + "email": "blathleiff1@mozilla.com", + "avatar": "https://www.dropbox.com/s/64y9lcnca22p1jx/avatar1.png?dl=1" + }, + "card": { + "card_number": "5602250166453938", + "card_type": "bankcard" + }, + "last_transfer": "2023-02-06T10:51:10Z", + "note": null, + "more_info": { + "number_of_transfers": 81, + "total_transfer": 37807212 + } + } +] +"""#.data(using: .utf8) + let httpResponse = HTTPURLResponse(url: url, statusCode: 200, + httpVersion: nil, headerFields: nil) + let response = ApiMockResponse(url: url, data: data, + httpResponse: httpResponse, + error: nil) + + let strUrl2 = "https://191da1ac-768c-4c6a-80ad-b533beafec25.mock.pstmn.io/transfer-list/11" + let url2 = URL(string: strUrl2)! + let data2 = #""" +[ +] +"""#.data(using: .utf8) + let httpResponse2 = HTTPURLResponse(url: url, statusCode: 200, + httpVersion: nil, headerFields: nil) + let response2 = ApiMockResponse(url: url2, data: data2, + httpResponse: httpResponse2, + error: nil) + + return [url: response, url2: response2] + } +} diff --git a/Domain/Sources/Domain/Entities/Person.swift b/Domain/Sources/Domain/Entities/Person.swift index 0d27034..1906b2f 100644 --- a/Domain/Sources/Domain/Entities/Person.swift +++ b/Domain/Sources/Domain/Entities/Person.swift @@ -10,12 +10,12 @@ import Foundation public struct Person { public var name: String? - public var emial: String? + public var email: String? public var avatar: String? - public init(name: String?, emial: String?, avatar: String?) { + public init(name: String?, email: String?, avatar: String?) { self.name = name - self.emial = emial + self.email = email self.avatar = avatar } } diff --git a/TransferList.xcodeproj/project.pbxproj b/TransferList.xcodeproj/project.pbxproj index 843dbf2..e8ba71b 100644 --- a/TransferList.xcodeproj/project.pbxproj +++ b/TransferList.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + DE3EF77C2AF6F9B40071E5E4 /* Data */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Data; sourceTree = ""; }; DEBE4ABF2AF6C3B000A58501 /* TransferList.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TransferList.app; sourceTree = BUILT_PRODUCTS_DIR; }; DEBE4AC22AF6C3B000A58501 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DEBE4AC42AF6C3B000A58501 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -41,6 +42,7 @@ DEBE4AB62AF6C3B000A58501 = { isa = PBXGroup; children = ( + DE3EF77C2AF6F9B40071E5E4 /* Data */, DEBE4AD62AF6C8F600A58501 /* Domain */, DEBE4AC12AF6C3B000A58501 /* TransferList */, DEBE4AC02AF6C3B000A58501 /* Products */, diff --git a/TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..bc7ce25 --- /dev/null +++ b/TransferList.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", + "version" : "5.8.1" + } + } + ], + "version" : 2 +} diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 097b2a4..5eb9d26 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 1 + 3 From 2366efb635c37b5839e048e84d40db4ce4a19fe5 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 03:45:45 +0330 Subject: [PATCH 07/16] Add Mapper, Rename Folder, and Implement Repository - Added a Mapper component for converting DTOs to entities and vice versa. - Renamed the Network folder to Http for improved clarity. - Created the `PersonBankAccountRepositoryImpl` and implemented account fetching functionality. --- Data/Package.swift | 2 +- Data/Sources/Data/Common/Mapper.swift | 29 ++++++++++ Data/Sources/Data/{Network => Http}/Api.swift | 2 +- .../Data/{Network => Http}/ApiImpl.swift | 0 .../Data/{Network => Http}/ApiRouter.swift | 0 .../DataResponse}/CardDTO.swift | 0 .../DataResponse}/CardTransferCountDTO.swift | 0 .../DataResponse}/PersonBankAccountDTO.swift | 2 +- .../DataResponse}/PersonDTO.swift | 0 .../Sources/Data/Http/Mapper/CardMapper.swift | 25 +++++++++ .../Http/Mapper/CardTransferCountMapper.swift | 27 +++++++++ .../Http/Mapper/PersonBankAccountMapper.swift | 42 ++++++++++++++ .../Data/Http/Mapper/PersonMapper.swift | 27 +++++++++ .../Data/{Network => Http}/NetworkError.swift | 0 .../PersonBankAccountRepositoryImpl.swift | 44 +++++++++++++++ .../ApiRouterTests.swift | 0 .../ApiTests.swift | 0 .../Mock/ApiMockResponse.swift | 0 .../Mock/BadResponseMockURLProtocol.swift | 0 .../Mock/OfflineServerMockURLProtocol.swift | 0 .../Mock/ResponseMockURLProtocol.swift | 0 .../RepositoriesTests/Mock/MockApi.swift | 35 ++++++++++++ .../PersonBankAccountRepositoryTests.swift | 56 +++++++++++++++++++ .../UseCasesTests/MockRepository.swift | 2 +- 24 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 Data/Sources/Data/Common/Mapper.swift rename Data/Sources/Data/{Network => Http}/Api.swift (89%) rename Data/Sources/Data/{Network => Http}/ApiImpl.swift (100%) rename Data/Sources/Data/{Network => Http}/ApiRouter.swift (100%) rename Data/Sources/Data/{Network => Http/DataResponse}/CardDTO.swift (100%) rename Data/Sources/Data/{Network => Http/DataResponse}/CardTransferCountDTO.swift (100%) rename Data/Sources/Data/{Network => Http/DataResponse}/PersonBankAccountDTO.swift (84%) rename Data/Sources/Data/{Network => Http/DataResponse}/PersonDTO.swift (100%) create mode 100644 Data/Sources/Data/Http/Mapper/CardMapper.swift create mode 100644 Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift create mode 100644 Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift create mode 100644 Data/Sources/Data/Http/Mapper/PersonMapper.swift rename Data/Sources/Data/{Network => Http}/NetworkError.swift (100%) create mode 100644 Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift rename Data/Tests/DataTests/{NetworkTests => HttpTests}/ApiRouterTests.swift (100%) rename Data/Tests/DataTests/{NetworkTests => HttpTests}/ApiTests.swift (100%) rename Data/Tests/DataTests/{NetworkTests => HttpTests}/Mock/ApiMockResponse.swift (100%) rename Data/Tests/DataTests/{NetworkTests => HttpTests}/Mock/BadResponseMockURLProtocol.swift (100%) rename Data/Tests/DataTests/{NetworkTests => HttpTests}/Mock/OfflineServerMockURLProtocol.swift (100%) rename Data/Tests/DataTests/{NetworkTests => HttpTests}/Mock/ResponseMockURLProtocol.swift (100%) create mode 100644 Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift create mode 100644 Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift diff --git a/Data/Package.swift b/Data/Package.swift index bcdb3c0..02a4cae 100644 --- a/Data/Package.swift +++ b/Data/Package.swift @@ -23,7 +23,7 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "Data", - dependencies: ["Alamofire"]), + dependencies: ["Alamofire", "Domain"]), .testTarget( name: "DataTests", dependencies: ["Data"]), diff --git a/Data/Sources/Data/Common/Mapper.swift b/Data/Sources/Data/Common/Mapper.swift new file mode 100644 index 0000000..6ebf7f0 --- /dev/null +++ b/Data/Sources/Data/Common/Mapper.swift @@ -0,0 +1,29 @@ +// +// Mapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +public protocol Mapper { + + associatedtype Entity + associatedtype Dto + + func mapEntityToDto(input: Entity) -> Dto + func mapDtoToEntity(input: Dto) -> Entity +} + +public extension Mapper { + + func mapEntitiesToDtos(input: [Entity]) -> [Dto] { + return input.map { mapEntityToDto(input: $0) } + } + + func mapDtosToEntities(input: [Dto]) -> [Entity] { + return input.map { mapDtoToEntity(input: $0) } + } +} + diff --git a/Data/Sources/Data/Network/Api.swift b/Data/Sources/Data/Http/Api.swift similarity index 89% rename from Data/Sources/Data/Network/Api.swift rename to Data/Sources/Data/Http/Api.swift index b3cd02b..270aca8 100644 --- a/Data/Sources/Data/Network/Api.swift +++ b/Data/Sources/Data/Http/Api.swift @@ -7,7 +7,7 @@ import Foundation -protocol Api { +public protocol Api { func callApi(route: ApiRouter, decodeType type: T.Type) async throws -> T } diff --git a/Data/Sources/Data/Network/ApiImpl.swift b/Data/Sources/Data/Http/ApiImpl.swift similarity index 100% rename from Data/Sources/Data/Network/ApiImpl.swift rename to Data/Sources/Data/Http/ApiImpl.swift diff --git a/Data/Sources/Data/Network/ApiRouter.swift b/Data/Sources/Data/Http/ApiRouter.swift similarity index 100% rename from Data/Sources/Data/Network/ApiRouter.swift rename to Data/Sources/Data/Http/ApiRouter.swift diff --git a/Data/Sources/Data/Network/CardDTO.swift b/Data/Sources/Data/Http/DataResponse/CardDTO.swift similarity index 100% rename from Data/Sources/Data/Network/CardDTO.swift rename to Data/Sources/Data/Http/DataResponse/CardDTO.swift diff --git a/Data/Sources/Data/Network/CardTransferCountDTO.swift b/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift similarity index 100% rename from Data/Sources/Data/Network/CardTransferCountDTO.swift rename to Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift diff --git a/Data/Sources/Data/Network/PersonBankAccountDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift similarity index 84% rename from Data/Sources/Data/Network/PersonBankAccountDTO.swift rename to Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift index 3b2a675..d2f4f90 100644 --- a/Data/Sources/Data/Network/PersonBankAccountDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift @@ -7,7 +7,7 @@ import Foundation -public struct PersonBankAccountDTO: Decodable { +struct PersonBankAccountDTO: Decodable { var person: PersonDTO? var card: CardDTO? diff --git a/Data/Sources/Data/Network/PersonDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonDTO.swift similarity index 100% rename from Data/Sources/Data/Network/PersonDTO.swift rename to Data/Sources/Data/Http/DataResponse/PersonDTO.swift diff --git a/Data/Sources/Data/Http/Mapper/CardMapper.swift b/Data/Sources/Data/Http/Mapper/CardMapper.swift new file mode 100644 index 0000000..371895c --- /dev/null +++ b/Data/Sources/Data/Http/Mapper/CardMapper.swift @@ -0,0 +1,25 @@ +// +// CardMapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain + +struct CardMapper: Mapper { + + typealias Entity = Card? + typealias Dto = CardDTO? + + func mapDtoToEntity(input: CardDTO?) -> Card? { + guard let input else { return nil } + return .init(cardNumber: input.card_number, cardType: input.card_type) + } + + func mapEntityToDto(input: Card?) -> CardDTO? { + guard let input else { return nil } + return .init(card_number: input.cardNumber, card_type: input.cardType) + } +} diff --git a/Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift b/Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift new file mode 100644 index 0000000..3cf421d --- /dev/null +++ b/Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift @@ -0,0 +1,27 @@ +// +// CardTransferCountMapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain + +struct CardTransferCountMapper: Mapper { + + typealias Entity = CardTransferCount? + typealias Dto = CardTransferCountDTO? + + func mapDtoToEntity(input: CardTransferCountDTO?) -> CardTransferCount? { + guard let input else { return nil } + return .init(numberOfTransfers: input.number_of_transfers, + totalTransfer: input.total_transfer) + } + + func mapEntityToDto(input: CardTransferCount?) -> CardTransferCountDTO? { + guard let input else { return nil } + return .init(number_of_transfers: input.numberOfTransfers, + total_transfer: input.totalTransfer) + } +} diff --git a/Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift b/Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift new file mode 100644 index 0000000..0d15b59 --- /dev/null +++ b/Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift @@ -0,0 +1,42 @@ +// +// PersonBankAccountMapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain + +struct PersonBankAccountMapper: Mapper { + + typealias Entity = PersonBankAccount + typealias Dto = PersonBankAccountDTO + + private let personMapper: PersonMapper + private let cardMapper: CardMapper + private let cardTransferCountMapper: CardTransferCountMapper + + init(personMapper: PersonMapper, cardMapper: CardMapper, + cardTransferCountMapper: CardTransferCountMapper) { + self.personMapper = personMapper + self.cardMapper = cardMapper + self.cardTransferCountMapper = cardTransferCountMapper + } + + func mapDtoToEntity(input: PersonBankAccountDTO) -> PersonBankAccount { + let person = personMapper.mapDtoToEntity(input: input.person) + let card = cardMapper.mapDtoToEntity(input: input.card) + let transferCount = cardTransferCountMapper.mapDtoToEntity(input: input.more_info) + return .init(person: person, card: card, cardTransferCount: transferCount, note: input.note, + lastDateTransfer: input.last_transfer) + } + + func mapEntityToDto(input: PersonBankAccount) -> PersonBankAccountDTO { + let person = personMapper.mapEntityToDto(input: input.person) + let card = cardMapper.mapEntityToDto(input: input.card) + let transferCount = cardTransferCountMapper.mapEntityToDto(input: input.cardTransferCount) + return .init(person: person, card: card, more_info: transferCount, note: input.note, + last_transfer: input.lastDateTransfer) + } +} diff --git a/Data/Sources/Data/Http/Mapper/PersonMapper.swift b/Data/Sources/Data/Http/Mapper/PersonMapper.swift new file mode 100644 index 0000000..7b26946 --- /dev/null +++ b/Data/Sources/Data/Http/Mapper/PersonMapper.swift @@ -0,0 +1,27 @@ +// +// PersonMapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain + +struct PersonMapper: Mapper { + + typealias Entity = Person? + typealias Dto = PersonDTO? + + func mapDtoToEntity(input: PersonDTO?) -> Person? { + guard let input else { return nil } + return .init(name: input.full_name, email: input.email, + avatar: input.avatar) + } + + func mapEntityToDto(input: Person?) -> PersonDTO? { + guard let input else { return nil } + return .init(full_name: input.name, email: input.email, + avatar: input.avatar) + } +} diff --git a/Data/Sources/Data/Network/NetworkError.swift b/Data/Sources/Data/Http/NetworkError.swift similarity index 100% rename from Data/Sources/Data/Network/NetworkError.swift rename to Data/Sources/Data/Http/NetworkError.swift diff --git a/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift b/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift new file mode 100644 index 0000000..ecbbedd --- /dev/null +++ b/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift @@ -0,0 +1,44 @@ +// +// PersonBankAccountRepositoryImpl.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain + +public class PersonBankAccountRepositoryImpl: PersonBankAccountRepository { + + private var api: Api + private let mapper: PersonBankAccountMapper + + public init(api: Api) { + self.api = api + mapper = .init(personMapper: .init(), + cardMapper: .init(), + cardTransferCountMapper: .init()) + } + + public func fetchPersonAccounts(withOffest offset: Int) async throws -> [Domain.PersonBankAccount] { + let accounts = try await api.callApi(route: .transferList(offset: offset), + decodeType: [PersonBankAccountDTO].self) + return mapper.mapDtosToEntities(input: accounts) + } + + public func fetchFavoritePersonAccounts() async throws -> [Domain.PersonBankAccount] { + fatalError() + } + + public func savePersonAccountToFavorites(_ personBankAccount: Domain.PersonBankAccount) async throws -> Domain.PersonBankAccount { + fatalError() + } + + public func removePersonAccountFromFavorites(_ personBankAccount: Domain.PersonBankAccount) async throws -> Domain.PersonBankAccount { + fatalError() + } + + public func updatefavoriteStatusForPersonAccount(_ personBankAccount: Domain.PersonBankAccount) async -> Domain.PersonBankAccount { + fatalError() + } +} diff --git a/Data/Tests/DataTests/NetworkTests/ApiRouterTests.swift b/Data/Tests/DataTests/HttpTests/ApiRouterTests.swift similarity index 100% rename from Data/Tests/DataTests/NetworkTests/ApiRouterTests.swift rename to Data/Tests/DataTests/HttpTests/ApiRouterTests.swift diff --git a/Data/Tests/DataTests/NetworkTests/ApiTests.swift b/Data/Tests/DataTests/HttpTests/ApiTests.swift similarity index 100% rename from Data/Tests/DataTests/NetworkTests/ApiTests.swift rename to Data/Tests/DataTests/HttpTests/ApiTests.swift diff --git a/Data/Tests/DataTests/NetworkTests/Mock/ApiMockResponse.swift b/Data/Tests/DataTests/HttpTests/Mock/ApiMockResponse.swift similarity index 100% rename from Data/Tests/DataTests/NetworkTests/Mock/ApiMockResponse.swift rename to Data/Tests/DataTests/HttpTests/Mock/ApiMockResponse.swift diff --git a/Data/Tests/DataTests/NetworkTests/Mock/BadResponseMockURLProtocol.swift b/Data/Tests/DataTests/HttpTests/Mock/BadResponseMockURLProtocol.swift similarity index 100% rename from Data/Tests/DataTests/NetworkTests/Mock/BadResponseMockURLProtocol.swift rename to Data/Tests/DataTests/HttpTests/Mock/BadResponseMockURLProtocol.swift diff --git a/Data/Tests/DataTests/NetworkTests/Mock/OfflineServerMockURLProtocol.swift b/Data/Tests/DataTests/HttpTests/Mock/OfflineServerMockURLProtocol.swift similarity index 100% rename from Data/Tests/DataTests/NetworkTests/Mock/OfflineServerMockURLProtocol.swift rename to Data/Tests/DataTests/HttpTests/Mock/OfflineServerMockURLProtocol.swift diff --git a/Data/Tests/DataTests/NetworkTests/Mock/ResponseMockURLProtocol.swift b/Data/Tests/DataTests/HttpTests/Mock/ResponseMockURLProtocol.swift similarity index 100% rename from Data/Tests/DataTests/NetworkTests/Mock/ResponseMockURLProtocol.swift rename to Data/Tests/DataTests/HttpTests/Mock/ResponseMockURLProtocol.swift diff --git a/Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift b/Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift new file mode 100644 index 0000000..d10ef81 --- /dev/null +++ b/Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift @@ -0,0 +1,35 @@ +// +// File.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +@testable import Data + +final class MockApi: Api { + + func callApi(route: ApiRouter, decodeType type: T.Type) async throws -> T where T : Decodable { + let person = PersonDTO(full_name: "hessam", email: "h.mahdi", avatar: nil) + let card = CardDTO(card_number: "123", card_type: "master") + let cardCount = CardTransferCountDTO(number_of_transfers: 12, total_transfer: 12) + let note = "note" + let account = PersonBankAccountDTO(person: person, card: card, + more_info: cardCount, note: note, + last_transfer: nil) + + guard let retVal = [account] as? T else { + throw NetworkError.cannotParseJson + } + + return retVal + } +} + +final class MockFailConnectToServerApi: Api { + + func callApi(route: ApiRouter, decodeType type: T.Type) async throws -> T where T : Decodable { + throw NetworkError.cannotConnectToServer + } +} diff --git a/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift b/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift new file mode 100644 index 0000000..9cf0cbc --- /dev/null +++ b/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift @@ -0,0 +1,56 @@ +// +// PersonBankAccountRepositoryTests.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import XCTest +import Domain +@testable import Data + +final class PersonBankAccountRepositoryTests: XCTestCase { + + var repository: PersonBankAccountRepository! + + override func tearDown() { + repository = nil + } + + func testSuccessFetchMediaList() async { + + // given + repository = PersonBankAccountRepositoryImpl(api: MockApi()) + + do { + // when + let accounts = try await repository.fetchPersonAccounts(withOffest: 10) + + // then + XCTAssertEqual(accounts.count, 1) + XCTAssertEqual(accounts.first?.person?.name, "hessam") + XCTAssertEqual(accounts.first?.person?.email, "h.mahdi") + XCTAssertEqual(accounts.first?.card?.cardNumber, "123") + + } catch { + // then + XCTAssertNil(error) + } + } + + func testFailFetchMediaList() async { + + // given + repository = PersonBankAccountRepositoryImpl(api: MockFailConnectToServerApi()) + + do { + // when + let _ = try await repository.fetchPersonAccounts(withOffest: 10) + + } catch { + + // then + XCTAssertNotNil(error) + } + } +} diff --git a/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift b/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift index f073348..77c6cc5 100644 --- a/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift +++ b/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift @@ -11,7 +11,7 @@ import Foundation class MockPersonBankAccountRepository: PersonBankAccountRepository { func createMockPersonAccount() -> PersonBankAccount { - let person = Person(name: "hessam", emial: "h.mahdi", avatar: nil) + let person = Person(name: "hessam", email: "h.mahdi", avatar: nil) let card = Card(cardNumber: "123", cardType: "master") let cardCount = CardTransferCount(numberOfTransfers: 12, totalTransfer: 12) let note = "note" From b12c3fcf93d528440b688490bbf834877ff8cec2 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:03:40 +0330 Subject: [PATCH 08/16] Add Database Protocol and Error Handling - Added a Database protocol for repository usage. - Added a Database Error for improved error handling. --- Data/Sources/Data/Local/Database.swift | 40 +++++++++++++++++++ Data/Sources/Data/Local/DatabaseError.swift | 23 +++++++++++ Data/Sources/Data/Local/DatabaseImpl.swift | 43 +++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 Data/Sources/Data/Local/Database.swift create mode 100644 Data/Sources/Data/Local/DatabaseError.swift create mode 100644 Data/Sources/Data/Local/DatabaseImpl.swift diff --git a/Data/Sources/Data/Local/Database.swift b/Data/Sources/Data/Local/Database.swift new file mode 100644 index 0000000..8ed3762 --- /dev/null +++ b/Data/Sources/Data/Local/Database.swift @@ -0,0 +1,40 @@ +// +// Database.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import CoreData + +public protocol Database { + + var persistentContainer: NSPersistentContainer! { get } + var viewContext: NSManagedObjectContext { get } + var backgroundContext: NSManagedObjectContext { get } + func saveContext() throws + func saveBackgroundContext() throws +} + +public extension Database { + + func saveContext() throws { + guard viewContext.hasChanges else { return } + do { + try viewContext.save() + } catch { + throw DatabaseError.unexpectedError + } + } + + func saveBackgroundContext() throws { + guard backgroundContext.hasChanges else { return } + do { + try backgroundContext.save() + try saveContext() + } catch { + throw DatabaseError.unexpectedError + } + } +} diff --git a/Data/Sources/Data/Local/DatabaseError.swift b/Data/Sources/Data/Local/DatabaseError.swift new file mode 100644 index 0000000..0fac364 --- /dev/null +++ b/Data/Sources/Data/Local/DatabaseError.swift @@ -0,0 +1,23 @@ +// +// DatabaseError.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +enum DatabaseError: Error { + + case unexpectedError + case cannotSavePersonAccountToFavorites + case cannotRemovePersonAccountFromFavorites + + var errorDescription: String? { + switch self { + case .unexpectedError: return "You seem to be offline!" + case .cannotSavePersonAccountToFavorites: return "Failed to save person accounts to favorites" + case .cannotRemovePersonAccountFromFavorites: return "Failed to remove person account from favorites" + } + } +} diff --git a/Data/Sources/Data/Local/DatabaseImpl.swift b/Data/Sources/Data/Local/DatabaseImpl.swift new file mode 100644 index 0000000..6ef2a20 --- /dev/null +++ b/Data/Sources/Data/Local/DatabaseImpl.swift @@ -0,0 +1,43 @@ +// +// DatabaseImpl.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import CoreData + +public class DatabaseImpl: Database { + + private let modelName = "db" + public private(set) var persistentContainer: NSPersistentContainer! + + public required init() { + setupPersistentContainer() + } + + lazy public var viewContext: NSManagedObjectContext = { + let context = persistentContainer.viewContext + return context + }() + + lazy public var backgroundContext: NSManagedObjectContext = { + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = viewContext + context.automaticallyMergesChangesFromParent = true + context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + return context + }() + + private func setupPersistentContainer() { + let container = NSPersistentContainer(name: modelName) + container.loadPersistentStores(completionHandler: { _, error in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + + persistentContainer = container + } +} From 7fc9d28fbfec56858cc3322c7aa6a8a1d8b8d921 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 15:25:57 +0330 Subject: [PATCH 09/16] Add Core Data Transformer, Convert DTOs to Classes, and Implement Persistent Container - Added a Core Data transformer for saving custom objects. - Converted `CardDTO`, `PersonDTO`, and `CardTransferCountDTO` to classes. - Added the Persistent Container for Core Data usage in the Swift Package Manager. --- .../Data/Http/DataResponse/CardDTO.swift | 27 +++++- .../DataResponse/CardTransferCountDTO.swift | 27 +++++- .../Data/Http/DataResponse/PersonDTO.swift | 32 ++++++- Data/Sources/Data/Local/Database.swift | 3 +- Data/Sources/Data/Local/DatabaseImpl.swift | 21 +++-- ...ersonBankAccountEntity+CoreDataClass.swift | 15 ++++ ...BankAccountEntity+CoreDataProperties.swift | 34 +++++++ .../Data/Local/PersistentContainer.swift | 11 +++ .../Transformer/CardDTOTransformer.swift | 24 +++++ .../CardTransferCountDTOTransformer.swift | 24 +++++ .../Transformer/PersonDTOTransformer.swift | 24 +++++ .../db.xcdatamodeld/db.xcdatamodel/contents | 11 +++ .../DataTests/LocalTests/DatabaseTests.swift | 90 +++++++++++++++++++ .../LocalTests/Mock/MockDatabase.swift | 44 +++++++++ .../xcschemes/xcschememanagement.plist | 2 +- .../db.xcdatamodeld/db.xcdatamodel/contents | 2 + 16 files changed, 378 insertions(+), 13 deletions(-) create mode 100644 Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift create mode 100644 Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift create mode 100644 Data/Sources/Data/Local/PersistentContainer.swift create mode 100644 Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift create mode 100644 Data/Sources/Data/Local/Transformer/CardTransferCountDTOTransformer.swift create mode 100644 Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift create mode 100644 Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents create mode 100644 Data/Tests/DataTests/LocalTests/DatabaseTests.swift create mode 100644 Data/Tests/DataTests/LocalTests/Mock/MockDatabase.swift create mode 100644 TransferList/db.xcdatamodeld/db.xcdatamodel/contents diff --git a/Data/Sources/Data/Http/DataResponse/CardDTO.swift b/Data/Sources/Data/Http/DataResponse/CardDTO.swift index 9805228..7515bdb 100644 --- a/Data/Sources/Data/Http/DataResponse/CardDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/CardDTO.swift @@ -7,8 +7,33 @@ import Foundation -struct CardDTO: Decodable { +class CardDTO: NSObject, NSSecureCoding, Decodable { + enum Key: String { + case cardNumber + case cardType + } + + static var supportsSecureCoding: Bool = true var card_number: String? var card_type: String? + + init(card_number: String? = nil, card_type: String? = nil) { + self.card_number = card_number + self.card_type = card_type + } + + func encode(with coder: NSCoder) { + coder.encode(card_number, forKey: Key.cardNumber.rawValue) + coder.encode(card_type, forKey: Key.cardType.rawValue) + } + + required convenience init?(coder: NSCoder) { + let card_number = coder.decodeObject(of: NSString.self, + forKey: Key.cardNumber.rawValue) as? String + let card_type = coder.decodeObject(of: NSString.self, + forKey: Key.cardType.rawValue) as? String + + self.init(card_number: card_number, card_type: card_type) + } } diff --git a/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift b/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift index d7b26f0..b19f0d4 100644 --- a/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift @@ -7,8 +7,33 @@ import Foundation -struct CardTransferCountDTO: Decodable { +class CardTransferCountDTO: NSObject, NSSecureCoding, Decodable { + enum Key: String { + case numberOfTransfers + case totalTransfer + } + + static var supportsSecureCoding: Bool = true var number_of_transfers: Int? var total_transfer: Int? + + init(number_of_transfers: Int? = nil, total_transfer: Int? = nil) { + self.number_of_transfers = number_of_transfers + self.total_transfer = total_transfer + } + + func encode(with coder: NSCoder) { + coder.encode(number_of_transfers, forKey: Key.numberOfTransfers.rawValue) + coder.encode(total_transfer, forKey: Key.totalTransfer.rawValue) + } + + required convenience init?(coder: NSCoder) { + let number_of_transfers = coder.decodeObject(of: NSNumber.self, + forKey: Key.numberOfTransfers.rawValue)?.intValue + let total_transfer = coder.decodeObject(of: NSNumber.self, + forKey: Key.totalTransfer.rawValue)?.intValue + + self.init(number_of_transfers: number_of_transfers, total_transfer: total_transfer) + } } diff --git a/Data/Sources/Data/Http/DataResponse/PersonDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonDTO.swift index b4fdb30..06d0be7 100644 --- a/Data/Sources/Data/Http/DataResponse/PersonDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/PersonDTO.swift @@ -7,9 +7,39 @@ import Foundation -struct PersonDTO: Decodable { +class PersonDTO: NSObject, NSSecureCoding, Decodable { + enum Key: String { + case fullName + case email + case avatar + } + + static var supportsSecureCoding: Bool = true var full_name: String? var email: String? var avatar: String? + + init(full_name: String? = nil, email: String? = nil, avatar: String? = nil) { + self.full_name = full_name + self.email = email + self.avatar = avatar + } + + func encode(with coder: NSCoder) { + coder.encode(full_name, forKey: Key.fullName.rawValue) + coder.encode(email, forKey: Key.email.rawValue) + coder.encode(avatar, forKey: Key.avatar.rawValue) + } + + required convenience init?(coder: NSCoder) { + let full_name = coder.decodeObject(of: NSString.self, + forKey: Key.fullName.rawValue) as? String + let email = coder.decodeObject(of: NSString.self, + forKey: Key.email.rawValue) as? String + let avatar = coder.decodeObject(of: NSString.self, + forKey: Key.avatar.rawValue) as? String + + self.init(full_name: full_name, email: email, avatar: avatar) + } } diff --git a/Data/Sources/Data/Local/Database.swift b/Data/Sources/Data/Local/Database.swift index 8ed3762..3a1709b 100644 --- a/Data/Sources/Data/Local/Database.swift +++ b/Data/Sources/Data/Local/Database.swift @@ -10,8 +10,7 @@ import CoreData public protocol Database { - var persistentContainer: NSPersistentContainer! { get } - var viewContext: NSManagedObjectContext { get } + var viewContext: NSManagedObjectContext { set get } var backgroundContext: NSManagedObjectContext { get } func saveContext() throws func saveBackgroundContext() throws diff --git a/Data/Sources/Data/Local/DatabaseImpl.swift b/Data/Sources/Data/Local/DatabaseImpl.swift index 6ef2a20..3bad091 100644 --- a/Data/Sources/Data/Local/DatabaseImpl.swift +++ b/Data/Sources/Data/Local/DatabaseImpl.swift @@ -10,19 +10,23 @@ import CoreData public class DatabaseImpl: Database { - private let modelName = "db" - public private(set) var persistentContainer: NSPersistentContainer! + private(set) var modelName = "db" + private var persistentContainer: PersistentContainer! - public required init() { + public required init() { setupPersistentContainer() + + PersonDTOTransformer.register() + CardDTOTransformer.register() + CardTransferCountDTOTransformer.register() } - lazy public var viewContext: NSManagedObjectContext = { + public lazy var viewContext: NSManagedObjectContext = { let context = persistentContainer.viewContext return context }() - lazy public var backgroundContext: NSManagedObjectContext = { + public lazy var backgroundContext: NSManagedObjectContext = { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = viewContext context.automaticallyMergesChangesFromParent = true @@ -31,13 +35,16 @@ public class DatabaseImpl: Database { }() private func setupPersistentContainer() { - let container = NSPersistentContainer(name: modelName) + guard let modelURL = Bundle.module.url(forResource: modelName, withExtension: "momd") else { return } + guard let model = NSManagedObjectModel(contentsOf: modelURL) else { return } + + let container = PersistentContainer(name: modelName, managedObjectModel: model) container.loadPersistentStores(completionHandler: { _, error in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) - + persistentContainer = container } } diff --git a/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift b/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift new file mode 100644 index 0000000..3ceca85 --- /dev/null +++ b/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// PersonBankAccountEntity+CoreDataClass.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/5/23. +// +// + +import Foundation +import CoreData + +@objc(PersonBankAccountEntity) +public class PersonBankAccountEntity: NSManagedObject { + +} diff --git a/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift b/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift new file mode 100644 index 0000000..eb6d5ea --- /dev/null +++ b/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift @@ -0,0 +1,34 @@ +// +// PersonBankAccountEntity+CoreDataProperties.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/5/23. +// +// + +import Foundation +import CoreData + +extension PersonBankAccountEntity { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "PersonBankAccountEntity") + } + + @NSManaged public var note: String? + @NSManaged public var lastTransfer: Date? + @NSManaged public var dateSaved: Date? + @NSManaged var person: PersonDTO? + @NSManaged var card: CardDTO? + @NSManaged var more_info: CardTransferCountDTO? + +} + +extension PersonBankAccountEntity: Identifiable {} + +extension PersonBankAccountEntity { + + static var sortDescriptor: NSSortDescriptor { + NSSortDescriptor(key: #keyPath(PersonBankAccountEntity.dateSaved), ascending: false) + } +} diff --git a/Data/Sources/Data/Local/PersistentContainer.swift b/Data/Sources/Data/Local/PersistentContainer.swift new file mode 100644 index 0000000..5f78e1e --- /dev/null +++ b/Data/Sources/Data/Local/PersistentContainer.swift @@ -0,0 +1,11 @@ +// +// PersistentContainer.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import CoreData + +public class PersistentContainer: NSPersistentContainer {} diff --git a/Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift b/Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift new file mode 100644 index 0000000..0fd4095 --- /dev/null +++ b/Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift @@ -0,0 +1,24 @@ +// +// CardDTOTransformer.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +@objc(CardDTOTransformer) +class CardDTOTransformer: NSSecureUnarchiveFromDataTransformer { + + override class var allowedTopLevelClasses: [AnyClass] { + return [CardDTO.self] + } + + static func register() { + let className = String(describing: CardDTOTransformer.self) + let name = NSValueTransformerName(className) + + let transformer = CardDTOTransformer() + ValueTransformer.setValueTransformer(transformer, forName: name) + } +} diff --git a/Data/Sources/Data/Local/Transformer/CardTransferCountDTOTransformer.swift b/Data/Sources/Data/Local/Transformer/CardTransferCountDTOTransformer.swift new file mode 100644 index 0000000..8fac661 --- /dev/null +++ b/Data/Sources/Data/Local/Transformer/CardTransferCountDTOTransformer.swift @@ -0,0 +1,24 @@ +// +// CardTransferCountDTOTransformer.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +@objc(CardTransferCountDTOTransformer) +class CardTransferCountDTOTransformer: NSSecureUnarchiveFromDataTransformer { + + override class var allowedTopLevelClasses: [AnyClass] { + return [CardTransferCountDTO.self] + } + + static func register() { + let className = String(describing: CardTransferCountDTOTransformer.self) + let name = NSValueTransformerName(className) + + let transformer = CardTransferCountDTOTransformer() + ValueTransformer.setValueTransformer(transformer, forName: name) + } +} diff --git a/Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift b/Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift new file mode 100644 index 0000000..07b920d --- /dev/null +++ b/Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift @@ -0,0 +1,24 @@ +// +// PersonDTOTransformer.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +@objc(PersonDTOTransformer) +class PersonDTOTransformer: NSSecureUnarchiveFromDataTransformer { + + override class var allowedTopLevelClasses: [AnyClass] { + return [PersonDTO.self] + } + + static func register() { + let className = String(describing: PersonDTOTransformer.self) + let name = NSValueTransformerName(className) + + let transformer = PersonDTOTransformer() + ValueTransformer.setValueTransformer(transformer, forName: name) + } +} diff --git a/Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents b/Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents new file mode 100644 index 0000000..be8f753 --- /dev/null +++ b/Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Data/Tests/DataTests/LocalTests/DatabaseTests.swift b/Data/Tests/DataTests/LocalTests/DatabaseTests.swift new file mode 100644 index 0000000..cf6cb92 --- /dev/null +++ b/Data/Tests/DataTests/LocalTests/DatabaseTests.swift @@ -0,0 +1,90 @@ +// +// DatabaseTests.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import XCTest +@testable import Data +import CoreData + +final class DatabaseTests: XCTestCase { + + var database: Database! + + override func tearDown() { + database = nil + } + + private func createPersonAccountEntity(inContext: NSManagedObjectContext) { + let account = PersonBankAccountEntity(context: inContext) + let dateSaved = Date() + let note = "note" + account.dateSaved = dateSaved + account.note = note + account.lastTransfer = nil + account.person = PersonDTO(full_name: "hessam", email: "h.mahdi", avatar: nil) + account.card = .init(card_number: "123", card_type: "master") + account.more_info = .init(number_of_transfers: 1, total_transfer: nil) + } + + func testSavePersonAccount() { + database = MockDatabase() + + database.backgroundContext.performAndWait { + createPersonAccountEntity(inContext: database.backgroundContext) + + do { + try database.saveBackgroundContext() + + let fetchRequest: + NSFetchRequest = PersonBankAccountEntity.fetchRequest() + let sort = PersonBankAccountEntity.sortDescriptor + fetchRequest.sortDescriptors = [sort] + + let results = try database.backgroundContext.fetch(fetchRequest) + if results.isEmpty { + throw DatabaseError.unexpectedError + } else { + + XCTAssertEqual(results.count, 1) + XCTAssertEqual(results.first?.note, "note") + XCTAssertEqual(results.first?.person?.full_name, "hessam") + XCTAssertEqual(results.first?.card?.card_number, "123") + XCTAssertEqual(results.first?.more_info?.number_of_transfers, 1) + } + + } catch { + XCTAssertNil(error) + } + } + } + + func testRemovePersonAccount() { + database = MockDatabase() + + database.backgroundContext.performAndWait { + createPersonAccountEntity(inContext: database.backgroundContext) + + do { + try database.saveBackgroundContext() + + let fetchRequest: + NSFetchRequest = PersonBankAccountEntity.fetchRequest() + let sort = PersonBankAccountEntity.sortDescriptor + fetchRequest.sortDescriptors = [sort] + + let account = try database.backgroundContext.fetch(fetchRequest).first! + + database.backgroundContext.delete(account) + + let results = try database.backgroundContext.fetch(fetchRequest) + XCTAssertTrue(results.isEmpty) + + } catch { + XCTAssertNil(error) + } + } + } +} diff --git a/Data/Tests/DataTests/LocalTests/Mock/MockDatabase.swift b/Data/Tests/DataTests/LocalTests/Mock/MockDatabase.swift new file mode 100644 index 0000000..697715f --- /dev/null +++ b/Data/Tests/DataTests/LocalTests/Mock/MockDatabase.swift @@ -0,0 +1,44 @@ +// +// MockDatabase.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import CoreData +@testable import Data + +final class MockDatabase: DatabaseImpl { + + required init() { + super.init() + + viewContext = createInMemoryViewContext() + } + + private func createInMemoryViewContext() -> NSManagedObjectContext { + let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) + context.persistentStoreCoordinator = self.persistentStoreCoordinator + return context + } + + private lazy var managedObjectModel: NSManagedObjectModel = { + let modelURL = Bundle.module.url(forResource: modelName, withExtension: "momd")! + return NSManagedObjectModel(contentsOf: modelURL)! + }() + + private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel) + do { + try coordinator.addPersistentStore(ofType: NSInMemoryStoreType, + configurationName: nil, + at: nil, + options: nil) + } catch { + fatalError("Failed to initialize in-memory store type: \(error)") + } + return coordinator + }() +} + diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 5eb9d26..7e70a09 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 3 + 2 diff --git a/TransferList/db.xcdatamodeld/db.xcdatamodel/contents b/TransferList/db.xcdatamodeld/db.xcdatamodel/contents new file mode 100644 index 0000000..9ce099f --- /dev/null +++ b/TransferList/db.xcdatamodeld/db.xcdatamodel/contents @@ -0,0 +1,2 @@ + + \ No newline at end of file From 4f4eb30ff1175066b5a1ce50a86a140aebee94a6 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 19:39:53 +0330 Subject: [PATCH 10/16] Add One-to-One Relationships, Local Protocol, and New Database Entities - Added one-to-one relationships between accounts, persons, and cards to facilitate filtering. - Added a Local protocol for repository usage. - Added two new entities in the schema database. --- .../Data/Http/DataResponse/CardDTO.swift | 27 +-- .../DataResponse/PersonBankAccountDTO.swift | 1 + .../Data/Http/DataResponse/PersonDTO.swift | 32 +--- .../Data/Local/{ => Database}/Database.swift | 0 .../Data/Local/Database/DatabaseError.swift | 17 ++ .../Local/{ => Database}/DatabaseImpl.swift | 2 - .../CardEntity+CoreDataClass.swift | 15 ++ .../CardEntity+CoreDataProperties.swift | 22 +++ ...ersonBankAccountEntity+CoreDataClass.swift | 0 ...BankAccountEntity+CoreDataProperties.swift | 14 +- .../PersonEntity+CoreDataClass.swift | 15 ++ .../PersonEntity+CoreDataProperties.swift | 23 +++ .../Database/Mapper/CardEntityMapper.swift | 35 ++++ .../PersonBankAccountEntityMapper.swift | 47 ++++++ .../Database/Mapper/PersonEntityMapper.swift | 37 +++++ .../{ => Database}/PersistentContainer.swift | 0 .../CardTransferCountDTOTransformer.swift | 0 .../db.xcdatamodeld/db.xcdatamodel/contents | 23 +++ Data/Sources/Data/Local/Local.swift | 16 ++ .../{DatabaseError.swift => LocalError.swift} | 8 +- Data/Sources/Data/Local/LocalImpl.swift | 154 ++++++++++++++++++ .../Transformer/CardDTOTransformer.swift | 24 --- .../Transformer/PersonDTOTransformer.swift | 24 --- .../db.xcdatamodeld/db.xcdatamodel/contents | 11 -- .../{ => DatabaseTests}/DatabaseTests.swift | 18 +- .../DataTests/LocalTests/LocalTests.swift | 83 ++++++++++ .../xcschemes/xcschememanagement.plist | 2 +- 27 files changed, 515 insertions(+), 135 deletions(-) rename Data/Sources/Data/Local/{ => Database}/Database.swift (100%) create mode 100644 Data/Sources/Data/Local/Database/DatabaseError.swift rename Data/Sources/Data/Local/{ => Database}/DatabaseImpl.swift (95%) create mode 100644 Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataClass.swift create mode 100644 Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataProperties.swift rename Data/Sources/Data/Local/{ => Database}/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift (100%) rename Data/Sources/Data/Local/{ => Database}/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift (71%) create mode 100644 Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataClass.swift create mode 100644 Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataProperties.swift create mode 100644 Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift create mode 100644 Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift create mode 100644 Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift rename Data/Sources/Data/Local/{ => Database}/PersistentContainer.swift (100%) rename Data/Sources/Data/Local/{ => Database}/Transformer/CardTransferCountDTOTransformer.swift (100%) create mode 100644 Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents create mode 100644 Data/Sources/Data/Local/Local.swift rename Data/Sources/Data/Local/{DatabaseError.swift => LocalError.swift} (75%) create mode 100644 Data/Sources/Data/Local/LocalImpl.swift delete mode 100644 Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift delete mode 100644 Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift delete mode 100644 Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents rename Data/Tests/DataTests/LocalTests/{ => DatabaseTests}/DatabaseTests.swift (84%) create mode 100644 Data/Tests/DataTests/LocalTests/LocalTests.swift diff --git a/Data/Sources/Data/Http/DataResponse/CardDTO.swift b/Data/Sources/Data/Http/DataResponse/CardDTO.swift index 7515bdb..9805228 100644 --- a/Data/Sources/Data/Http/DataResponse/CardDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/CardDTO.swift @@ -7,33 +7,8 @@ import Foundation -class CardDTO: NSObject, NSSecureCoding, Decodable { +struct CardDTO: Decodable { - enum Key: String { - case cardNumber - case cardType - } - - static var supportsSecureCoding: Bool = true var card_number: String? var card_type: String? - - init(card_number: String? = nil, card_type: String? = nil) { - self.card_number = card_number - self.card_type = card_type - } - - func encode(with coder: NSCoder) { - coder.encode(card_number, forKey: Key.cardNumber.rawValue) - coder.encode(card_type, forKey: Key.cardType.rawValue) - } - - required convenience init?(coder: NSCoder) { - let card_number = coder.decodeObject(of: NSString.self, - forKey: Key.cardNumber.rawValue) as? String - let card_type = coder.decodeObject(of: NSString.self, - forKey: Key.cardType.rawValue) as? String - - self.init(card_number: card_number, card_type: card_type) - } } diff --git a/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift index d2f4f90..1c2fcce 100644 --- a/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift @@ -14,4 +14,5 @@ struct PersonBankAccountDTO: Decodable { var more_info: CardTransferCountDTO? var note: String? var last_transfer: Date? + var isFavorite: Bool = false } diff --git a/Data/Sources/Data/Http/DataResponse/PersonDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonDTO.swift index 06d0be7..b4fdb30 100644 --- a/Data/Sources/Data/Http/DataResponse/PersonDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/PersonDTO.swift @@ -7,39 +7,9 @@ import Foundation -class PersonDTO: NSObject, NSSecureCoding, Decodable { +struct PersonDTO: Decodable { - enum Key: String { - case fullName - case email - case avatar - } - - static var supportsSecureCoding: Bool = true var full_name: String? var email: String? var avatar: String? - - init(full_name: String? = nil, email: String? = nil, avatar: String? = nil) { - self.full_name = full_name - self.email = email - self.avatar = avatar - } - - func encode(with coder: NSCoder) { - coder.encode(full_name, forKey: Key.fullName.rawValue) - coder.encode(email, forKey: Key.email.rawValue) - coder.encode(avatar, forKey: Key.avatar.rawValue) - } - - required convenience init?(coder: NSCoder) { - let full_name = coder.decodeObject(of: NSString.self, - forKey: Key.fullName.rawValue) as? String - let email = coder.decodeObject(of: NSString.self, - forKey: Key.email.rawValue) as? String - let avatar = coder.decodeObject(of: NSString.self, - forKey: Key.avatar.rawValue) as? String - - self.init(full_name: full_name, email: email, avatar: avatar) - } } diff --git a/Data/Sources/Data/Local/Database.swift b/Data/Sources/Data/Local/Database/Database.swift similarity index 100% rename from Data/Sources/Data/Local/Database.swift rename to Data/Sources/Data/Local/Database/Database.swift diff --git a/Data/Sources/Data/Local/Database/DatabaseError.swift b/Data/Sources/Data/Local/Database/DatabaseError.swift new file mode 100644 index 0000000..f9131de --- /dev/null +++ b/Data/Sources/Data/Local/Database/DatabaseError.swift @@ -0,0 +1,17 @@ +// +// DatabaseError.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +enum DatabaseError: Error { + + case unexpectedError + + var errorDescription: String? { + return "unexpected Error!" + } +} diff --git a/Data/Sources/Data/Local/DatabaseImpl.swift b/Data/Sources/Data/Local/Database/DatabaseImpl.swift similarity index 95% rename from Data/Sources/Data/Local/DatabaseImpl.swift rename to Data/Sources/Data/Local/Database/DatabaseImpl.swift index 3bad091..643172b 100644 --- a/Data/Sources/Data/Local/DatabaseImpl.swift +++ b/Data/Sources/Data/Local/Database/DatabaseImpl.swift @@ -16,8 +16,6 @@ public class DatabaseImpl: Database { public required init() { setupPersistentContainer() - PersonDTOTransformer.register() - CardDTOTransformer.register() CardTransferCountDTOTransformer.register() } diff --git a/Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataClass.swift b/Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataClass.swift new file mode 100644 index 0000000..6166ae6 --- /dev/null +++ b/Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// CardEntity+CoreDataClass.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/5/23. +// +// + +import Foundation +import CoreData + +@objc(CardEntity) +public class CardEntity: NSManagedObject { + +} diff --git a/Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataProperties.swift b/Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataProperties.swift new file mode 100644 index 0000000..9ffa20a --- /dev/null +++ b/Data/Sources/Data/Local/Database/ManagedObject/CardEntity+CoreDataProperties.swift @@ -0,0 +1,22 @@ +// +// CardEntity+CoreDataProperties.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/5/23. +// +// + +import Foundation +import CoreData + +extension CardEntity { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "CardEntity") + } + + @NSManaged var cardNumber: String? + @NSManaged var cardType: String? + @NSManaged var account: PersonBankAccountEntity? + +} diff --git a/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift b/Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift similarity index 100% rename from Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift rename to Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataClass.swift diff --git a/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift b/Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift similarity index 71% rename from Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift rename to Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift index eb6d5ea..7e41648 100644 --- a/Data/Sources/Data/Local/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift +++ b/Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift @@ -15,17 +15,15 @@ extension PersonBankAccountEntity { return NSFetchRequest(entityName: "PersonBankAccountEntity") } - @NSManaged public var note: String? - @NSManaged public var lastTransfer: Date? - @NSManaged public var dateSaved: Date? - @NSManaged var person: PersonDTO? - @NSManaged var card: CardDTO? + @NSManaged var dateSaved: Date? + @NSManaged var lastTransfer: Date? @NSManaged var more_info: CardTransferCountDTO? - + @NSManaged var note: String? + @NSManaged var isFavorite: Bool + @NSManaged var card: CardEntity? + @NSManaged var person: PersonEntity? } -extension PersonBankAccountEntity: Identifiable {} - extension PersonBankAccountEntity { static var sortDescriptor: NSSortDescriptor { diff --git a/Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataClass.swift b/Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataClass.swift new file mode 100644 index 0000000..3385965 --- /dev/null +++ b/Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// PersonEntity+CoreDataClass.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/5/23. +// +// + +import Foundation +import CoreData + +@objc(PersonEntity) +public class PersonEntity: NSManagedObject { + +} diff --git a/Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataProperties.swift b/Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataProperties.swift new file mode 100644 index 0000000..1f62ed5 --- /dev/null +++ b/Data/Sources/Data/Local/Database/ManagedObject/PersonEntity+CoreDataProperties.swift @@ -0,0 +1,23 @@ +// +// PersonEntity+CoreDataProperties.swift +// TransferList +// +// Created by Hessam Mahdiabadi on 11/5/23. +// +// + +import Foundation +import CoreData + +extension PersonEntity { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "PersonEntity") + } + + @NSManaged var avatar: String? + @NSManaged var email: String? + @NSManaged var name: String? + @NSManaged var account: PersonBankAccountEntity? + +} diff --git a/Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift b/Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift new file mode 100644 index 0000000..eb9baab --- /dev/null +++ b/Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift @@ -0,0 +1,35 @@ +// +// CardEntityMapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import CoreData + +struct CardEntityMapper: Mapper { + + typealias Entity = CardEntity? + typealias Dto = CardDTO? + + private let context: NSManagedObjectContext + + init(context: NSManagedObjectContext) { + self.context = context + } + + func mapDtoToEntity(input: CardDTO?) -> CardEntity? { + guard let input else { return nil } + let entity = CardEntity(context: context) + entity.cardType = input.card_type + entity.cardNumber = input.card_number + + return entity + } + + func mapEntityToDto(input: CardEntity?) -> CardDTO? { + guard let input else { return nil } + return .init(card_number: input.cardNumber, card_type: input.cardType) + } +} diff --git a/Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift b/Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift new file mode 100644 index 0000000..8515fe3 --- /dev/null +++ b/Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift @@ -0,0 +1,47 @@ +// +// PersonBankAccountEntityMapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import CoreData + +struct PersonBankAccountEntityMapper: Mapper { + + typealias Entity = PersonBankAccountEntity + typealias Dto = PersonBankAccountDTO + + private let context: NSManagedObjectContext + private let cardEntityMapper: CardEntityMapper + private let personEntityMapper: PersonEntityMapper + + init(context: NSManagedObjectContext, + cardEntityMapper: CardEntityMapper, + personEntityMapper: PersonEntityMapper) { + self.context = context + self.cardEntityMapper = cardEntityMapper + self.personEntityMapper = personEntityMapper + } + + func mapDtoToEntity(input: PersonBankAccountDTO) -> PersonBankAccountEntity { + let entity = PersonBankAccountEntity(context: context) + entity.person = personEntityMapper.mapDtoToEntity(input: input.person) + entity.more_info = input.more_info + entity.card = cardEntityMapper.mapDtoToEntity(input: input.card) + entity.note = input.note + entity.lastTransfer = input.last_transfer + entity.dateSaved = Date() + + return entity + } + + func mapEntityToDto(input: PersonBankAccountEntity) -> PersonBankAccountDTO { + let person = personEntityMapper.mapEntityToDto(input: input.person) + let card = cardEntityMapper.mapEntityToDto(input: input.card) + return .init(person: person, card: card, + more_info: input.more_info, note: input.note, + last_transfer: input.lastTransfer, isFavorite: input.isFavorite) + } +} diff --git a/Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift b/Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift new file mode 100644 index 0000000..57f6104 --- /dev/null +++ b/Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift @@ -0,0 +1,37 @@ +// +// PersonEntityMapper.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import CoreData + +struct PersonEntityMapper: Mapper { + + typealias Entity = PersonEntity? + typealias Dto = PersonDTO? + + private let context: NSManagedObjectContext + + init(context: NSManagedObjectContext) { + self.context = context + } + + func mapDtoToEntity(input: PersonDTO?) -> PersonEntity? { + guard let input else { return nil } + let entity = PersonEntity(context: context) + entity.name = input.full_name + entity.avatar = input.avatar + entity.email = input.email + + return entity + } + + func mapEntityToDto(input: PersonEntity?) -> PersonDTO? { + guard let input else { return nil } + return .init(full_name: input.name, email: input.email, + avatar: input.avatar) + } +} diff --git a/Data/Sources/Data/Local/PersistentContainer.swift b/Data/Sources/Data/Local/Database/PersistentContainer.swift similarity index 100% rename from Data/Sources/Data/Local/PersistentContainer.swift rename to Data/Sources/Data/Local/Database/PersistentContainer.swift diff --git a/Data/Sources/Data/Local/Transformer/CardTransferCountDTOTransformer.swift b/Data/Sources/Data/Local/Database/Transformer/CardTransferCountDTOTransformer.swift similarity index 100% rename from Data/Sources/Data/Local/Transformer/CardTransferCountDTOTransformer.swift rename to Data/Sources/Data/Local/Database/Transformer/CardTransferCountDTOTransformer.swift diff --git a/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents b/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents new file mode 100644 index 0000000..ac9abab --- /dev/null +++ b/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Data/Sources/Data/Local/Local.swift b/Data/Sources/Data/Local/Local.swift new file mode 100644 index 0000000..646c59a --- /dev/null +++ b/Data/Sources/Data/Local/Local.swift @@ -0,0 +1,16 @@ +// +// Local.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain + +public protocol Local { + + func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] + func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount + func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount +} diff --git a/Data/Sources/Data/Local/DatabaseError.swift b/Data/Sources/Data/Local/LocalError.swift similarity index 75% rename from Data/Sources/Data/Local/DatabaseError.swift rename to Data/Sources/Data/Local/LocalError.swift index 0fac364..7d4c882 100644 --- a/Data/Sources/Data/Local/DatabaseError.swift +++ b/Data/Sources/Data/Local/LocalError.swift @@ -1,5 +1,5 @@ // -// DatabaseError.swift +// LocalError.swift // // // Created by Hessam Mahdiabadi on 11/5/23. @@ -7,15 +7,15 @@ import Foundation -enum DatabaseError: Error { +enum LocalError: Error { - case unexpectedError + case cannotFetchFavorites case cannotSavePersonAccountToFavorites case cannotRemovePersonAccountFromFavorites var errorDescription: String? { switch self { - case .unexpectedError: return "You seem to be offline!" + case .cannotFetchFavorites: return "caanot fetch favorites accounts" case .cannotSavePersonAccountToFavorites: return "Failed to save person accounts to favorites" case .cannotRemovePersonAccountFromFavorites: return "Failed to remove person account from favorites" } diff --git a/Data/Sources/Data/Local/LocalImpl.swift b/Data/Sources/Data/Local/LocalImpl.swift new file mode 100644 index 0000000..87a8df3 --- /dev/null +++ b/Data/Sources/Data/Local/LocalImpl.swift @@ -0,0 +1,154 @@ +// +// LocalImpl.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain +import CoreData + +public class LocalImpl: Local { + + private let database: Database + private let coreDataMapper: PersonBankAccountEntityMapper + private let mapper: PersonBankAccountMapper + + public init(database: Database) { + self.database = database + + let context = database.backgroundContext + let personMapper = PersonEntityMapper(context: context) + let cardMapper = CardEntityMapper(context: context) + coreDataMapper = .init(context: context, + cardEntityMapper: cardMapper, + personEntityMapper: personMapper) + + mapper = .init(personMapper: .init(), cardMapper: .init(), + cardTransferCountMapper: .init()) + } + + public func fetchFavoritePersonAccounts() async throws -> [Domain.PersonBankAccount] { + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self else { + continuation.resume(throwing: LocalError.cannotFetchFavorites) + return + } + + self.database.backgroundContext.performAndWait { [weak self] in + guard let self else { + continuation.resume(throwing: LocalError.cannotFetchFavorites) + return + } + + do { + let fetchRequest: + NSFetchRequest = PersonBankAccountEntity.fetchRequest() + let sort = PersonBankAccountEntity.sortDescriptor + fetchRequest.sortDescriptors = [sort] + + let accountMangedObjects = try self.database.backgroundContext.fetch(fetchRequest) + let accountDtos = self.coreDataMapper.mapEntitiesToDtos(input: accountMangedObjects) + let accounts = self.mapper + .mapDtosToEntities(input: accountDtos) + .map { item in + var changeToFavoriteItem = item + changeToFavoriteItem.update(favoriteStatus: true) + return changeToFavoriteItem + } + + continuation.resume(returning: accounts) + + } catch { + continuation.resume(throwing: LocalError.cannotFetchFavorites) + } + } + } + } + + public func savePersonAccountToFavorites(_ personBankAccount: Domain.PersonBankAccount) async throws + -> Domain.PersonBankAccount { + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self else { + continuation.resume(throwing: LocalError.cannotSavePersonAccountToFavorites) + return + } + + self.database.backgroundContext.performAndWait { [weak self] in + guard let self else { + continuation.resume(throwing: LocalError.cannotFetchFavorites) + return + } + + do { + let accountDto = self.mapper.mapEntityToDto(input: personBankAccount) + let _ = self.coreDataMapper.mapDtoToEntity(input: accountDto) + + try Task.checkCancellation() + try self.database.saveBackgroundContext() + + var newPersonBankAccount = personBankAccount + newPersonBankAccount.update(favoriteStatus: true) + continuation.resume(returning: newPersonBankAccount) + + } catch { + self.database.backgroundContext.rollback() + continuation.resume(throwing: LocalError.cannotSavePersonAccountToFavorites) + } + } + } + } + + public func removePersonAccountFromFavorites(_ personBankAccount: Domain.PersonBankAccount) async throws + -> Domain.PersonBankAccount { + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self else { + continuation.resume(throwing: LocalError.cannotRemovePersonAccountFromFavorites) + return + } + + self.database.backgroundContext.performAndWait { [weak self] in + guard let self else { + continuation.resume(throwing: LocalError.cannotRemovePersonAccountFromFavorites) + return + } + + do { + let fetchRequest: + NSFetchRequest = PersonBankAccountEntity.fetchRequest() + let sort = PersonBankAccountEntity.sortDescriptor + fetchRequest.sortDescriptors = [sort] + fetchRequest.fetchLimit = 1 + + let namePredicate = NSPredicate(format: "person.name == %@", + personBankAccount.person?.name ?? "") + let cardNumberPredicate = NSPredicate(format: "card.cardNumber == %@", + personBankAccount.card?.cardNumber ?? "") + let predicate = NSCompoundPredicate(type: .and, + subpredicates: [namePredicate, cardNumberPredicate]) + fetchRequest.predicate = predicate + + try Task.checkCancellation() + let accounts = try self.database.backgroundContext.fetch(fetchRequest) + guard let entity = accounts.first else { + continuation.resume(throwing: LocalError.cannotRemovePersonAccountFromFavorites) + return + } + + try Task.checkCancellation() + self.database.backgroundContext.delete(entity) + try self.database.saveBackgroundContext() + + var newPersonBankAccount = personBankAccount + newPersonBankAccount.update(favoriteStatus: false) + continuation.resume(returning: newPersonBankAccount) + + } catch { + self.database.backgroundContext.rollback() + continuation.resume(throwing: LocalError.cannotRemovePersonAccountFromFavorites) + } + } + } + } +} diff --git a/Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift b/Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift deleted file mode 100644 index 0fd4095..0000000 --- a/Data/Sources/Data/Local/Transformer/CardDTOTransformer.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CardDTOTransformer.swift -// -// -// Created by Hessam Mahdiabadi on 11/5/23. -// - -import Foundation - -@objc(CardDTOTransformer) -class CardDTOTransformer: NSSecureUnarchiveFromDataTransformer { - - override class var allowedTopLevelClasses: [AnyClass] { - return [CardDTO.self] - } - - static func register() { - let className = String(describing: CardDTOTransformer.self) - let name = NSValueTransformerName(className) - - let transformer = CardDTOTransformer() - ValueTransformer.setValueTransformer(transformer, forName: name) - } -} diff --git a/Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift b/Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift deleted file mode 100644 index 07b920d..0000000 --- a/Data/Sources/Data/Local/Transformer/PersonDTOTransformer.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PersonDTOTransformer.swift -// -// -// Created by Hessam Mahdiabadi on 11/5/23. -// - -import Foundation - -@objc(PersonDTOTransformer) -class PersonDTOTransformer: NSSecureUnarchiveFromDataTransformer { - - override class var allowedTopLevelClasses: [AnyClass] { - return [PersonDTO.self] - } - - static func register() { - let className = String(describing: PersonDTOTransformer.self) - let name = NSValueTransformerName(className) - - let transformer = PersonDTOTransformer() - ValueTransformer.setValueTransformer(transformer, forName: name) - } -} diff --git a/Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents b/Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents deleted file mode 100644 index be8f753..0000000 --- a/Data/Sources/Data/Local/db.xcdatamodeld/db.xcdatamodel/contents +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Data/Tests/DataTests/LocalTests/DatabaseTests.swift b/Data/Tests/DataTests/LocalTests/DatabaseTests/DatabaseTests.swift similarity index 84% rename from Data/Tests/DataTests/LocalTests/DatabaseTests.swift rename to Data/Tests/DataTests/LocalTests/DatabaseTests/DatabaseTests.swift index cf6cb92..aa5af8d 100644 --- a/Data/Tests/DataTests/LocalTests/DatabaseTests.swift +++ b/Data/Tests/DataTests/LocalTests/DatabaseTests/DatabaseTests.swift @@ -18,14 +18,24 @@ final class DatabaseTests: XCTestCase { } private func createPersonAccountEntity(inContext: NSManagedObjectContext) { + + let person = PersonEntity(context: inContext) + person.name = "hessam" + person.email = "h.mahdi" + person.avatar = nil + + let card = CardEntity(context: inContext) + card.cardNumber = "123" + card.cardType = "master" + let account = PersonBankAccountEntity(context: inContext) let dateSaved = Date() let note = "note" account.dateSaved = dateSaved account.note = note account.lastTransfer = nil - account.person = PersonDTO(full_name: "hessam", email: "h.mahdi", avatar: nil) - account.card = .init(card_number: "123", card_type: "master") + account.person = person + account.card = card account.more_info = .init(number_of_transfers: 1, total_transfer: nil) } @@ -50,8 +60,8 @@ final class DatabaseTests: XCTestCase { XCTAssertEqual(results.count, 1) XCTAssertEqual(results.first?.note, "note") - XCTAssertEqual(results.first?.person?.full_name, "hessam") - XCTAssertEqual(results.first?.card?.card_number, "123") + XCTAssertEqual(results.first?.person?.name, "hessam") + XCTAssertEqual(results.first?.card?.cardNumber, "123") XCTAssertEqual(results.first?.more_info?.number_of_transfers, 1) } diff --git a/Data/Tests/DataTests/LocalTests/LocalTests.swift b/Data/Tests/DataTests/LocalTests/LocalTests.swift new file mode 100644 index 0000000..8a9982d --- /dev/null +++ b/Data/Tests/DataTests/LocalTests/LocalTests.swift @@ -0,0 +1,83 @@ +// +// LocalTests.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import XCTest +import Domain +@testable import Data + +final class LocalTests: XCTestCase { + + private var local: Local! + + override func tearDown() { + local = nil + } + + private func createBankAccount() -> PersonBankAccount { + let person = Person(name: "hessam", email: "h.mahdi", avatar: nil) + let card = Card(cardNumber: "123", cardType: "master") + let cardCount = CardTransferCount(numberOfTransfers: 12, totalTransfer: 12) + let note = "note" + return .init(person: person, card: card, + cardTransferCount: cardCount, note: note, lastDateTransfer: nil) + } + + func testSuccessSaveAccountToFavorites() async { + + // given + local = LocalImpl(database: MockDatabase()) + + // when + do { + let account = try await local.savePersonAccountToFavorites(createBankAccount()) + + XCTAssertEqual(account.isFavorite, true) + XCTAssertEqual(account.person?.name, "hessam") + + } catch { + // then + XCTAssertNil(error) + } + } + + func testSuccessRemoveAccountToFavorites() async { + + // given + local = LocalImpl(database: MockDatabase()) + + // when + do { + let account = try await local.savePersonAccountToFavorites(createBankAccount()) + let removedAccount = try await local.removePersonAccountFromFavorites(account) + + XCTAssertEqual(removedAccount.isFavorite, false) + XCTAssertEqual(account.person?.name, "hessam") + + } catch { + // then + XCTAssertNil(error) + } + } + + func testSuccessFetchFavoriteAccounts() async { + + // given + local = LocalImpl(database: MockDatabase()) + + // when + do { + let _ = try await local.savePersonAccountToFavorites(createBankAccount()) + let accounts = try await local.fetchFavoritePersonAccounts() + + XCTAssertEqual(accounts.count, 1) + + } catch { + // then + XCTAssertNil(error) + } + } +} diff --git a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist index 7e70a09..5eb9d26 100644 --- a/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TransferList.xcodeproj/xcuserdata/hessam.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TransferList.xcscheme_^#shared#^_ orderHint - 2 + 3 From 557d8739a27bc8dcd74f5f3e3f70957c375b357c Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 20:21:31 +0330 Subject: [PATCH 11/16] Add Local Repository Integration in PersonBankAccountRepositoryImpl - Added local repository integration in the `PersonBankAccountRepositoryImpl` to store data in the database. --- .../DataResponse/PersonBankAccountDTO.swift | 28 +++++ .../PersonBankAccountRepositoryImpl.swift | 10 +- Data/Tests/DataTests/HttpTests/ApiTests.swift | 2 +- .../RepositoriesTests/Mock/MockLocal.swift | 66 +++++++++++ .../PersonBankAccountRepositoryTests.swift | 111 ++++++++++++++++-- 5 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift diff --git a/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift index 1c2fcce..fceaae1 100644 --- a/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift @@ -15,4 +15,32 @@ struct PersonBankAccountDTO: Decodable { var note: String? var last_transfer: Date? var isFavorite: Bool = false + + init(person: PersonDTO?, card: CardDTO?, more_info: CardTransferCountDTO?, note: String?, + last_transfer: Date?, isFavorite: Bool = false) { + self.person = person + self.card = card + self.more_info = more_info + self.note = note + self.last_transfer = last_transfer + self.isFavorite = isFavorite + } + + enum CodingKeys: CodingKey { + case person + case card + case more_info + case note + case last_transfer + case isFavorite + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.person = try container.decodeIfPresent(PersonDTO.self, forKey: .person) + self.card = try container.decodeIfPresent(CardDTO.self, forKey: .card) + self.more_info = try container.decodeIfPresent(CardTransferCountDTO.self, forKey: .more_info) + self.note = try container.decodeIfPresent(String.self, forKey: .note) + self.last_transfer = try container.decodeIfPresent(Date.self, forKey: .last_transfer) + } } diff --git a/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift b/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift index ecbbedd..460a23f 100644 --- a/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift +++ b/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift @@ -11,10 +11,12 @@ import Domain public class PersonBankAccountRepositoryImpl: PersonBankAccountRepository { private var api: Api + private var local: Local private let mapper: PersonBankAccountMapper - public init(api: Api) { + public init(api: Api, local: Local) { self.api = api + self.local = local mapper = .init(personMapper: .init(), cardMapper: .init(), cardTransferCountMapper: .init()) @@ -27,15 +29,15 @@ public class PersonBankAccountRepositoryImpl: PersonBankAccountRepository { } public func fetchFavoritePersonAccounts() async throws -> [Domain.PersonBankAccount] { - fatalError() + try await local.fetchFavoritePersonAccounts() } public func savePersonAccountToFavorites(_ personBankAccount: Domain.PersonBankAccount) async throws -> Domain.PersonBankAccount { - fatalError() + try await local.savePersonAccountToFavorites(personBankAccount) } public func removePersonAccountFromFavorites(_ personBankAccount: Domain.PersonBankAccount) async throws -> Domain.PersonBankAccount { - fatalError() + try await local.removePersonAccountFromFavorites(personBankAccount) } public func updatefavoriteStatusForPersonAccount(_ personBankAccount: Domain.PersonBankAccount) async -> Domain.PersonBankAccount { diff --git a/Data/Tests/DataTests/HttpTests/ApiTests.swift b/Data/Tests/DataTests/HttpTests/ApiTests.swift index 0553216..8adb6ad 100644 --- a/Data/Tests/DataTests/HttpTests/ApiTests.swift +++ b/Data/Tests/DataTests/HttpTests/ApiTests.swift @@ -16,7 +16,7 @@ final class ApiTests: XCTestCase { api = nil } - func testRealMediaListApiCall() async { + func testRealTransferListApiCall() async { // given api = ApiImpl() diff --git a/Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift b/Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift new file mode 100644 index 0000000..600e595 --- /dev/null +++ b/Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift @@ -0,0 +1,66 @@ +// +// MockLocal.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation +import Domain +@testable import Data + +class MockLocal: Local { + + func createAccount() -> PersonBankAccount { + let person = Person(name: "hessam", email: "h.mahdi", avatar: nil) + let card = Card(cardNumber: "123", cardType: "master") + let cardCount = CardTransferCount(numberOfTransfers: 12, totalTransfer: 12) + let note = "note" + + return .init(person: person, card: card, + cardTransferCount: cardCount, note: note, lastDateTransfer: nil) + } + + func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] { + return [ + createAccount() + ] + } + + func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + var newAccount = personBankAccount + newAccount.update(favoriteStatus: true) + return newAccount + } + + func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + var newAccount = personBankAccount + newAccount.update(favoriteStatus: false) + return newAccount + } +} + +class MockSucceesRemoveLocal: MockLocal { + + override func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] { + [] + } +} + +class MockFailSaveOrRemoveLocal: MockSucceesRemoveLocal { + + override func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + throw LocalError.cannotSavePersonAccountToFavorites + } + + override func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount { + throw LocalError.cannotRemovePersonAccountFromFavorites + } +} + +class MockFailFetchFavoriteAccountLocal: MockLocal { + + override func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] { + throw LocalError.cannotFetchFavorites + } +} diff --git a/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift b/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift index 9cf0cbc..a596e2e 100644 --- a/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift +++ b/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift @@ -10,17 +10,18 @@ import Domain @testable import Data final class PersonBankAccountRepositoryTests: XCTestCase { - + var repository: PersonBankAccountRepository! override func tearDown() { repository = nil } - func testSuccessFetchMediaList() async { + func testSuccessFetchTransferList() async { // given - repository = PersonBankAccountRepositoryImpl(api: MockApi()) + repository = PersonBankAccountRepositoryImpl(api: MockApi(), + local: MockLocal()) do { // when @@ -37,20 +38,110 @@ final class PersonBankAccountRepositoryTests: XCTestCase { XCTAssertNil(error) } } - - func testFailFetchMediaList() async { - + + func testFailFetchTransferList() async { + // given - repository = PersonBankAccountRepositoryImpl(api: MockFailConnectToServerApi()) - + repository = PersonBankAccountRepositoryImpl(api: MockFailConnectToServerApi() + , local: MockLocal()) + do { // when let _ = try await repository.fetchPersonAccounts(withOffest: 10) - + + } catch { + + // then + XCTAssertNotNil(error) + } + } + + func testSuccessSaveToFavorite() async { + // given + let local = MockLocal() + repository = PersonBankAccountRepositoryImpl(api: MockFailConnectToServerApi() + , local: local) + + do { + // when + let account = try await repository.savePersonAccountToFavorites(local.createAccount()) + XCTAssertEqual(account.isFavorite, true) + } catch { - // then XCTAssertNotNil(error) } } + + func testSuccessRemoveFromFavorite() async { + // given + let local = MockSucceesRemoveLocal() + repository = PersonBankAccountRepositoryImpl(api: MockFailConnectToServerApi() + , local: local) + + do { + // when + let account = try await repository.removePersonAccountFromFavorites(local.createAccount()) + XCTAssertEqual(account.isFavorite, false) + + let accounts = try await repository.fetchFavoritePersonAccounts() + XCTAssertTrue(accounts.isEmpty) + + } catch { + // then + XCTAssertNotNil(error) + } + } + + func testFailSaveFromFavorite() async { + // given + let local = MockFailSaveOrRemoveLocal() + repository = PersonBankAccountRepositoryImpl(api: MockFailConnectToServerApi() + , local: local) + + do { + // when + let _ = try await repository.savePersonAccountToFavorites(local.createAccount()) + + let accounts = try await repository.fetchFavoritePersonAccounts() + XCTAssertTrue(accounts.isEmpty) + + } catch { + // then + XCTAssertEqual(error as? LocalError, .cannotSavePersonAccountToFavorites) + } + } + + func testFailRemoveFromFavorite() async { + // given + let local = MockFailSaveOrRemoveLocal() + repository = PersonBankAccountRepositoryImpl(api: MockFailConnectToServerApi() + , local: local) + + do { + // when + let _ = try await repository.removePersonAccountFromFavorites(local.createAccount()) + + } catch { + // then + XCTAssertEqual(error as? LocalError, .cannotRemovePersonAccountFromFavorites) + } + } + + func testSuccessFetchFavoriteAccounts() async { + + // given + let local = MockFailFetchFavoriteAccountLocal() + repository = PersonBankAccountRepositoryImpl(api: MockApi(), + local: local) + + do { + // when + let _ = try await repository.fetchFavoritePersonAccounts() + + } catch { + // then + XCTAssertEqual(error as? LocalError, .cannotFetchFavorites) + } + } } From e374d015b61045e90aa14b0d1a457ac9336f55f1 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 20:54:50 +0330 Subject: [PATCH 12/16] Fix Error Message Retrieval from Server on Invalid Page Number Request --- Data/Sources/Data/Http/ApiImpl.swift | 12 +++++- .../Http/DataResponse/ResponseError.swift | 34 +++++++++++++++++ Data/Sources/Data/Http/NetworkError.swift | 13 ++++++- Data/Tests/DataTests/HttpTests/ApiTests.swift | 38 +++++++++++++++++++ .../Mock/ResponseMockURLProtocol.swift | 15 +++++++- 5 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 Data/Sources/Data/Http/DataResponse/ResponseError.swift diff --git a/Data/Sources/Data/Http/ApiImpl.swift b/Data/Sources/Data/Http/ApiImpl.swift index 144d971..e1d9d6b 100644 --- a/Data/Sources/Data/Http/ApiImpl.swift +++ b/Data/Sources/Data/Http/ApiImpl.swift @@ -58,7 +58,17 @@ final public class ApiImpl: Api { } case .failure: - continuation.resume(throwing: NetworkError.cannotConnectToServer) + guard let data = responseData.data else { + continuation.resume(throwing: NetworkError.cannotConnectToServer) + return + } + + guard let responseError = try? decoder.decode(ResponseError.self, from: data) else { + continuation.resume(throwing: NetworkError.cannotConnectToServer) + return + } + + continuation.resume(throwing: NetworkError.serverError(message: responseError)) } } } diff --git a/Data/Sources/Data/Http/DataResponse/ResponseError.swift b/Data/Sources/Data/Http/DataResponse/ResponseError.swift new file mode 100644 index 0000000..212d234 --- /dev/null +++ b/Data/Sources/Data/Http/DataResponse/ResponseError.swift @@ -0,0 +1,34 @@ +// +// ResponseError.swift +// +// +// Created by Hessam Mahdiabadi on 11/5/23. +// + +import Foundation + +struct ResponseError: Decodable, CustomStringConvertible, Equatable { + + var message: String? + + var description: String { + return message ?? "Unexpected error" + } + + static func == (lhs: ResponseError, rhs: ResponseError) -> Bool { + return lhs.message == rhs.message + } + + enum CodingKeys: CodingKey { + case error + } + + init(message: String?) { + self.message = message + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.message = try container.decodeIfPresent(String.self, forKey: .error) + } +} diff --git a/Data/Sources/Data/Http/NetworkError.swift b/Data/Sources/Data/Http/NetworkError.swift index a0b4fde..1157dd3 100644 --- a/Data/Sources/Data/Http/NetworkError.swift +++ b/Data/Sources/Data/Http/NetworkError.swift @@ -7,15 +7,26 @@ import Foundation -enum NetworkError: Error, LocalizedError { +enum NetworkError: Error, LocalizedError, Equatable { case cannotConnectToServer case cannotParseJson + case serverError(message: ResponseError) var errorDescription: String? { switch self { case .cannotConnectToServer: return "You seem to be offline!" case .cannotParseJson: return "Unexpected error" + case .serverError(let error): return String(describing: error) + } + } + + static func == (lhs: NetworkError, rhs: NetworkError) -> Bool { + switch (lhs, rhs) { + case (.cannotConnectToServer, .cannotConnectToServer): return true + case (.cannotParseJson, .cannotParseJson): return true + case (.serverError(let lhsError), .serverError(let rhsError)): return lhsError == rhsError + default: return false } } } diff --git a/Data/Tests/DataTests/HttpTests/ApiTests.swift b/Data/Tests/DataTests/HttpTests/ApiTests.swift index 8adb6ad..e10eac0 100644 --- a/Data/Tests/DataTests/HttpTests/ApiTests.swift +++ b/Data/Tests/DataTests/HttpTests/ApiTests.swift @@ -37,6 +37,44 @@ final class ApiTests: XCTestCase { XCTAssertNil(error as? NetworkError) } } + + func testRealServerErrorTransferListApiCall() async { + + // given + api = ApiImpl() + do { + + // when + let _ = try await api.callApi(route: .transferList(offset: -1), + decodeType: [PersonBankAccountDTO].self) + + } catch { + // then + XCTAssertNotNil(error as? NetworkError) + let responseError = ResponseError(message: "page-number starts from 1") + XCTAssertEqual(error as? NetworkError, .serverError(message: responseError)) + } + } + + func testMockServerErrorTransferListApiCall() async { + + // given + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [ResponseMockURLProtocol.self] + let api = ApiImpl(configuration: configuration) + do { + + // when + let _ = try await api.callApi(route: .transferList(offset: -1), + decodeType: [PersonBankAccountDTO].self) + + } catch { + // then + XCTAssertNotNil(error as? NetworkError) + let responseError = ResponseError(message: "page-number starts from 1") + XCTAssertEqual(error as? NetworkError, .serverError(message: responseError)) + } + } func testSuccessMockResponse() async { diff --git a/Data/Tests/DataTests/HttpTests/Mock/ResponseMockURLProtocol.swift b/Data/Tests/DataTests/HttpTests/Mock/ResponseMockURLProtocol.swift index fedb270..b27f0dc 100644 --- a/Data/Tests/DataTests/HttpTests/Mock/ResponseMockURLProtocol.swift +++ b/Data/Tests/DataTests/HttpTests/Mock/ResponseMockURLProtocol.swift @@ -98,6 +98,19 @@ class ResponseMockURLProtocol: URLProtocol { httpResponse: httpResponse2, error: nil) - return [url: response, url2: response2] + let strUrl3 = "https://191da1ac-768c-4c6a-80ad-b533beafec25.mock.pstmn.io/transfer-list/-1" + let url3 = URL(string: strUrl3)! + let data3 = #""" +{ + "error": "page-number starts from 1" +} +"""#.data(using: .utf8) + let httpResponse3 = HTTPURLResponse(url: url, statusCode: 400, + httpVersion: nil, headerFields: nil) + let response3 = ApiMockResponse(url: url3, data: data3, + httpResponse: httpResponse3, + error: nil) + + return [url: response, url2: response2, url3: response3] } } From 852364d92a8a282c8e7d2dd9c3bdb037262ef219 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 21:27:08 +0330 Subject: [PATCH 13/16] Resolve Name Violation: Variable Name Contains Only Alphanumeric Characters for DTOs Models --- .../Data/Http/DataResponse/CardDTO.swift | 20 +++++++++- .../DataResponse/CardTransferCountDTO.swift | 40 +++++++++++-------- .../DataResponse/PersonBankAccountDTO.swift | 29 +++++++------- .../Data/Http/DataResponse/PersonDTO.swift | 21 +++++++++- .../Sources/Data/Http/Mapper/CardMapper.swift | 4 +- .../Http/Mapper/CardTransferCountMapper.swift | 8 ++-- .../Http/Mapper/PersonBankAccountMapper.swift | 8 ++-- .../Data/Http/Mapper/PersonMapper.swift | 4 +- ...BankAccountEntity+CoreDataProperties.swift | 2 +- .../Database/Mapper/CardEntityMapper.swift | 6 +-- .../PersonBankAccountEntityMapper.swift | 8 ++-- .../Database/Mapper/PersonEntityMapper.swift | 4 +- .../db.xcdatamodeld/db.xcdatamodel/contents | 2 +- Data/Tests/DataTests/HttpTests/ApiTests.swift | 9 +++-- .../DatabaseTests/DatabaseTests.swift | 4 +- .../RepositoriesTests/Mock/MockApi.swift | 10 ++--- 16 files changed, 110 insertions(+), 69 deletions(-) diff --git a/Data/Sources/Data/Http/DataResponse/CardDTO.swift b/Data/Sources/Data/Http/DataResponse/CardDTO.swift index 9805228..fb91e9f 100644 --- a/Data/Sources/Data/Http/DataResponse/CardDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/CardDTO.swift @@ -9,6 +9,22 @@ import Foundation struct CardDTO: Decodable { - var card_number: String? - var card_type: String? + var cardNumber: String? + var cardType: String? + + init(cardNumber: String? = nil, cardType: String? = nil) { + self.cardNumber = cardNumber + self.cardType = cardType + } + + enum CodingKeys: String, CodingKey { + case cardNumber = "card_number" + case cardType = "card_type" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.cardNumber = try container.decodeIfPresent(String.self, forKey: .cardNumber) + self.cardType = try container.decodeIfPresent(String.self, forKey: .cardType) + } } diff --git a/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift b/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift index b19f0d4..629f753 100644 --- a/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/CardTransferCountDTO.swift @@ -9,31 +9,37 @@ import Foundation class CardTransferCountDTO: NSObject, NSSecureCoding, Decodable { - enum Key: String { - case numberOfTransfers - case totalTransfer - } - static var supportsSecureCoding: Bool = true - var number_of_transfers: Int? - var total_transfer: Int? + var numberOfTransfers: Int? + var totalTransfer: Int? - init(number_of_transfers: Int? = nil, total_transfer: Int? = nil) { - self.number_of_transfers = number_of_transfers - self.total_transfer = total_transfer + init(numberOfTransfers: Int? = nil, totalTransfer: Int? = nil) { + self.numberOfTransfers = numberOfTransfers + self.totalTransfer = totalTransfer } func encode(with coder: NSCoder) { - coder.encode(number_of_transfers, forKey: Key.numberOfTransfers.rawValue) - coder.encode(total_transfer, forKey: Key.totalTransfer.rawValue) + coder.encode(numberOfTransfers, forKey: CodingKeys.numberOfTransfers.rawValue) + coder.encode(totalTransfer, forKey: CodingKeys.totalTransfer.rawValue) + } + + enum CodingKeys: String, CodingKey { + case numberOfTransfers = "number_of_transfers" + case totalTransfer = "total_transfer" + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.numberOfTransfers = try container.decodeIfPresent(Int.self, forKey: .numberOfTransfers) + self.totalTransfer = try container.decodeIfPresent(Int.self, forKey: .totalTransfer) } required convenience init?(coder: NSCoder) { - let number_of_transfers = coder.decodeObject(of: NSNumber.self, - forKey: Key.numberOfTransfers.rawValue)?.intValue - let total_transfer = coder.decodeObject(of: NSNumber.self, - forKey: Key.totalTransfer.rawValue)?.intValue + let numberOfTransfers = coder.decodeObject(of: NSNumber.self, + forKey: CodingKeys.numberOfTransfers.rawValue)?.intValue + let totalTransfer = coder.decodeObject(of: NSNumber.self, + forKey: CodingKeys.totalTransfer.rawValue)?.intValue - self.init(number_of_transfers: number_of_transfers, total_transfer: total_transfer) + self.init(numberOfTransfers: numberOfTransfers, totalTransfer: totalTransfer) } } diff --git a/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift index fceaae1..ca16c8c 100644 --- a/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/PersonBankAccountDTO.swift @@ -11,36 +11,35 @@ struct PersonBankAccountDTO: Decodable { var person: PersonDTO? var card: CardDTO? - var more_info: CardTransferCountDTO? + var moreInfo: CardTransferCountDTO? var note: String? - var last_transfer: Date? + var lastTransfer: Date? var isFavorite: Bool = false - init(person: PersonDTO?, card: CardDTO?, more_info: CardTransferCountDTO?, note: String?, - last_transfer: Date?, isFavorite: Bool = false) { + init(person: PersonDTO?, card: CardDTO?, moreInfo: CardTransferCountDTO?, note: String?, + lastTransfer: Date?, isFavorite: Bool = false) { self.person = person self.card = card - self.more_info = more_info + self.moreInfo = moreInfo self.note = note - self.last_transfer = last_transfer + self.lastTransfer = lastTransfer self.isFavorite = isFavorite } - enum CodingKeys: CodingKey { - case person - case card - case more_info - case note - case last_transfer - case isFavorite + enum CodingKeys: String, CodingKey { + case person = "person" + case card = "card" + case moreInfo = "more_info" + case note = "note" + case lastTransfer = "lastTransfer" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.person = try container.decodeIfPresent(PersonDTO.self, forKey: .person) self.card = try container.decodeIfPresent(CardDTO.self, forKey: .card) - self.more_info = try container.decodeIfPresent(CardTransferCountDTO.self, forKey: .more_info) + self.moreInfo = try container.decodeIfPresent(CardTransferCountDTO.self, forKey: .moreInfo) self.note = try container.decodeIfPresent(String.self, forKey: .note) - self.last_transfer = try container.decodeIfPresent(Date.self, forKey: .last_transfer) + self.lastTransfer = try container.decodeIfPresent(Date.self, forKey: .lastTransfer) } } diff --git a/Data/Sources/Data/Http/DataResponse/PersonDTO.swift b/Data/Sources/Data/Http/DataResponse/PersonDTO.swift index b4fdb30..dc7410c 100644 --- a/Data/Sources/Data/Http/DataResponse/PersonDTO.swift +++ b/Data/Sources/Data/Http/DataResponse/PersonDTO.swift @@ -9,7 +9,26 @@ import Foundation struct PersonDTO: Decodable { - var full_name: String? + var fullName: String? var email: String? var avatar: String? + + init(fullName: String? = nil, email: String? = nil, avatar: String? = nil) { + self.fullName = fullName + self.email = email + self.avatar = avatar + } + + enum CodingKeys: String, CodingKey { + case fullName = "full_name" + case email = "email" + case avatar = "avatar" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.fullName = try container.decodeIfPresent(String.self, forKey: .fullName) + self.email = try container.decodeIfPresent(String.self, forKey: .email) + self.avatar = try container.decodeIfPresent(String.self, forKey: .avatar) + } } diff --git a/Data/Sources/Data/Http/Mapper/CardMapper.swift b/Data/Sources/Data/Http/Mapper/CardMapper.swift index 371895c..b9f0592 100644 --- a/Data/Sources/Data/Http/Mapper/CardMapper.swift +++ b/Data/Sources/Data/Http/Mapper/CardMapper.swift @@ -15,11 +15,11 @@ struct CardMapper: Mapper { func mapDtoToEntity(input: CardDTO?) -> Card? { guard let input else { return nil } - return .init(cardNumber: input.card_number, cardType: input.card_type) + return .init(cardNumber: input.cardNumber, cardType: input.cardType) } func mapEntityToDto(input: Card?) -> CardDTO? { guard let input else { return nil } - return .init(card_number: input.cardNumber, card_type: input.cardType) + return .init(cardNumber: input.cardNumber, cardType: input.cardType) } } diff --git a/Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift b/Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift index 3cf421d..7c1cfb3 100644 --- a/Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift +++ b/Data/Sources/Data/Http/Mapper/CardTransferCountMapper.swift @@ -15,13 +15,13 @@ struct CardTransferCountMapper: Mapper { func mapDtoToEntity(input: CardTransferCountDTO?) -> CardTransferCount? { guard let input else { return nil } - return .init(numberOfTransfers: input.number_of_transfers, - totalTransfer: input.total_transfer) + return .init(numberOfTransfers: input.numberOfTransfers, + totalTransfer: input.totalTransfer) } func mapEntityToDto(input: CardTransferCount?) -> CardTransferCountDTO? { guard let input else { return nil } - return .init(number_of_transfers: input.numberOfTransfers, - total_transfer: input.totalTransfer) + return .init(numberOfTransfers: input.numberOfTransfers, + totalTransfer: input.totalTransfer) } } diff --git a/Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift b/Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift index 0d15b59..860973f 100644 --- a/Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift +++ b/Data/Sources/Data/Http/Mapper/PersonBankAccountMapper.swift @@ -27,16 +27,16 @@ struct PersonBankAccountMapper: Mapper { func mapDtoToEntity(input: PersonBankAccountDTO) -> PersonBankAccount { let person = personMapper.mapDtoToEntity(input: input.person) let card = cardMapper.mapDtoToEntity(input: input.card) - let transferCount = cardTransferCountMapper.mapDtoToEntity(input: input.more_info) + let transferCount = cardTransferCountMapper.mapDtoToEntity(input: input.moreInfo) return .init(person: person, card: card, cardTransferCount: transferCount, note: input.note, - lastDateTransfer: input.last_transfer) + lastDateTransfer: input.lastTransfer) } func mapEntityToDto(input: PersonBankAccount) -> PersonBankAccountDTO { let person = personMapper.mapEntityToDto(input: input.person) let card = cardMapper.mapEntityToDto(input: input.card) let transferCount = cardTransferCountMapper.mapEntityToDto(input: input.cardTransferCount) - return .init(person: person, card: card, more_info: transferCount, note: input.note, - last_transfer: input.lastDateTransfer) + return .init(person: person, card: card, moreInfo: transferCount, note: input.note, + lastTransfer: input.lastDateTransfer) } } diff --git a/Data/Sources/Data/Http/Mapper/PersonMapper.swift b/Data/Sources/Data/Http/Mapper/PersonMapper.swift index 7b26946..0e168cd 100644 --- a/Data/Sources/Data/Http/Mapper/PersonMapper.swift +++ b/Data/Sources/Data/Http/Mapper/PersonMapper.swift @@ -15,13 +15,13 @@ struct PersonMapper: Mapper { func mapDtoToEntity(input: PersonDTO?) -> Person? { guard let input else { return nil } - return .init(name: input.full_name, email: input.email, + return .init(name: input.fullName, email: input.email, avatar: input.avatar) } func mapEntityToDto(input: Person?) -> PersonDTO? { guard let input else { return nil } - return .init(full_name: input.name, email: input.email, + return .init(fullName: input.name, email: input.email, avatar: input.avatar) } } diff --git a/Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift b/Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift index 7e41648..8f5f802 100644 --- a/Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift +++ b/Data/Sources/Data/Local/Database/ManagedObject/PersonBankAccountEntity+CoreDataProperties.swift @@ -17,7 +17,7 @@ extension PersonBankAccountEntity { @NSManaged var dateSaved: Date? @NSManaged var lastTransfer: Date? - @NSManaged var more_info: CardTransferCountDTO? + @NSManaged var moreInfo: CardTransferCountDTO? @NSManaged var note: String? @NSManaged var isFavorite: Bool @NSManaged var card: CardEntity? diff --git a/Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift b/Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift index eb9baab..00e5afe 100644 --- a/Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift +++ b/Data/Sources/Data/Local/Database/Mapper/CardEntityMapper.swift @@ -22,14 +22,14 @@ struct CardEntityMapper: Mapper { func mapDtoToEntity(input: CardDTO?) -> CardEntity? { guard let input else { return nil } let entity = CardEntity(context: context) - entity.cardType = input.card_type - entity.cardNumber = input.card_number + entity.cardType = input.cardType + entity.cardNumber = input.cardNumber return entity } func mapEntityToDto(input: CardEntity?) -> CardDTO? { guard let input else { return nil } - return .init(card_number: input.cardNumber, card_type: input.cardType) + return .init(cardNumber: input.cardNumber, cardType: input.cardType) } } diff --git a/Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift b/Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift index 8515fe3..de57b09 100644 --- a/Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift +++ b/Data/Sources/Data/Local/Database/Mapper/PersonBankAccountEntityMapper.swift @@ -28,10 +28,10 @@ struct PersonBankAccountEntityMapper: Mapper { func mapDtoToEntity(input: PersonBankAccountDTO) -> PersonBankAccountEntity { let entity = PersonBankAccountEntity(context: context) entity.person = personEntityMapper.mapDtoToEntity(input: input.person) - entity.more_info = input.more_info + entity.moreInfo = input.moreInfo entity.card = cardEntityMapper.mapDtoToEntity(input: input.card) entity.note = input.note - entity.lastTransfer = input.last_transfer + entity.lastTransfer = input.lastTransfer entity.dateSaved = Date() return entity @@ -41,7 +41,7 @@ struct PersonBankAccountEntityMapper: Mapper { let person = personEntityMapper.mapEntityToDto(input: input.person) let card = cardEntityMapper.mapEntityToDto(input: input.card) return .init(person: person, card: card, - more_info: input.more_info, note: input.note, - last_transfer: input.lastTransfer, isFavorite: input.isFavorite) + moreInfo: input.moreInfo, note: input.note, + lastTransfer: input.lastTransfer, isFavorite: input.isFavorite) } } diff --git a/Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift b/Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift index 57f6104..2649b87 100644 --- a/Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift +++ b/Data/Sources/Data/Local/Database/Mapper/PersonEntityMapper.swift @@ -22,7 +22,7 @@ struct PersonEntityMapper: Mapper { func mapDtoToEntity(input: PersonDTO?) -> PersonEntity? { guard let input else { return nil } let entity = PersonEntity(context: context) - entity.name = input.full_name + entity.name = input.fullName entity.avatar = input.avatar entity.email = input.email @@ -31,7 +31,7 @@ struct PersonEntityMapper: Mapper { func mapEntityToDto(input: PersonEntity?) -> PersonDTO? { guard let input else { return nil } - return .init(full_name: input.name, email: input.email, + return .init(fullName: input.name, email: input.email, avatar: input.avatar) } } diff --git a/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents b/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents index ac9abab..f276418 100644 --- a/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents +++ b/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents @@ -9,7 +9,7 @@ - + diff --git a/Data/Tests/DataTests/HttpTests/ApiTests.swift b/Data/Tests/DataTests/HttpTests/ApiTests.swift index e10eac0..0d29f29 100644 --- a/Data/Tests/DataTests/HttpTests/ApiTests.swift +++ b/Data/Tests/DataTests/HttpTests/ApiTests.swift @@ -28,9 +28,10 @@ final class ApiTests: XCTestCase { // then XCTAssertEqual(accounts.count, 10) - XCTAssertEqual(accounts.first?.person?.full_name, "Jemimah Sprott") + XCTAssertEqual(accounts.first?.person?.fullName, "Jemimah Sprott") XCTAssertEqual(accounts.first?.person?.email, nil) - XCTAssertEqual(accounts.first?.card?.card_number, "5602217292772382") + XCTAssertEqual(accounts.first?.card?.cardNumber, "5602217292772382") + XCTAssertEqual(accounts.first?.moreInfo?.numberOfTransfers, 74) } catch { // then @@ -90,9 +91,9 @@ final class ApiTests: XCTestCase { // then XCTAssertEqual(accounts.count, 2) - XCTAssertEqual(accounts.first?.person?.full_name, "Jemimah Sprott") + XCTAssertEqual(accounts.first?.person?.fullName, "Jemimah Sprott") XCTAssertEqual(accounts.first?.person?.email, nil) - XCTAssertEqual(accounts.first?.card?.card_number, "5602217292772382") + XCTAssertEqual(accounts.first?.card?.cardNumber, "5602217292772382") } catch { diff --git a/Data/Tests/DataTests/LocalTests/DatabaseTests/DatabaseTests.swift b/Data/Tests/DataTests/LocalTests/DatabaseTests/DatabaseTests.swift index aa5af8d..f78451a 100644 --- a/Data/Tests/DataTests/LocalTests/DatabaseTests/DatabaseTests.swift +++ b/Data/Tests/DataTests/LocalTests/DatabaseTests/DatabaseTests.swift @@ -36,7 +36,7 @@ final class DatabaseTests: XCTestCase { account.lastTransfer = nil account.person = person account.card = card - account.more_info = .init(number_of_transfers: 1, total_transfer: nil) + account.moreInfo = .init(numberOfTransfers: 1, totalTransfer: nil) } func testSavePersonAccount() { @@ -62,7 +62,7 @@ final class DatabaseTests: XCTestCase { XCTAssertEqual(results.first?.note, "note") XCTAssertEqual(results.first?.person?.name, "hessam") XCTAssertEqual(results.first?.card?.cardNumber, "123") - XCTAssertEqual(results.first?.more_info?.number_of_transfers, 1) + XCTAssertEqual(results.first?.moreInfo?.numberOfTransfers, 1) } } catch { diff --git a/Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift b/Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift index d10ef81..03149c7 100644 --- a/Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift +++ b/Data/Tests/DataTests/RepositoriesTests/Mock/MockApi.swift @@ -11,13 +11,13 @@ import Foundation final class MockApi: Api { func callApi(route: ApiRouter, decodeType type: T.Type) async throws -> T where T : Decodable { - let person = PersonDTO(full_name: "hessam", email: "h.mahdi", avatar: nil) - let card = CardDTO(card_number: "123", card_type: "master") - let cardCount = CardTransferCountDTO(number_of_transfers: 12, total_transfer: 12) + let person = PersonDTO(fullName: "hessam", email: "h.mahdi", avatar: nil) + let card = CardDTO(cardNumber: "123", cardType: "master") + let cardCount = CardTransferCountDTO(numberOfTransfers: 12, totalTransfer: 12) let note = "note" let account = PersonBankAccountDTO(person: person, card: card, - more_info: cardCount, note: note, - last_transfer: nil) + moreInfo: cardCount, note: note, + lastTransfer: nil) guard let retVal = [account] as? T else { throw NetworkError.cannotParseJson From 16929860f48226e8f8b470080f1cc2ba6d8b4046 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 21:50:59 +0330 Subject: [PATCH 14/16] Add Update of Favorite Status If Exist in Database --- Data/Sources/Data/Local/Local.swift | 1 + Data/Sources/Data/Local/LocalImpl.swift | 46 +++++++++++++++++++ .../PersonBankAccountRepositoryImpl.swift | 4 +- .../DataTests/LocalTests/LocalTests.swift | 41 ++++++++++++++++- .../RepositoriesTests/Mock/MockLocal.swift | 15 ++++++ .../PersonBankAccountRepositoryTests.swift | 40 ++++++++++++++++ .../PersonBankAccountRepository.swift | 2 +- .../PersonBankAccountUseCaseImpl.swift | 2 +- 8 files changed, 146 insertions(+), 5 deletions(-) diff --git a/Data/Sources/Data/Local/Local.swift b/Data/Sources/Data/Local/Local.swift index 646c59a..5ae0531 100644 --- a/Data/Sources/Data/Local/Local.swift +++ b/Data/Sources/Data/Local/Local.swift @@ -13,4 +13,5 @@ public protocol Local { func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount + func updatefavoriteStatusBasedOnFavorites(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount } diff --git a/Data/Sources/Data/Local/LocalImpl.swift b/Data/Sources/Data/Local/LocalImpl.swift index 87a8df3..8c3bdd7 100644 --- a/Data/Sources/Data/Local/LocalImpl.swift +++ b/Data/Sources/Data/Local/LocalImpl.swift @@ -151,4 +151,50 @@ public class LocalImpl: Local { } } } + + public func updatefavoriteStatusBasedOnFavorites(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount { + return await withCheckedContinuation { [weak self] continuation in + guard let self else { + continuation.resume(returning: personBankAccount) + return + } + + self.database.backgroundContext.performAndWait { [weak self] in + guard let self else { + continuation.resume(returning: personBankAccount) + return + } + + do { + let fetchRequest: + NSFetchRequest = PersonBankAccountEntity.fetchRequest() + let sort = PersonBankAccountEntity.sortDescriptor + fetchRequest.sortDescriptors = [sort] + fetchRequest.fetchLimit = 1 + + let namePredicate = NSPredicate(format: "person.name == %@", + personBankAccount.person?.name ?? "") + let cardNumberPredicate = NSPredicate(format: "card.cardNumber == %@", + personBankAccount.card?.cardNumber ?? "") + let predicate = NSCompoundPredicate(type: .and, + subpredicates: [namePredicate, cardNumberPredicate]) + fetchRequest.predicate = predicate + + try Task.checkCancellation() + let accounts = try self.database.backgroundContext.fetch(fetchRequest) + guard let entity = accounts.first else { + continuation.resume(returning: personBankAccount) + return + } + + var newPersonBankAccount = personBankAccount + newPersonBankAccount.update(favoriteStatus: entity.isFavorite) + continuation.resume(returning: newPersonBankAccount) + + } catch { + continuation.resume(returning: personBankAccount) + } + } + } + } } diff --git a/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift b/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift index 460a23f..542cb43 100644 --- a/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift +++ b/Data/Sources/Data/Repositories/PersonBankAccountRepositoryImpl.swift @@ -40,7 +40,7 @@ public class PersonBankAccountRepositoryImpl: PersonBankAccountRepository { try await local.removePersonAccountFromFavorites(personBankAccount) } - public func updatefavoriteStatusForPersonAccount(_ personBankAccount: Domain.PersonBankAccount) async -> Domain.PersonBankAccount { - fatalError() + public func updatefavoriteStatusBasedOnFavorites(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount { + await local.updatefavoriteStatusBasedOnFavorites(personBankAccount) } } diff --git a/Data/Tests/DataTests/LocalTests/LocalTests.swift b/Data/Tests/DataTests/LocalTests/LocalTests.swift index 8a9982d..a16a06d 100644 --- a/Data/Tests/DataTests/LocalTests/LocalTests.swift +++ b/Data/Tests/DataTests/LocalTests/LocalTests.swift @@ -70,7 +70,7 @@ final class LocalTests: XCTestCase { // when do { - let _ = try await local.savePersonAccountToFavorites(createBankAccount()) + let _ = try await local.savePersonAccountToFavorites(createBankAccount()) let accounts = try await local.fetchFavoritePersonAccounts() XCTAssertEqual(accounts.count, 1) @@ -80,4 +80,43 @@ final class LocalTests: XCTestCase { XCTAssertNil(error) } } + + func testSuccessUpdateFavoriteStatus() async { + + // given + local = LocalImpl(database: MockDatabase()) + + // when + do { + let _ = try await local.savePersonAccountToFavorites(createBankAccount()) + let account = await local.updatefavoriteStatusBasedOnFavorites(createBankAccount()) + + XCTAssertEqual(account.isFavorite, true) + + } catch { + // then + XCTAssertNil(error) + } + } + + func testDoesNotUpdateFavoriteStatus() async { + + // given + local = LocalImpl(database: MockDatabase()) + + // when + do { + let _ = try await local.savePersonAccountToFavorites(createBankAccount()) + var newAccountDoesNotExsit = createBankAccount() + newAccountDoesNotExsit.person?.name = "new person" + newAccountDoesNotExsit.card?.cardNumber = "453" + let account = await local.updatefavoriteStatusBasedOnFavorites(newAccountDoesNotExsit) + + XCTAssertEqual(account.isFavorite, false) + + } catch { + // then + XCTAssertNil(error) + } + } } diff --git a/Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift b/Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift index 600e595..afd3043 100644 --- a/Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift +++ b/Data/Tests/DataTests/RepositoriesTests/Mock/MockLocal.swift @@ -38,6 +38,12 @@ class MockLocal: Local { newAccount.update(favoriteStatus: false) return newAccount } + + func updatefavoriteStatusBasedOnFavorites(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount { + var newAccount = personBankAccount + newAccount.update(favoriteStatus: true) + return newAccount + } } class MockSucceesRemoveLocal: MockLocal { @@ -64,3 +70,12 @@ class MockFailFetchFavoriteAccountLocal: MockLocal { throw LocalError.cannotFetchFavorites } } + +class MockDoesNotExistInFavoriteLocal: MockLocal { + + override func updatefavoriteStatusBasedOnFavorites(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount { + var newAccount = personBankAccount + newAccount.update(favoriteStatus: false) + return newAccount + } +} diff --git a/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift b/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift index a596e2e..5763e75 100644 --- a/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift +++ b/Data/Tests/DataTests/RepositoriesTests/PersonBankAccountRepositoryTests.swift @@ -144,4 +144,44 @@ final class PersonBankAccountRepositoryTests: XCTestCase { XCTAssertEqual(error as? LocalError, .cannotFetchFavorites) } } + + func testSuccessUpdateFavoriteStatus() async { + + // given + let local = MockLocal() + repository = PersonBankAccountRepositoryImpl(api: MockApi(), + local: local) + + // when + do { + let _ = try await repository.savePersonAccountToFavorites(local.createAccount()) + let account = await repository.updatefavoriteStatusBasedOnFavorites(local.createAccount()) + + XCTAssertEqual(account.isFavorite, true) + + } catch { + // then + XCTAssertNil(error) + } + } + + func testDoesNotUpdateFavoriteStatus() async { + + // given + let local = MockDoesNotExistInFavoriteLocal() + repository = PersonBankAccountRepositoryImpl(api: MockApi(), + local: local) + + // when + do { + let _ = try await repository.savePersonAccountToFavorites(local.createAccount()) + let account = await repository.updatefavoriteStatusBasedOnFavorites(local.createAccount()) + + XCTAssertEqual(account.isFavorite, false) + + } catch { + // then + XCTAssertNil(error) + } + } } diff --git a/Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift b/Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift index 44b9f89..74de235 100644 --- a/Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift +++ b/Domain/Sources/Domain/Repositories/PersonBankAccountRepository.swift @@ -13,5 +13,5 @@ public protocol PersonBankAccountRepository { func fetchFavoritePersonAccounts() async throws -> [PersonBankAccount] func savePersonAccountToFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount func removePersonAccountFromFavorites(_ personBankAccount: PersonBankAccount) async throws -> PersonBankAccount - func updatefavoriteStatusForPersonAccount(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount + func updatefavoriteStatusBasedOnFavorites(_ personBankAccount: PersonBankAccount) async -> PersonBankAccount } diff --git a/Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift b/Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift index 7cbf183..261fefe 100644 --- a/Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift +++ b/Domain/Sources/Domain/UseCases/PersonBankAccountUseCaseImpl.swift @@ -84,7 +84,7 @@ public class PersonBankAccountUseCaseImpl: PersonBankAccountUseCase { func createUpdateFavoriteStatusTask(atIndex index: Int) { taskGroup.addTask { var updatedAccount = await self.repository - .updatefavoriteStatusForPersonAccount(accounts[index]) + .updatefavoriteStatusBasedOnFavorites(accounts[index]) updatedAccount.update(indexAtList: index) return updatedAccount } From 653f3a446ebceb0806e3bc93c823f77d1dc26be3 Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 22:01:02 +0330 Subject: [PATCH 15/16] fixes compiler error in mock test. implemented updatefavoriteStatusBasedOnFavorites function --- Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift b/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift index 77c6cc5..c812693 100644 --- a/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift +++ b/Domain/Tests/DomainTests/UseCasesTests/MockRepository.swift @@ -9,7 +9,7 @@ import Foundation @testable import Domain class MockPersonBankAccountRepository: PersonBankAccountRepository { - + func createMockPersonAccount() -> PersonBankAccount { let person = Person(name: "hessam", email: "h.mahdi", avatar: nil) let card = Card(cardNumber: "123", cardType: "master") @@ -38,6 +38,10 @@ class MockPersonBankAccountRepository: PersonBankAccountRepository { func updatefavoriteStatusForPersonAccount(_ personBankAccount: Domain.PersonBankAccount) async -> Domain.PersonBankAccount { return personBankAccount } + + func updatefavoriteStatusBasedOnFavorites(_ personBankAccount: Domain.PersonBankAccount) async -> Domain.PersonBankAccount { + return personBankAccount + } } class MockSuccessEmptyFetchAccountRepository: MockPersonBankAccountRepository { From 7998b8468bbac70f0259f3711ef32b61c67ea9cc Mon Sep 17 00:00:00 2001 From: Hessam Mahdiabadi <67460597+iamHEssam@users.noreply.github.com> Date: Sun, 5 Nov 2023 22:30:45 +0330 Subject: [PATCH 16/16] Add GitHub Workflow for Linting and Testing and Add Fetch Index - Added a GitHub workflow to perform linting and tests on the Data package for every pull request. - Added a fetch index to database to enhance performance. --- .github/workflows/Data.yml | 29 +++++++++++++++++++ .github/workflows/Domain.yml | 4 ++- .../db.xcdatamodeld/db.xcdatamodel/contents | 16 ++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/Data.yml diff --git a/.github/workflows/Data.yml b/.github/workflows/Data.yml new file mode 100644 index 0000000..db32a1a --- /dev/null +++ b/.github/workflows/Data.yml @@ -0,0 +1,29 @@ +name: Data Layer + +on: + pull_request: + branches: + - '*' + - '*/*' + +jobs: + build: + runs-on: macos-13 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up SwiftLint + run: brew install swiftlint + + - name: Lint code + run: | + cd Data/Sources/ + swiftlint + + - name: run unit test + run: | + cd Data/ + swift build + swift test \ No newline at end of file diff --git a/.github/workflows/Domain.yml b/.github/workflows/Domain.yml index a0ffcab..5a9a94a 100644 --- a/.github/workflows/Domain.yml +++ b/.github/workflows/Domain.yml @@ -18,7 +18,9 @@ jobs: run: brew install swiftlint - name: Lint code - run: swiftlint + run: | + cd Domain/Sources/ + swiftlint - name: run unit test run: | diff --git a/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents b/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents index f276418..8f92080 100644 --- a/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents +++ b/Data/Sources/Data/Local/Database/db.xcdatamodeld/db.xcdatamodel/contents @@ -4,6 +4,9 @@ + + + @@ -13,11 +16,24 @@ + + + + + + + + + + + + + \ No newline at end of file