1pub mod auth;
8pub mod library;
9pub mod metadata;
10pub mod stream;
11
12use std::fmt;
13
14use reqwest::StatusCode;
15use reqwest::header::HeaderMap;
16use serde::de::DeserializeOwned;
17
18pub const SUB_STATUS_NOT_AVAILABLE: u64 = 4005;
22
23#[derive(Clone, PartialEq, Eq)]
26pub struct ApiResponse {
27 status: StatusCode,
28 headers: HeaderMap,
29 body: Vec<u8>,
30}
31
32impl ApiResponse {
33 pub(crate) fn new(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
34 Self {
35 status,
36 headers,
37 body,
38 }
39 }
40
41 pub fn header(&self, name: &str) -> Option<&str> {
43 self.headers.get(name)?.to_str().ok()
44 }
45
46 pub fn status(&self) -> StatusCode {
48 self.status
49 }
50
51 pub fn body(&self) -> &[u8] {
53 &self.body
54 }
55
56 pub fn json<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
58 serde_json::from_slice(&self.body)
59 }
60
61 pub fn sub_status(&self) -> Option<u64> {
63 #[derive(serde::Deserialize)]
64 struct ErrorBody {
65 #[serde(rename = "subStatus")]
66 sub_status: Option<u64>,
67 }
68 self.json::<ErrorBody>().ok()?.sub_status
69 }
70}
71
72impl fmt::Debug for ApiResponse {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 f.debug_struct("ApiResponse")
76 .field("status", &self.status)
77 .field("body_len", &self.body.len())
78 .finish()
79 }
80}
81
82#[derive(Debug, thiserror::Error)]
84pub enum ApiError {
85 #[error("transport error: {0}")]
87 Transport(#[from] reqwest::Error),
88 #[error("unexpected HTTP status {0}")]
90 Status(u16),
91}
92
93#[derive(Debug, Clone)]
95pub struct Client {
96 base_url: String,
97 http: reqwest::Client,
98}
99
100impl Client {
101 pub fn new(base_url: impl Into<String>) -> Result<Self, ApiError> {
103 Ok(Self {
104 base_url: base_url.into(),
105 http: reqwest::Client::builder().build()?,
106 })
107 }
108
109 fn url(&self, path: &str) -> String {
111 format!(
112 "{}/{}",
113 self.base_url.trim_end_matches('/'),
114 path.trim_start_matches('/')
115 )
116 }
117
118 pub async fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T, ApiError> {
120 let response = self.http.get(self.url(path)).send().await?;
121 let status = response.status();
122 if !status.is_success() {
123 return Err(ApiError::Status(status.as_u16()));
124 }
125 Ok(response.json().await?)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn url_has_exactly_one_slash() {
135 for base in ["http://h:1", "http://h:1/"] {
136 for path in ["/v1/echo", "v1/echo"] {
137 let c = Client::new(base).unwrap();
138 assert_eq!(c.url(path), "http://h:1/v1/echo", "{base} + {path}");
139 }
140 }
141 }
142}