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::de::DeserializeOwned,
8    serde_json::{Value, from_str},
9};
10
11use crate::errors::QobuzApiError::{
12    self, ApiErrorResponse, ApiResponseParseError, ResourceNotFoundError,
13};
14
15/// Parses an API response, handling error status codes and JSON deserialization.
16///
17/// # Arguments
18///
19/// * `response` - The HTTP response to parse
20/// * `endpoint` - API endpoint path for error context
21///
22/// # Returns
23///
24/// The deserialized response body of type `T`.
25///
26/// # Errors
27///
28/// Returns a `QobuzApiError` on non-success status codes or JSON deserialization failure.
29pub async fn parse_response<T: DeserializeOwned>(
30    response: Response,
31    endpoint: &str,
32) -> Result<T, QobuzApiError> {
33    let status = response.status();
34    let body = response.text().await?;
35
36    if status.is_success() {
37        return from_str::<T>(&body).map_err(|e| ApiResponseParseError {
38            content: truncate(&body, 500),
39            source_info: Some(format!("{e}")),
40        });
41    }
42
43    if let Ok(err) = from_str::<Value>(&body) {
44        let code = i32::try_from(err.get("code").and_then(Value::as_i64).unwrap_or_default())
45            .unwrap_or_default();
46        let message = err
47            .get("message")
48            .and_then(|v| v.as_str())
49            .unwrap_or("Unknown error");
50
51        if status.as_u16() == 404 {
52            return Err(ResourceNotFoundError {
53                resource_type: endpoint.trim_start_matches('/').to_string(),
54                resource_id: message.to_string(),
55            });
56        }
57
58        return Err(ApiErrorResponse {
59            code,
60            message: message.to_string(),
61            status: status.to_string(),
62        });
63    }
64
65    Err(ApiErrorResponse {
66        code: i32::from(status.as_u16()),
67        message: truncate(&body, 200),
68        status: status.to_string(),
69    })
70}
71
72/// Generates a Unix timestamp string for request signing.
73///
74/// # Returns
75///
76/// The current Unix timestamp as a string.
77#[must_use]
78pub fn timestamp() -> String {
79    let now = SystemTime::now()
80        .duration_since(UNIX_EPOCH)
81        .unwrap_or_default();
82    now.as_secs().to_string()
83}
84
85/// Truncates a string to `max_len` characters with ellipsis.
86///
87/// # Arguments
88///
89/// * `s` - The string to truncate
90/// * `max_len` - Maximum character length before truncation
91///
92/// # Returns
93///
94/// The original string if short enough, or a truncated version with `...`.
95#[must_use]
96pub fn truncate(s: &str, max_len: usize) -> String {
97    if s.len() <= max_len {
98        s.to_string()
99    } else {
100        format!("{}...", &s[..max_len])
101    }
102}