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
14 changes: 10 additions & 4 deletions EssentialApp/EssentialApp/CombineHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,24 @@ public extension Paginated {
}
}

@MainActor
public extension HTTPClient {
typealias Publisher = AnyPublisher<(Data, HTTPURLResponse), Error>

func getPublisher(url: URL) -> Publisher {
var task: HTTPClientTask?
var task: Task<Void, Never>?

return Deferred {
Future { completion in
nonisolated(unsafe) let uncheckedCompletion = completion
task = self.get(from: url, completion: {
uncheckedCompletion($0)
})
task = Task.immediate {
do {
let result = try await self.get(from: url)
uncheckedCompletion(.success(result))
} catch {
uncheckedCompletion(.failure(error))
}
}
}
}
.handleEvents(receiveCancel: { task?.cancel() })
Expand Down
13 changes: 4 additions & 9 deletions EssentialApp/EssentialAppTests/Helpers/HTTPClientStub.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,14 @@ import Foundation
import EssentialFeed

class HTTPClientStub: HTTPClient {
private class Task: HTTPClientTask {
func cancel() {}
}

private let stub: (URL) -> HTTPClient.Result
private let stub: (URL) -> Result<(Data, HTTPURLResponse), Error>

init(stub: @escaping (URL) -> HTTPClient.Result) {
init(stub: @escaping (URL) -> Result<(Data, HTTPURLResponse), Error>) {
self.stub = stub
}

func get(from url: URL, completion: @escaping (HTTPClient.Result) -> Void) -> HTTPClientTask {
completion(stub(url))
return Task()
func get(from url: URL) async throws -> (Data, HTTPURLResponse) {
try stub(url).get()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,12 @@ public final class URLSessionHTTPClient: HTTPClient {
}

private struct UnexpectedValuesRepresentation: Error {}

private struct URLSessionTaskWrapper: HTTPClientTask {
let wrapped: URLSessionTask

func cancel() {
wrapped.cancel()
public func get(from url: URL) async throws -> (Data, HTTPURLResponse) {
let (data, response) = try await session.data(from: url)
guard let response = response as? HTTPURLResponse else {
throw UnexpectedValuesRepresentation()
}
}

public func get(from url: URL, completion: @Sendable @escaping (HTTPClient.Result) -> Void) -> HTTPClientTask {
let task = session.dataTask(with: url) { data, response, error in
completion(Result {
if let error = error {
throw error
} else if let data = data, let response = response as? HTTPURLResponse {
return (data, response)
} else {
throw UnexpectedValuesRepresentation()
}
})
}
task.resume()
return URLSessionTaskWrapper(wrapped: task)
}
return (data, response)
}
}
11 changes: 1 addition & 10 deletions EssentialFeed/EssentialFeed/Shared API/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@

import Foundation

public protocol HTTPClientTask {
func cancel()
}

public protocol HTTPClient {
typealias Result = Swift.Result<(Data, HTTPURLResponse), Error>

/// The completion handler can be invoked in any thread.
/// Clients are responsible to dispatch to appropriate threads, if needed.
@discardableResult
func get(from url: URL, completion: @Sendable @escaping (Result) -> Void) -> HTTPClientTask
func get(from url: URL) async throws -> (Data, HTTPURLResponse)
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,33 +47,23 @@ class EssentialFeedAPIEndToEndTests: XCTestCase {
private func getFeedResult(file: StaticString = #filePath, line: UInt = #line) async -> Swift.Result<[FeedImage], Error>? {
let client = ephemeralClient()

return await withCheckedContinuation { continuation in
client.get(from: feedTestServerURL) { result in
continuation.resume(returning: result.flatMap { (data, response) in
do {
return .success(try FeedItemsMapper.map(data, from: response))
} catch {
return .failure(error)
}
})
}
do {
let (data, response) = try await client.get(from: feedTestServerURL)
return .success(try FeedItemsMapper.map(data, from: response))
} catch {
return .failure(error)
}
}

private func getFeedImageDataResult(file: StaticString = #filePath, line: UInt = #line) async -> Result<Data, Error>? {
let client = ephemeralClient()
let url = feedTestServerURL.appendingPathComponent("73A7F70C-75DA-4C2E-B5A3-EED40DC53AA6/image")

return await withCheckedContinuation { continuation in
client.get(from: url) { result in
continuation.resume(returning: result.flatMap { (data, response) in
do {
return .success(try FeedImageDataMapper.map(data, from: response))
} catch {
return .failure(error)
}
})
}
do {
let (data, response) = try await client.get(from: url)
return .success(try FeedImageDataMapper.map(data, from: response))
} catch {
return .failure(error)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class URLSessionHTTPClientTests: XCTestCase {
URLProtocolStub.removeStub()
}

func test_getFromURL_performsGETRequestWithURL() {
func test_getFromURL_performsGETRequestWithURL() async throws {
let url = anyURL()
let exp = expectation(description: "Wait for request")

Expand All @@ -24,13 +24,13 @@ class URLSessionHTTPClientTests: XCTestCase {
exp.fulfill()
}

makeSUT().get(from: url) { _ in }
_ = try await makeSUT().get(from: url)

wait(for: [exp], timeout: 1.0)
await fulfillment(of: [exp], timeout: 1.0)
}

func test_cancelGetFromURLTask_cancelsURLRequest() async {
var task: HTTPClientTask?
var task: Task<(Data, HTTPURLResponse), Error>?
URLProtocolStub.onStartLoading { task?.cancel() }

let receivedError = await resultErrorFor(taskHandler: { task = $0 }) as NSError?
Expand All @@ -47,9 +47,7 @@ class URLSessionHTTPClientTests: XCTestCase {
}

func test_getFromURL_failsOnAllInvalidRepresentationCases() async {
assertNotNil(await resultErrorFor((data: nil, response: nil, error: nil)))
assertNotNil(await resultErrorFor((data: nil, response: nonHTTPURLResponse(), error: nil)))
assertNotNil(await resultErrorFor((data: anyData(), response: nil, error: nil)))
assertNotNil(await resultErrorFor((data: anyData(), response: nil, error: anyNSError())))
assertNotNil(await resultErrorFor((data: nil, response: nonHTTPURLResponse(), error: anyNSError())))
assertNotNil(await resultErrorFor((data: nil, response: anyHTTPURLResponse(), error: anyNSError())))
Expand Down Expand Up @@ -93,39 +91,35 @@ class URLSessionHTTPClientTests: XCTestCase {
}

private func resultValuesFor(_ values: (data: Data?, response: URLResponse?, error: Error?), file: StaticString = #filePath, line: UInt = #line) async -> (data: Data, response: HTTPURLResponse)? {
let result = await resultFor(values, file: file, line: line)

switch result {
case let .success(values):
return values
default:
XCTFail("Expected success, got \(result) instead", file: file, line: line)
do {
let result = try await resultFor(values, file: file, line: line)
return result
} catch {
XCTFail("Expected success, got \(error) instead", file: file, line: line)
return nil
}
}

private func resultErrorFor(_ values: (data: Data?, response: URLResponse?, error: Error?)? = nil, taskHandler: (HTTPClientTask) -> Void = { _ in }, file: StaticString = #filePath, line: UInt = #line) async -> Error? {
let result = await resultFor(values, taskHandler: taskHandler, file: file, line: line)

switch result {
case let .failure(error):
return error
default:
private func resultErrorFor(_ values: (data: Data?, response: URLResponse?, error: Error?)? = nil, taskHandler: (Task<(Data, HTTPURLResponse), Error>) -> Void = { _ in }, file: StaticString = #filePath, line: UInt = #line) async -> Error? {
do {
let result = try await resultFor(values, taskHandler: taskHandler, file: file, line: line)
XCTFail("Expected failure, got \(result) instead", file: file, line: line)
return nil
} catch {
return error
}
}

private func resultFor(_ values: (data: Data?, response: URLResponse?, error: Error?)?, taskHandler: (HTTPClientTask) -> Void = { _ in }, file: StaticString = #filePath, line: UInt = #line) async -> HTTPClient.Result {
private func resultFor(_ values: (data: Data?, response: URLResponse?, error: Error?)?, taskHandler: (Task<(Data, HTTPURLResponse), Error>) -> Void = { _ in }, file: StaticString = #filePath, line: UInt = #line) async throws -> (Data, HTTPURLResponse) {
values.map { URLProtocolStub.stub(data: $0, response: $1, error: $2) }

let sut = makeSUT(file: file, line: line)

return await withCheckedContinuation { continuation in
taskHandler(sut.get(from: anyURL()) { result in
continuation.resume(returning: result)
})
let task = Task {
return try await sut.get(from: anyURL())
}
taskHandler(task)
return try await task.value
}

private func anyHTTPURLResponse() -> HTTPURLResponse {
Expand Down