use std::time::Duration;
use anyhow::{Context as _, Result};
pub const USER_AGENT: &str = concat!("anodizer/", env!("CARGO_PKG_VERSION"));
pub fn blocking_client(timeout: Duration) -> Result<reqwest::blocking::Client> {
reqwest::blocking::Client::builder()
.user_agent(USER_AGENT)
.timeout(timeout)
.build()
.context("build blocking HTTP client")
}
pub fn async_client(timeout: Duration) -> Result<reqwest::Client> {
reqwest::Client::builder()
.user_agent(USER_AGENT)
.timeout(timeout)
.build()
.context("build async HTTP client")
}
pub fn github_api_base<E: crate::EnvSource + ?Sized>(env: &E) -> String {
let raw = env
.var("ANODIZER_GITHUB_API_BASE")
.unwrap_or_else(|| "https://api.github.com".to_string());
raw.trim_end_matches('/').to_string()
}
pub fn github_api_base_with_config<E: crate::EnvSource + ?Sized>(
github_urls: Option<&crate::config::GitHubUrlsConfig>,
env: &E,
) -> String {
github_urls
.and_then(|u| u.api.as_deref())
.map(|api| api.trim_end_matches('/').to_string())
.unwrap_or_else(|| github_api_base(env))
}
pub fn body_read_error_message<E: std::fmt::Display>(err: E) -> String {
format!("could not read response body: {err}")
}
pub async fn body_of(resp: reqwest::Response) -> String {
match resp.text().await {
Ok(s) => s,
Err(err) => body_read_error_message(err),
}
}
pub fn body_of_blocking(resp: reqwest::blocking::Response) -> String {
match resp.text() {
Ok(s) => s,
Err(err) => body_read_error_message(err),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn github_api_base_strips_trailing_slash() {
let env =
crate::MapEnvSource::new().with("ANODIZER_GITHUB_API_BASE", "https://example.com/api/");
assert_eq!(github_api_base(&env), "https://example.com/api");
}
#[test]
fn github_api_base_defaults_when_env_unset() {
let env = crate::MapEnvSource::new();
assert_eq!(github_api_base(&env), "https://api.github.com");
}
#[test]
fn github_api_base_with_config_prefers_configured_ghes_api() {
let urls = crate::config::GitHubUrlsConfig {
api: Some("https://github.example.com/api/v3/".to_string()),
..Default::default()
};
let env =
crate::MapEnvSource::new().with("ANODIZER_GITHUB_API_BASE", "https://override.test");
assert_eq!(
github_api_base_with_config(Some(&urls), &env),
"https://github.example.com/api/v3"
);
}
#[test]
fn github_api_base_with_config_falls_back_to_env_resolver() {
let env =
crate::MapEnvSource::new().with("ANODIZER_GITHUB_API_BASE", "https://override.test/");
assert_eq!(
github_api_base_with_config(None, &env),
"https://override.test"
);
let unset = crate::MapEnvSource::new();
assert_eq!(
github_api_base_with_config(None, &unset),
"https://api.github.com"
);
}
#[test]
fn test_body_read_error_message_uses_descriptive_prefix() {
let formatted = body_read_error_message("connection reset by peer");
assert_eq!(
formatted,
"could not read response body: connection reset by peer"
);
}
#[test]
fn test_body_read_error_message_with_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "stream ended early");
let formatted = body_read_error_message(io_err);
assert!(
formatted.starts_with("could not read response body: "),
"format must keep the descriptive prefix: {formatted}"
);
assert!(
formatted.contains("stream ended early"),
"format must include the underlying error: {formatted}"
);
}
}