import Foundation struct APIEnvironment { let baseURL: URL init(baseURL: URL) { self.baseURL = baseURL.absoluteString.hasSuffix("/") ? baseURL : baseURL.appendingPathComponent("") } } enum HermesAPIError: Error { case invalidURL(String) case invalidResponse case transport(Error) case unexpectedStatus(Int, Data) case decoding(Error) } struct HermesAPIClient { let environment: APIEnvironment let session: URLSession private let encoder: JSONEncoder private let decoder: JSONDecoder init(environment: APIEnvironment, session: URLSession = .shared) { self.environment = environment self.session = session let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase encoder.dateEncodingStrategy = .iso8601 self.encoder = encoder let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .iso8601 self.decoder = decoder } func startSession(_ payload: HermesSessionStartRequest) async throws -> HermesSessionResponse { try await send(path: "api/v1/session/start", method: "POST", body: payload) } func health() async throws -> HermesHealthResponse { try await send(path: "health") } func endSession() async throws -> HermesSessionResponse { try await send(path: "api/v1/session/end", method: "POST") } func currentSession() async throws -> HermesSessionResponse { try await send(path: "api/v1/session/me") } func nextEvent() async throws -> HermesEvent { try await send(path: "api/v1/feed/next") } func eventManifest(eventID: UUID) async throws -> HermesEventManifest { try await send(path: "api/v1/events/\(eventID.uuidString)/manifest") } func markets(eventID: UUID) async throws -> [HermesMarket] { try await send(path: "api/v1/events/\(eventID.uuidString)/markets") } func currentOdds(marketID: UUID) async throws -> HermesOddsVersion { try await send(path: "api/v1/markets/\(marketID.uuidString)/odds/current") } func submitBetIntent(_ payload: HermesBetIntentRequest) async throws -> HermesBetIntentResponse { try await send(path: "api/v1/bets/intent", method: "POST", body: payload) } func betIntent(id: UUID) async throws -> HermesBetIntentResponse { try await send(path: "api/v1/bets/\(id.uuidString)") } func settlement(eventID: UUID) async throws -> HermesSettlement { try await send(path: "api/v1/events/\(eventID.uuidString)/result") } func experimentConfig() async throws -> HermesExperimentConfig { try await send(path: "api/v1/experiments/config") } func localization(localeCode: String) async throws -> HermesLocalizationBundle { try await send(path: "api/v1/localization/\(localeCode)") } func submitAnalyticsBatch(_ payload: HermesAnalyticsBatchRequest) async throws { let encodedBody = try encoder.encode(payload) _ = try await perform(path: "api/v1/analytics/batch", method: "POST", body: encodedBody) } private func send(path: String, method: String = "GET") async throws -> Response { let (data, _) = try await perform(path: path, method: method) do { return try decoder.decode(Response.self, from: data) } catch { throw HermesAPIError.decoding(error) } } private func send(path: String, method: String, body: Body) async throws -> Response { let encodedBody = try encoder.encode(body) let (data, _) = try await perform(path: path, method: method, body: encodedBody) do { return try decoder.decode(Response.self, from: data) } catch { throw HermesAPIError.decoding(error) } } private func perform(path: String, method: String, body: Data? = nil) async throws -> (Data, HTTPURLResponse) { let request = try makeRequest(path: path, method: method, body: body) do { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw HermesAPIError.invalidResponse } guard (200...299).contains(httpResponse.statusCode) else { throw HermesAPIError.unexpectedStatus(httpResponse.statusCode, data) } return (data, httpResponse) } catch let error as HermesAPIError { throw error } catch { throw HermesAPIError.transport(error) } } private func makeRequest(path: String, method: String, body: Data? = nil) throws -> URLRequest { let normalizedPath = path.hasPrefix("/") ? String(path.dropFirst()) : path guard let url = URL(string: normalizedPath, relativeTo: environment.baseURL)?.absoluteURL else { throw HermesAPIError.invalidURL(path) } var request = URLRequest(url: url) request.httpMethod = method request.setValue("application/json", forHTTPHeaderField: "Accept") if let body { request.httpBody = body request.setValue("application/json", forHTTPHeaderField: "Content-Type") } return request } }