use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use reqwest::Error;
use reqwest::StatusCode;
use reqwest::header::HeaderMap;
use serde_json::Value;
use super::ServiceKind;
use super::ServiceSignal;
use super::constants::GITHUB_CORE_BUCKET;
use super::constants::GITHUB_GRAPHQL_BUCKET;
use super::constants::GRAPHQL_RATE_LIMITED_ERROR_TYPE;
use super::constants::GRAPHQL_RESPONSE_ERRORS_KEY;
use super::constants::GRAPHQL_RESPONSE_TYPE_KEY;
use super::constants::RATE_LIMIT_LIMIT_HEADER;
use super::constants::RATE_LIMIT_LIMIT_KEY;
use super::constants::RATE_LIMIT_REMAINING_HEADER;
use super::constants::RATE_LIMIT_REMAINING_KEY;
use super::constants::RATE_LIMIT_RESET_HEADER;
use super::constants::RATE_LIMIT_RESET_KEY;
use super::constants::RATE_LIMIT_RESOURCE_HEADER;
use super::constants::RATE_LIMIT_RESOURCES_KEY;
use super::constants::RATE_LIMIT_USED_HEADER;
use super::constants::RATE_LIMIT_USED_KEY;
pub(super) use super::constants::SYNTHETIC_RATE_LIMIT_SECS;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RateLimitBucket {
Core,
GraphQl,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct RateLimitQuota {
pub limit: u64,
pub used: u64,
pub remaining: u64,
pub reset_at: Option<u64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct GitHubRateLimit {
pub core: Option<RateLimitQuota>,
pub graphql: Option<RateLimitQuota>,
}
pub(crate) fn parse_rate_limit_headers(
headers: &HeaderMap,
) -> Option<(RateLimitBucket, RateLimitQuota)> {
let resource = headers.get(RATE_LIMIT_RESOURCE_HEADER)?.to_str().ok()?;
let bucket = match resource {
GITHUB_CORE_BUCKET => RateLimitBucket::Core,
GITHUB_GRAPHQL_BUCKET => RateLimitBucket::GraphQl,
_ => return None,
};
let parse = |name: &str| -> Option<u64> { headers.get(name)?.to_str().ok()?.parse().ok() };
let limit = parse(RATE_LIMIT_LIMIT_HEADER)?;
let used = parse(RATE_LIMIT_USED_HEADER)?;
let remaining = parse(RATE_LIMIT_REMAINING_HEADER)?;
let reset_at = parse(RATE_LIMIT_RESET_HEADER);
Some((
bucket,
RateLimitQuota {
limit,
used,
remaining,
reset_at,
},
))
}
pub(crate) fn parse_rate_limit_response(value: &Value) -> GitHubRateLimit {
let resources = value.get(RATE_LIMIT_RESOURCES_KEY);
let bucket = |name: &str| -> Option<RateLimitQuota> {
let entry = resources?.get(name)?;
Some(RateLimitQuota {
limit: entry.get(RATE_LIMIT_LIMIT_KEY)?.as_u64()?,
used: entry.get(RATE_LIMIT_USED_KEY)?.as_u64()?,
remaining: entry.get(RATE_LIMIT_REMAINING_KEY)?.as_u64()?,
reset_at: entry
.get(RATE_LIMIT_RESET_KEY)
.and_then(serde_json::Value::as_u64),
})
};
GitHubRateLimit {
core: bucket(GITHUB_CORE_BUCKET),
graphql: bucket(GITHUB_GRAPHQL_BUCKET),
}
}
pub(crate) fn github_is_rate_limited(status: StatusCode, headers: &HeaderMap) -> bool {
if status == StatusCode::TOO_MANY_REQUESTS {
return true;
}
if status == StatusCode::FORBIDDEN {
return headers
.get(RATE_LIMIT_REMAINING_HEADER)
.and_then(|value| value.to_str().ok())
.and_then(|text| text.parse::<u64>().ok())
.is_some_and(|remaining| remaining == 0);
}
false
}
pub(crate) fn graphql_body_is_rate_limited(body: &Value) -> bool {
body.get(GRAPHQL_RESPONSE_ERRORS_KEY)
.and_then(serde_json::Value::as_array)
.is_some_and(|errors| {
errors.iter().any(|err| {
err.get(GRAPHQL_RESPONSE_TYPE_KEY)
.and_then(serde_json::Value::as_str)
.is_some_and(|t| t == GRAPHQL_RATE_LIMITED_ERROR_TYPE)
})
})
}
pub(super) fn classify_network_error(service: ServiceKind, error: &Error) -> Option<ServiceSignal> {
if error.is_connect() || error.is_timeout() {
Some(ServiceSignal::Unreachable(service))
} else {
None
}
}
pub(super) fn now_epoch_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}