diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d99c23e..da68bfe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,17 @@ Breaking changes: * Remove deprecated endpoint `r0::contact::create_contact` (deprecated in r0.6.0) * Add lazy-loading options to `r0::filter::RoomEventFilter` (introduced in r0.5.0) * Change type for `limit` request parameter of `r0::context::get_context` from `u8` to `Option` +* Use `std::time::Duration` for appropriate fields on several endpoints: + ``` + r0::{ + account::request_openid_token, + keys::{claim_keys, get_keys}, + presence::get_presence, + sync::sync_events, + typing::create_typing_event, + voip::get_turn_server_info + } + ``` # 0.6.0 diff --git a/Cargo.toml b/Cargo.toml index 1aef59f4..1b45821c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" [dependencies] http = "0.2.0" -js_int = { version = "0.1.2", features = ["serde"] } +js_int = { version = "0.1.3", features = ["serde"] } ruma-api = "0.14.0" ruma-events = "0.17.0" ruma-identifiers = "0.14.1" diff --git a/src/lib.rs b/src/lib.rs index 4b08448a..9d8e94f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,3 +11,5 @@ pub mod error; pub mod r0; pub mod unversioned; + +mod serde; diff --git a/src/r0/account/request_openid_token.rs b/src/r0/account/request_openid_token.rs index e4ac9362..b3b96bcc 100644 --- a/src/r0/account/request_openid_token.rs +++ b/src/r0/account/request_openid_token.rs @@ -1,6 +1,7 @@ //! [POST /_matrix/client/r0/user/{userId}/openid/request_token](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-user-userid-openid-request-token) -use js_int::UInt; +use std::time::Duration; + use ruma_api::ruma_api; use ruma_identifiers::UserId; use serde::{Deserialize, Serialize}; @@ -29,7 +30,8 @@ ruma_api! { /// Homeserver domain for verification of user's identity. pub matrix_server_name: String, /// Seconds until token expiration. - pub expires_in: UInt, + #[serde(with = "crate::serde::duration::secs")] + pub expires_in: Duration, } } diff --git a/src/r0/keys/claim_keys.rs b/src/r0/keys/claim_keys.rs index b21a62bd..3b14c0a2 100644 --- a/src/r0/keys/claim_keys.rs +++ b/src/r0/keys/claim_keys.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; -use js_int::UInt; +use std::time::Duration; + use ruma_api::ruma_api; use ruma_identifiers::{DeviceId, UserId}; use serde_json::Value; @@ -23,7 +24,8 @@ ruma_api! { /// The time (in milliseconds) to wait when downloading keys from remote servers. /// 10 seconds is the recommended default. #[serde(skip_serializing_if = "Option::is_none")] - pub timeout: Option, + #[serde(default, with = "crate::serde::duration::opt_ms")] + pub timeout: Option, /// The keys to be claimed. pub one_time_keys: HashMap>, diff --git a/src/r0/keys/get_keys.rs b/src/r0/keys/get_keys.rs index 704d8ee6..526a3359 100644 --- a/src/r0/keys/get_keys.rs +++ b/src/r0/keys/get_keys.rs @@ -1,8 +1,7 @@ //! [POST /_matrix/client/r0/keys/query](https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-keys-query) -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; -use js_int::UInt; use ruma_api::ruma_api; use ruma_identifiers::{DeviceId, UserId}; use serde_json::Value; @@ -23,7 +22,8 @@ ruma_api! { /// The time (in milliseconds) to wait when downloading keys from remote servers. /// 10 seconds is the recommended default. #[serde(skip_serializing_if = "Option::is_none")] - pub timeout: Option, + #[serde(default, with = "crate::serde::duration::opt_ms")] + pub timeout: Option, /// The keys to be downloaded. An empty list indicates all devices for the corresponding user. pub device_keys: HashMap>, diff --git a/src/r0/presence/get_presence.rs b/src/r0/presence/get_presence.rs index 0d76d74c..2a32e1a4 100644 --- a/src/r0/presence/get_presence.rs +++ b/src/r0/presence/get_presence.rs @@ -1,6 +1,7 @@ //! [GET /_matrix/client/r0/presence/{userId}/status](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-presence-userid-status) -use js_int::UInt; +use std::time::Duration; + use ruma_api::ruma_api; use ruma_events::presence::PresenceState; use ruma_identifiers::UserId; @@ -30,7 +31,8 @@ ruma_api! { pub currently_active: Option, /// The length of time in milliseconds since an action was performed by the user. #[serde(skip_serializing_if = "Option::is_none")] - pub last_active_ago: Option, + #[serde(default, with = "crate::serde::duration::opt_ms")] + pub last_active_ago: Option, /// The user's presence state. pub presence: PresenceState, } diff --git a/src/r0/sync/sync_events.rs b/src/r0/sync/sync_events.rs index e8593d6e..4eb454d1 100644 --- a/src/r0/sync/sync_events.rs +++ b/src/r0/sync/sync_events.rs @@ -1,6 +1,6 @@ //! [GET /_matrix/client/r0/sync](https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-sync) -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; use js_int::UInt; use ruma_api::{ruma_api, Outgoing}; @@ -48,8 +48,9 @@ ruma_api! { pub set_presence: Option, /// The maximum time to poll in milliseconds before returning this request. #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "crate::serde::duration::opt_ms")] #[ruma_api(query)] - pub timeout: Option, + pub timeout: Option, } response { diff --git a/src/r0/typing/create_typing_event.rs b/src/r0/typing/create_typing_event.rs index 29356e12..3dad3ef2 100644 --- a/src/r0/typing/create_typing_event.rs +++ b/src/r0/typing/create_typing_event.rs @@ -1,6 +1,7 @@ //! [PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}](https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-rooms-roomid-typing-userid) -use js_int::UInt; +use std::time::Duration; + use ruma_api::ruma_api; use ruma_identifiers::{RoomId, UserId}; @@ -20,7 +21,8 @@ ruma_api! { pub room_id: RoomId, /// The length of time in milliseconds to mark this user as typing. #[serde(skip_serializing_if = "Option::is_none")] - pub timeout: Option, + #[serde(default, with = "crate::serde::duration::opt_ms")] + pub timeout: Option, /// Whether the user is typing or not. If `false`, the `timeout` key can be omitted. pub typing: bool, /// The user who has started to type. diff --git a/src/r0/voip/get_turn_server_info.rs b/src/r0/voip/get_turn_server_info.rs index 15df4e77..91ce8597 100644 --- a/src/r0/voip/get_turn_server_info.rs +++ b/src/r0/voip/get_turn_server_info.rs @@ -1,6 +1,7 @@ //! [GET /_matrix/client/r0/voip/turnServer](https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-voip-turnserver) -use js_int::UInt; +use std::time::Duration; + use ruma_api::ruma_api; ruma_api! { @@ -19,7 +20,8 @@ ruma_api! { /// The password to use. pub password: String, /// The time-to-live in seconds. - pub ttl: UInt, + #[serde(with = "crate::serde::duration::secs")] + pub ttl: Duration, /// A list of TURN URIs. pub uris: Vec, /// The username to use. diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 00000000..4cb6139c --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,3 @@ +//! Modules to hold functions for de-/serializing remote types + +pub mod duration; diff --git a/src/serde/duration.rs b/src/serde/duration.rs new file mode 100644 index 00000000..d8c895fa --- /dev/null +++ b/src/serde/duration.rs @@ -0,0 +1,4 @@ +//! De-/serialization functions for `std::time::Duration` objects + +pub mod opt_ms; +pub mod secs; diff --git a/src/serde/duration/opt_ms.rs b/src/serde/duration/opt_ms.rs new file mode 100644 index 00000000..9e304eda --- /dev/null +++ b/src/serde/duration/opt_ms.rs @@ -0,0 +1,90 @@ +//! De-/serialization functions for `Option` objects represented as milliseconds. +//! Delegates to `js_int::UInt` to ensure integer size is within bounds. + +use std::{convert::TryFrom, time::Duration}; + +use js_int::UInt; +use serde::{ + de::{Deserialize, Deserializer}, + ser::{Error, Serialize, Serializer}, +}; + +/// Serialize an Option. +/// Will fail if integer is greater than the maximum integer that can be +/// unambiguously represented by an f64. +pub fn serialize(opt_duration: &Option, serializer: S) -> Result +where + S: Serializer, +{ + match opt_duration { + Some(duration) => match UInt::try_from(duration.as_millis()) { + Ok(uint) => uint.serialize(serializer), + Err(err) => Err(S::Error::custom(err)), + }, + None => serializer.serialize_none(), + } +} + +/// Deserializes an Option. +/// Will fail if integer is greater than the maximum integer that can be +/// unambiguously represented by an f64. +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Ok(Option::::deserialize(deserializer)? + .map(|millis| Duration::from_millis(millis.into()))) +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use serde_json::json; + use std::time::Duration; + + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] + struct DurationTest { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, with = "crate::serde::duration::opt_ms")] + timeout: Option, + } + + #[test] + fn test_deserialize_some_duration_as_milliseconds() { + let json = json!({ "timeout": 3000 }); + + assert_eq!( + serde_json::from_value::(json).unwrap(), + DurationTest { + timeout: Some(Duration::from_millis(3000)) + }, + ); + } + + #[test] + fn test_deserialize_empty_duration_as_milliseconds() { + let json = json!({}); + + assert_eq!( + serde_json::from_value::(json).unwrap(), + DurationTest { timeout: None }, + ); + } + + #[test] + fn test_serialize_some_duration_as_milliseconds() { + let request = DurationTest { + timeout: Some(Duration::new(2, 0)), + }; + assert_eq!( + serde_json::to_value(&request).unwrap(), + json!({ "timeout": 2000 }) + ); + } + + #[test] + fn test_serialize_empty_duration_as_milliseconds() { + let request = DurationTest { timeout: None }; + assert_eq!(serde_json::to_value(&request).unwrap(), json!({})); + } +} diff --git a/src/serde/duration/secs.rs b/src/serde/duration/secs.rs new file mode 100644 index 00000000..1e60052a --- /dev/null +++ b/src/serde/duration/secs.rs @@ -0,0 +1,66 @@ +//! De-/serialization functions for `Option` objects represented as milliseconds. +//! Delegates to `js_int::UInt` to ensure integer size is within bounds. + +use std::{convert::TryFrom, time::Duration}; + +use js_int::UInt; +use serde::{ + de::{Deserialize, Deserializer}, + ser::{Error, Serialize, Serializer}, +}; + +/// Serializes a Duration to an integer representing seconds. +/// Will fail if integer is greater than the maximum integer that can be +/// unambiguously represented by an f64. +pub fn serialize(duration: &Duration, serializer: S) -> Result +where + S: Serializer, +{ + match UInt::try_from(duration.as_secs()) { + Ok(uint) => uint.serialize(serializer), + Err(err) => Err(S::Error::custom(err)), + } +} + +/// Deserializes an integer representing seconds into a Duration. +/// Will fail if integer is greater than the maximum integer that can be +/// unambiguously represented by an f64. +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + UInt::deserialize(deserializer).map(|secs| Duration::from_secs(secs.into())) +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use serde_json::json; + use std::time::Duration; + + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] + struct DurationTest { + #[serde(with = "crate::serde::duration::secs")] + timeout: Duration, + } + + #[test] + fn test_deserialize_duration_as_seconds() { + let json = json!({ "timeout": 3 }); + + assert_eq!( + serde_json::from_value::(json).unwrap(), + DurationTest { + timeout: Duration::from_secs(3) + }, + ); + } + + #[test] + fn test_serialize_duration_as_seconds() { + let test = DurationTest { + timeout: Duration::from_millis(7000), + }; + assert_eq!(serde_json::to_value(test).unwrap(), json!({ "timeout": 7 }),); + } +}