Skip to content

Commit

Permalink
Use dependency clients (#2653)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis authored Dec 13, 2023
1 parent cb9c1f8 commit 1aaeecb
Show file tree
Hide file tree
Showing 35 changed files with 136 additions and 210 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import SwiftUI
import XCTestDynamicOverlay

private let readMe = """
This application demonstrates how to handle long-living effects, for example notifications from \
Expand Down Expand Up @@ -64,9 +63,6 @@ private enum ScreenshotsKey: DependencyKey {
.map { _ in }
)
}
static let testValue: @Sendable () async -> AsyncStream<Void> = unimplemented(
#"@Dependency(\.screenshots)"#, placeholder: .finished
)
}

// MARK: - Feature view
Expand Down
32 changes: 16 additions & 16 deletions Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import SwiftUI
import XCTestDynamicOverlay

private let readMe = """
This application demonstrates how to work with a web socket in the Composable Architecture.
Expand Down Expand Up @@ -57,8 +56,11 @@ struct WebSocket {
case .disconnected:
state.connectivityState = .connecting
return .run { send in
let actions = await self.webSocket
.open(WebSocketClient.ID(), URL(string: "wss://echo.websocket.events")!, [])
let actions = await self.webSocket.open(
id: WebSocketClient.ID(),
url: URL(string: "wss://echo.websocket.events")!,
protocols: []
)
await withThrowingTaskGroup(of: Void.self) { group in
for await action in actions {
// NB: Can't call `await send` here outside of `group.addTask` due to task local
Expand All @@ -71,11 +73,11 @@ struct WebSocket {
group.addTask {
while !Task.isCancelled {
try await self.clock.sleep(for: .seconds(10))
try? await self.webSocket.sendPing(WebSocketClient.ID())
try? await self.webSocket.sendPing(id: WebSocketClient.ID())
}
}
group.addTask {
for await result in try await self.webSocket.receive(WebSocketClient.ID()) {
for await result in try await self.webSocket.receive(id: WebSocketClient.ID()) {
await send(.receivedSocketMessage(result))
}
}
Expand Down Expand Up @@ -105,7 +107,7 @@ struct WebSocket {
let messageToSend = state.messageToSend
state.messageToSend = ""
return .run { send in
try await self.webSocket.send(WebSocketClient.ID(), .string(messageToSend))
try await self.webSocket.send(id: WebSocketClient.ID(), message: .string(messageToSend))
await send(.sendResponse(didSucceed: true))
} catch: { _, send in
await send(.sendResponse(didSucceed: false))
Expand Down Expand Up @@ -194,6 +196,7 @@ struct WebSocketView: View {

// MARK: - WebSocketClient

@DependencyClient
struct WebSocketClient {
struct ID: Hashable, @unchecked Sendable {
let rawValue: AnyHashable
Expand Down Expand Up @@ -230,10 +233,12 @@ struct WebSocketClient {
}
}

var open: @Sendable (ID, URL, [String]) async -> AsyncStream<Action>
var receive: @Sendable (ID) async throws -> AsyncStream<Result<Message, Error>>
var send: @Sendable (ID, URLSessionWebSocketTask.Message) async throws -> Void
var sendPing: @Sendable (ID) async throws -> Void
var open: @Sendable (_ id: ID, _ url: URL, _ protocols: [String]) async -> AsyncStream<Action> = {
_, _, _ in .finished
}
var receive: @Sendable (_ id: ID) async throws -> AsyncStream<Result<Message, Error>>
var send: @Sendable (_ id: ID, _ message: URLSessionWebSocketTask.Message) async throws -> Void
var sendPing: @Sendable (_ id: ID) async throws -> Void
}

extension WebSocketClient: DependencyKey {
Expand Down Expand Up @@ -343,12 +348,7 @@ extension WebSocketClient: DependencyKey {
}
}

static let testValue = Self(
open: unimplemented("\(Self.self).open", placeholder: AsyncStream.never),
receive: unimplemented("\(Self.self).receive"),
send: unimplemented("\(Self.self).send"),
sendPing: unimplemented("\(Self.self).sendPing")
)
static let testValue = Self()
}

extension DependencyValues {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay

@DependencyClient
struct DownloadClient {
var download: @Sendable (URL) -> AsyncThrowingStream<Event, Error>
var download: @Sendable (_ url: URL) -> AsyncThrowingStream<Event, Error> = { _ in .finished() }

@CasePathable
enum Event: Equatable {
Expand Down Expand Up @@ -47,7 +47,5 @@ extension DownloadClient: DependencyKey {
}
)

static let testValue = Self(
download: unimplemented("\(Self.self).download")
)
static let testValue = Self()
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct DownloadComponent {
state.mode = .startingToDownload

return .run { [url = state.url] send in
for try await event in self.downloadClient.download(url) {
for try await event in self.downloadClient.download(url: url) {
await send(.downloadClient(.success(event)), animation: .default)
}
} catch: { error, send in
Expand Down
6 changes: 2 additions & 4 deletions Examples/CaseStudies/SwiftUICaseStudies/FactClient.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay

@DependencyClient
struct FactClient {
var fetch: @Sendable (Int) async throws -> String
}
Expand All @@ -28,7 +28,5 @@ extension FactClient: DependencyKey {

/// This is the "unimplemented" fact dependency that is useful to plug into tests that you want
/// to prove do not need the dependency.
static let testValue = Self(
fetch: unimplemented("\(Self.self).fetch")
)
static let testValue = Self()
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ final class WebSocketTests: XCTestCase {
WebSocket()
} withDependencies: {
$0.continuousClock = ImmediateClock()
$0.webSocket.open = { _, _, _ in actions.stream }
$0.webSocket.receive = { _ in messages.stream }
$0.webSocket.send = { _, _ in }
$0.webSocket.sendPing = { _ in try await Task.never() }
$0.webSocket.open = { @Sendable _, _, _ in actions.stream }
$0.webSocket.receive = { @Sendable _ in messages.stream }
$0.webSocket.send = { @Sendable _, _ in }
$0.webSocket.sendPing = { @Sendable _ in try await Task.never() }
}

// Connect to the socket
Expand Down Expand Up @@ -64,13 +64,13 @@ final class WebSocketTests: XCTestCase {
WebSocket()
} withDependencies: {
$0.continuousClock = ImmediateClock()
$0.webSocket.open = { _, _, _ in actions.stream }
$0.webSocket.receive = { _ in messages.stream }
$0.webSocket.send = { _, _ in
$0.webSocket.open = { @Sendable _, _, _ in actions.stream }
$0.webSocket.receive = { @Sendable _ in messages.stream }
$0.webSocket.send = { @Sendable _, _ in
struct SendFailure: Error, Equatable {}
throw SendFailure()
}
$0.webSocket.sendPing = { _ in try await Task.never() }
$0.webSocket.sendPing = { @Sendable _ in try await Task.never() }
}

// Connect to the socket
Expand Down Expand Up @@ -111,9 +111,9 @@ final class WebSocketTests: XCTestCase {
WebSocket()
} withDependencies: {
$0.continuousClock = clock
$0.webSocket.open = { _, _, _ in actions.stream }
$0.webSocket.receive = { _ in try await Task.never() }
$0.webSocket.sendPing = { @MainActor _ in pingsCount += 1 }
$0.webSocket.open = { @Sendable _, _, _ in actions.stream }
$0.webSocket.receive = { @Sendable _ in try await Task.never() }
$0.webSocket.sendPing = { @Sendable @MainActor _ in pingsCount += 1 }
}

// Connect to the socket
Expand Down Expand Up @@ -143,9 +143,9 @@ final class WebSocketTests: XCTestCase {
WebSocket()
} withDependencies: {
$0.continuousClock = ImmediateClock()
$0.webSocket.open = { _, _, _ in actions.stream }
$0.webSocket.receive = { _ in try await Task.never() }
$0.webSocket.sendPing = { _ in try await Task.never() }
$0.webSocket.open = { @Sendable _, _, _ in actions.stream }
$0.webSocket.receive = { @Sendable _ in try await Task.never() }
$0.webSocket.sendPing = { @Sendable _ in try await Task.never() }
}

// Attempt to connect to the socket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
) {
DownloadComponent()
} withDependencies: {
$0.downloadClient.download = { _ in self.download.stream }
$0.downloadClient.download = { @Sendable _ in self.download.stream }
}

await store.send(.buttonTapped) {
Expand Down Expand Up @@ -46,7 +46,7 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
) {
DownloadComponent()
} withDependencies: {
$0.downloadClient.download = { _ in self.download.stream }
$0.downloadClient.download = { @Sendable _ in self.download.stream }
}

await store.send(.buttonTapped) {
Expand Down Expand Up @@ -87,7 +87,7 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
) {
DownloadComponent()
} withDependencies: {
$0.downloadClient.download = { _ in self.download.stream }
$0.downloadClient.download = { @Sendable _ in self.download.stream }
}

let task = await store.send(.buttonTapped) {
Expand Down Expand Up @@ -127,7 +127,7 @@ final class ReusableComponentsDownloadComponentTests: XCTestCase {
) {
DownloadComponent()
} withDependencies: {
$0.downloadClient.download = { _ in self.download.stream }
$0.downloadClient.download = { @Sendable _ in self.download.stream }
}

await store.send(.buttonTapped) {
Expand Down
4 changes: 2 additions & 2 deletions Examples/Search/Search/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ struct Search {
return .none
}
return .run { [query = state.searchQuery] send in
await send(.searchResponse(Result { try await self.weatherClient.search(query) }))
await send(.searchResponse(Result { try await self.weatherClient.search(query: query) }))
}
.cancellable(id: CancelID.location)

Expand All @@ -102,7 +102,7 @@ struct Search {
await send(
.forecastResponse(
location.id,
Result { try await self.weatherClient.forecast(location) }
Result { try await self.weatherClient.forecast(location: location) }
)
)
}
Expand Down
11 changes: 4 additions & 7 deletions Examples/Search/Search/WeatherClient.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay

// MARK: - API models

Expand Down Expand Up @@ -38,9 +37,10 @@ struct Forecast: Decodable, Equatable, Sendable {
// Typically this interface would live in its own module, separate from the live implementation.
// This allows the search feature to compile faster since it only depends on the interface.

@DependencyClient
struct WeatherClient {
var forecast: @Sendable (GeocodingSearch.Result) async throws -> Forecast
var search: @Sendable (String) async throws -> GeocodingSearch
var forecast: @Sendable (_ location: GeocodingSearch.Result) async throws -> Forecast
var search: @Sendable (_ query: String) async throws -> GeocodingSearch
}

extension WeatherClient: TestDependencyKey {
Expand All @@ -49,10 +49,7 @@ extension WeatherClient: TestDependencyKey {
search: { _ in .mock }
)

static let testValue = Self(
forecast: unimplemented("\(Self.self).forecast"),
search: unimplemented("\(Self.self).search")
)
static let testValue = Self()
}

extension DependencyValues {
Expand Down
12 changes: 6 additions & 6 deletions Examples/Search/SearchTests/SearchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ final class SearchTests: XCTestCase {
let store = TestStore(initialState: Search.State()) {
Search()
} withDependencies: {
$0.weatherClient.search = { _ in .mock }
$0.weatherClient.search = { @Sendable _ in .mock }
}

await store.send(.searchQueryChanged("S")) {
Expand All @@ -29,7 +29,7 @@ final class SearchTests: XCTestCase {
let store = TestStore(initialState: Search.State()) {
Search()
} withDependencies: {
$0.weatherClient.search = { _ in throw SomethingWentWrong() }
$0.weatherClient.search = { @Sendable _ in throw SomethingWentWrong() }
}

await store.send(.searchQueryChanged("S")) {
Expand All @@ -43,7 +43,7 @@ final class SearchTests: XCTestCase {
let store = TestStore(initialState: Search.State()) {
Search()
} withDependencies: {
$0.weatherClient.search = { _ in .mock }
$0.weatherClient.search = { @Sendable _ in .mock }
}

let searchQueryChanged = await store.send(.searchQueryChanged("S")) {
Expand All @@ -70,7 +70,7 @@ final class SearchTests: XCTestCase {
let store = TestStore(initialState: Search.State(results: results)) {
Search()
} withDependencies: {
$0.weatherClient.forecast = { _ in .mock }
$0.weatherClient.forecast = { @Sendable _ in .mock }
}

await store.send(.searchResultTapped(specialResult)) {
Expand Down Expand Up @@ -124,7 +124,7 @@ final class SearchTests: XCTestCase {
let store = TestStore(initialState: Search.State(results: results)) {
Search()
} withDependencies: {
$0.weatherClient.forecast = { _ in
$0.weatherClient.forecast = { @Sendable _ in
try await clock.sleep(for: .seconds(0))
return .mock
}
Expand Down Expand Up @@ -174,7 +174,7 @@ final class SearchTests: XCTestCase {
let store = TestStore(initialState: Search.State(results: results)) {
Search()
} withDependencies: {
$0.weatherClient.forecast = { _ in throw SomethingWentWrong() }
$0.weatherClient.forecast = { @Sendable _ in throw SomethingWentWrong() }
}

await store.send(.searchResultTapped(results.first!)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import Dependencies
import ComposableArchitecture
import Speech
import XCTestDynamicOverlay

@DependencyClient
struct SpeechClient {
var finishTask: @Sendable () async -> Void
var requestAuthorization: @Sendable () async -> SFSpeechRecognizerAuthorizationStatus
var requestAuthorization: @Sendable () async -> SFSpeechRecognizerAuthorizationStatus = {
.notDetermined
}
var startTask:
@Sendable (SFSpeechAudioBufferRecognitionRequest) async -> AsyncThrowingStream<
@Sendable (_ request: SFSpeechAudioBufferRecognitionRequest) async -> AsyncThrowingStream<
SpeechRecognitionResult, Error
>
> = { _ in .finished() }

enum Failure: Error, Equatable {
case taskError
Expand Down Expand Up @@ -62,13 +64,7 @@ extension SpeechClient: TestDependencyKey {
)
}

static let testValue = Self(
finishTask: unimplemented("\(Self.self).finishTask"),
requestAuthorization: unimplemented(
"\(Self.self).requestAuthorization", placeholder: .notDetermined
),
startTask: unimplemented("\(Self.self).recognitionTask", placeholder: .never)
)
static let testValue = Self()
}

extension DependencyValues {
Expand Down
Loading

0 comments on commit 1aaeecb

Please sign in to comment.