Skip to main content

qobuz_api/api/
response.rs

1//! API response parsing, URL-encoded body truncation, and timestamp generation.
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use {
6    reqwest::Response,
7    serde::{
8        Deserialize, Deserializer,
9        de::{DeserializeOwned, Error},
10    },
11    serde_json::{
12        Value::{self, Null, Object, String as SerdeString},
13        from_str, from_value,
14    },
15};
16
17use crate::{
18    api::content::albums::Image,
19    errors::QobuzApiError::{self, ApiErrorResponse, ApiResponseParseError, ResourceNotFoundError},
20};
21
22/// Parses an API response, handling error status codes and JSON deserialization.
23///
24/// # Arguments
25///
26/// * `response` - The HTTP response to parse
27/// * `endpoint` - API endpoint path for error context
28///
29/// # Returns
30///
31/// The deserialized response body of type `T`.
32///
33/// # Errors
34///
35/// Returns a `QobuzApiError` on non-success status codes or JSON deserialization failure.
36pub async fn parse_response<T: DeserializeOwned>(
37    response: Response,
38    endpoint: &str,
39) -> Result<T, QobuzApiError> {
40    let status = response.status();
41    let body = response.text().await?;
42
43    if status.is_success() {
44        return from_str::<T>(&body).map_err(|e| ApiResponseParseError {
45            content: truncate(&body, 500),
46            source_info: Some(format!("{e}")),
47        });
48    }
49
50    if let Ok(err) = from_str::<Value>(&body) {
51        let code = i32::try_from(err.get("code").and_then(Value::as_i64).unwrap_or_default())
52            .unwrap_or_default();
53        let message = err
54            .get("message")
55            .and_then(|v| v.as_str())
56            .unwrap_or("Unknown error");
57
58        if status.as_u16() == 404 {
59            return Err(ResourceNotFoundError {
60                resource_type: endpoint.trim_start_matches('/').to_string(),
61                resource_id: message.to_string(),
62            });
63        }
64
65        return Err(ApiErrorResponse {
66            code,
67            message: message.to_string(),
68            status: status.to_string(),
69        });
70    }
71
72    Err(ApiErrorResponse {
73        code: i32::from(status.as_u16()),
74        message: truncate(&body, 200),
75        status: status.to_string(),
76    })
77}
78
79/// Generates a Unix timestamp string for request signing.
80///
81/// # Returns
82///
83/// The current Unix timestamp as a string.
84#[must_use]
85pub fn timestamp() -> String {
86    let now = SystemTime::now()
87        .duration_since(UNIX_EPOCH)
88        .unwrap_or_default();
89    now.as_secs().to_string()
90}
91
92/// Truncates a string to `max_len` characters with ellipsis.
93///
94/// # Arguments
95///
96/// * `s` - The string to truncate
97/// * `max_len` - Maximum character length before truncation
98///
99/// # Returns
100///
101/// The original string if short enough, or a truncated version with `...`.
102#[must_use]
103pub fn truncate(s: &str, max_len: usize) -> String {
104    if s.len() <= max_len {
105        s.to_string()
106    } else {
107        let mut truncated: String = s.chars().take(max_len).collect();
108        truncated.push_str("...");
109        truncated
110    }
111}
112
113/// Deserializes an optional JSON value.
114///
115/// # Arguments
116///
117/// * `deserializer` - The serde deserializer
118///
119/// # Returns
120///
121/// An optional JSON value.
122///
123/// # Errors
124///
125/// Returns a deserialization error if deserialization fails.
126fn deserialize_optional_value<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
127where
128    D: Deserializer<'de>,
129{
130    Option::<Value>::deserialize(deserializer)
131}
132
133/// Maps a deserialized value to a string, handling common cases.
134///
135/// # Arguments
136///
137/// * `value` - Optional JSON value to convert
138///
139/// # Returns
140///
141/// A string representation if conversion succeeds, or `None`.
142fn map_value_to_string(value: Option<Value>) -> Option<String> {
143    match value {
144        None | Some(Null) => None,
145        Some(SerdeString(s)) => Some(s),
146        Some(v) => Some(v.to_string()),
147    }
148}
149
150/// Deserializes fields that the API returns as either a plain string or `{"display":"Name"}`.
151///
152/// # Arguments
153///
154/// * `deserializer` - The serde deserializer
155///
156/// # Errors
157///
158/// Returns a deserialization error if the value is an invalid type.
159///
160/// # Returns
161///
162/// `Ok(Some(string))` for string values or objects with a `display` field,
163/// `Ok(None)` for null/missing.
164pub fn deserialize_flexible_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
165where
166    D: Deserializer<'de>,
167{
168    let value = deserialize_optional_value(deserializer)?;
169    match value {
170        None | Some(Null) => Ok(None),
171        Some(SerdeString(s)) => Ok(Some(s)),
172        Some(Object(map)) => map
173            .get("display")
174            .and_then(Value::as_str)
175            .map(|s| Some(s.to_string()))
176            .ok_or_else(|| Error::custom("object missing 'display' field")),
177        Some(v) => Ok(map_value_to_string(Some(v))),
178    }
179}
180
181/// Deserializes fields that the API returns as either a string, number, or other value.
182///
183/// # Arguments
184///
185/// * `deserializer` - The serde deserializer
186///
187/// # Errors
188///
189/// Returns a deserialization error if the value cannot be deserialized.
190///
191/// # Returns
192///
193/// `Ok(Some(string))` for string/number values, `Ok(None)` for null/missing.
194pub fn deserialize_flexible_string_id<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
195where
196    D: Deserializer<'de>,
197{
198    let value = deserialize_optional_value(deserializer)?;
199    Ok(map_value_to_string(value))
200}
201
202/// Deserializes picture/image fields the API returns as either a string URL, null, or an Image
203/// object.
204///
205/// # Arguments
206///
207/// * `deserializer` - The serde deserializer
208///
209/// # Errors
210///
211/// Returns a deserialization error if the value is an invalid image object.
212///
213/// # Returns
214///
215/// `Ok(None)` for string URLs and null, `Ok(Some(Image))` for valid image objects.
216pub fn deserialize_picture<'de, D>(deserializer: D) -> Result<Option<Image>, D::Error>
217where
218    D: Deserializer<'de>,
219{
220    let value = deserialize_optional_value(deserializer)?;
221    match value {
222        None | Some(Null | SerdeString(_)) => Ok(None),
223        Some(v) => from_value(v).map_err(Error::custom),
224    }
225}