Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 54 additions & 41 deletions Core/EntityGateway/Cache/CachePersonsGateway.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,57 +21,91 @@ public final class CachePersonsGateway: PersonsGateway {
public func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonEntityGatewayCompletionHandler) {
Task {
do {
let savedImages = try await save(newImages(in: parameters))
let result = await addToCoreData(parameters: parameters)
if case .failure = result {
await deleteBestEffort(savedImages)
}
await complete(result, using: completionHandler)
await complete(.success(try await add(parameters: parameters)), using: completionHandler)
} catch let error as CoreError {
await complete(.failure(error), using: completionHandler)
} catch {
await complete(.failure(CoreError(error: error)), using: completionHandler)
}
}
}

public func add(parameters: AddPersonParameters) async throws -> Person {
try Task.checkCancellation()
let savedImages = try await save(newImages(in: parameters))
do {
try Task.checkCancellation()
return try await coreDataGateway.add(parameters: parameters)
} catch {
await deleteBestEffort(savedImages)
throw error
}
}

public func fetchPersons(completionHandler: @escaping FetchPersonsEntityGatewayCompletionHandler) {
coreDataGateway.fetchPersons(completionHandler: completionHandler)
}

public func fetchPersons() async throws -> [Person] {
try await coreDataGateway.fetchPersons()
}

public func fetchWidgetPersons(completion: @escaping FetchPersonsEntityGatewayCompletionHandler) {
coreDataGateway.fetchWidgetPersons(completion: completion)
}

public func fetchWidgetPersons() async throws -> [Person] {
try await coreDataGateway.fetchWidgetPersons()
}

public func edit(
person: Person, with parameters: AddPersonParameters,
completionHandler: @escaping EditPersonEntityGatewayCompletionHandler
) {
Task {
do {
let savedImages = try await save(newImages(in: parameters))
let result = await editInCoreData(person: person, parameters: parameters)
switch result {
case .success:
await deleteBestEffort(outdatedImages(for: person, parameters: parameters))
case .failure:
await deleteBestEffort(savedImages)
}
await complete(result, using: completionHandler)
await complete(.success(try await edit(person: person, with: parameters)), using: completionHandler)
} catch let error as CoreError {
await complete(.failure(error), using: completionHandler)
} catch {
await complete(.failure(CoreError(error: error)), using: completionHandler)
}
}
}

public func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person {
try Task.checkCancellation()
let savedImages = try await save(newImages(in: parameters))
do {
try Task.checkCancellation()
let updatedPerson = try await coreDataGateway.edit(person: person, with: parameters)
await deleteBestEffort(outdatedImages(for: person, parameters: parameters))
return updatedPerson
} catch {
await deleteBestEffort(savedImages)
throw error
}
}

