1use reqwest::Url;
2use serde::Serialize;
3use serde::de::DeserializeOwned;
4use serde_json::Value;
5
6use crate::error::{ApiStatus, Error, Result};
7
8pub const DEFAULT_BASE_URL: &str = "https://voip.ms/api/v1/rest.php";
10
11#[derive(Debug, Clone)]
16pub struct Client {
17 http: reqwest::Client,
18 base_url: Url,
19 api_username: String,
20 api_password: String,
21}
22
23impl Client {
24 pub fn new(api_username: impl Into<String>, api_password: impl Into<String>) -> Self {
32 Self::builder(api_username, api_password)
33 .build()
34 .expect("default voip.ms base URL must parse")
35 }
36
37 pub fn builder(
39 api_username: impl Into<String>,
40 api_password: impl Into<String>,
41 ) -> ClientBuilder {
42 ClientBuilder {
43 http: None,
44 base_url: None,
45 api_username: api_username.into(),
46 api_password: api_password.into(),
47 }
48 }
49
50 pub async fn call_raw<P>(&self, method: &str, params: &P) -> Result<Value>
61 where
62 P: Serialize + ?Sized,
63 {
64 let response = self
65 .http
66 .get(self.base_url.clone())
67 .query(&[
68 ("api_username", self.api_username.as_str()),
69 ("api_password", self.api_password.as_str()),
70 ("method", method),
71 ])
72 .query(params)
73 .send()
74 .await?
75 .error_for_status()?;
76
77 let body: Value = response.json().await?;
78 check_status(&body)?;
79 Ok(body)
80 }
81
82 pub async fn call<P, T>(&self, method: &str, params: &P) -> Result<T>
87 where
88 P: Serialize + ?Sized,
89 T: DeserializeOwned,
90 {
91 let body = self.call_raw(method, params).await?;
92 serde_json::from_value(body)
93 .map_err(|e| Error::InvalidResponse(format!("failed to deserialize response: {e}")))
94 }
95
96 pub async fn call_at<P, T>(&self, method: &str, params: &P, pointer: &str) -> Result<T>
101 where
102 P: Serialize + ?Sized,
103 T: DeserializeOwned,
104 {
105 let body = self.call_raw(method, params).await?;
106 let subtree = body.pointer(pointer).cloned().ok_or_else(|| {
107 Error::InvalidResponse(format!(
108 "response missing JSON pointer `{pointer}` for method `{method}`"
109 ))
110 })?;
111
112 serde_json::from_value(subtree).map_err(|e| {
113 Error::InvalidResponse(format!(
114 "failed to deserialize JSON pointer `{pointer}` for method `{method}`: {e}"
115 ))
116 })
117 }
118
119 pub fn base_url(&self) -> &Url {
121 &self.base_url
122 }
123}
124
125#[derive(Debug)]
127pub struct ClientBuilder {
128 http: Option<reqwest::Client>,
129 base_url: Option<Url>,
130 api_username: String,
131 api_password: String,
132}
133
134impl ClientBuilder {
135 pub fn http_client(mut self, http: reqwest::Client) -> Self {
138 self.http = Some(http);
139 self
140 }
141
142 pub fn base_url(mut self, url: Url) -> Self {
144 self.base_url = Some(url);
145 self
146 }
147
148 pub fn build(self) -> Result<Client> {
150 let base_url = match self.base_url {
151 Some(u) => u,
152 None => Url::parse(DEFAULT_BASE_URL).map_err(|e| {
153 Error::InvalidResponse(format!("default base URL failed to parse: {e}"))
154 })?,
155 };
156 let http = self.http.unwrap_or_default();
157 Ok(Client {
158 http,
159 base_url,
160 api_username: self.api_username,
161 api_password: self.api_password,
162 })
163 }
164}
165
166fn check_status(body: &Value) -> Result<()> {
167 let status = body
168 .get("status")
169 .and_then(Value::as_str)
170 .ok_or_else(|| Error::InvalidResponse("response missing `status` field".into()))?;
171 if status == "success" {
172 Ok(())
173 } else {
174 Err(Error::Api(ApiStatus(status.to_owned())))
175 }
176}