// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// What a reply means for the request that produced it.
///
/// The three answers are the OTLP protocol's own: an acceptance, a refusal that
/// retrying cannot fix, and a refusal that only time can. Getting the third one
/// wrong is how an exporter either gives up on a busy collector or hammers a
/// broken one.
#derive(Eq, Debug)
pub(all) enum Verdict {
/// `2xx`, including a partial success: the request was taken, whatever the
/// server chose to do with every record in it.
Accepted
/// A refusal that repeating the request would repeat. Bad data is the usual
/// cause, and the specification says such a request MUST NOT be retried.
Rejected
/// A refusal that waiting may fix: `429`, `502`, `503` and `504`.
Retry
}
///|
pub extend Verdict with Eq::{equal, not_equal}
///|
pub extend Verdict with Debug::{to_repr}
///|
/// The verdict for a status code.
///
/// The four retryable codes are the specification's table and nothing else: every
/// other `4xx` and `5xx` MUST NOT be retried, and a `3xx` that reached this point
/// came from a client that was told not to follow it.
pub fn verdict(status : Int32) -> Verdict {
if status >= 200 && status < 300 {
Accepted
} else if status == 429 || status == 502 || status == 503 || status == 504 {
Retry
} else {
Rejected
}
}
///|
/// The wait a `Retry-After` value asks for, in milliseconds, or `None` when the
/// value is neither a number of seconds nor an HTTP date.
///
/// `now` is milliseconds since the Unix epoch, which is what a date has to be
/// compared against. A date in the past answers zero rather than a negative wait:
/// the server is telling this client to come back now.
pub fn retry_after_milliseconds(value : StringView, now~ : Int64) -> Int64? {
let text = trimmed(value)
match parse_decimal(text) {
Some(seconds) => return Some(seconds * 1000L)
None => ()
}
match http_date_milliseconds(text) {
Some(at) => {
let wait = at - now
Some(if wait < 0L { 0L } else { wait })
}
None => None
}
}
///|
/// A decimal integer, and nothing else: a sign, a space or a fraction is not one.
fn parse_decimal(text : StringView) -> Int64? {
if text.length() == 0 || text.length() > 18 {
return None
}
let mut value : Int64 = 0L
for i in 0.. 0x39 {
return None
}
value = value * 10L + (c - 0x30).to_int64()
}
Some(value)
}
///|
/// The moment an IMF-fixdate names, in milliseconds since the Unix epoch.
///
/// RFC 7231 also allows two obsolete formats; a server that sends one is out of
/// date by three decades, and answering `None` lets the caller fall back to its
/// own backoff instead of guessing at a date it cannot read.
fn http_date_milliseconds(text : StringView) -> Int64? {
// "Sun, 06 Nov 1994 08:49:37 GMT" is 29 characters, and every field sits at a
// fixed offset, so the separators are checked rather than scanned for.
if text.length() != 29 {
return None
}
if text[3].to_int() != 0x2c ||
text[4].to_int() != 0x20 ||
text[7].to_int() != 0x20 ||
text[11].to_int() != 0x20 ||
text[16].to_int() != 0x20 ||
text[19].to_int() != 0x3a ||
text[22].to_int() != 0x3a ||
text[25].to_int() != 0x20 {
return None
}
if text.exact_view(start=26, end=29).to_owned() != "GMT" {
return None
}
let day = match two_digits(text, 5) {
Some(day) => day
None => return None
}
let month = match month_number(text.exact_view(start=8, end=11)) {
Some(month) => month
None => return None
}
let year = match four_digits(text, 12) {
Some(year) => year
None => return None
}
let hour = match two_digits(text, 17) {
Some(hour) => hour
None => return None
}
let minute = match two_digits(text, 20) {
Some(minute) => minute
None => return None
}
let second = match two_digits(text, 23) {
Some(second) => second
None => return None
}
if hour > 23 || minute > 59 || second > 60 || day < 1 || day > 31 {
return None
}
let days = days_from_civil(year, month, day)
let seconds = hour * 3600 + minute * 60 + second
Some(days * 86400000L + seconds.to_int64() * 1000L)
}
///|
fn two_digits(text : StringView, at : Int32) -> Int32? {
let high = text[at].to_int()
let low = text[at + 1].to_int()
if high < 0x30 || high > 0x39 || low < 0x30 || low > 0x39 {
return None
}
Some((high - 0x30) * 10 + (low - 0x30))
}
///|
fn four_digits(text : StringView, at : Int32) -> Int32? {
let a = two_digits(text, at)
let b = two_digits(text, at + 2)
match (a, b) {
(Some(a), Some(b)) => Some(a * 100 + b)
_ => None
}
}
///|
fn month_number(name : StringView) -> Int32? {
match name.to_owned().to_upper() {
"JAN" => Some(1)
"FEB" => Some(2)
"MAR" => Some(3)
"APR" => Some(4)
"MAY" => Some(5)
"JUN" => Some(6)
"JUL" => Some(7)
"AUG" => Some(8)
"SEP" => Some(9)
"OCT" => Some(10)
"NOV" => Some(11)
"DEC" => Some(12)
_ => None
}
}
///|
/// Days from 1970-01-01 to `year-month-day`, counting backwards before it. The
/// proleptic Gregorian calendar, by the shift-to-March trick that keeps the leap
/// day at the end of the year.
fn days_from_civil(year : Int32, month : Int32, day : Int32) -> Int64 {
let y = if month <= 2 { year - 1 } else { year }
let era = if y >= 0 { y / 400 } else { (y - 399) / 400 }
let yoe = y - era * 400
let mp = (month + 9) % 12
let doy = (153 * mp + 2) / 5 + day - 1
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
(era * 146097 + doe - 719468).to_int64()
}
///|
/// `text` without the spaces and tabs around it.
fn trimmed(text : StringView) -> StringView {
let mut lo = 0
let mut hi = text.length()
while lo < hi && is_space(text[lo].to_int()) {
lo = lo + 1
}
while hi > lo && is_space(text[hi - 1].to_int()) {
hi = hi - 1
}
text.exact_view(start=lo, end=hi)
}
///|
fn is_space(c : Int32) -> Bool {
c == 0x20 || c == 0x09
}