Functional Weave
Code in Rust

http.retry-after@1.0.0

impl/rust.rs

2,903 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_is_leap_year::is_leap_year;  ← from dates.is-leap-year ^1.0.0 · built alongside by fune
use super::time_iso_to_unix::iso_to_unix;  ← from time.iso-to-unix ^1.0.0 · built alongside by fune

const DAY_NAMES: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const MONTH_DAYS: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// Nine digits is 31 years; anything longer is a broken header, not a wait.
const MAX_DIGITS: usize = 9;

fn all_digits(text: &str) -> bool {
    !text.is_empty() && text.bytes().all(|b| b.is_ascii_digit())
}

fn number(text: &str) -> i64 {
    text.bytes().fold(0, |n, b| n * 10 + (b - b'0') as i64)
}

/// An IMF-fixdate, "Sun, 06 Nov 1994 08:49:37 GMT", to Unix seconds; None if it is not one.
fn imf_fixdate(t: &str) -> Option<i64> {
    // Byte positions are only safe once the text is known to be ASCII.
    if t.len() != 29 || !t.is_ascii() {
        return None;
    }
    if !DAY_NAMES.contains(&&t[0..3]) || &t[3..5] != ", " || &t[7..8] != " " || &t[11..12] != " " || &t[16..17] != " " {
        return None;
    }
    if &t[19..20] != ":" || &t[22..23] != ":" || &t[25..] != " GMT" {
        return None;
    }
    let month = MONTHS.iter().position(|m| *m == &t[8..11])?;
    let (dd, yyyy, hh, mi, ss) = (&t[5..7], &t[12..16], &t[17..19], &t[20..22], &t[23..25]);
    if ![dd, yyyy, hh, mi, ss].iter().all(|part| all_digits(part)) {
        return None;
    }
    let (year, day) = (number(yyyy), number(dd));
    if year < 1 || number(hh) > 23 || number(mi) > 59 || number(ss) > 59 {
        return None;
    }
    let month_days = if month == 1 && is_leap_year(year) { 29 } else { MONTH_DAYS[month] };
    if day < 1 || day > month_days {
        return None;
    }
    Some(iso_to_unix(&format!("{}-{:02}-{}T{}:{}:{}Z", yyyy, month + 1, dd, hh, mi, ss)))
}

/// How long a client should wait before retrying, from a Retry-After header
/// (RFC 9110 10.2.3): delay-seconds, or an HTTP date turned into seconds from
/// `now`. A date in the past is 0. Anything else is None.
pub fn retry_after_seconds(header: Option<&str>, now: i64) -> Option<i64> {
    let header = header?;
    // HTTP's optional whitespace is space and tab only (RFC 9110 5.6.3).
    let t = header.trim_matches(|c| c == ' ' || c == '\t');
    if all_digits(t) {
        return if t.len() <= MAX_DIGITS { Some(number(t)) } else { None };
    }
    let at = imf_fixdate(t)?;
    Some(if at > now { at - now } else { 0 })
}

pub fn fune_vector(args: &[Value]) -> Value {
    let now = match &args[1] {
        Value::Int(i) => *i,
        _ => panic!("now must be a whole number of seconds"),
    };
    let header = match &args[0] {
        Value::Str(s) => Some(s.as_str()),
        _ => None,
    };
    match retry_after_seconds(header, now) {
        Some(seconds) => Value::Int(seconds),
        None => Value::Null,
    }
}