Files
steipete--codexbar/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebExtraRateWindowParser.swift
T
2026-07-13 12:22:33 +08:00

124 lines
4.5 KiB
Swift

import Foundation
enum ClaudeWebExtraRateWindowParser {
private static let definitions: [(id: String, title: String, keys: [String])] = [
(
id: "claude-routines",
title: "Daily Routines",
keys: [
"seven_day_routines",
"seven_day_claude_routines",
"claude_routines",
"routines",
"routine",
"seven_day_cowork",
"cowork",
]),
]
static func parse(from json: [String: Any]) -> (windows: [NamedRateWindow], sourceKeys: [String: String]) {
var windows: [NamedRateWindow] = []
var sourceKeys: [String: String] = [:]
windows.reserveCapacity(Self.definitions.count)
for definition in Self.definitions {
if let foundWindow = Self.firstUsageWindow(in: json, keys: definition.keys) {
let rawWindow = foundWindow.window
guard let utilization = Self.percentValue(from: rawWindow["utilization"]) else { continue }
let resetsAt = (rawWindow["resets_at"] as? String).flatMap(Self.parseISO8601Date)
windows.append(Self.namedWindow(
id: definition.id,
title: definition.title,
usedPercent: utilization,
resetsAt: resetsAt))
sourceKeys[definition.id] = foundWindow.sourceKey
continue
}
// Some accounts expose the key with null payloads (for example `seven_day_cowork: null`).
// Preserve the bar in that case with a 0% window so the product section remains visible.
if let key = Self.firstUsageKey(in: json, keys: definition.keys) {
windows.append(Self.namedWindow(
id: definition.id,
title: definition.title,
usedPercent: 0,
resetsAt: nil))
sourceKeys[definition.id] = key
}
}
windows.append(contentsOf: Self.scopedWeeklyLimitWindows(from: json))
return (windows, sourceKeys)
}
private static func scopedWeeklyLimitWindows(from json: [String: Any]) -> [NamedRateWindow] {
guard let limits = json["limits"] as? [[String: Any]] else { return [] }
let mappedLimits = limits.map { entry in
let scope = entry["scope"] as? [String: Any]
let model = scope?["model"] as? [String: Any]
return ClaudeScopedWeeklyLimitMapper.Limit(
kind: entry["kind"] as? String,
group: entry["group"] as? String,
percent: Self.percentValue(from: entry["percent"]),
resetsAt: (entry["resets_at"] as? String).flatMap(Self.parseISO8601Date),
modelID: model?["id"] as? String,
modelName: model?["display_name"] as? String)
}
return ClaudeScopedWeeklyLimitMapper.extraRateWindows(from: mappedLimits)
}
private static func namedWindow(
id: String,
title: String,
usedPercent: Double,
resetsAt: Date?) -> NamedRateWindow
{
NamedRateWindow(
id: id,
title: title,
window: RateWindow(
usedPercent: usedPercent,
windowMinutes: 7 * 24 * 60,
resetsAt: resetsAt,
resetDescription: nil))
}
private static func firstUsageWindow(
in json: [String: Any],
keys: [String]) -> (window: [String: Any], sourceKey: String)?
{
for key in keys {
if let window = json[key] as? [String: Any] {
return (window, key)
}
}
return nil
}
private static func firstUsageKey(in json: [String: Any], keys: [String]) -> String? {
for key in keys where json.keys.contains(key) {
return key
}
return nil
}
private static func percentValue(from value: Any?) -> Double? {
if let intValue = value as? Int {
return Double(intValue)
}
if let doubleValue = value as? Double {
return doubleValue
}
return nil
}
private static func parseISO8601Date(_ string: String) -> Date? {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: string) {
return date
}
formatter.formatOptions = [.withInternetDateTime]
return formatter.date(from: string)
}
}