1use serde_json::{json, Value};
34use std::fmt;
35
36const USER_AGENT_STR: &str = "Bizowie::API";
37
38#[derive(Debug)]
40pub enum Error {
41 Http(reqwest::Error),
43 MissingMethod,
45}
46
47impl fmt::Display for Error {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 Error::Http(e) => write!(f, "HTTP error: {}", e),
51 Error::MissingMethod => {
52 write!(f, "[Bizowie::API] fatal error: no method given")
53 }
54 }
55 }
56}
57
58impl std::error::Error for Error {
59 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60 match self {
61 Error::Http(e) => Some(e),
62 Error::MissingMethod => None,
63 }
64 }
65}
66
67impl From<reqwest::Error> for Error {
68 fn from(e: reqwest::Error) -> Self {
69 Error::Http(e)
70 }
71}
72
73#[derive(Debug, Clone)]
79pub struct BizowieAPIResponse {
80 pub data: Value,
82 pub success: i64,
84}
85
86pub struct BizowieAPI {
92 api_key: String,
93 secret_key: String,
94 site: String,
95 v2: bool,
96 api_version: Option<String>,
97 debug: bool,
98 client: reqwest::Client,
99}
100
101impl BizowieAPI {
102 pub fn new(
109 api_key: impl Into<String>,
110 secret_key: impl Into<String>,
111 site: impl Into<String>,
112 ) -> Self {
113 Self {
114 api_key: api_key.into(),
115 secret_key: secret_key.into(),
116 site: site.into(),
117 v2: false,
118 api_version: None,
119 debug: false,
120 client: reqwest::Client::builder()
121 .user_agent(USER_AGENT_STR)
122 .build()
123 .expect("failed to build reqwest client"),
124 }
125 }
126
127 pub fn v2(mut self, v2: bool) -> Self {
130 self.v2 = v2;
131 self
132 }
133
134 pub fn api_version(mut self, version: impl Into<String>) -> Self {
137 self.api_version = Some(version.into());
138 self
139 }
140
141 pub fn debug(mut self, debug: bool) -> Self {
144 self.debug = debug;
145 self
146 }
147
148 pub async fn call(
160 &self,
161 method: &str,
162 params: Option<&Value>,
163 ) -> Result<BizowieAPIResponse, Error> {
164 if self.v2 {
165 self.call_v2(method, params).await
166 } else {
167 self.call_v1(method, params).await
168 }
169 }
170
171 async fn call_v1(
172 &self,
173 method: &str,
174 params: Option<&Value>,
175 ) -> Result<BizowieAPIResponse, Error> {
176 if method.is_empty() {
177 return Err(Error::MissingMethod);
178 }
179
180 let empty = json!({});
181 let request_body = serde_json::to_string(params.unwrap_or(&empty))
182 .expect("serializing serde_json::Value to String cannot fail");
183
184 let form = reqwest::multipart::Form::new()
185 .text("api_key", self.api_key.clone())
186 .text("secret_key", self.secret_key.clone())
187 .text("site", self.site.clone())
188 .text("request", request_body);
189
190 let res = self
191 .client
192 .post(format!("https://{}/bz/api/{}", self.site, method))
193 .multipart(form)
194 .send()
195 .await?;
196
197 self.parse_response(res).await
198 }
199
200 async fn call_v2(
201 &self,
202 method: &str,
203 params: Option<&Value>,
204 ) -> Result<BizowieAPIResponse, Error> {
205 if method.is_empty() {
206 return Err(Error::MissingMethod);
207 }
208
209 let mut payload = match params {
210 Some(Value::Object(map)) => map.clone(),
211 Some(other) => {
212 let mut m = serde_json::Map::new();
213 m.insert("_params".into(), other.clone());
214 m
215 }
216 None => serde_json::Map::new(),
217 };
218 payload.insert("api_key".into(), Value::String(self.api_key.clone()));
219 payload.insert("secret_key".into(), Value::String(self.secret_key.clone()));
220 payload
221 .entry("api_version".to_string())
222 .or_insert_with(|| {
223 Value::String(self.api_version.clone().unwrap_or_else(|| "1.00".to_string()))
224 });
225
226 let body = serde_json::to_string(&Value::Object(payload))
227 .expect("serializing serde_json::Value to String cannot fail");
228
229 let res = self
230 .client
231 .post(format!("https://{}/bz/apiv2/call/{}", self.site, method))
232 .header(reqwest::header::CONTENT_TYPE, "form-data")
233 .body(body)
234 .send()
235 .await?;
236
237 self.parse_response(res).await
238 }
239
240 async fn parse_response(
241 &self,
242 res: reqwest::Response,
243 ) -> Result<BizowieAPIResponse, Error> {
244 let status = res.status();
245 let text = res.text().await?;
246
247 let mut data: Value = match serde_json::from_str(&text) {
248 Ok(v) => v,
249 Err(_) => {
250 if self.debug {
251 eprintln!("[Bizowie::API] {}\n{}", status, text);
252 }
253 json!({ "unprocessed": 1 })
254 }
255 };
256
257 let success = match data.as_object_mut().and_then(|m| m.remove("success")) {
258 Some(Value::Number(n)) => n.as_i64().unwrap_or(0),
259 Some(Value::Bool(b)) => {
260 if b {
261 1
262 } else {
263 0
264 }
265 }
266 _ => 0,
267 };
268
269 Ok(BizowieAPIResponse { data, success })
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn builder_chain() {
279 let bz = BizowieAPI::new("a", "b", "c")
280 .v2(true)
281 .api_version("1.00")
282 .debug(true);
283 assert!(bz.v2);
284 assert_eq!(bz.api_version.as_deref(), Some("1.00"));
285 assert!(bz.debug);
286 assert_eq!(bz.api_key, "a");
287 assert_eq!(bz.secret_key, "b");
288 assert_eq!(bz.site, "c");
289 }
290}