1use super::{Api, ApiError, Error, Result};
2use reqwest::{Method, Url, header::HeaderValue};
3use serde_json::Value;
4use std::time::Duration;
5
6const DEFAULT_USER_AGENT: &str = concat!("podcast-api-rust ", env!("CARGO_PKG_VERSION"));
7
8pub struct Client<'a> {
10 client: reqwest::Client,
11 api: Api<'a>,
12 user_agent: &'a str,
13 base_url: Url,
14}
15
16#[derive(Debug)]
18pub struct Response {
19 pub response: reqwest::Response,
21 pub request: reqwest::Request,
23}
24
25impl Response {
26 pub async fn json(self) -> Result<Value> {
28 Ok(self.response.json().await?)
29 }
30}
31
32impl<'a> Client<'a> {
33 pub fn new(api_key: Option<&'a str>) -> Self {
36 Self::new_custom(
37 Self::http_client_builder()
38 .build()
39 .expect("build Listen API HTTP client"),
40 api_key,
41 None,
42 )
43 }
44
45 pub fn http_client_builder() -> reqwest::ClientBuilder {
48 reqwest::Client::builder()
49 .timeout(Duration::from_secs(30))
50 .connect_timeout(Duration::from_secs(10))
51 .redirect(reqwest::redirect::Policy::none())
52 .retry(reqwest::retry::never())
53 }
54
55 pub fn new_custom(client: reqwest::Client, api_key: Option<&'a str>, user_agent: Option<&'a str>) -> Self {
58 let api = api_key.map_or(Api::Mock, Api::Production);
59 let base_url = Url::parse(api.url()).expect("valid Listen API base URL");
60 Self {
61 client,
62 api,
63 user_agent: user_agent.unwrap_or(DEFAULT_USER_AGENT),
64 base_url,
65 }
66 }
67
68 pub fn with_base_url(mut self, base_url: &str) -> Result<Self> {
71 let url = Url::parse(base_url).map_err(|_| Error::InvalidParameter("invalid base URL".into()))?;
72 if !matches!(url.scheme(), "http" | "https")
73 || url.host_str().is_none()
74 || !url.username().is_empty()
75 || url.password().is_some()
76 || url.query().is_some()
77 || url.fragment().is_some()
78 {
79 return Err(Error::InvalidParameter(
80 "base URL must be HTTP(S) without credentials, query, or fragment".into(),
81 ));
82 }
83 self.base_url = url;
84 Ok(self)
85 }
86
87 pub(crate) async fn request_api(
88 &self,
89 method: Method,
90 path: &str,
91 path_params: &[(&str, &str)],
92 query_names: &[&str],
93 parameters: &Value,
94 ) -> Result<Response> {
95 let parameters = parameters
96 .as_object()
97 .ok_or_else(|| Error::InvalidParameter("parameters must be a JSON object".into()))?;
98 let mut url = self.base_url.clone();
99 {
100 let mut segments = url
101 .path_segments_mut()
102 .map_err(|_| Error::InvalidParameter("invalid base URL".into()))?;
103 segments.pop_if_empty();
104 for segment in path.trim_start_matches('/').split('/') {
105 if let Some(name) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
106 let value = path_params
107 .iter()
108 .find(|(key, _)| *key == name)
109 .map(|(_, value)| *value)
110 .ok_or_else(|| Error::InvalidParameter(format!("missing path parameter: {name}")))?;
111 if value.is_empty() || value == "." || value == ".." {
113 return Err(Error::InvalidParameter(format!("invalid path parameter: {name}")));
114 }
115 segments.push(value);
116 } else {
117 segments.push(segment);
118 }
119 }
120 }
121 let has_body = method == Method::POST || method == Method::PUT;
122 let mut query = Vec::new();
123 let mut body = Vec::new();
124 for (name, value) in parameters {
125 if value.is_null() || path_params.iter().any(|(key, _)| key == name) {
126 continue;
127 }
128 let encoded = value.as_str().map(str::to_owned).unwrap_or_else(|| value.to_string());
129 if !has_body || query_names.contains(&name.as_str()) {
130 query.push((name, encoded));
131 } else {
132 body.push((name, encoded));
133 }
134 }
135 let mut builder = self
136 .client
137 .request(method, url)
138 .query(&query)
139 .header("User-Agent", self.user_agent);
140 if let Api::Production(key) = self.api {
141 let mut header =
142 HeaderValue::from_str(key).map_err(|_| Error::InvalidParameter("invalid API key header".into()))?;
143 header.set_sensitive(true);
144 builder = builder.header("X-ListenAPI-Key", header);
145 }
146 if has_body {
147 builder = builder.form(&body);
148 }
149 let request = builder.build()?;
150 let outgoing = request
151 .try_clone()
152 .ok_or_else(|| Error::InvalidParameter("request body cannot be cloned".into()))?;
153 let response = self.client.execute(outgoing).await?;
154 let status = response.status();
155 if !status.is_success() {
156 let headers = response.headers().clone();
157 let body = response.text().await?;
158 return Err(Error::from_api(Box::new(ApiError { status, headers, body })));
159 }
160 Ok(Response { response, request })
161 }
162}