build iOS study scaffold
This commit is contained in:
@@ -2,23 +2,149 @@ 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 get(path: String) async throws -> (Data, HTTPURLResponse) {
|
||||
let url = environment.baseURL.appendingPathComponent(path)
|
||||
let (data, response) = try await session.data(from: url)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
func startSession(_ payload: HermesSessionStartRequest) async throws -> HermesSessionResponse {
|
||||
try await send(path: "api/v1/session/start", method: "POST", body: payload)
|
||||
}
|
||||
|
||||
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 {
|
||||
try await perform(path: "api/v1/analytics/batch", method: "POST", body: payload)
|
||||
}
|
||||
|
||||
private func send<Response: Decodable>(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)
|
||||
}
|
||||
return (data, httpResponse)
|
||||
}
|
||||
|
||||
private func send<Response: Decodable, Body: Encodable>(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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import Foundation
|
||||
|
||||
struct HermesSessionStartRequest: Codable {
|
||||
var externalRef: String?
|
||||
var localeCode: String?
|
||||
var devicePlatform: String?
|
||||
var deviceModel: String?
|
||||
var osVersion: String?
|
||||
var appVersion: String?
|
||||
var experimentVariant: String?
|
||||
}
|
||||
|
||||
struct HermesSessionResponse: Codable {
|
||||
var sessionId: UUID
|
||||
var userId: UUID
|
||||
var startedAt: Date
|
||||
var endedAt: Date?
|
||||
var experimentVariant: String
|
||||
var appVersion: String
|
||||
var deviceModel: String?
|
||||
var osVersion: String?
|
||||
var localeCode: String
|
||||
var devicePlatform: String
|
||||
}
|
||||
|
||||
struct HermesEvent: Codable {
|
||||
var id: UUID
|
||||
var sportType: String
|
||||
var sourceRef: String
|
||||
var titleEn: String
|
||||
var titleSv: String
|
||||
var status: String
|
||||
var previewStartMs: Int
|
||||
var previewEndMs: Int
|
||||
var revealStartMs: Int
|
||||
var revealEndMs: Int
|
||||
var lockAt: Date
|
||||
var settleAt: Date
|
||||
}
|
||||
|
||||
struct HermesEventMedia: Codable {
|
||||
var id: UUID
|
||||
var eventId: UUID
|
||||
var mediaType: String
|
||||
var hlsMasterURL: URL
|
||||
var posterURL: URL?
|
||||
var durationMs: Int
|
||||
var previewStartMs: Int
|
||||
var previewEndMs: Int
|
||||
var revealStartMs: Int
|
||||
var revealEndMs: Int
|
||||
}
|
||||
|
||||
struct HermesOutcome: Codable {
|
||||
var id: UUID
|
||||
var marketId: UUID
|
||||
var outcomeCode: String
|
||||
var labelKey: String
|
||||
var sortOrder: Int
|
||||
}
|
||||
|
||||
struct HermesMarket: Codable {
|
||||
var id: UUID
|
||||
var eventId: UUID
|
||||
var questionKey: String
|
||||
var marketType: String
|
||||
var status: String
|
||||
var lockAt: Date
|
||||
var settlementRuleKey: String
|
||||
var outcomes: [HermesOutcome]
|
||||
}
|
||||
|
||||
struct HermesOutcomeOdds: Codable {
|
||||
var id: UUID
|
||||
var oddsVersionId: UUID
|
||||
var outcomeId: UUID
|
||||
var decimalOdds: Double
|
||||
var fractionalNum: Int
|
||||
var fractionalDen: Int
|
||||
}
|
||||
|
||||
struct HermesOddsVersion: Codable {
|
||||
var id: UUID
|
||||
var marketId: UUID
|
||||
var versionNo: Int
|
||||
var createdAt: Date
|
||||
var isCurrent: Bool
|
||||
var odds: [HermesOutcomeOdds]
|
||||
}
|
||||
|
||||
struct HermesEventManifest: Codable {
|
||||
var event: HermesEvent
|
||||
var media: [HermesEventMedia]
|
||||
var markets: [HermesMarket]
|
||||
}
|
||||
|
||||
struct HermesBetIntentRequest: Codable {
|
||||
var sessionId: UUID
|
||||
var eventId: UUID
|
||||
var marketId: UUID
|
||||
var outcomeId: UUID
|
||||
var idempotencyKey: String
|
||||
var clientSentAt: Date
|
||||
}
|
||||
|
||||
struct HermesBetIntentResponse: Codable {
|
||||
var id: UUID
|
||||
var accepted: Bool
|
||||
var acceptanceCode: String
|
||||
var acceptedOddsVersionId: UUID?
|
||||
var serverReceivedAt: Date
|
||||
}
|
||||
|
||||
struct HermesSettlement: Codable {
|
||||
var id: UUID
|
||||
var marketId: UUID
|
||||
var settledAt: Date
|
||||
var winningOutcomeId: UUID
|
||||
}
|
||||
|
||||
struct HermesAnalyticsAttributeInput: Codable {
|
||||
var key: String
|
||||
var value: String
|
||||
}
|
||||
|
||||
struct HermesAnalyticsEventInput: Codable {
|
||||
var eventName: String
|
||||
var occurredAt: Date
|
||||
var attributes: [HermesAnalyticsAttributeInput]?
|
||||
}
|
||||
|
||||
struct HermesAnalyticsBatchRequest: Codable {
|
||||
var events: [HermesAnalyticsEventInput]
|
||||
}
|
||||
|
||||
struct HermesExperimentConfig: Codable {
|
||||
var variant: String
|
||||
var featureFlags: [String: Bool]
|
||||
}
|
||||
|
||||
struct HermesLocalizationBundle: Codable {
|
||||
var localeCode: String
|
||||
var values: [String: String]
|
||||
}
|
||||
Reference in New Issue
Block a user