#[derive(Debug)]
pub struct ApiResponse<T> {
pub data: T,
pub request_id: Option<String>,
pub rate_limit_remaining: Option<u32>,
pub rate_limit_reset: Option<u64>,
pub cycles_tenant: Option<String>,
pub date_ms: Option<u64>,
}
impl<T> ApiResponse<T> {
pub fn into_inner(self) -> T {
self.data
}
pub(crate) fn from_response(data: T, headers: &reqwest::header::HeaderMap) -> Self {
let header_str = |name: &str| -> Option<String> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.map(String::from)
};
let header_u32 = |name: &str| -> Option<u32> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
};
let header_u64 = |name: &str| -> Option<u64> {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
};
Self {
data,
request_id: header_str("x-request-id"),
rate_limit_remaining: header_u32("x-ratelimit-remaining"),
rate_limit_reset: header_u64("x-ratelimit-reset"),
cycles_tenant: header_str("x-cycles-tenant"),
date_ms: headers
.get("date")
.and_then(|v| v.to_str().ok())
.and_then(parse_http_date_ms),
}
}
}
pub(crate) fn parse_http_date_ms(value: &str) -> Option<u64> {
let time = httpdate::parse_http_date(value).ok()?;
let ms = time.duration_since(std::time::UNIX_EPOCH).ok()?.as_millis();
u64::try_from(ms).ok()
}
impl<T> std::ops::Deref for ApiResponse<T> {
type Target = T;
fn deref(&self) -> &T {
&self.data
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_imf_fixdate() {
assert_eq!(
parse_http_date_ms("Tue, 14 Nov 2023 22:13:20 GMT"),
Some(1_700_000_000_000)
);
}
#[test]
fn garbage_and_empty_dates_are_none() {
assert_eq!(parse_http_date_ms("not-a-date"), None);
assert_eq!(parse_http_date_ms(""), None);
assert_eq!(parse_http_date_ms("Tue, 99 Nov 2023 22:13:20 GMT"), None);
}
}