use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Image {
pub url: String,
pub width: Option<u32>,
pub height: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalUrls {
pub spotify: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalIds {
pub isrc: Option<String>,
pub ean: Option<String>,
pub upc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Followers {
pub href: Option<String>,
pub total: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Copyright {
pub text: String,
#[serde(rename = "type")]
pub copyright_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Restrictions {
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Paginated<T> {
pub href: String,
pub limit: u32,
pub next: Option<String>,
pub offset: u32,
pub previous: Option<String>,
pub total: u32,
pub items: Vec<T>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursored<T> {
pub href: String,
pub limit: u32,
pub next: Option<String>,
pub cursors: Option<Cursors>,
pub total: Option<u32>,
pub items: Vec<T>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursors {
pub after: Option<String>,
pub before: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResumePoint {
pub fully_played: bool,
pub resume_position_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkedFrom {
pub external_urls: Option<ExternalUrls>,
pub href: Option<String>,
pub id: Option<String>,
#[serde(rename = "type")]
pub item_type: Option<String>,
pub uri: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn image_deserializes() {
let json = json!({
"url": "https://image.jpg",
"width": 640,
"height": 480
});
let image: Image = serde_json::from_value(json).unwrap();
assert_eq!(image.url, "https://image.jpg");
assert_eq!(image.width, Some(640));
assert_eq!(image.height, Some(480));
}
#[test]
fn image_deserializes_with_null_dimensions() {
let json = json!({
"url": "https://image.jpg"
});
let image: Image = serde_json::from_value(json).unwrap();
assert!(image.width.is_none());
assert!(image.height.is_none());
}
#[test]
fn external_urls_deserializes() {
let json = json!({
"spotify": "https://open.spotify.com/track/123"
});
let urls: ExternalUrls = serde_json::from_value(json).unwrap();
assert_eq!(
urls.spotify,
Some("https://open.spotify.com/track/123".to_string())
);
}
#[test]
fn external_ids_deserializes() {
let json = json!({
"isrc": "USRC12345678",
"ean": "1234567890123",
"upc": "012345678905"
});
let ids: ExternalIds = serde_json::from_value(json).unwrap();
assert_eq!(ids.isrc, Some("USRC12345678".to_string()));
assert_eq!(ids.ean, Some("1234567890123".to_string()));
assert_eq!(ids.upc, Some("012345678905".to_string()));
}
#[test]
fn followers_deserializes() {
let json = json!({
"total": 1000000
});
let followers: Followers = serde_json::from_value(json).unwrap();
assert_eq!(followers.total, 1000000);
assert!(followers.href.is_none());
}
#[test]
fn copyright_deserializes() {
let json = json!({
"text": "(C) 2024 Test Records",
"type": "C"
});
let copyright: Copyright = serde_json::from_value(json).unwrap();
assert_eq!(copyright.text, "(C) 2024 Test Records");
assert_eq!(copyright.copyright_type, "C");
}
#[test]
fn restrictions_deserializes() {
let json = json!({
"reason": "market"
});
let restrictions: Restrictions = serde_json::from_value(json).unwrap();
assert_eq!(restrictions.reason, "market");
}
#[test]
fn paginated_deserializes() {
let json = json!({
"href": "https://api.spotify.com/v1/me/tracks",
"limit": 20,
"offset": 0,
"total": 100,
"items": [1, 2, 3]
});
let paginated: Paginated<i32> = serde_json::from_value(json).unwrap();
assert_eq!(paginated.limit, 20);
assert_eq!(paginated.total, 100);
assert_eq!(paginated.items, vec![1, 2, 3]);
}
#[test]
fn paginated_with_next_prev() {
let json = json!({
"href": "https://api.spotify.com/v1/me/tracks?offset=20",
"limit": 20,
"offset": 20,
"total": 100,
"next": "https://api.spotify.com/v1/me/tracks?offset=40",
"previous": "https://api.spotify.com/v1/me/tracks?offset=0",
"items": []
});
let paginated: Paginated<i32> = serde_json::from_value(json).unwrap();
assert!(paginated.next.is_some());
assert!(paginated.previous.is_some());
}
#[test]
fn cursored_deserializes() {
let json = json!({
"href": "https://api.spotify.com/v1/me/following",
"limit": 20,
"total": 50,
"items": ["a", "b"],
"cursors": {"after": "cursor123", "before": "cursor000"}
});
let cursored: Cursored<String> = serde_json::from_value(json).unwrap();
assert_eq!(cursored.limit, 20);
assert_eq!(cursored.items.len(), 2);
assert!(cursored.cursors.is_some());
}
#[test]
fn cursors_deserializes() {
let json = json!({
"after": "next_cursor",
"before": "prev_cursor"
});
let cursors: Cursors = serde_json::from_value(json).unwrap();
assert_eq!(cursors.after, Some("next_cursor".to_string()));
assert_eq!(cursors.before, Some("prev_cursor".to_string()));
}
#[test]
fn resume_point_deserializes() {
let json = json!({
"fully_played": false,
"resume_position_ms": 120000
});
let resume: ResumePoint = serde_json::from_value(json).unwrap();
assert!(!resume.fully_played);
assert_eq!(resume.resume_position_ms, 120000);
}
#[test]
fn linked_from_deserializes() {
let json = json!({
"id": "track123",
"type": "track",
"uri": "spotify:track:track123",
"href": "https://api.spotify.com/v1/tracks/track123"
});
let linked: LinkedFrom = serde_json::from_value(json).unwrap();
assert_eq!(linked.id, Some("track123".to_string()));
assert_eq!(linked.item_type, Some("track".to_string()));
}
}