public func remove(person: Person, completionHandler: @escaping RemovePersonEntityGatewayCompletionHandler) {
Task {
let result = await removeFromCoreData(person: person)
if case .success = result {
await deleteBestEffort(storedImages(for: person))
do {
try await remove(person: person)
await complete(.success(()), using: completionHandler)
} catch let error as CoreError {
await complete(.failure(error), using: completionHandler)
} catch {
await complete(.failure(CoreError(error: error)), using: completionHandler)
}
await complete(result, using: completionHandler)
}
}

public func remove(person: Person) async throws {
try Task.checkCancellation()
try await coreDataGateway.remove(person: person)
await deleteBestEffort(storedImages(for: person))
}

// MARK: - Images

private func newImages(in parameters: AddPersonParameters) -> [PersonImage] {
Expand Down Expand Up @@ -100,6 +134,7 @@ public final class CachePersonsGateway: PersonsGateway {
var savedImages = [PersonImage]()
do {
for image in images {
try Task.checkCancellation()
try await imageStore.save(image)
savedImages.append(image)
}
Expand All @@ -120,28 +155,6 @@ public final class CachePersonsGateway: PersonsGateway {
}
}

// MARK: - Core Data bridge

private func addToCoreData(parameters: AddPersonParameters) async -> Result<Person, CoreError> {
await withCheckedContinuation { continuation in
coreDataGateway.add(parameters: parameters) { continuation.resume(returning: $0) }
}
}

private func editInCoreData(
person: Person, parameters: AddPersonParameters
) async -> Result<Person, CoreError> {
await withCheckedContinuation { continuation in
coreDataGateway.edit(person: person, with: parameters) { continuation.resume(returning: $0) }
}
}

private func removeFromCoreData(person: Person) async -> Result<Void, CoreError> {
await withCheckedContinuation { continuation in
coreDataGateway.remove(person: person) { continuation.resume(returning: $0) }
}
}

private func complete<Value>(
_ result: Result<Value, CoreError>, using completion: @escaping (Result<Value, CoreError>) -> Void
) async {
Expand Down
47 changes: 47 additions & 0 deletions Core/Gateways/PersonsGateway.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,57 @@ public typealias RemovePersonEntityGatewayCompletionHandler = (_ result: Result<

public protocol PersonsGateway {
func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonEntityGatewayCompletionHandler)
func add(parameters: AddPersonParameters) async throws -> Person
func fetchPersons(completionHandler: @escaping FetchPersonsEntityGatewayCompletionHandler)
func fetchPersons() async throws -> [Person]
func edit(
person: Person, with parameters: AddPersonParameters,
completionHandler: @escaping EditPersonEntityGatewayCompletionHandler)
func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person
func remove(person: Person, completionHandler: @escaping RemovePersonEntityGatewayCompletionHandler)
func remove(person: Person) async throws
func fetchWidgetPersons(completion: @escaping FetchPersonsEntityGatewayCompletionHandler)
func fetchWidgetPersons() async throws -> [Person]
}

public extension PersonsGateway {

func add(parameters: AddPersonParameters) async throws -> Person {
try Task.checkCancellation()
return try await withCheckedThrowingContinuation { continuation in
add(parameters: parameters) { continuation.resume(with: $0) }
}
}

func fetchPersons() async throws -> [Person] {
try Task.checkCancellation()
let persons = try await withCheckedThrowingContinuation { continuation in
fetchPersons { continuation.resume(with: $0) }
}
try Task.checkCancellation()
return persons
}

func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person {
try Task.checkCancellation()
return try await withCheckedThrowingContinuation { continuation in
edit(person: person, with: parameters) { continuation.resume(with: $0) }
}
}

func remove(person: Person) async throws {
try Task.checkCancellation()
try await withCheckedThrowingContinuation { continuation in
remove(person: person) { continuation.resume(with: $0) }
}
}

func fetchWidgetPersons() async throws -> [Person] {
try Task.checkCancellation()
let persons = try await withCheckedThrowingContinuation { continuation in
fetchWidgetPersons { continuation.resume(with: $0) }
}
try Task.checkCancellation()
return persons
}
}
5 changes: 5 additions & 0 deletions Core/UseCases/AddPersonUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public typealias AddPersonUseCaseCompletionHandler = (_ person: Result<Person, C

public protocol AddPersonUseCase {
func add(parameters: AddPersonParameters, completionHandler: @escaping AddPersonUseCaseCompletionHandler)
func add(parameters: AddPersonParameters) async throws -> Person
}

public final class AddPersonUseCaseImplementation: AddPersonUseCase {
Expand All @@ -27,4 +28,8 @@ public final class AddPersonUseCaseImplementation: AddPersonUseCase {
}
}

public func add(parameters: AddPersonParameters) async throws -> Person {
try await personsGateway.add(parameters: parameters)
}

}
5 changes: 5 additions & 0 deletions Core/UseCases/EditPersonUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public protocol EditPersonUseCase {
func edit(
person: Person, with parameters: AddPersonParameters,
completionHandler: @escaping EditPersonUseCaseCompletionHandler)
func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person
}

public final class EditPersonUseCaseImplementation: EditPersonUseCase {
Expand All @@ -32,4 +33,8 @@ public final class EditPersonUseCaseImplementation: EditPersonUseCase {
}
}

public func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person {
try await personsGateway.edit(person: person, with: parameters)
}

}
10 changes: 10 additions & 0 deletions Core/UseCases/FetchPersonsUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public typealias FetchPersonsUseCaseCompletionHandler = (_ persons: Result<[Pers

public protocol FetchPersonsUseCase {
func fetchPersons(completionHandler: @escaping FetchPersonsUseCaseCompletionHandler)
func fetchPersons() async throws -> [Person]
func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler)
func fetchWidgetPersons() async throws -> [Person]
}

public final class FetchPersonsUseCaseImplementation: FetchPersonsUseCase {
Expand All @@ -33,4 +35,12 @@ public final class FetchPersonsUseCaseImplementation: FetchPersonsUseCase {
public func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler) {
personsGateway.fetchWidgetPersons(completion: completion)
}

public func fetchPersons() async throws -> [Person] {
try await personsGateway.fetchPersons()
}

public func fetchWidgetPersons() async throws -> [Person] {
try await personsGateway.fetchWidgetPersons()
}
}
5 changes: 5 additions & 0 deletions Core/UseCases/RemovePersonUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public typealias RemovePersonUseCaseCompletionHandler = (_ result: Result<Void,

public protocol RemovePersonUseCase {
func remove(person: Person, completionHandler: @escaping RemovePersonUseCaseCompletionHandler)
func remove(person: Person) async throws
}

public final class RemovePersonUseCaseImplementation: RemovePersonUseCase {
Expand All @@ -24,4 +25,8 @@ public final class RemovePersonUseCaseImplementation: RemovePersonUseCase {
public func remove(person: Person, completionHandler: @escaping RemovePersonUseCaseCompletionHandler) {
personsGateway.remove(person: person, completionHandler: completionHandler)
}

public func remove(person: Person) async throws {
try await personsGateway.remove(person: person)
}
}
21 changes: 19 additions & 2 deletions GrowingUpTests/Gateways/CachePersonsGatewayTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ final class CachePersonsGatewayTests: XCTestCase {
waitForExpectations(timeout: 1)
}

func testRemoveReportsCoreDataSuccessAfterBestEffortImageCleanup() {
func testRemoveReportsCoreDataSuccessAfterBestEffortImageCleanup() async {
let person = Person.createPerson()
coreDataGatewaySpy.removePersonResultToBeReturned = .success(())
imageStoreSpy.deleteError = CoreError.unknownError
Expand All @@ -113,10 +113,27 @@ final class CachePersonsGatewayTests: XCTestCase {
completed.fulfill()
}

waitForExpectations(timeout: 1)
await fulfillment(of: [completed], timeout: 1)
XCTAssertEqual(completionCount, 1)
}

func testAsyncAddHonorsCancellationBeforeSideEffects() async {
let operation = Task {
try await sut.add(parameters: parameters(appImage: PersonImage(uiImage: UIImage()), widgetImage: nil))
}
operation.cancel()

do {
_ = try await operation.value
XCTFail("Expected cancellation")
} catch is CancellationError {
XCTAssertTrue(imageStoreSpy.savedImages.isEmpty)
XCTAssertFalse(coreDataGatewaySpy.addPersonCalled)
} catch {
XCTFail("Expected CancellationError, got \(error)")
}
}

private func parameters(appImage: PersonImage?, widgetImage: PersonImage?) -> AddPersonParameters {
AddPersonParameters(
name: "John", dayOfBirth: Date(), timeOfBirth: Date(),
Expand Down
6 changes: 6 additions & 0 deletions GrowingUpTests/Helpers/UserCases/AddPersonUseCaseSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class AddPersonUseCaseSpy: AddPersonUseCase {
}
}

func add(parameters: AddPersonParameters) async throws -> Person {
try await withCheckedThrowingContinuation { continuation in
add(parameters: parameters) { continuation.resume(with: $0) }
}
}

func callCompletionHandler() {
self.completionHandler?(resultToBeReturned)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@ class DisplayPersonsUseCaseSpy: FetchPersonsUseCase {
func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler) {
completion(resultToBeReturned)
}

func fetchPersons() async throws -> [Person] {
displayPersonsCalled = true
return try resultToBeReturned.get()
}

func fetchWidgetPersons() async throws -> [Person] {
try resultToBeReturned.get()
}
}
6 changes: 6 additions & 0 deletions GrowingUpTests/Helpers/UserCases/EditPersonUseCaseSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ class EditPersonUseCaseSpy: EditPersonUseCase {
}
}

func edit(person: Person, with parameters: AddPersonParameters) async throws -> Person {
try await withCheckedThrowingContinuation { continuation in
edit(person: person, with: parameters) { continuation.resume(with: $0) }
}
}

func callCompletionHandler() {
self.completionHandler?(resultToBeReturned)
}
Expand Down
8 changes: 8 additions & 0 deletions GrowingUpTests/Helpers/UserCases/FetchPersonsUseCaseSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ final class FetchPersonsUseCaseSpy: FetchPersonsUseCase {
func fetchWidgetPersons(completion: @escaping FetchPersonsUseCaseCompletionHandler) {
completion(completionResult)
}

func fetchPersons() async throws -> [Person] {
try completionResult.get()
}

func fetchWidgetPersons() async throws -> [Person] {
try completionResult.get()
}
}
5 changes: 5 additions & 0 deletions GrowingUpTests/Helpers/UserCases/RemovePersonUseCaseSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ final class RemovePersonUseCaseSpy: RemovePersonUseCase {
didCallRemovePerson = true
completionHandler(resultToBeReturned)
}

func remove(person: Person) async throws {
didCallRemovePerson = true
try resultToBeReturned.get()
}
}
11 changes: 11 additions & 0 deletions GrowingUpTests/UseCases/AddPersonUseCaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,15 @@ class AddPersonUseCaseTests: XCTestCase {
}
waitForExpectations(timeout: 1, handler: nil)
}

func testAsyncAddReturnsPerson() async throws {
let parameters = AddPersonParameters.createParameters()
let expectedPerson = Person.createPerson()
personsGatewaySpy.addPersonResultToBeReturned = .success(expectedPerson)

let person = try await sut.add(parameters: parameters)

XCTAssertEqual(person, expectedPerson)
XCTAssertEqual(personsGatewaySpy.addPersonParameters, parameters)
}
}
Loading
Loading