mirror of
https://github.com/OMGeeky/google-apis-rs.git
synced 2026-01-03 09:58:13 +01:00
316 lines
9.9 KiB
Rust
316 lines
9.9 KiB
Rust
pub mod duration {
|
|
use serde::{Deserialize, Deserializer};
|
|
use serde_with::{DeserializeAs, SerializeAs};
|
|
use std::fmt::Formatter;
|
|
use std::str::FromStr;
|
|
|
|
use chrono::Duration;
|
|
|
|
const MAX_SECONDS: i64 = 315576000000i64;
|
|
|
|
#[derive(Debug)]
|
|
enum ParseDurationError {
|
|
MissingSecondSuffix,
|
|
NanosTooSmall,
|
|
ParseIntError(std::num::ParseIntError),
|
|
SecondOverflow { seconds: i64, max_seconds: i64 },
|
|
SecondUnderflow { seconds: i64, min_seconds: i64 },
|
|
}
|
|
|
|
impl From<std::num::ParseIntError> for ParseDurationError {
|
|
fn from(pie: std::num::ParseIntError) -> Self {
|
|
ParseDurationError::ParseIntError(pie)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for ParseDurationError {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ParseDurationError::MissingSecondSuffix => write!(f, "'s' suffix was not present"),
|
|
ParseDurationError::NanosTooSmall => {
|
|
write!(f, "more than 9 digits of second precision required")
|
|
}
|
|
ParseDurationError::ParseIntError(pie) => write!(f, "{:?}", pie),
|
|
ParseDurationError::SecondOverflow {
|
|
seconds,
|
|
max_seconds,
|
|
} => write!(
|
|
f,
|
|
"seconds overflow (got {}, maximum seconds possible {})",
|
|
seconds, max_seconds
|
|
),
|
|
ParseDurationError::SecondUnderflow {
|
|
seconds,
|
|
min_seconds,
|
|
} => write!(
|
|
f,
|
|
"seconds underflow (got {}, minimum seconds possible {})",
|
|
seconds, min_seconds
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ParseDurationError {}
|
|
|
|
fn duration_from_str(s: &str) -> Result<Duration, ParseDurationError> {
|
|
// TODO: Test strings like -.s, -0.0s
|
|
let value = match s.strip_suffix('s') {
|
|
None => return Err(ParseDurationError::MissingSecondSuffix),
|
|
Some(v) => v,
|
|
};
|
|
|
|
let (seconds, nanoseconds) = if let Some((seconds, nanos)) = value.split_once('.') {
|
|
let is_neg = seconds.starts_with('-');
|
|
let seconds = i64::from_str(seconds)?;
|
|
let nano_magnitude = nanos.chars().filter(|c| c.is_ascii_digit()).count() as u32;
|
|
if nano_magnitude > 9 {
|
|
// not enough precision to model the remaining digits
|
|
return Err(ParseDurationError::NanosTooSmall);
|
|
}
|
|
|
|
// u32::from_str prevents negative nanos (eg '0.-12s) -> lossless conversion to i32
|
|
// 10_u32.pow(...) scales number to appropriate # of nanoseconds
|
|
let nanos = u32::from_str(nanos)? as i32;
|
|
|
|
let mut nanos = nanos * 10_i32.pow(9 - nano_magnitude);
|
|
if is_neg {
|
|
nanos = -nanos;
|
|
}
|
|
(seconds, nanos)
|
|
} else {
|
|
(i64::from_str(value)?, 0)
|
|
};
|
|
|
|
if seconds >= MAX_SECONDS {
|
|
Err(ParseDurationError::SecondOverflow {
|
|
seconds,
|
|
max_seconds: MAX_SECONDS,
|
|
})
|
|
} else if seconds <= -MAX_SECONDS {
|
|
Err(ParseDurationError::SecondUnderflow {
|
|
seconds,
|
|
min_seconds: -MAX_SECONDS,
|
|
})
|
|
} else {
|
|
Ok(Duration::seconds(seconds) + Duration::nanoseconds(nanoseconds.into()))
|
|
}
|
|
}
|
|
|
|
pub fn to_string(duration: &Duration) -> String {
|
|
let seconds = duration.num_seconds();
|
|
let nanoseconds = (*duration - Duration::seconds(seconds))
|
|
.num_nanoseconds()
|
|
.expect("absolute number of nanoseconds is less than 1 billion")
|
|
as i32;
|
|
if nanoseconds != 0 {
|
|
if seconds == 0 && nanoseconds.is_negative() {
|
|
format!("-0.{:0>9}s", nanoseconds.abs())
|
|
} else {
|
|
format!("{}.{:0>9}s", seconds, nanoseconds.abs())
|
|
}
|
|
} else {
|
|
format!("{}s", seconds)
|
|
}
|
|
}
|
|
|
|
pub struct Wrapper;
|
|
|
|
impl SerializeAs<Duration> for Wrapper {
|
|
fn serialize_as<S>(value: &Duration, s: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
s.serialize_str(&to_string(value))
|
|
}
|
|
}
|
|
|
|
impl<'de> DeserializeAs<'de, Duration> for Wrapper {
|
|
fn deserialize_as<D>(deserializer: D) -> Result<Duration, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s = Deserialize::deserialize(deserializer)?;
|
|
duration_from_str(s).map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub mod urlsafe_base64 {
|
|
use serde::{Deserialize, Deserializer, Serializer};
|
|
use serde_with::{DeserializeAs, SerializeAs};
|
|
|
|
pub struct Wrapper;
|
|
|
|
pub fn to_string(bytes: &Vec<u8>) -> String {
|
|
base64::encode_config(bytes, base64::URL_SAFE)
|
|
}
|
|
|
|
impl SerializeAs<Vec<u8>> for Wrapper {
|
|
fn serialize_as<S>(value: &Vec<u8>, s: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
s.serialize_str(&to_string(value))
|
|
}
|
|
}
|
|
|
|
impl<'de> DeserializeAs<'de, Vec<u8>> for Wrapper {
|
|
fn deserialize_as<D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s: &str = Deserialize::deserialize(deserializer)?;
|
|
base64::decode_config(s, base64::URL_SAFE).map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn datetime_to_string(datetime: &chrono::DateTime<chrono::offset::Utc>) -> String {
|
|
datetime.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::{duration, urlsafe_base64};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_with::{serde_as, DisplayFromStr};
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
struct DurationWrapper {
|
|
#[serde_as(as = "Option<duration::Wrapper>")]
|
|
duration: Option<chrono::Duration>,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
struct Base64Wrapper {
|
|
#[serde_as(as = "Option<urlsafe_base64::Wrapper>")]
|
|
bytes: Option<Vec<u8>>,
|
|
}
|
|
|
|
#[serde_as]
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
struct I64Wrapper {
|
|
#[serde_as(as = "Option<DisplayFromStr>")]
|
|
num: Option<i64>,
|
|
}
|
|
|
|
#[test]
|
|
fn test_duration_de_success_cases() {
|
|
let durations = [
|
|
("-0.2s", -200_000_000),
|
|
("0.000000001s", 1),
|
|
("999.999999999s", 999_999_999_999),
|
|
("129s", 129_000_000_000),
|
|
("0.123456789s", 123_456_789),
|
|
];
|
|
for (repr, nanos) in durations.into_iter() {
|
|
let wrapper: DurationWrapper =
|
|
serde_json::from_str(&format!("{{\"duration\": \"{}\"}}", repr)).unwrap();
|
|
assert_eq!(
|
|
Some(nanos),
|
|
wrapper.duration.unwrap().num_nanoseconds(),
|
|
"parsed \"{}\" expecting Duration with {}ns",
|
|
repr,
|
|
nanos
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_duration_de_failure_cases() {
|
|
let durations = ["1.-3s", "1.1111111111s", "1.2"];
|
|
for repr in durations.into_iter() {
|
|
assert!(
|
|
serde_json::from_str::<DurationWrapper>(&format!("{{\"duration\": \"{}\"}}", repr))
|
|
.is_err(),
|
|
"parsed \"{}\" expecting err",
|
|
repr
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_duration_ser_success_cases() {
|
|
let durations = [
|
|
-200_000_000,
|
|
1,
|
|
999_999_999_999,
|
|
129_000_000_000,
|
|
123_456_789,
|
|
];
|
|
|
|
for nanos in durations.into_iter() {
|
|
let wrapper = DurationWrapper {
|
|
duration: Some(chrono::Duration::nanoseconds(nanos)),
|
|
};
|
|
let s = serde_json::to_string(&wrapper);
|
|
assert!(s.is_ok(), "Could not serialize {}ns", nanos);
|
|
let s = s.unwrap();
|
|
assert_eq!(
|
|
wrapper,
|
|
serde_json::from_str(&s).unwrap(),
|
|
"round trip should return same duration"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn urlsafe_base64_de_success_cases() {
|
|
let wrapper: Base64Wrapper =
|
|
serde_json::from_str(r#"{"bytes": "aGVsbG8gd29ybGQ="}"#).unwrap();
|
|
assert_eq!(Some(b"hello world".as_slice()), wrapper.bytes.as_deref());
|
|
}
|
|
|
|
#[test]
|
|
fn urlsafe_base64_de_failure_cases() {
|
|
assert!(serde_json::from_str::<Base64Wrapper>(r#"{"bytes": "aGVsbG8gd29ybG+Q"}"#).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn urlsafe_base64_roundtrip() {
|
|
let wrapper = Base64Wrapper {
|
|
bytes: Some(b"Hello world!".to_vec()),
|
|
};
|
|
let s = serde_json::to_string(&wrapper).expect("serialization of bytes infallible");
|
|
assert_eq!(wrapper, serde_json::from_str::<Base64Wrapper>(&s).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn num_roundtrip() {
|
|
let wrapper = I64Wrapper {
|
|
num: Some(i64::MAX),
|
|
};
|
|
|
|
let json_repr = &serde_json::to_string(&wrapper);
|
|
assert!(json_repr.is_ok(), "serialization should succeed");
|
|
assert_eq!(
|
|
wrapper,
|
|
serde_json::from_str(&format!("{{\"num\": \"{}\"}}", i64::MAX)).unwrap()
|
|
);
|
|
assert_eq!(
|
|
wrapper,
|
|
serde_json::from_str(json_repr.as_ref().unwrap()).unwrap(),
|
|
"round trip should succeed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_wrapper() {
|
|
assert_eq!(
|
|
DurationWrapper { duration: None },
|
|
serde_json::from_str("{}").unwrap()
|
|
);
|
|
assert_eq!(
|
|
Base64Wrapper { bytes: None },
|
|
serde_json::from_str("{}").unwrap()
|
|
);
|
|
assert_eq!(
|
|
I64Wrapper { num: None },
|
|
serde_json::from_str("{}").unwrap()
|
|
);
|
|
}
|
|
}
|