use reqwest::Url;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::error::{ApiStatus, Error, Result};
pub const DEFAULT_BASE_URL: &str = "https://voip.ms/api/v1/rest.php";
#[derive(Debug, Clone)]
pub struct Client {
http: reqwest::Client,
base_url: Url,
api_username: String,
api_password: String,
}
impl Client {
pub fn new(api_username: impl Into<String>, api_password: impl Into<String>) -> Self {
Self::builder(api_username, api_password)
.build()
.expect("default VoIP.ms base URL must parse")
}
pub fn builder(
api_username: impl Into<String>,
api_password: impl Into<String>,
) -> ClientBuilder {
ClientBuilder {
http: None,
base_url: None,
api_username: api_username.into(),
api_password: api_password.into(),
}
}
async fn fetch<P>(&self, method: &str, params: &P) -> Result<(Value, Option<ApiStatus>)>
where
P: Serialize + ?Sized,
{
let response = self
.http
.get(self.base_url.clone())
.query(&[
("api_username", self.api_username.as_str()),
("api_password", self.api_password.as_str()),
("method", method),
])
.query(params)
.send()
.await?
.error_for_status()?;
let body: Value = response.json().await?;
let empty = check_status(&body)?;
Ok((body, empty))
}
pub async fn call_raw<P>(&self, method: &str, params: &P) -> Result<Value>
where
P: Serialize + ?Sized,
{
let (body, empty) = self.fetch(method, params).await?;
if let Some(status) = empty {
return Err(Error::Api(status));
}
Ok(body)
}
pub async fn call<P, T>(&self, method: &str, params: &P) -> Result<T>
where
P: Serialize + ?Sized,
T: DeserializeOwned,
{
let (body, _empty) = self.fetch(method, params).await?;
serde_json::from_value(body)
.map_err(|e| Error::InvalidResponse(format!("failed to deserialize response: {e}")))
}
pub async fn call_at<P, T>(&self, method: &str, params: &P, pointer: &str) -> Result<T>
where
P: Serialize + ?Sized,
T: DeserializeOwned,
{
let (body, empty) = self.fetch(method, params).await?;
let subtree = match body.pointer(pointer) {
Some(v) => v.clone(),
None if empty.is_some() => Value::Null,
None => {
return Err(Error::InvalidResponse(format!(
"response missing JSON pointer `{pointer}` for method `{method}`"
)));
}
};
serde_json::from_value(subtree).map_err(|e| {
Error::InvalidResponse(format!(
"failed to deserialize JSON pointer `{pointer}` for method `{method}`: {e}"
))
})
}
pub fn base_url(&self) -> &Url {
&self.base_url
}
}
#[derive(Debug)]
pub struct ClientBuilder {
http: Option<reqwest::Client>,
base_url: Option<Url>,
api_username: String,
api_password: String,
}
impl ClientBuilder {
pub fn http_client(mut self, http: reqwest::Client) -> Self {
self.http = Some(http);
self
}
pub fn base_url(mut self, url: Url) -> Self {
self.base_url = Some(url);
self
}
pub fn build(self) -> Result<Client> {
let base_url = match self.base_url {
Some(u) => u,
None => Url::parse(DEFAULT_BASE_URL).map_err(|e| {
Error::InvalidResponse(format!("default base URL failed to parse: {e}"))
})?,
};
let http = self.http.unwrap_or_default();
Ok(Client {
http,
base_url,
api_username: self.api_username,
api_password: self.api_password,
})
}
}
fn check_status(body: &Value) -> Result<Option<ApiStatus>> {
let status = body
.get("status")
.and_then(Value::as_str)
.ok_or_else(|| Error::InvalidResponse("response missing `status` field".into()))?;
if status == "success" {
return Ok(None);
}
let status = ApiStatus::from_wire(status);
if status.is_empty() {
Ok(Some(status))
} else {
Err(Error::Api(status))
}
}