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

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 2 commits intomainfromethan/api-errs
Dec 16, 2024
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

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

39 changes: 22 additions & 17 deletionsCoder Desktop/Coder Desktop/Preview Content/PreviewClient.swift
View file
Open in desktop
Original file line numberDiff line numberDiff 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: "admin@coder.com",
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: "admin@coder.com",
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 deletionsCoder Desktop/Coder Desktop/SDK/Client.swift
View file
Open in desktop
Original file line numberDiff line numberDiff 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
) asyncthrows(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> {
) asyncthrows(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 deletionsCoder Desktop/Coder Desktop/SDK/User.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
import Foundation

extension CoderClient {
func user(_ ident: String) async throws -> User {
letresp = await request("/api/v2/users/\(ident)", method: .get)
guardlet response = resp.response, response.statusCode == 200 else {
throwClientError.unexpectedStatusCode
func user(_ ident: String) async throws(ClientError) -> User {
letres = try await request("/api/v2/users/\(ident)", method: .get)
guardres.resp.statusCode == 200 else {
throwresponseAsError(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 deletionsCoder Desktop/Coder Desktop/Views/LoginForm.swift
View file
Open in desktop
Original file line numberDiff line numberDiff 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 {
dothrows(ClientError){
_ = try await client.user("me")
} catch {
loginError = .failedAuth
print("Set error")
loginError = .failedAuth(error)
return
}
session.store(baseAccessURL: url, sessionToken: sessionToken)
Expand DownExpand 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 deletionCoder Desktop/Coder DesktopTests/AgentsTests.swift
View file
Open in desktop
Original file line numberDiff line numberDiff 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

[8]ページ先頭

©2009-2025 Movatter.jp