use std::fmt;
use crate::error::{Error, Result};
#[derive(Clone, Default)]
pub struct HttpOptions {
headers: Vec<(String, String)>,
proxy: ProxyChoice,
}
#[derive(Clone, Default)]
enum ProxyChoice {
#[default]
FromEnv,
Off,
Url(String),
}
pub(crate) static DEFAULT: HttpOptions = HttpOptions::new();
impl HttpOptions {
#[must_use]
pub const fn new() -> Self {
Self {
headers: Vec::new(),
proxy: ProxyChoice::FromEnv,
}
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn proxy(mut self, url: impl Into<String>) -> Self {
self.proxy = ProxyChoice::Url(url.into());
self
}
#[must_use]
pub fn no_proxy(mut self) -> Self {
self.proxy = ProxyChoice::Off;
self
}
pub(crate) fn resolve_proxy(&self) -> Result<Option<ureq::Proxy>> {
match &self.proxy {
ProxyChoice::FromEnv => Ok(ureq::Proxy::try_from_env()),
ProxyChoice::Off => Ok(None),
ProxyChoice::Url(url) => ureq::Proxy::new(url)
.map(Some)
.map_err(|e| Error::Http(format!("invalid proxy URL: {e}"))),
}
}
pub(crate) fn merge(&self, own: &[(&str, &str)]) -> Vec<(String, String)> {
let mut merged: Vec<(String, String)> = own
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
for (name, value) in &self.headers {
if !own.iter().any(|(k, _)| k.eq_ignore_ascii_case(name)) {
merged.push((name.clone(), value.clone()));
}
}
merged
}
}
impl fmt::Debug for HttpOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let names: Vec<&str> = self.headers.iter().map(|(k, _)| k.as_str()).collect();
let proxy = match self.proxy {
ProxyChoice::FromEnv => "from environment",
ProxyChoice::Off => "none",
ProxyChoice::Url(_) => "configured",
};
f.debug_struct("HttpOptions")
.field("headers", &names)
.field("proxy", &proxy)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn own_headers_win_over_configured_ones() {
let http = HttpOptions::new()
.header("Authorization", "Bearer mine")
.header("x-team", "desktop");
let merged = http.merge(&[("authorization", "Bearer source")]);
assert_eq!(
merged,
[
("authorization".to_string(), "Bearer source".to_string()),
("x-team".to_string(), "desktop".to_string()),
]
);
}
#[test]
fn debug_hides_header_values_and_the_proxy_url() {
let http = HttpOptions::new()
.header("authorization", "Bearer s3cr3t")
.proxy("http://user:hunter2@proxy:3128");
let shown = format!("{http:?}");
assert!(shown.contains("authorization"), "{shown}");
assert!(
!shown.contains("s3cr3t") && !shown.contains("hunter2"),
"{shown}"
);
}
#[test]
fn rejects_an_invalid_proxy_url_without_repeating_it() {
let err = HttpOptions::new()
.proxy("ftp://user:hunter2@proxy")
.resolve_proxy()
.unwrap_err();
assert!(!err.to_string().contains("hunter2"), "{err}");
assert!(
HttpOptions::new()
.no_proxy()
.resolve_proxy()
.unwrap()
.is_none()
);
}
}