use reqwest::Response;
use serde_json::Value;
use crate::core::error::{Error, Result};
use crate::core::secret::ApiToken;
use crate::vendors::http::HttpClient;
#[derive(Clone, Debug)]
pub struct PangolinClient {
http: HttpClient,
pub org_id: String,
}
impl PangolinClient {
pub fn new(base_url: String, token: ApiToken, org_id: String) -> Result<Self> {
Ok(Self {
http: HttpClient::new(base_url, token, false)?,
org_id,
})
}
pub fn base_url(&self) -> &str {
&self.http.base_url
}
pub async fn get(&self, path: &str, params: &[(&str, String)]) -> Result<Value> {
let req = self.http.get(path).query(params);
let resp = self.http.send("GET", path, req).await?;
parse_response(resp).await
}
}
async fn parse_response(resp: Response) -> Result<Value> {
let http_status = resp.status();
let body: Value = resp.json().await.map_err(|e| {
if e.is_decode() {
Error::InvalidJson(e)
} else {
Error::Network(e)
}
})?;
let success = body
.get("success")
.and_then(|s| s.as_bool())
.unwrap_or(false);
let api_status = body.get("status").and_then(|s| s.as_u64()).unwrap_or(0);
if success {
return Ok(body.get("data").cloned().unwrap_or(Value::Null));
}
let message = body
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error")
.to_string();
if http_status.as_u16() == 403 || api_status == 403 {
return Err(Error::forbidden(message));
}
if body.get("error").and_then(|e| e.as_bool()).unwrap_or(false) || !http_status.is_success() {
return Err(Error::Api { message });
}
Err(Error::Http {
status: http_status.as_u16(),
body: body.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_resp(status: u16, body: Value) -> reqwest::Response {
http::Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body.to_string())
.map(reqwest::Response::from)
.unwrap()
}
#[tokio::test]
async fn success_envelope_returns_data() {
let resp = make_resp(
200,
json!({
"data": { "orgs": [] },
"success": true,
"error": false,
"message": "ok",
"status": 200
}),
);
let val = parse_response(resp).await.unwrap();
assert_eq!(val, json!({ "orgs": [] }));
}
#[tokio::test]
async fn forbidden_envelope_returns_forbidden_error() {
let resp = make_resp(
403,
json!({
"data": null,
"success": false,
"error": true,
"message": "Key does not have root access",
"status": 403,
"stack": null
}),
);
let err = parse_response(resp).await.unwrap_err();
assert!(
matches!(err, Error::Forbidden { ref message } if message == "Key does not have root access")
);
}
#[tokio::test]
async fn api_error_envelope_returns_api_error() {
let resp = make_resp(
400,
json!({
"data": null,
"success": false,
"error": true,
"message": "zone not found",
"status": 400,
"stack": null
}),
);
let err = parse_response(resp).await.unwrap_err();
assert!(matches!(err, Error::Api { ref message } if message == "zone not found"));
}
}