///|
/// Parses nonnegative seconds (including decimals) or, given a clock, an
/// HTTP-date in any of RFC 9110's three forms.
/// Results are truncated to milliseconds and saturated at Int max.
///
/// ```mbt check
/// test {
/// assert_eq(@http.parse_retry_after("1.2349"), Some(1234))
/// }
/// ```
pub fn parse_retry_after(value : String, now_unix_ms? : Int64) -> Int32? {
let value = value.trim().to_owned()
if decimal_ms(value, 1000) is Some(ms) {
return Some(ms)
}
guard now_unix_ms is Some(now) else { return None }
guard http_date_ms(value, now) is Some(date) else { return None }
if date <= now {
return Some(0)
}
// Compare before subtracting: even an Int64-min clock must not overflow.
if now < date - 2147483647L {
Some(2147483647)
} else {
Some((date - now).to_int())
}
}
///|
/// Prefers the first retry-after-ms value; falls back to retry-after when that
/// header is absent or unparseable, as the official OpenAI SDKs do.
pub fn retry_after_ms(headers : Headers, now_unix_ms? : Int64) -> Int32? {
if headers.get("retry-after-ms") is Some(value) &&
decimal_ms(value.trim().to_owned(), 1) is Some(ms) {
return Some(ms)
}
match headers.get("retry-after") {
Some(value) => parse_retry_after(value, now_unix_ms?)
None => None
}
}
///|
// Validate the whole input even after saturation. No floating-point rounding.
fn decimal_ms(value : String, scale : Int32) -> Int32? {
let mut whole = 0L
let mut fraction = 0
let mut weight = scale
let mut dot = false
let mut before = 0
let mut after = 0
for c in value {
if c == '.' && !dot {
dot = true
} else if c >= '0' && c <= '9' {
let digit = c.to_int() - 48
if dot {
after += 1
weight /= 10
fraction += digit * weight
} else {
before += 1
whole = (whole * 10 + digit.to_int64()).min(2147483647L)
}
} else {
return None
}
}
if before == 0 || (dot && after == 0) {
return None
}
Some(
(whole * scale.to_int64() + fraction.to_int64()).min(2147483647L).to_int(),
)
}