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(),
}
}
pub async fn call_raw<P>(&self, method: &str, params: &P) -> Result<Value>
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?;
check_status(&body)?;
Ok(body)
}
pub async fn call<P, T>(&self, method: &str, params: &P) -> Result<T>
where
P: Serialize + ?Sized,
T: DeserializeOwned,
{
let body = self.call_raw(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 = self.call_raw(method, params).await?;
let subtree = body.pointer(pointer).cloned().ok_or_else(|| {
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<()> {
let status = body
.get("status")
.and_then(Value::as_str)
.ok_or_else(|| Error::InvalidResponse("response missing `status` field".into()))?;
if status == "success" {
Ok(())
} else {
Err(Error::Api(ApiStatus(status.to_owned())))
}
}