use std::time::Duration;
use base64::Engine as _;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use url::Url;
use crate::error::Error;
#[derive(Debug, Clone)]
pub enum Auth {
ApiKey(
String,
),
Basic {
username: String,
password: String,
},
IbssoToken(
String,
),
Bearer(
String,
),
}
impl Auth {
pub fn api_key(key: impl Into<String>) -> Self {
Self::ApiKey(key.into())
}
pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
Self::Basic {
username: username.into(),
password: password.into(),
}
}
pub fn ibsso(token: impl Into<String>) -> Self {
Self::IbssoToken(token.into())
}
pub fn bearer(token: impl Into<String>) -> Self {
Self::Bearer(token.into())
}
fn header_value(&self) -> Result<HeaderValue, Error> {
let raw = match self {
Auth::ApiKey(k) => format!("App {k}"),
Auth::Basic { username, password } => {
let encoded = base64::engine::general_purpose::STANDARD
.encode(format!("{username}:{password}"));
format!("Basic {encoded}")
}
Auth::IbssoToken(t) => format!("IBSSO {t}"),
Auth::Bearer(t) => format!("Bearer {t}"),
};
HeaderValue::from_str(&raw)
.map_err(|e| Error::Config(format!("invalid auth header: {e}")))
}
}
#[derive(Debug, Default)]
pub struct ClientBuilder {
base_url: Option<String>,
auth: Option<Auth>,
timeout: Option<Duration>,
user_agent: Option<String>,
http: Option<reqwest::Client>,
}
impl ClientBuilder {
pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn auth(mut self, auth: Auth) -> Self {
self.auth = Some(auth);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn http_client(mut self, http: reqwest::Client) -> Self {
self.http = Some(http);
self
}
pub fn build(self) -> Result<Client, Error> {
let base_url = self
.base_url
.ok_or_else(|| Error::Config("base_url is required".into()))?;
let auth = self
.auth
.ok_or_else(|| Error::Config("auth is required".into()))?;
let base_url = Url::parse(&base_url)?;
let http = match self.http {
Some(c) => c,
None => {
let mut builder = reqwest::Client::builder().user_agent(
self.user_agent
.unwrap_or_else(|| format!("infobip-sms-rust/{}", env!("CARGO_PKG_VERSION"))),
);
if let Some(t) = self.timeout {
builder = builder.timeout(t);
}
builder
.build()
.map_err(|e| Error::Config(format!("failed to build HTTP client: {e}")))?
}
};
let mut default_headers = HeaderMap::new();
default_headers.insert(AUTHORIZATION, auth.header_value()?);
default_headers.insert(
reqwest::header::ACCEPT,
HeaderValue::from_static("application/json"),
);
Ok(Client {
base_url,
http,
default_headers,
})
}
}
#[derive(Debug, Clone)]
pub struct Client {
pub(crate) base_url: Url,
pub(crate) http: reqwest::Client,
pub(crate) default_headers: HeaderMap,
}
impl Client {
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub(crate) fn url(&self, path: &str) -> Result<Url, Error> {
let trimmed = path.trim_start_matches('/');
Ok(self.base_url.join(trimmed)?)
}
pub(crate) fn request(
&self,
method: reqwest::Method,
path: &str,
) -> Result<reqwest::RequestBuilder, Error> {
let url = self.url(path)?;
Ok(self.http.request(method, url).headers(self.default_headers.clone()))
}
}