Skip to content

Commit 918bacd

Browse files
refactor(CoderSDK): share code between Client and AgentClient (#132)
Refactor to address review feedback that `AgentClient` extending the regular `Client` was confusing.
1 parent 8067574 commit 918bacd

File tree

11 files changed

+167
-102
lines changed

11 files changed

+167
-102
lines changed

Diff for: Coder-Desktop/Coder-Desktop/State.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class AppState: ObservableObject {
122122
let client = Client(url: baseAccessURL!, token: sessionToken!)
123123
do {
124124
_ = try await client.user("me")
125-
} catch let ClientError.api(apiErr) {
125+
} catch let SDKError.api(apiErr) {
126126
// Expired token
127127
if apiErr.statusCode == 401 {
128128
clearSession()

Diff for: Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ struct FilePicker: View {
7272
class FilePickerModel: ObservableObject {
7373
@Published var rootEntries: [FilePickerEntryModel] = []
7474
@Published var rootIsLoading: Bool = false
75-
@Published var error: ClientError?
75+
@Published var error: SDKError?
7676

7777
// It's important that `AgentClient` is a reference type (class)
7878
// as we were having performance issues with a struct (unless it was a binding).
@@ -87,7 +87,7 @@ class FilePickerModel: ObservableObject {
8787
rootIsLoading = true
8888
Task {
8989
defer { rootIsLoading = false }
90-
do throws(ClientError) {
90+
do throws(SDKError) {
9191
rootEntries = try await client
9292
.listAgentDirectory(.init(path: [], relativity: .root))
9393
.toModels(client: client)
@@ -149,7 +149,7 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
149149

150150
@Published var entries: [FilePickerEntryModel]?
151151
@Published var isLoading = false
152-
@Published var error: ClientError?
152+
@Published var error: SDKError?
153153
@Published private var innerIsExpanded = false
154154
var isExpanded: Bool {
155155
get { innerIsExpanded }
@@ -193,7 +193,7 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
193193
innerIsExpanded = true
194194
}
195195
}
196-
do throws(ClientError) {
196+
do throws(SDKError) {
197197
entries = try await client
198198
.listAgentDirectory(.init(path: path, relativity: .root))
199199
.toModels(client: client)

Diff for: Coder-Desktop/Coder-Desktop/Views/LoginForm.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ enum LoginError: Error {
207207
case invalidURL
208208
case outdatedCoderVersion
209209
case missingServerVersion
210-
case failedAuth(ClientError)
210+
case failedAuth(SDKError)
211211

212212
var description: String {
213213
switch self {

Diff for: Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ struct FilePickerTests {
6060
try Mock(
6161
url: url.appendingPathComponent("/api/v0/list-directory"),
6262
statusCode: 200,
63-
data: [.post: Client.encoder.encode(mockResponse)]
63+
data: [.post: CoderSDK.encoder.encode(mockResponse)]
6464
).register()
6565

6666
try await ViewHosting.host(view) {
@@ -88,7 +88,7 @@ struct FilePickerTests {
8888
try Mock(
8989
url: url.appendingPathComponent("/api/v0/list-directory"),
9090
statusCode: 200,
91-
data: [.post: Client.encoder.encode(mockResponse)]
91+
data: [.post: CoderSDK.encoder.encode(mockResponse)]
9292
).register()
9393

9494
try await ViewHosting.host(view) {

Diff for: Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ struct LoginTests {
7979
try Mock(
8080
url: url.appendingPathComponent("/api/v2/buildinfo"),
8181
statusCode: 200,
82-
data: [.get: Client.encoder.encode(buildInfo)]
82+
data: [.get: CoderSDK.encoder.encode(buildInfo)]
8383
).register()
8484
Mock(url: url.appendingPathComponent("/api/v2/users/me"), statusCode: 401, data: [.get: Data()]).register()
8585

@@ -104,13 +104,13 @@ struct LoginTests {
104104
try Mock(
105105
url: url.appendingPathComponent("/api/v2/buildinfo"),
106106
statusCode: 200,
107-
data: [.get: Client.encoder.encode(buildInfo)]
107+
data: [.get: CoderSDK.encoder.encode(buildInfo)]
108108
).register()
109109

110110
try Mock(
111111
url: url.appendingPathComponent("/api/v2/users/me"),
112112
statusCode: 200,
113-
data: [.get: Client.encoder.encode(User(id: UUID(), username: "username"))]
113+
data: [.get: CoderSDK.encoder.encode(User(id: UUID(), username: "username"))]
114114
).register()
115115

116116
try await ViewHosting.host(view) {
@@ -140,13 +140,13 @@ struct LoginTests {
140140
try Mock(
141141
url: url.appendingPathComponent("/api/v2/users/me"),
142142
statusCode: 200,
143-
data: [.get: Client.encoder.encode(user)]
143+
data: [.get: CoderSDK.encoder.encode(user)]
144144
).register()
145145

146146
try Mock(
147147
url: url.appendingPathComponent("/api/v2/buildinfo"),
148148
statusCode: 200,
149-
data: [.get: Client.encoder.encode(buildInfo)]
149+
data: [.get: CoderSDK.encoder.encode(buildInfo)]
150150
).register()
151151

152152
try await ViewHosting.host(view) {

Diff for: Coder-Desktop/CoderSDK/AgentClient.swift

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
public final class AgentClient: Sendable {
2-
let client: Client
2+
let agentURL: URL
33

44
public init(agentHost: String) {
5-
client = Client(url: URL(string: "http://\(agentHost):4")!)
5+
agentURL = URL(string: "http://\(agentHost):4")!
6+
}
7+
8+
func request(
9+
_ path: String,
10+
method: HTTPMethod
11+
) async throws(SDKError) -> HTTPResponse {
12+
try await CoderSDK.request(baseURL: agentURL, path: path, method: method)
13+
}
14+
15+
func request(
16+
_ path: String,
17+
method: HTTPMethod,
18+
body: some Encodable & Sendable
19+
) async throws(SDKError) -> HTTPResponse {
20+
try await CoderSDK.request(baseURL: agentURL, path: path, method: method, body: body)
621
}
722
}

Diff for: Coder-Desktop/CoderSDK/AgentLS.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
public extension AgentClient {
2-
func listAgentDirectory(_ req: LSRequest) async throws(ClientError) -> LSResponse {
3-
let res = try await client.request("/api/v0/list-directory", method: .post, body: req)
2+
func listAgentDirectory(_ req: LSRequest) async throws(SDKError) -> LSResponse {
3+
let res = try await request("/api/v0/list-directory", method: .post, body: req)
44
guard res.resp.statusCode == 200 else {
5-
throw client.responseAsError(res)
5+
throw responseAsError(res)
66
}
7-
return try client.decode(LSResponse.self, from: res.data)
7+
return try decode(LSResponse.self, from: res.data)
88
}
99
}
1010

Diff for: Coder-Desktop/CoderSDK/Client.swift

+129-79
Original file line numberDiff line numberDiff line change
@@ -11,95 +11,38 @@ public struct Client: Sendable {
1111
self.headers = headers
1212
}
1313

14-
static let decoder: JSONDecoder = {
15-
var dec = JSONDecoder()
16-
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
17-
return dec
18-
}()
19-
20-
static let encoder: JSONEncoder = {
21-
var enc = JSONEncoder()
22-
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
23-
return enc
24-
}()
25-
26-
private func doRequest(
27-
path: String,
28-
method: HTTPMethod,
29-
body: Data? = nil
30-
) async throws(ClientError) -> HTTPResponse {
31-
let url = url.appendingPathComponent(path)
32-
var req = URLRequest(url: url)
33-
if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) }
34-
req.httpMethod = method.rawValue
35-
for header in headers {
36-
req.addValue(header.value, forHTTPHeaderField: header.name)
37-
}
38-
req.httpBody = body
39-
let data: Data
40-
let resp: URLResponse
41-
do {
42-
(data, resp) = try await URLSession.shared.data(for: req)
43-
} catch {
44-
throw .network(error)
45-
}
46-
guard let httpResponse = resp as? HTTPURLResponse else {
47-
throw .unexpectedResponse(String(data: data, encoding: .utf8) ?? "<non-utf8 data>")
48-
}
49-
return HTTPResponse(resp: httpResponse, data: data, req: req)
50-
}
51-
5214
func request(
5315
_ path: String,
5416
method: HTTPMethod,
5517
body: some Encodable & Sendable
56-
) async throws(ClientError) -> HTTPResponse {
57-
let encodedBody: Data?
58-
do {
59-
encodedBody = try Client.encoder.encode(body)
60-
} catch {
61-
throw .encodeFailure(error)
18+
) async throws(SDKError) -> HTTPResponse {
19+
var headers = headers
20+
if let token {
21+
headers += [.init(name: Headers.sessionToken, value: token)]
6222
}
63-
return try await doRequest(path: path, method: method, body: encodedBody)
23+
return try await CoderSDK.request(
24+
baseURL: url,
25+
path: path,
26+
method: method,
27+
headers: headers,
28+
body: body
29+
)
6430
}
6531

6632
func request(
6733
_ path: String,
6834
method: HTTPMethod
69-
) async throws(ClientError) -> HTTPResponse {
70-
try await doRequest(path: path, method: method)
71-
}
72-
73-
func responseAsError(_ resp: HTTPResponse) -> ClientError {
74-
do {
75-
let body = try decode(Response.self, from: resp.data)
76-
let out = APIError(
77-
response: body,
78-
statusCode: resp.resp.statusCode,
79-
method: resp.req.httpMethod!,
80-
url: resp.req.url!
81-
)
82-
return .api(out)
83-
} catch {
84-
return .unexpectedResponse(String(data: resp.data, encoding: .utf8) ?? "<non-utf8 data>")
85-
}
86-
}
87-
88-
// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`.
89-
func decode<T>(_: T.Type, from data: Data) throws(ClientError) -> T where T: Decodable {
90-
do {
91-
return try Client.decoder.decode(T.self, from: data)
92-
} catch let DecodingError.keyNotFound(_, context) {
93-
throw .unexpectedResponse("Key not found: \(context.debugDescription)")
94-
} catch let DecodingError.valueNotFound(_, context) {
95-
throw .unexpectedResponse("Value not found: \(context.debugDescription)")
96-
} catch let DecodingError.typeMismatch(_, context) {
97-
throw .unexpectedResponse("Type mismatch: \(context.debugDescription)")
98-
} catch let DecodingError.dataCorrupted(context) {
99-
throw .unexpectedResponse("Data corrupted: \(context.debugDescription)")
100-
} catch {
101-
throw .unexpectedResponse(String(data: data.prefix(1024), encoding: .utf8) ?? "<non-utf8 data>")
35+
) async throws(SDKError) -> HTTPResponse {
36+
var headers = headers
37+
if let token {
38+
headers += [.init(name: Headers.sessionToken, value: token)]
10239
}
40+
return try await CoderSDK.request(
41+
baseURL: url,
42+
path: path,
43+
method: method,
44+
headers: headers
45+
)
10346
}
10447
}
10548

@@ -133,7 +76,7 @@ public struct FieldValidation: Decodable, Sendable {
13376
let detail: String
13477
}
13578

136-
public enum ClientError: Error {
79+
public enum SDKError: Error {
13780
case api(APIError)
13881
case network(any Error)
13982
case unexpectedResponse(String)
@@ -154,3 +97,110 @@ public enum ClientError: Error {
15497

15598
public var localizedDescription: String { description }
15699
}
100+
101+
let decoder: JSONDecoder = {
102+
var dec = JSONDecoder()
103+
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
104+
return dec
105+
}()
106+
107+
let encoder: JSONEncoder = {
108+
var enc = JSONEncoder()
109+
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
110+
return enc
111+
}()
112+
113+
func doRequest(
114+
baseURL: URL,
115+
path: String,
116+
method: HTTPMethod,
117+
headers: [HTTPHeader] = [],
118+
body: Data? = nil
119+
) async throws(SDKError) -> HTTPResponse {
120+
let url = baseURL.appendingPathComponent(path)
121+
var req = URLRequest(url: url)
122+
req.httpMethod = method.rawValue
123+
for header in headers {
124+
req.addValue(header.value, forHTTPHeaderField: header.name)
125+
}
126+
req.httpBody = body
127+
let data: Data
128+
let resp: URLResponse
129+
do {
130+
(data, resp) = try await URLSession.shared.data(for: req)
131+
} catch {
132+
throw .network(error)
133+
}
134+
guard let httpResponse = resp as? HTTPURLResponse else {
135+
throw .unexpectedResponse(String(data: data, encoding: .utf8) ?? "<non-utf8 data>")
136+
}
137+
return HTTPResponse(resp: httpResponse, data: data, req: req)
138+
}
139+
140+
func request(
141+
baseURL: URL,
142+
path: String,
143+
method: HTTPMethod,
144+
headers: [HTTPHeader] = [],
145+
body: some Encodable & Sendable
146+
) async throws(SDKError) -> HTTPResponse {
147+
let encodedBody: Data
148+
do {
149+
encodedBody = try encoder.encode(body)
150+
} catch {
151+
throw .encodeFailure(error)
152+
}
153+
return try await doRequest(
154+
baseURL: baseURL,
155+
path: path,
156+
method: method,
157+
headers: headers,
158+
body: encodedBody
159+
)
160+
}
161+
162+
func request(
163+
baseURL: URL,
164+
path: String,
165+
method: HTTPMethod,
166+
headers: [HTTPHeader] = []
167+
) async throws(SDKError) -> HTTPResponse {
168+
try await doRequest(
169+
baseURL: baseURL,
170+
path: path,
171+
method: method,
172+
headers: headers
173+
)
174+
}
175+
176+
func responseAsError(_ resp: HTTPResponse) -> SDKError {
177+
do {
178+
let body = try decode(Response.self, from: resp.data)
179+
let out = APIError(
180+
response: body,
181+
statusCode: resp.resp.statusCode,
182+
method: resp.req.httpMethod!,
183+
url: resp.req.url!
184+
)
185+
return .api(out)
186+
} catch {
187+
return .unexpectedResponse(String(data: resp.data, encoding: .utf8) ?? "<non-utf8 data>")
188+
}
189+
}
190+
191+
// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`.
192+
func decode<T: Decodable>(_: T.Type, from data: Data) throws(SDKError) -> T {
193+
do {
194+
return try decoder.decode(T.self, from: data)
195+
} catch let DecodingError.keyNotFound(_, context) {
196+
throw .unexpectedResponse("Key not found: \(context.debugDescription)")
197+
} catch let DecodingError.valueNotFound(_, context) {
198+
throw .unexpectedResponse("Value not found: \(context.debugDescription)")
199+
} catch let DecodingError.typeMismatch(_, context) {
200+
throw .unexpectedResponse("Type mismatch: \(context.debugDescription)")
201+
} catch let DecodingError.dataCorrupted(context) {
202+
throw .unexpectedResponse("Data corrupted: \(context.debugDescription)")
203+
} catch {
204+
throw .unexpectedResponse(String(data: data.prefix(1024), encoding: .utf8) ?? "<non-utf8 data>")
205+
}
206+
}

Diff for: Coder-Desktop/CoderSDK/Deployment.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
public extension Client {
4-
func buildInfo() async throws(ClientError) -> BuildInfoResponse {
4+
func buildInfo() async throws(SDKError) -> BuildInfoResponse {
55
let res = try await request("/api/v2/buildinfo", method: .get)
66
guard res.resp.statusCode == 200 else {
77
throw responseAsError(res)

0 commit comments

Comments
 (0)