pub mod auth;
pub mod library;
pub mod metadata;
pub mod stream;
use std::fmt;
use reqwest::StatusCode;
use reqwest::header::HeaderMap;
use serde::de::DeserializeOwned;
pub const SUB_STATUS_NOT_AVAILABLE: u64 = 4005;
#[derive(Clone, PartialEq, Eq)]
pub struct ApiResponse {
status: StatusCode,
headers: HeaderMap,
body: Vec<u8>,
}
impl ApiResponse {
pub(crate) fn new(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
Self {
status,
headers,
body,
}
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name)?.to_str().ok()
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn json<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_slice(&self.body)
}
pub fn sub_status(&self) -> Option<u64> {
#[derive(serde::Deserialize)]
struct ErrorBody {
#[serde(rename = "subStatus")]
sub_status: Option<u64>,
}
self.json::<ErrorBody>().ok()?.sub_status
}
}
impl fmt::Debug for ApiResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ApiResponse")
.field("status", &self.status)
.field("body_len", &self.body.len())
.finish()
}
}
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("transport error: {0}")]
Transport(#[from] reqwest::Error),
#[error("unexpected HTTP status {0}")]
Status(u16),
}
#[derive(Debug, Clone)]
pub struct Client {
base_url: String,
http: reqwest::Client,
}
impl Client {
pub fn new(base_url: impl Into<String>) -> Result<Self, ApiError> {
Ok(Self {
base_url: base_url.into(),
http: reqwest::Client::builder().build()?,
})
}
fn url(&self, path: &str) -> String {
format!(
"{}/{}",
self.base_url.trim_end_matches('/'),
path.trim_start_matches('/')
)
}
pub async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, ApiError> {
let response = self.http.get(self.url(path)).send().await?;
let status = response.status();
if !status.is_success() {
return Err(ApiError::Status(status.as_u16()));
}
Ok(response.json().await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn url_has_exactly_one_slash() {
for base in ["http://h:1", "http://h:1/"] {
for path in ["/v1/echo", "v1/echo"] {
let c = Client::new(base).unwrap();
assert_eq!(c.url(path), "http://h:1/v1/echo", "{base} + {path}");
}
}
}
}