586 lines
23 KiB
Swift
586 lines
23 KiB
Swift
import Foundation
|
|
|
|
public struct ProviderDiagnosticBatchExport: Codable, Sendable {
|
|
public let schemaVersion: String
|
|
public let timestamp: Date
|
|
public let diagnostics: [ProviderDiagnosticExport]
|
|
|
|
public init(
|
|
schemaVersion: String = "1.0",
|
|
timestamp: Date,
|
|
diagnostics: [ProviderDiagnosticExport])
|
|
{
|
|
self.schemaVersion = schemaVersion
|
|
self.timestamp = timestamp
|
|
self.diagnostics = diagnostics
|
|
}
|
|
}
|
|
|
|
public struct ProviderDiagnosticExport: Codable, Sendable {
|
|
public let schemaVersion: String
|
|
public let timestamp: Date
|
|
public let platform: String
|
|
public let appVersion: String?
|
|
public let provider: String
|
|
public let displayName: String
|
|
public let source: String
|
|
public let sourceMode: String
|
|
public let auth: ProviderDiagnosticAuthSummary
|
|
public let usage: ProviderDiagnosticUsageSummary?
|
|
public let fetchAttempts: [ProviderDiagnosticFetchAttempt]
|
|
public let error: ProviderDiagnosticError?
|
|
public let settings: ProviderDiagnosticSettingsSummary
|
|
public let details: ProviderDiagnosticDetails?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case schemaVersion
|
|
case timestamp
|
|
case platform
|
|
case appVersion
|
|
case provider
|
|
case displayName
|
|
case source
|
|
case sourceMode
|
|
case auth
|
|
case usage
|
|
case fetchAttempts
|
|
case error
|
|
case settings
|
|
case details
|
|
}
|
|
|
|
public init(
|
|
schemaVersion: String = "1.0",
|
|
timestamp: Date,
|
|
platform: String = ProviderDiagnosticPlatform.current,
|
|
appVersion: String? = nil,
|
|
provider: String,
|
|
displayName: String,
|
|
source: String,
|
|
sourceMode: String,
|
|
auth: ProviderDiagnosticAuthSummary,
|
|
usage: ProviderDiagnosticUsageSummary?,
|
|
fetchAttempts: [ProviderDiagnosticFetchAttempt],
|
|
error: ProviderDiagnosticError?,
|
|
settings: ProviderDiagnosticSettingsSummary,
|
|
details: ProviderDiagnosticDetails?)
|
|
{
|
|
self.schemaVersion = schemaVersion
|
|
self.timestamp = timestamp
|
|
self.platform = platform
|
|
self.appVersion = appVersion
|
|
self.provider = provider
|
|
self.displayName = displayName
|
|
self.source = source
|
|
self.sourceMode = sourceMode
|
|
self.auth = auth
|
|
self.usage = usage
|
|
self.fetchAttempts = fetchAttempts
|
|
self.error = error
|
|
self.settings = settings
|
|
self.details = details
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
try self.init(
|
|
schemaVersion: container.decode(String.self, forKey: .schemaVersion),
|
|
timestamp: container.decode(Date.self, forKey: .timestamp),
|
|
platform: container.decodeIfPresent(String.self, forKey: .platform)
|
|
?? ProviderDiagnosticPlatform.current,
|
|
appVersion: container.decodeIfPresent(String.self, forKey: .appVersion),
|
|
provider: container.decode(String.self, forKey: .provider),
|
|
displayName: container.decode(String.self, forKey: .displayName),
|
|
source: container.decode(String.self, forKey: .source),
|
|
sourceMode: container.decode(String.self, forKey: .sourceMode),
|
|
auth: container.decode(ProviderDiagnosticAuthSummary.self, forKey: .auth),
|
|
usage: container.decodeIfPresent(ProviderDiagnosticUsageSummary.self, forKey: .usage),
|
|
fetchAttempts: container.decode([ProviderDiagnosticFetchAttempt].self, forKey: .fetchAttempts),
|
|
error: container.decodeIfPresent(ProviderDiagnosticError.self, forKey: .error),
|
|
settings: container.decode(ProviderDiagnosticSettingsSummary.self, forKey: .settings),
|
|
details: container.decodeIfPresent(ProviderDiagnosticDetails.self, forKey: .details))
|
|
}
|
|
}
|
|
|
|
public enum ProviderDiagnosticPlatform {
|
|
public static var current: String {
|
|
#if os(macOS)
|
|
"macOS"
|
|
#elseif os(Linux)
|
|
"Linux"
|
|
#else
|
|
"unknown"
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public struct ProviderDiagnosticAuthSummary: Codable, Sendable {
|
|
public let configured: Bool
|
|
public let modes: [String]
|
|
|
|
public init(configured: Bool, modes: [String]) {
|
|
self.configured = configured
|
|
self.modes = modes
|
|
}
|
|
|
|
public func resolved(with outcome: ProviderFetchOutcome) -> ProviderDiagnosticAuthSummary {
|
|
var resolvedModes = self.modes
|
|
if outcome.isSuccess {
|
|
for attempt in outcome.attempts where attempt.wasAvailable {
|
|
let mode = ProviderDiagnosticFetchAttempt.kindLabel(attempt.kind)
|
|
if !resolvedModes.contains(mode) {
|
|
resolvedModes.append(mode)
|
|
}
|
|
}
|
|
}
|
|
let configured = self.configured || outcome.isSuccess
|
|
return ProviderDiagnosticAuthSummary(configured: configured, modes: resolvedModes)
|
|
}
|
|
}
|
|
|
|
public struct ProviderDiagnosticUsageSummary: Codable, Sendable {
|
|
public let updatedAt: Date
|
|
public let dataConfidence: String
|
|
public let windows: [ProviderDiagnosticRateWindow]
|
|
public let extraWindowCount: Int
|
|
public let providerCostPresent: Bool
|
|
public let providerSpecificData: [String]
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case updatedAt
|
|
case dataConfidence
|
|
case windows
|
|
case extraWindowCount
|
|
case providerCostPresent
|
|
case providerSpecificData
|
|
}
|
|
|
|
public init(from snapshot: UsageSnapshot) {
|
|
var windows: [ProviderDiagnosticRateWindow] = []
|
|
if let primary = snapshot.primary {
|
|
windows.append(ProviderDiagnosticRateWindow(label: "primary", window: primary))
|
|
}
|
|
if let secondary = snapshot.secondary {
|
|
windows.append(ProviderDiagnosticRateWindow(label: "secondary", window: secondary))
|
|
}
|
|
if let tertiary = snapshot.tertiary {
|
|
windows.append(ProviderDiagnosticRateWindow(label: "tertiary", window: tertiary))
|
|
}
|
|
for extra in snapshot.extraRateWindows ?? [] {
|
|
windows.append(ProviderDiagnosticRateWindow(
|
|
label: extra.title,
|
|
window: extra.window,
|
|
usageKnown: extra.usageKnown))
|
|
}
|
|
|
|
var providerSpecificData: [String] = []
|
|
if snapshot.kiroUsage != nil { providerSpecificData.append("kiroUsage") }
|
|
if snapshot.ampUsage != nil { providerSpecificData.append("ampUsage") }
|
|
if snapshot.zaiUsage != nil { providerSpecificData.append("zaiUsage") }
|
|
if snapshot.minimaxUsage != nil { providerSpecificData.append("minimaxUsage") }
|
|
if snapshot.deepseekUsage != nil { providerSpecificData.append("deepseekUsage") }
|
|
if snapshot.openRouterUsage != nil { providerSpecificData.append("openRouterUsage") }
|
|
if snapshot.sakanaPayAsYouGo != nil { providerSpecificData.append("sakanaPayAsYouGo") }
|
|
if snapshot.openAIAPIUsage != nil { providerSpecificData.append("openAIAPIUsage") }
|
|
if snapshot.claudeAdminAPIUsage != nil { providerSpecificData.append("claudeAdminAPIUsage") }
|
|
if snapshot.mistralUsage != nil { providerSpecificData.append("mistralUsage") }
|
|
if snapshot.deepgramUsage != nil { providerSpecificData.append("deepgramUsage") }
|
|
if snapshot.cursorRequests != nil { providerSpecificData.append("cursorRequests") }
|
|
if snapshot.crossModelUsage != nil { providerSpecificData.append("crossModelUsage") }
|
|
|
|
self.updatedAt = snapshot.updatedAt
|
|
self.dataConfidence = snapshot.dataConfidence.rawValue
|
|
self.windows = windows
|
|
self.extraWindowCount = snapshot.extraRateWindows?.count ?? 0
|
|
self.providerCostPresent = snapshot.providerCost != nil
|
|
self.providerSpecificData = providerSpecificData.sorted()
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.updatedAt = try container.decode(Date.self, forKey: .updatedAt)
|
|
self.dataConfidence = try container.decodeIfPresent(String.self, forKey: .dataConfidence)
|
|
?? UsageDataConfidence.unknown.rawValue
|
|
self.windows = try container.decode([ProviderDiagnosticRateWindow].self, forKey: .windows)
|
|
self.extraWindowCount = try container.decode(Int.self, forKey: .extraWindowCount)
|
|
self.providerCostPresent = try container.decode(Bool.self, forKey: .providerCostPresent)
|
|
self.providerSpecificData = try container.decode([String].self, forKey: .providerSpecificData)
|
|
}
|
|
}
|
|
|
|
public struct ProviderDiagnosticRateWindow: Codable, Sendable {
|
|
public let label: String
|
|
public let usedPercent: Double
|
|
public let windowMinutes: Int?
|
|
public let resetsAt: Date?
|
|
public let hasResetDescription: Bool
|
|
public let nextRegenPercent: Double?
|
|
public let usageKnown: Bool
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case label
|
|
case usedPercent
|
|
case windowMinutes
|
|
case resetsAt
|
|
case hasResetDescription
|
|
case nextRegenPercent
|
|
case usageKnown
|
|
}
|
|
|
|
public init(label: String, window: RateWindow, usageKnown: Bool = true) {
|
|
self.label = label
|
|
self.usedPercent = window.usedPercent
|
|
self.windowMinutes = window.windowMinutes
|
|
self.resetsAt = window.resetsAt
|
|
self.hasResetDescription = window.resetDescription?.isEmpty == false
|
|
self.nextRegenPercent = window.nextRegenPercent
|
|
self.usageKnown = usageKnown
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.label = try container.decode(String.self, forKey: .label)
|
|
self.usedPercent = try container.decode(Double.self, forKey: .usedPercent)
|
|
self.windowMinutes = try container.decodeIfPresent(Int.self, forKey: .windowMinutes)
|
|
self.resetsAt = try container.decodeIfPresent(Date.self, forKey: .resetsAt)
|
|
self.hasResetDescription = try container.decode(Bool.self, forKey: .hasResetDescription)
|
|
self.nextRegenPercent = try container.decodeIfPresent(Double.self, forKey: .nextRegenPercent)
|
|
self.usageKnown = try container.decodeIfPresent(Bool.self, forKey: .usageKnown) ?? true
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(self.label, forKey: .label)
|
|
try container.encode(self.usedPercent, forKey: .usedPercent)
|
|
try container.encodeIfPresent(self.windowMinutes, forKey: .windowMinutes)
|
|
try container.encodeIfPresent(self.resetsAt, forKey: .resetsAt)
|
|
try container.encode(self.hasResetDescription, forKey: .hasResetDescription)
|
|
try container.encodeIfPresent(self.nextRegenPercent, forKey: .nextRegenPercent)
|
|
if !self.usageKnown {
|
|
try container.encode(false, forKey: .usageKnown)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct ProviderDiagnosticFetchAttempt: Codable, Sendable {
|
|
public let kind: String
|
|
public let wasAvailable: Bool
|
|
public let errorCategory: String?
|
|
|
|
public init(
|
|
kind: String,
|
|
wasAvailable: Bool,
|
|
errorCategory: String?)
|
|
{
|
|
self.kind = kind
|
|
self.wasAvailable = wasAvailable
|
|
self.errorCategory = errorCategory
|
|
}
|
|
|
|
public init(from attempt: ProviderFetchAttempt) {
|
|
self.kind = Self.kindLabel(attempt.kind)
|
|
self.wasAvailable = attempt.wasAvailable
|
|
self.errorCategory = attempt.errorDescription.map(Self.errorCategoryLabel)
|
|
}
|
|
|
|
public static func kindLabel(_ kind: ProviderFetchKind) -> String {
|
|
switch kind {
|
|
case .cli: "cli"
|
|
case .web: "web"
|
|
case .oauth: "oauth"
|
|
case .apiToken: "api"
|
|
case .localProbe: "local"
|
|
case .webDashboard: "web"
|
|
}
|
|
}
|
|
|
|
public static func errorCategoryLabel(_ description: String?) -> String {
|
|
guard let desc = description?.lowercased() else { return "unknown" }
|
|
if desc.contains("endpoint override") {
|
|
return "configuration"
|
|
}
|
|
if desc.contains("network") || desc.contains("timeout") || desc.contains("connection") {
|
|
return "network"
|
|
}
|
|
if desc.contains("auth") || desc.contains("credential") || desc.contains("token") || desc.contains("cookie") ||
|
|
desc.contains("api key") || desc.contains("key not configured") || desc.contains("missing key")
|
|
{
|
|
return "auth"
|
|
}
|
|
if desc.contains("source") || desc.contains("not supported") || desc.contains("unavailable") {
|
|
return "configuration"
|
|
}
|
|
if desc.contains("api") || desc.contains("http") || desc.contains("404") || desc.contains("403") {
|
|
return "api"
|
|
}
|
|
if desc.contains("parse") || desc.contains("format") || desc.contains("decode") {
|
|
return "parse"
|
|
}
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
public struct ProviderDiagnosticError: Codable, Sendable {
|
|
public let category: String
|
|
public let safeDescription: String
|
|
|
|
public init(category: String, safeDescription: String) {
|
|
self.category = category
|
|
self.safeDescription = safeDescription
|
|
}
|
|
|
|
public init(from error: Error, authConfigured: Bool) {
|
|
self.category = Self.errorCategory(error, authConfigured: authConfigured)
|
|
self.safeDescription = Self.safeDescription(category: self.category)
|
|
}
|
|
|
|
private static func errorCategory(_ error: Error, authConfigured: Bool) -> String {
|
|
if case ProviderFetchError.noAvailableStrategy = error {
|
|
return authConfigured ? "configuration" : "auth"
|
|
}
|
|
if error is ProviderEndpointOverrideError {
|
|
return "configuration"
|
|
}
|
|
if let minimaxError = error as? MiniMaxUsageError {
|
|
switch minimaxError {
|
|
case .networkError: return "network"
|
|
case .invalidCredentials: return "auth"
|
|
case .apiError: return "api"
|
|
case .parseFailed: return "parse"
|
|
}
|
|
}
|
|
if let alibabaError = error as? AlibabaCodingPlanUsageError {
|
|
switch alibabaError {
|
|
case .networkError: return "network"
|
|
case .loginRequired, .invalidCredentials: return "auth"
|
|
case .apiError, .apiKeyUnavailableInRegion: return "api"
|
|
case .parseFailed: return "parse"
|
|
}
|
|
}
|
|
if error is MiniMaxSettingsError || error is MiniMaxAPISettingsError { return "auth" }
|
|
return ProviderDiagnosticFetchAttempt.errorCategoryLabel(error.localizedDescription)
|
|
}
|
|
|
|
private static func safeDescription(category: String) -> String {
|
|
switch category {
|
|
case "network":
|
|
"Network error - check your connection"
|
|
case "auth":
|
|
"Authentication or setup issue - check provider credentials"
|
|
case "api":
|
|
"API error - service returned an unexpected response"
|
|
case "parse":
|
|
"Parse error - unexpected response format"
|
|
case "configuration":
|
|
"Configuration issue - check provider source and settings"
|
|
default:
|
|
"An unexpected error occurred"
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct ProviderDiagnosticSettingsSummary: Codable, Sendable {
|
|
public let sourceMode: String
|
|
public let apiRegion: String?
|
|
|
|
public init(sourceMode: ProviderSourceMode, apiRegion: String? = nil) {
|
|
self.sourceMode = sourceMode.rawValue
|
|
self.apiRegion = apiRegion
|
|
}
|
|
}
|
|
|
|
public enum ProviderDiagnosticDetails: Codable, Sendable {
|
|
case minimax(MiniMaxDiagnosticDetails)
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case type
|
|
case minimax
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let type = try container.decode(String.self, forKey: .type)
|
|
switch type {
|
|
case "minimax":
|
|
self = try .minimax(container.decode(MiniMaxDiagnosticDetails.self, forKey: .minimax))
|
|
default:
|
|
throw DecodingError.dataCorruptedError(
|
|
forKey: .type,
|
|
in: container,
|
|
debugDescription: "Unknown provider diagnostic detail type")
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case let .minimax(details):
|
|
try container.encode("minimax", forKey: .type)
|
|
try container.encode(details, forKey: .minimax)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct MiniMaxDiagnosticDetails: Codable, Sendable {
|
|
public let planName: String?
|
|
public let availablePrompts: Int?
|
|
public let currentPrompts: Int?
|
|
public let remainingPrompts: Int?
|
|
public let windowMinutes: Int?
|
|
public let usedPercent: Double?
|
|
public let resetsAt: Date?
|
|
public let services: [MiniMaxDiagnosticServiceUsage]?
|
|
public let billingSummaryPresent: Bool
|
|
|
|
public init(from snapshot: MiniMaxUsageSnapshot) {
|
|
self.planName = snapshot.planName
|
|
self.availablePrompts = snapshot.availablePrompts
|
|
self.currentPrompts = snapshot.currentPrompts
|
|
self.remainingPrompts = snapshot.remainingPrompts
|
|
self.windowMinutes = snapshot.windowMinutes
|
|
self.usedPercent = snapshot.usedPercent
|
|
self.resetsAt = snapshot.resetsAt
|
|
self.services = snapshot.services?.map { MiniMaxDiagnosticServiceUsage(from: $0) }
|
|
self.billingSummaryPresent = snapshot.billingSummary != nil
|
|
}
|
|
}
|
|
|
|
public struct MiniMaxDiagnosticServiceUsage: Codable, Sendable {
|
|
public let displayName: String
|
|
public let percent: Double
|
|
public let usage: Int
|
|
public let limit: Int
|
|
public let remaining: Int?
|
|
public let isUnlimited: Bool
|
|
public let windowType: String
|
|
public let resetsAt: Date?
|
|
public let hasResetDescription: Bool
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case displayName
|
|
case percent
|
|
case usage
|
|
case limit
|
|
case remaining
|
|
case isUnlimited
|
|
case windowType
|
|
case resetsAt
|
|
case hasResetDescription
|
|
}
|
|
|
|
public init(from service: MiniMaxServiceUsage) {
|
|
self.displayName = service.displayName
|
|
self.percent = service.percent
|
|
self.usage = service.usage
|
|
self.limit = service.limit
|
|
self.remaining = service.isUnlimited ? nil : max(0, service.limit - service.usage)
|
|
self.isUnlimited = service.isUnlimited
|
|
self.windowType = service.windowType
|
|
self.resetsAt = service.resetsAt
|
|
self.hasResetDescription = !service.resetDescription.isEmpty
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.displayName = try container.decode(String.self, forKey: .displayName)
|
|
self.percent = try container.decode(Double.self, forKey: .percent)
|
|
self.usage = try container.decodeIfPresent(Int.self, forKey: .usage) ?? 0
|
|
self.limit = try container.decodeIfPresent(Int.self, forKey: .limit) ?? 0
|
|
self.remaining = try container.decodeIfPresent(Int.self, forKey: .remaining)
|
|
self.isUnlimited = try container.decodeIfPresent(Bool.self, forKey: .isUnlimited) ?? false
|
|
self.windowType = try container.decode(String.self, forKey: .windowType)
|
|
self.resetsAt = try container.decodeIfPresent(Date.self, forKey: .resetsAt)
|
|
self.hasResetDescription = try container.decode(Bool.self, forKey: .hasResetDescription)
|
|
}
|
|
}
|
|
|
|
public enum ProviderDiagnosticExportBuilder {
|
|
public struct Input: Sendable {
|
|
public let provider: UsageProvider
|
|
public let descriptor: ProviderDescriptor
|
|
public let outcome: ProviderFetchOutcome
|
|
public let sourceMode: ProviderSourceMode
|
|
public let settings: ProviderSettingsSnapshot?
|
|
public let auth: ProviderDiagnosticAuthSummary
|
|
public let appVersion: String?
|
|
|
|
public init(
|
|
provider: UsageProvider,
|
|
descriptor: ProviderDescriptor,
|
|
outcome: ProviderFetchOutcome,
|
|
sourceMode: ProviderSourceMode,
|
|
settings: ProviderSettingsSnapshot?,
|
|
auth: ProviderDiagnosticAuthSummary,
|
|
appVersion: String? = nil)
|
|
{
|
|
self.provider = provider
|
|
self.descriptor = descriptor
|
|
self.outcome = outcome
|
|
self.sourceMode = sourceMode
|
|
self.settings = settings
|
|
self.auth = auth
|
|
self.appVersion = appVersion
|
|
}
|
|
}
|
|
|
|
public static func build(_ input: Input) -> ProviderDiagnosticExport {
|
|
let resolvedAuth = input.auth.resolved(with: input.outcome)
|
|
let usage = input.outcome.usageSnapshot.map { ProviderDiagnosticUsageSummary(from: $0) }
|
|
let error = input.outcome.failureError
|
|
.map { ProviderDiagnosticError(from: $0, authConfigured: resolvedAuth.configured) }
|
|
let settingsSummary = ProviderDiagnosticSettingsSummary(
|
|
sourceMode: input.sourceMode,
|
|
apiRegion: Self.safeAPIRegion(provider: input.provider, settings: input.settings))
|
|
|
|
return ProviderDiagnosticExport(
|
|
timestamp: Date(),
|
|
appVersion: input.appVersion,
|
|
provider: input.provider.rawValue,
|
|
displayName: input.descriptor.metadata.displayName,
|
|
source: input.outcome.sourceLabel,
|
|
sourceMode: input.sourceMode.rawValue,
|
|
auth: resolvedAuth,
|
|
usage: usage,
|
|
fetchAttempts: input.outcome.attempts.map { ProviderDiagnosticFetchAttempt(from: $0) },
|
|
error: error,
|
|
settings: settingsSummary,
|
|
details: Self.details(provider: input.provider, outcome: input.outcome))
|
|
}
|
|
|
|
private static func safeAPIRegion(provider: UsageProvider, settings: ProviderSettingsSnapshot?) -> String? {
|
|
guard provider == .minimax else { return nil }
|
|
return settings?.minimax?.apiRegion.rawValue ?? "global"
|
|
}
|
|
|
|
private static func details(provider: UsageProvider, outcome: ProviderFetchOutcome) -> ProviderDiagnosticDetails? {
|
|
guard provider == .minimax,
|
|
let usage = outcome.usageSnapshot?.minimaxUsage
|
|
else {
|
|
return nil
|
|
}
|
|
return .minimax(MiniMaxDiagnosticDetails(from: usage))
|
|
}
|
|
}
|
|
|
|
extension ProviderFetchOutcome {
|
|
fileprivate var isSuccess: Bool {
|
|
guard case .success = self.result else { return false }
|
|
return true
|
|
}
|
|
|
|
fileprivate var sourceLabel: String {
|
|
guard case let .success(result) = result else { return "failed" }
|
|
return result.sourceLabel
|
|
}
|
|
|
|
fileprivate var usageSnapshot: UsageSnapshot? {
|
|
guard case let .success(result) = result else { return nil }
|
|
return result.usage
|
|
}
|
|
|
|
fileprivate var failureError: Error? {
|
|
guard case let .failure(error) = result else { return nil }
|
|
return error
|
|
}
|
|
}
|