Skip to content

Commit

Permalink
Migrate the deprecated API in chrono to the recommended ones.
Browse files Browse the repository at this point in the history
  • Loading branch information
ramsayleung committed Mar 8, 2024
1 parent b339153 commit 75ceb28
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 96 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ authors = [
"Mario Ortiz Manero <[email protected]>"
]
name = "rspotify"
version = "0.12.0"
version = "0.13.0"
license = "MIT"
readme = "README.md"
description = "Spotify API wrapper"
Expand Down Expand Up @@ -33,7 +33,7 @@ rspotify-http = { path = "rspotify-http", version = "0.12.0", default-features =
async-stream = { version = "0.3.2", optional = true }
async-trait = { version = "0.1.51", optional = true }
base64 = "0.22.0"
chrono = { version = "0.4.19", features = ["serde"] }
chrono = { version = "0.4.35", features = ["serde"] }
dotenvy = { version = "0.15.0", optional = true }
futures = { version = "0.3.17", optional = true }

Expand Down
4 changes: 2 additions & 2 deletions examples/with_auto_reauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ async fn expire_token<S: BaseClient>(spotify: &S) {
let token = token.as_mut().expect("Token can't be empty as this point");
// In a regular case, the token would expire with time. Here we just do
// it manually.
let now = Utc::now().checked_sub_signed(Duration::seconds(10));
let now = Utc::now().checked_sub_signed(Duration::try_seconds(10).unwrap());
token.expires_at = now;
token.expires_in = Duration::seconds(0);
token.expires_in = Duration::try_seconds(0).unwrap();
// We also use a garbage access token to make sure it's actually
// refreshed.
token.access_token = "garbage".to_owned();
Expand Down
6 changes: 3 additions & 3 deletions rspotify-model/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::{
path::Path,
};

use chrono::{DateTime, Duration, Utc};
use chrono::{DateTime, Duration, TimeDelta, Utc};
use serde::{Deserialize, Serialize};

/// Spotify access token information
Expand Down Expand Up @@ -46,7 +46,7 @@ impl Default for Token {
fn default() -> Self {
Self {
access_token: String::new(),
expires_in: Duration::seconds(0),
expires_in: Duration::try_seconds(0).unwrap(),
expires_at: Some(Utc::now()),
refresh_token: None,
scopes: HashSet::new(),
Expand Down Expand Up @@ -81,7 +81,7 @@ impl Token {
#[must_use]
pub fn is_expired(&self) -> bool {
self.expires_at.map_or(true, |expiration| {
Utc::now() + Duration::seconds(10) >= expiration
Utc::now() + TimeDelta::try_seconds(10).unwrap() >= expiration
})
}

Expand Down
9 changes: 5 additions & 4 deletions rspotify-model/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
//! All objects related to context
use chrono::serde::ts_milliseconds;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Deserializer, Serialize};

use std::collections::HashMap;

use crate::{
custom_serde::{millisecond_timestamp, option_duration_ms},
CurrentlyPlayingType, Device, DisallowKey, PlayableItem, RepeatState, Type,
custom_serde::option_duration_ms, CurrentlyPlayingType, Device, DisallowKey, PlayableItem,
RepeatState, Type,
};

/// Context object
Expand All @@ -25,7 +26,7 @@ pub struct Context {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct CurrentlyPlayingContext {
pub context: Option<Context>,
#[serde(with = "millisecond_timestamp")]
#[serde(with = "ts_milliseconds")]
pub timestamp: DateTime<Utc>,
#[serde(default)]
#[serde(with = "option_duration_ms", rename = "progress_ms")]
Expand All @@ -42,7 +43,7 @@ pub struct CurrentPlaybackContext {
pub repeat_state: RepeatState,
pub shuffle_state: bool,
pub context: Option<Context>,
#[serde(with = "millisecond_timestamp")]
#[serde(with = "ts_milliseconds")]
pub timestamp: DateTime<Utc>,
#[serde(default)]
#[serde(with = "option_duration_ms", rename = "progress_ms")]
Expand Down
79 changes: 23 additions & 56 deletions rspotify-model/src/custom_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,32 @@ pub mod duration_ms {
where
E: de::Error,
{
Ok(Duration::milliseconds(v))
Duration::try_milliseconds(v).ok_or_else(|| {
E::invalid_value(
serde::de::Unexpected::Signed(v),
&"an invalid duration in milliseconds",
)
})
}

// JSON deserializer calls visit_u64 for non-negative intgers
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
i64::try_from(v)
.map(Duration::milliseconds)
.map_err(E::custom)
match i64::try_from(v) {
Ok(val) => Duration::try_milliseconds(val).ok_or_else(|| {
E::invalid_value(
serde::de::Unexpected::Signed(val),
&"a valid duration in
milliseconds",
)
}),
Err(_) => Err(E::custom(format!(
"Conversion error: u64 to i64 conversion failed for value {}",
v
))),
}
}
}

Expand All @@ -49,57 +64,6 @@ pub mod duration_ms {
}
}

pub mod millisecond_timestamp {
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{de, Serializer};
use std::fmt;

/// Vistor to help deserialize unix millisecond timestamp to
/// `chrono::DateTime`.
struct DateTimeVisitor;

impl<'de> de::Visitor<'de> for DateTimeVisitor {
type Value = DateTime<Utc>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"an unix millisecond timestamp represents DataTime<UTC>"
)
}

fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
let second = (v - v % 1000) / 1000;
let nanosecond = ((v % 1000) * 1_000_000) as u32;
// The maximum value of i64 is large enough to hold milliseconds,
// so it would be safe to convert it i64.
match NaiveDateTime::from_timestamp_opt(second as i64, nanosecond) {
Some(ndt) => Ok(DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc)),
None => Err(E::custom(format!("v is invalid second: {v}"))),
}
}
}

/// Deserialize Unix millisecond timestamp to `DateTime<Utc>`
pub fn deserialize<'de, D>(d: D) -> Result<DateTime<Utc>, D::Error>
where
D: de::Deserializer<'de>,
{
d.deserialize_u64(DateTimeVisitor)
}

/// Serialize `DateTime<Utc>` to Unix millisecond timestamp
pub fn serialize<S>(x: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_i64(x.timestamp_millis())
}
}

pub mod option_duration_ms {
use crate::custom_serde::duration_ms;
use chrono::Duration;
Expand Down Expand Up @@ -202,7 +166,10 @@ pub mod duration_second {
D: de::Deserializer<'de>,
{
let duration: i64 = Deserialize::deserialize(d)?;
Ok(Duration::seconds(duration))
Duration::try_seconds(duration).ok_or(serde::de::Error::invalid_value(
serde::de::Unexpected::Signed(duration),
&"an invalid duration in seconds",
))
}

/// Serialize `chrono::Duration` to seconds (represented as u64)
Expand Down
6 changes: 3 additions & 3 deletions rspotify-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ pub mod track;
pub mod user;

pub use {
album::*, artist::*, audio::*, auth::*, category::*, context::*,
device::*, enums::*, error::*, idtypes::*, image::*, offset::*, page::*, playing::*,
playlist::*, recommend::*, search::*, show::*, track::*, user::*,
album::*, artist::*, audio::*, auth::*, category::*, context::*, device::*, enums::*, error::*,
idtypes::*, image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*,
show::*, track::*, user::*,
};

use serde::{Deserialize, Serialize};
Expand Down
2 changes: 1 addition & 1 deletion src/clients/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ mod test {
async fn test_auth_headers() {
let tok = Token {
access_token: "test-access_token".to_string(),
expires_in: Duration::seconds(1),
expires_in: Duration::try_seconds(1).unwrap(),
expires_at: Some(Utc::now()),
scopes: scopes!("playlist-read-private"),
refresh_token: Some("...".to_string()),
Expand Down
28 changes: 11 additions & 17 deletions tests/test_models.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use chrono::{DateTime, Duration};
use rspotify::model::*;
use serde::de::DeserializeOwned;
use wasm_bindgen_test::*;
Expand Down Expand Up @@ -52,7 +52,7 @@ fn test_simplified_track() {
"#;
let track: SimplifiedTrack = deserialize(json_str);
let duration = Duration::milliseconds(276773);
let duration = Duration::try_milliseconds(276773).unwrap();
assert_eq!(track.duration, duration);
}

Expand Down Expand Up @@ -200,7 +200,7 @@ fn test_simplified_episode() {
simplified_episode.release_date_precision,
DatePrecision::Day
);
let duration = Duration::milliseconds(2685023);
let duration = Duration::try_milliseconds(2685023).unwrap();
assert_eq!(simplified_episode.duration, duration);
}

Expand Down Expand Up @@ -269,7 +269,7 @@ fn test_full_episode() {
"#;
let full_episode: FullEpisode = deserialize(json_str);
assert_eq!(full_episode.release_date_precision, DatePrecision::Day);
let duration = Duration::milliseconds(1502795);
let duration = Duration::try_milliseconds(1502795).unwrap();
assert_eq!(full_episode.duration, duration);
}

Expand Down Expand Up @@ -531,7 +531,7 @@ fn test_audio_features() {
}
"#;
let audio_features: AudioFeatures = deserialize(json);
let duration = Duration::milliseconds(255349);
let duration = Duration::try_milliseconds(255349).unwrap();
assert_eq!(audio_features.duration, duration);
}

Expand Down Expand Up @@ -611,7 +611,7 @@ fn test_full_track() {
}
"#;
let full_track: FullTrack = deserialize(json);
let duration = Duration::milliseconds(207959);
let duration = Duration::try_milliseconds(207959).unwrap();
assert_eq!(full_track.duration, duration);
}

Expand All @@ -625,7 +625,7 @@ fn test_resume_point() {
}
"#;
let resume_point: ResumePoint = deserialize(json);
let duration = Duration::milliseconds(423432);
let duration = Duration::try_milliseconds(423432).unwrap();
assert_eq!(resume_point.resume_position, duration);
}

Expand All @@ -639,7 +639,7 @@ fn test_resume_point_negative() {
}
"#;
let resume_point: ResumePoint = deserialize(json);
let duration = Duration::milliseconds(-1000);
let duration = Duration::try_milliseconds(-1000).unwrap();
assert_eq!(resume_point.resume_position, duration);
}

Expand Down Expand Up @@ -748,13 +748,10 @@ fn test_currently_playing_context() {
let timestamp = 1607769168429;
let second: i64 = (timestamp - timestamp % 1000) / 1000;
let nanosecond = (timestamp % 1000) * 1000000;
let dt = DateTime::<Utc>::from_naive_utc_and_offset(
NaiveDateTime::from_timestamp_opt(second, nanosecond as u32).unwrap(),
Utc,
);
let dt = DateTime::from_timestamp(second, nanosecond as u32).unwrap();
assert_eq!(currently_playing_context.timestamp, dt);

let duration = Duration::milliseconds(22270);
let duration = Duration::try_milliseconds(22270).unwrap();
assert_eq!(currently_playing_context.progress, Some(duration));
}

Expand Down Expand Up @@ -864,10 +861,7 @@ fn test_current_playback_context() {
let timestamp = 1607774342714;
let second: i64 = (timestamp - timestamp % 1000) / 1000;
let nanosecond = (timestamp % 1000) * 1000000;
let dt = DateTime::<Utc>::from_naive_utc_and_offset(
NaiveDateTime::from_timestamp_opt(second, nanosecond as u32).unwrap(),
Utc,
);
let dt = DateTime::from_timestamp(second, nanosecond as u32).unwrap();
assert_eq!(current_playback_context.timestamp, dt);
assert!(current_playback_context.progress.is_none());
}
Expand Down
18 changes: 12 additions & 6 deletions tests/test_oauth2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn test_get_authorize_url() {

#[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))]
async fn test_read_token_cache() {
let expires_in = Duration::seconds(3600);
let expires_in = Duration::try_seconds(3600).unwrap();
let expires_at = Some(Utc::now() + expires_in);
let scopes = scopes!("playlist-read-private", "playlist-read-collaborative");

Expand Down Expand Up @@ -67,7 +67,10 @@ async fn test_read_token_cache() {
let tok_from_file = spotify.read_token_cache().await.unwrap().unwrap();
assert_eq!(tok_from_file.scopes, scopes);
assert_eq!(tok_from_file.refresh_token.unwrap(), "...");
assert_eq!(tok_from_file.expires_in, Duration::seconds(3600));
assert_eq!(
tok_from_file.expires_in,
Duration::try_seconds(3600).unwrap()
);
assert_eq!(tok_from_file.expires_at, expires_at);

// delete cache file in the end
Expand All @@ -81,7 +84,7 @@ async fn test_write_token() {

let tok = Token {
access_token: "test-access_token".to_owned(),
expires_in: Duration::seconds(3600),
expires_in: Duration::try_seconds(3600).unwrap(),
expires_at: Some(now),
scopes: scopes.clone(),
refresh_token: Some("...".to_owned()),
Expand All @@ -105,7 +108,10 @@ async fn test_write_token() {
assert_eq!(tok_str, tok_str_file);
let tok_from_file: Token = serde_json::from_str(&tok_str_file).unwrap();
assert_eq!(tok_from_file.scopes, scopes);
assert_eq!(tok_from_file.expires_in, Duration::seconds(3600));
assert_eq!(
tok_from_file.expires_in,
Duration::try_seconds(3600).unwrap()
);
assert_eq!(tok_from_file.expires_at.unwrap(), now);

// delete cache file in the end
Expand All @@ -115,7 +121,7 @@ async fn test_write_token() {
#[test]
#[wasm_bindgen_test]
fn test_token_is_expired() {
let expires_in = Duration::seconds(20);
let expires_in = Duration::try_seconds(20).unwrap();
let tok = Token {
scopes: scopes!("playlist-read-private", "playlist-read-collaborative"),
access_token: "test-access_token".to_owned(),
Expand All @@ -125,7 +131,7 @@ fn test_token_is_expired() {
};
assert!(!tok.is_expired());

let expires_in = Duration::seconds(3); // There's a margin of 10 seconds
let expires_in = Duration::try_seconds(3).unwrap(); // There's a margin of 10 seconds
let tok = Token {
scopes: scopes!("playlist-read-private", "playlist-read-collaborative"),
access_token: "test-access_token".to_owned(),
Expand Down
4 changes: 2 additions & 2 deletions tests/test_with_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ async fn test_current_user_playing_track() {
)]
#[ignore]
async fn test_current_user_recently_played() {
let limit = TimeLimits::After(Utc::now() - Duration::days(2));
let limit = TimeLimits::After(Utc::now() - Duration::try_days(2).unwrap());
oauth_client()
.await
.current_user_recently_played(Some(10), Some(limit))
Expand Down Expand Up @@ -635,7 +635,7 @@ async fn test_seek_track() {
let backup = client.current_playback(None, None::<&[_]>).await.unwrap();

client
.seek_track(chrono::Duration::seconds(25), None)
.seek_track(chrono::Duration::try_seconds(25).unwrap(), None)
.await
.unwrap();

Expand Down

0 comments on commit 75ceb28

Please sign in to comment.