use std::borrow::Cow;
use std::fmt::Debug;
use std::time::Duration;
use crate::{cross_log, errors::BuildError};
const DEFAULT_USER_AGENT: &str = concat!("pubky.org", "@", env!("CARGO_PKG_VERSION"),);
#[derive(Debug, Clone)]
#[must_use]
#[derive(Default)]
pub struct PubkyHttpClientBuilder {
pkarr: pkarr::ClientBuilder,
http_request_timeout: Option<Duration>,
user_agent_extra: Option<String>,
#[cfg(target_arch = "wasm32")]
testnet_host: Option<String>,
}
impl PubkyHttpClientBuilder {
pub fn testnet(&mut self) -> &mut Self {
self.testnet_with_host("localhost")
}
#[cfg_attr(
target_arch = "wasm32",
allow(
clippy::semicolon_outside_block,
clippy::unnecessary_operation,
clippy::semicolon_if_nothing_returned,
reason = "WASM-only block preserves conditional assignment formatting"
)
)]
pub fn testnet_with_host(&mut self, host: &str) -> &mut Self {
cross_log!(info, "Configuring testnet builders for host {host}");
#[cfg(not(target_arch = "wasm32"))]
self.pkarr.bootstrap(&[format!(
"{}:{}",
host,
pubky_common::constants::testnet_ports::BOOTSTRAP
)]);
self.pkarr
.relays(&[format!(
"http://{}:{}",
host,
pubky_common::constants::testnet_ports::PKARR_RELAY
)])
.expect("relays urls infallible");
#[cfg(target_arch = "wasm32")]
{
self.testnet_host = Some(host.to_string());
}
self
}
pub fn pkarr<F>(&mut self, f: F) -> &mut Self
where
F: FnOnce(&mut pkarr::ClientBuilder) -> &mut pkarr::ClientBuilder,
{
f(&mut self.pkarr);
self
}
pub const fn request_timeout(&mut self, timeout: Duration) -> &mut Self {
self.http_request_timeout = Some(timeout);
self
}
pub fn user_agent_extra<S: Into<String>>(&mut self, extra: S) -> &mut Self {
self.user_agent_extra = Some(extra.into());
self
}
pub fn build(&self) -> Result<PubkyHttpClient, BuildError> {
let pkarr = self.pkarr.build()?;
let user_agent = self
.user_agent_extra
.as_deref()
.map(str::trim)
.filter(|extra| !extra.is_empty())
.map_or_else(
|| Cow::Borrowed(DEFAULT_USER_AGENT),
|extra| Cow::Owned(format!("{DEFAULT_USER_AGENT} {extra}")),
);
cross_log!(
info,
"Building PubkyHttpClient (timeout: {:?}, user_agent: {})",
self.http_request_timeout,
user_agent
);
#[cfg(not(target_arch = "wasm32"))]
let mut http_builder =
reqwest::ClientBuilder::from(pkarr.clone()).user_agent(user_agent.as_ref());
#[cfg(target_arch = "wasm32")]
let http_builder = reqwest::Client::builder().user_agent(user_agent.as_ref());
#[cfg(not(target_arch = "wasm32"))]
let mut icann_http_builder = reqwest::Client::builder().user_agent(user_agent.as_ref());
#[cfg(not(target_arch = "wasm32"))]
if let Some(timeout) = self.http_request_timeout {
http_builder = http_builder.timeout(timeout);
icann_http_builder = icann_http_builder.timeout(timeout);
}
Ok(PubkyHttpClient {
pkarr,
http: http_builder.build()?,
#[cfg(not(target_arch = "wasm32"))]
icann_http: icann_http_builder.build()?,
#[cfg(target_arch = "wasm32")]
testnet_host: self.testnet_host.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct PubkyHttpClient {
pub(crate) http: reqwest::Client,
pub(crate) pkarr: pkarr::Client,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) icann_http: reqwest::Client,
#[cfg(target_arch = "wasm32")]
pub(crate) testnet_host: Option<String>,
}
impl PubkyHttpClient {
pub fn new() -> Result<Self, BuildError> {
cross_log!(
info,
"Constructing PubkyHttpClient with default configuration"
);
Self::builder().build()
}
pub fn builder() -> PubkyHttpClientBuilder {
PubkyHttpClientBuilder::default()
}
pub fn testnet() -> Result<Self, BuildError> {
cross_log!(
info,
"Constructing PubkyHttpClient configured for local testnet"
);
let mut builder = Self::builder();
builder.testnet();
builder.build()
}
#[must_use]
pub const fn pkarr(&self) -> &pkarr::Client {
&self.pkarr
}
}
#[cfg(test)]
mod test {
use httpmock::MockServer;
use reqwest::{Method, StatusCode};
use super::*;
#[tokio::test]
async fn test_fetch() {
let server = MockServer::start();
let mock = server.mock(|when, then| {
when.method("GET").path("/health");
then.status(200).body("ok");
});
let client = PubkyHttpClient::new().unwrap();
let response = client
.request(Method::GET, &server.url("/health"))
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
mock.assert();
}
}