Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

refactor(CoderSDK): share code between Client and AgentClient#132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 1 commit intomainfromethan/refactor-sdk
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes fromall 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
2 changes: 1 addition & 1 deletionCoder-Desktop/Coder-Desktop/State.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -122,7 +122,7 @@ class AppState: ObservableObject {
let client = Client(url: baseAccessURL!, token: sessionToken!)
do {
_ = try await client.user("me")
} catch letClientError.api(apiErr) {
} catch letSDKError.api(apiErr) {
// Expired token
if apiErr.statusCode == 401 {
clearSession()
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -72,7 +72,7 @@ struct FilePicker: View {
class FilePickerModel: ObservableObject {
@Published var rootEntries: [FilePickerEntryModel] = []
@Published var rootIsLoading: Bool = false
@Published var error:ClientError?
@Published var error:SDKError?

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

@Published var entries: [FilePickerEntryModel]?
@Published var isLoading = false
@Published var error:ClientError?
@Published var error:SDKError?
@Published private var innerIsExpanded = false
var isExpanded: Bool {
get { innerIsExpanded }
Expand DownExpand Up@@ -193,7 +193,7 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
innerIsExpanded = true
}
}
do throws(ClientError) {
do throws(SDKError) {
entries = try await client
.listAgentDirectory(.init(path: path, relativity: .root))
.toModels(client: client)
Expand Down
2 changes: 1 addition & 1 deletionCoder-Desktop/Coder-Desktop/Views/LoginForm.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -207,7 +207,7 @@ enum LoginError: Error {
case invalidURL
case outdatedCoderVersion
case missingServerVersion
case failedAuth(ClientError)
case failedAuth(SDKError)

var description: String {
switch self {
Expand Down
4 changes: 2 additions & 2 deletionsCoder-Desktop/Coder-DesktopTests/FilePickerTests.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,7 +60,7 @@ struct FilePickerTests {
try Mock(
url: url.appendingPathComponent("/api/v0/list-directory"),
statusCode: 200,
data: [.post:Client.encoder.encode(mockResponse)]
data: [.post:CoderSDK.encoder.encode(mockResponse)]
).register()

try await ViewHosting.host(view) {
Expand DownExpand Up@@ -88,7 +88,7 @@ struct FilePickerTests {
try Mock(
url: url.appendingPathComponent("/api/v0/list-directory"),
statusCode: 200,
data: [.post:Client.encoder.encode(mockResponse)]
data: [.post:CoderSDK.encoder.encode(mockResponse)]
).register()

try await ViewHosting.host(view) {
Expand Down
10 changes: 5 additions & 5 deletionsCoder-Desktop/Coder-DesktopTests/LoginFormTests.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,7 +79,7 @@ struct LoginTests {
try Mock(
url: url.appendingPathComponent("/api/v2/buildinfo"),
statusCode: 200,
data: [.get:Client.encoder.encode(buildInfo)]
data: [.get:CoderSDK.encoder.encode(buildInfo)]
).register()
Mock(url: url.appendingPathComponent("/api/v2/users/me"), statusCode: 401, data: [.get: Data()]).register()

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

try Mock(
url: url.appendingPathComponent("/api/v2/users/me"),
statusCode: 200,
data: [.get:Client.encoder.encode(User(id: UUID(), username: "username"))]
data: [.get:CoderSDK.encoder.encode(User(id: UUID(), username: "username"))]
).register()

try await ViewHosting.host(view) {
Expand DownExpand Up@@ -140,13 +140,13 @@ struct LoginTests {
try Mock(
url: url.appendingPathComponent("/api/v2/users/me"),
statusCode: 200,
data: [.get:Client.encoder.encode(user)]
data: [.get:CoderSDK.encoder.encode(user)]
).register()

try Mock(
url: url.appendingPathComponent("/api/v2/buildinfo"),
statusCode: 200,
data: [.get:Client.encoder.encode(buildInfo)]
data: [.get:CoderSDK.encoder.encode(buildInfo)]
).register()

try await ViewHosting.host(view) {
Expand Down
19 changes: 17 additions & 2 deletionsCoder-Desktop/CoderSDK/AgentClient.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
public final class AgentClient: Sendable {
letclient: Client
letagentURL: URL

public init(agentHost: String) {
client = Client(url: URL(string: "http://\(agentHost):4")!)
agentURL = URL(string: "http://\(agentHost):4")!
}

func request(
_ path: String,
method: HTTPMethod
) async throws(SDKError) -> HTTPResponse {
try await CoderSDK.request(baseURL: agentURL, path: path, method: method)
}

func request(
_ path: String,
method: HTTPMethod,
body: some Encodable & Sendable
) async throws(SDKError) -> HTTPResponse {
try await CoderSDK.request(baseURL: agentURL, path: path, method: method, body: body)
}
}
8 changes: 4 additions & 4 deletionsCoder-Desktop/CoderSDK/AgentLS.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
public extension AgentClient {
func listAgentDirectory(_ req: LSRequest) async throws(ClientError) -> LSResponse {
let res = try awaitclient.request("/api/v0/list-directory", method: .post, body: req)
func listAgentDirectory(_ req: LSRequest) async throws(SDKError) -> LSResponse {
let res = try await request("/api/v0/list-directory", method: .post, body: req)
guard res.resp.statusCode == 200 else {
throwclient.responseAsError(res)
throw responseAsError(res)
}
return tryclient.decode(LSResponse.self, from: res.data)
return try decode(LSResponse.self, from: res.data)
}
}

Expand Down
208 changes: 129 additions & 79 deletionsCoder-Desktop/CoderSDK/Client.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,95 +11,38 @@ public struct Client: Sendable {
self.headers = headers
}

static let decoder: JSONDecoder = {
var dec = JSONDecoder()
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
return dec
}()

static let encoder: JSONEncoder = {
var enc = JSONEncoder()
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
return enc
}()

private func doRequest(
path: String,
method: HTTPMethod,
body: Data? = nil
) async throws(ClientError) -> HTTPResponse {
let url = url.appendingPathComponent(path)
var req = URLRequest(url: url)
if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) }
req.httpMethod = method.rawValue
for header in headers {
req.addValue(header.value, forHTTPHeaderField: header.name)
}
req.httpBody = body
let data: Data
let resp: URLResponse
do {
(data, resp) = try await URLSession.shared.data(for: req)
} catch {
throw .network(error)
}
guard let httpResponse = resp as? HTTPURLResponse else {
throw .unexpectedResponse(String(data: data, encoding: .utf8) ?? "<non-utf8 data>")
}
return HTTPResponse(resp: httpResponse, data: data, req: req)
}

func request(
_ path: String,
method: HTTPMethod,
body: some Encodable & Sendable
) async throws(ClientError) -> HTTPResponse {
let encodedBody: Data?
do {
encodedBody = try Client.encoder.encode(body)
} catch {
throw .encodeFailure(error)
) async throws(SDKError) -> HTTPResponse {
var headers = headers
if let token {
headers += [.init(name: Headers.sessionToken, value: token)]
}
return try await doRequest(path: path, method: method, body: encodedBody)
return try await CoderSDK.request(
baseURL: url,
path: path,
method: method,
headers: headers,
body: body
)
}

func request(
_ path: String,
method: HTTPMethod
) async throws(ClientError) -> HTTPResponse {
try await doRequest(path: path, method: method)
}

func responseAsError(_ resp: HTTPResponse) -> ClientError {
do {
let body = try decode(Response.self, from: resp.data)
let out = APIError(
response: body,
statusCode: resp.resp.statusCode,
method: resp.req.httpMethod!,
url: resp.req.url!
)
return .api(out)
} catch {
return .unexpectedResponse(String(data: resp.data, encoding: .utf8) ?? "<non-utf8 data>")
}
}

// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`.
func decode<T>(_: T.Type, from data: Data) throws(ClientError) -> T where T: Decodable {
do {
return try Client.decoder.decode(T.self, from: data)
} catch let DecodingError.keyNotFound(_, context) {
throw .unexpectedResponse("Key not found: \(context.debugDescription)")
} catch let DecodingError.valueNotFound(_, context) {
throw .unexpectedResponse("Value not found: \(context.debugDescription)")
} catch let DecodingError.typeMismatch(_, context) {
throw .unexpectedResponse("Type mismatch: \(context.debugDescription)")
} catch let DecodingError.dataCorrupted(context) {
throw .unexpectedResponse("Data corrupted: \(context.debugDescription)")
} catch {
throw .unexpectedResponse(String(data: data.prefix(1024), encoding: .utf8) ?? "<non-utf8 data>")
) async throws(SDKError) -> HTTPResponse {
var headers = headers
if let token {
headers += [.init(name: Headers.sessionToken, value: token)]
}
return try await CoderSDK.request(
baseURL: url,
path: path,
method: method,
headers: headers
)
}
}

Expand DownExpand Up@@ -133,7 +76,7 @@ public struct FieldValidation: Decodable, Sendable {
let detail: String
}

public enumClientError: Error {
public enumSDKError: Error {
case api(APIError)
case network(any Error)
case unexpectedResponse(String)
Expand All@@ -154,3 +97,110 @@ public enum ClientError: Error {

public var localizedDescription: String { description }
}

let decoder: JSONDecoder = {
var dec = JSONDecoder()
dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
return dec
}()

let encoder: JSONEncoder = {
var enc = JSONEncoder()
enc.dateEncodingStrategy = .iso8601withFractionalSeconds
return enc
}()

func doRequest(
baseURL: URL,
path: String,
method: HTTPMethod,
headers: [HTTPHeader] = [],
body: Data? = nil
) async throws(SDKError) -> HTTPResponse {
let url = baseURL.appendingPathComponent(path)
var req = URLRequest(url: url)
req.httpMethod = method.rawValue
for header in headers {
req.addValue(header.value, forHTTPHeaderField: header.name)
}
req.httpBody = body
let data: Data
let resp: URLResponse
do {
(data, resp) = try await URLSession.shared.data(for: req)
} catch {
throw .network(error)
}
guard let httpResponse = resp as? HTTPURLResponse else {
throw .unexpectedResponse(String(data: data, encoding: .utf8) ?? "<non-utf8 data>")
}
return HTTPResponse(resp: httpResponse, data: data, req: req)
}

func request(
baseURL: URL,
path: String,
method: HTTPMethod,
headers: [HTTPHeader] = [],
body: some Encodable & Sendable
) async throws(SDKError) -> HTTPResponse {
let encodedBody: Data
do {
encodedBody = try encoder.encode(body)
} catch {
throw .encodeFailure(error)
}
return try await doRequest(
baseURL: baseURL,
path: path,
method: method,
headers: headers,
body: encodedBody
)
}

func request(
baseURL: URL,
path: String,
method: HTTPMethod,
headers: [HTTPHeader] = []
) async throws(SDKError) -> HTTPResponse {
try await doRequest(
baseURL: baseURL,
path: path,
method: method,
headers: headers
)
}

func responseAsError(_ resp: HTTPResponse) -> SDKError {
do {
let body = try decode(Response.self, from: resp.data)
let out = APIError(
response: body,
statusCode: resp.resp.statusCode,
method: resp.req.httpMethod!,
url: resp.req.url!
)
return .api(out)
} catch {
return .unexpectedResponse(String(data: resp.data, encoding: .utf8) ?? "<non-utf8 data>")
}
}

// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`.
func decode<T: Decodable>(_: T.Type, from data: Data) throws(SDKError) -> T {
do {
return try decoder.decode(T.self, from: data)
} catch let DecodingError.keyNotFound(_, context) {
throw .unexpectedResponse("Key not found: \(context.debugDescription)")
} catch let DecodingError.valueNotFound(_, context) {
throw .unexpectedResponse("Value not found: \(context.debugDescription)")
} catch let DecodingError.typeMismatch(_, context) {
throw .unexpectedResponse("Type mismatch: \(context.debugDescription)")
} catch let DecodingError.dataCorrupted(context) {
throw .unexpectedResponse("Data corrupted: \(context.debugDescription)")
} catch {
throw .unexpectedResponse(String(data: data.prefix(1024), encoding: .utf8) ?? "<non-utf8 data>")
}
}
2 changes: 1 addition & 1 deletionCoder-Desktop/CoderSDK/Deployment.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
import Foundation

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

[8]ページ先頭

©2009-2025 Movatter.jp