Skip to content

chore: add API errors to SDK #12

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 16, 2024
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "42dc2e0a0e0417a7f4f62b3e875c9559038beef7d2265073dd4fc81f2e11ee13",
"originHash" : "aa8dd97dc6e28dedc4a5c45c435467a247486474bf3c1caf5e67085d52325132",
"pins" : [
{
"identity" : "alamofire",
Expand Down
39 changes: 22 additions & 17 deletions Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import SwiftUI
import Alamofire

struct PreviewClient: Client {
init(url _: URL, token _: String? = nil) {}

func user(_: String) async throws -> User {
try await Task.sleep(for: .seconds(1))
return User(
id: UUID(),
username: "admin",
avatar_url: "",
name: "admin",
email: "[email protected]",
created_at: Date.now,
updated_at: Date.now,
last_seen_at: Date.now,
status: "active",
login_type: "none",
theme_preference: "dark",
organization_ids: [],
roles: []
)
func user(_: String) async throws(ClientError) -> User {
do {
try await Task.sleep(for: .seconds(1))
return User(
id: UUID(),
username: "admin",
avatar_url: "",
name: "admin",
email: "[email protected]",
created_at: Date.now,
updated_at: Date.now,
last_seen_at: Date.now,
status: "active",
login_type: "none",
theme_preference: "dark",
organization_ids: [],
roles: []
)
} catch {
throw ClientError.reqError(AFError.explicitlyCancelled)
}
}
}
107 changes: 93 additions & 14 deletions Coder Desktop/Coder Desktop/SDK/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation

protocol Client {
init(url: URL, token: String?)
func user(_ ident: String) async throws -> User
func user(_ ident: String) async throws(ClientError) -> User
}

struct CoderClient: Client {
Expand All @@ -25,38 +25,117 @@ struct CoderClient: Client {
func request<T: Encodable>(
_ path: String,
method: HTTPMethod,
body: T
) async -> DataResponse<Data, AFError> {
body: T? = nil
) async throws(ClientError) -> HTTPResponse {
let url = self.url.appendingPathComponent(path)
let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""]
return await AF.request(
let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] }
let out = await AF.request(
url,
method: method,
parameters: body,
encoder: JSONParameterEncoder.default,
headers: headers
).serializingData().response
switch out.result {
case .success(let data):
return HTTPResponse(resp: out.response!, data: data, req: out.request)
case .failure(let error):
throw ClientError.reqError(error)
}
}

func request(
_ path: String,
method: HTTPMethod
) async -> DataResponse<Data, AFError> {
) async throws(ClientError) -> HTTPResponse {
let url = self.url.appendingPathComponent(path)
let headers: HTTPHeaders = [Headers.sessionToken: token ?? ""]
return await AF.request(
let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] }
let out = await AF.request(
url,
method: method,
headers: headers
).serializingData().response
switch out.result {
case .success(let data):
return HTTPResponse(resp: out.response!, data: data, req: out.request)
case .failure(let error):
throw ClientError.reqError(error)
}
}

func responseAsError(_ resp: HTTPResponse) -> ClientError {
do {
let body = try CoderClient.decoder.decode(Response.self, from: resp.data)
let out = APIError(
response: body,
statusCode: resp.resp.statusCode,
method: resp.req?.httpMethod,
url: resp.req?.url
)
return ClientError.apiError(out)
} catch {
return ClientError.unexpectedResponse(resp.data[...1024])
}
}

enum Headers {
static let sessionToken = "Coder-Session-Token"
}

}

enum ClientError: Error {
case unexpectedStatusCode
case badResponse
struct HTTPResponse {
let resp: HTTPURLResponse
let data: Data
let req: URLRequest?
}

enum Headers {
static let sessionToken = "Coder-Session-Token"
struct APIError: Decodable {
let response: Response
let statusCode: Int
let method: String?
let url: URL?

var description: String {
var components: [String] = []
if let method = method, let url = url {
components.append("\(method) \(url.absoluteString)")
}
components.append("Unexpected status code \(statusCode):\n\(response.message)")
if let detail = response.detail {
components.append("\tError: \(detail)")
}
if let validations = response.validations, !validations.isEmpty {
let validationMessages = validations.map { "\t\($0.field): \($0.detail)" }
components.append(contentsOf: validationMessages)
}
return components.joined(separator: "\n")
}
}

struct Response: Decodable {
let message: String
let detail: String?
let validations: [ValidationError]?
}

struct ValidationError: Decodable {
let field: String
let detail: String
}

enum ClientError: Error {
case apiError(APIError)
case reqError(AFError)
case unexpectedResponse(Data)

var description: String {
switch self {
case .apiError(let error):
return error.description
case .reqError(let error):
return error.localizedDescription
case .unexpectedResponse(let data):
return "Unexpected response: \(data)"
}
}
}
15 changes: 8 additions & 7 deletions Coder Desktop/Coder Desktop/SDK/User.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import Foundation

extension CoderClient {
func user(_ ident: String) async throws -> User {
let resp = await request("/api/v2/users/\(ident)", method: .get)
guard let response = resp.response, response.statusCode == 200 else {
throw ClientError.unexpectedStatusCode
func user(_ ident: String) async throws(ClientError) -> User {
let res = try await request("/api/v2/users/\(ident)", method: .get)
guard res.resp.statusCode == 200 else {
throw responseAsError(res)
}
guard let data = resp.data else {
throw ClientError.badResponse
do {
return try CoderClient.decoder.decode(User.self, from: res.data)
} catch {
throw ClientError.unexpectedResponse(res.data[...1024])
}
return try CoderClient.decoder.decode(User.self, from: data)
}
}

Expand Down
43 changes: 20 additions & 23 deletions Coder Desktop/Coder Desktop/Views/LoginForm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,29 @@ struct LoginForm<C: Client, S: Session>: View {
}
.animation(.easeInOut, value: currentPage)
.onAppear {
loginError = nil
baseAccessURL = session.baseAccessURL?.absoluteString ?? baseAccessURL
sessionToken = ""
}.padding(.top, 35)
VStack(alignment: .center) {
if let loginError {
Text("\(loginError.description)")
.font(.headline)
.foregroundColor(.red)
.multilineTextAlignment(.center)
}.padding(.vertical, 35)
.alert("Error", isPresented: Binding(
get: { loginError != nil },
set: { isPresented in
if !isPresented {
loginError = nil
}
}
)) {
Button("OK", role: .cancel) {}.keyboardShortcut(.defaultAction)
} message: {
Text(loginError?.description ?? "")
}
}
.frame(height: 35)
}.padding()
.frame(width: 450, height: 220)
.disabled(loading)
.onReceive(inspection.notice) { self.inspection.visit(self, $0) } // ViewInspector
}

internal func submit() async {
loginError = nil
guard sessionToken != "" else {
loginError = .invalidToken
return
}
guard let url = URL(string: baseAccessURL), url.scheme == "https" else {
Expand All @@ -69,11 +69,10 @@ struct LoginForm<C: Client, S: Session>: View {
loading = true
defer { loading = false}
let client = C(url: url, token: sessionToken)
do {
do throws(ClientError) {
_ = try await client.user("me")
} catch {
loginError = .failedAuth
print("Set error")
loginError = .failedAuth(error)
return
}
session.store(baseAccessURL: url, sessionToken: sessionToken)
Expand Down Expand Up @@ -142,7 +141,9 @@ struct LoginForm<C: Client, S: Session>: View {
}

private func next() {
loginError = nil
guard baseAccessURL != "" else {
return
}
guard let url = URL(string: baseAccessURL), url.scheme == "https" else {
loginError = .invalidURL
return
Expand All @@ -155,7 +156,6 @@ struct LoginForm<C: Client, S: Session>: View {

private func back() {
withAnimation {
loginError = nil
currentPage = .serverURL
focusedField = .baseAccessURL
}
Expand All @@ -164,17 +164,14 @@ struct LoginForm<C: Client, S: Session>: View {

enum LoginError {
case invalidURL
case invalidToken
case failedAuth
case failedAuth(ClientError)

var description: String {
switch self {
case .invalidURL:
return "Invalid URL"
case .invalidToken:
return "Invalid Session Token"
case .failedAuth:
return "Could not authenticate with Coder deployment"
case .failedAuth(let err):
return "Could not authenticate with Coder deployment:\n\(err.description)"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Coder Desktop/Coder DesktopTests/AgentsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct AgentsTests {
vpn.state = .connected
vpn.agents = createMockAgents(count: 7)

try await ViewHosting.host(view) { _ in
try await ViewHosting.host(view) {
try await sut.inspection.inspect { view in
var toggle = try view.find(ViewType.Toggle.self)
#expect(try toggle.labelView().text().string() == "Show All")
Expand Down
Loading