diff --git a/google-apis-common/Cargo.toml b/google-apis-common/Cargo.toml index 4bbe0e0b40..ac27adbe23 100644 --- a/google-apis-common/Cargo.toml +++ b/google-apis-common/Cargo.toml @@ -29,6 +29,3 @@ hyper = "^ 0.14" http = "^0.2" tokio = "^1.0" tower-service = "^0.3.1" - -[dev-dependencies] -serde = { version = "^ 1.0", features = ["derive"] } diff --git a/google-apis-common/src/lib.rs b/google-apis-common/src/lib.rs index 5396398932..cbd7bb20a5 100644 --- a/google-apis-common/src/lib.rs +++ b/google-apis-common/src/lib.rs @@ -1,3 +1,5 @@ +pub mod serde; + use std::error; use std::error::Error as StdError; use std::fmt::{self, Display}; @@ -844,138 +846,6 @@ mod yup_oauth2_impl { } } -pub mod types { - use std::fmt::Formatter; - use std::str::FromStr; - use serde::{Deserialize, Deserializer, Serializer}; - // https://github.com/protocolbuffers/protobuf-go/blob/6875c3d7242d1a3db910ce8a504f124cb840c23a/types/known/durationpb/duration.pb.go#L148 - - #[derive(Debug)] - enum ParseDurationError { - MissingSecondSuffix, - NanosTooSmall, - ParseIntError(std::num::ParseIntError), - SecondOverflow { seconds: i64, max_seconds: i64 }, - SecondUnderflow { seconds: i64, min_seconds: i64 } - } - - fn parse_duration_from_str(s: &str) -> Result { - let abs_duration = 315576000000i64; - // 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_digit(10)).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 >= abs_duration { - Err(ParseDurationError::SecondOverflow { seconds, max_seconds: abs_duration }) - } else if seconds <= -abs_duration { - Err(ParseDurationError::SecondUnderflow { seconds, min_seconds: -abs_duration }) - } else { - Ok(chrono::Duration::seconds(seconds) + chrono::Duration::nanoseconds(nanoseconds.into())) - } - } - - impl From 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 {} - - pub fn to_duration_str(x: &Option, s: S) -> Result - where - S: Serializer, - { - match x { - None => s.serialize_none(), - Some(x) => { - let seconds = x.num_seconds(); - let nanoseconds = (*x - chrono::Duration::seconds(seconds)) - .num_nanoseconds() - .expect("number of nanoseconds is less than or equal to 1 billion") as i32; - // might be left with -1 + non-zero nanos - if nanoseconds != 0 { - if seconds == 0 && nanoseconds.is_negative() { - s.serialize_str(&format!("-0.{}s", nanoseconds.abs())) - } else { - s.serialize_str(&format!("{}.{}s", seconds, nanoseconds.abs())) - } - } else { - s.serialize_str(&format!("{}s", seconds)) - } - } - } - } - // #[serde(deserialize_with = "path")] - pub fn from_duration_str<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let s: Option<&str> = Deserialize::deserialize(deserializer)?; - // TODO: Map error - Ok(s.map(|s| parse_duration_from_str(s).unwrap())) - } - - pub fn to_urlsafe_base64(x: &Option>, s: S) -> Result - where - S: Serializer, - { - match x { - None => s.serialize_none(), - Some(x) => s.serialize_some(&base64::encode_config(x, base64::URL_SAFE)) - } - } - - // #[serde(deserialize_with = "path")] - pub fn from_urlsafe_base64<'de, D>(deserializer: D) -> Result>, D::Error> - where - D: Deserializer<'de>, - { - let s: Option<&str> = Deserialize::deserialize(deserializer)?; - // TODO: Map error - Ok(s.map(|s| base64::decode_config(s, base64::URL_SAFE).unwrap())) - } - - // TODO: https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask - // "google-fieldmask", -} - #[cfg(test)] mod test_api { use super::*; @@ -983,7 +853,7 @@ mod test_api { use std::str::FromStr; use serde_json as json; - use serde::{Serialize, Deserialize}; + use ::serde::{Serialize, Deserialize}; #[test] fn serde() { diff --git a/google-apis-common/src/serde.rs b/google-apis-common/src/serde.rs new file mode 100644 index 0000000000..4208ba0140 --- /dev/null +++ b/google-apis-common/src/serde.rs @@ -0,0 +1,140 @@ +pub mod duration { + use std::fmt::Formatter; + use std::str::FromStr; + use serde::{Deserialize, Deserializer, Serializer}; + + #[derive(Debug)] + enum ParseDurationError { + MissingSecondSuffix, + NanosTooSmall, + ParseIntError(std::num::ParseIntError), + SecondOverflow { seconds: i64, max_seconds: i64 }, + SecondUnderflow { seconds: i64, min_seconds: i64 } + } + + impl From 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 parse_duration(s: &str) -> Result { + let abs_duration = 315576000000i64; + // 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_digit(10)).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 >= abs_duration { + Err(ParseDurationError::SecondOverflow { seconds, max_seconds: abs_duration }) + } else if seconds <= -abs_duration { + Err(ParseDurationError::SecondUnderflow { seconds, min_seconds: -abs_duration }) + } else { + Ok(chrono::Duration::seconds(seconds) + chrono::Duration::nanoseconds(nanoseconds.into())) + } + } + + pub fn serialize(x: &Option, s: S) -> Result + where + S: Serializer, + { + match x { + None => s.serialize_none(), + Some(x) => { + let seconds = x.num_seconds(); + let nanoseconds = (*x - chrono::Duration::seconds(seconds)) + .num_nanoseconds() + .expect("number of nanoseconds is less than or equal to 1 billion") as i32; + // might be left with -1 + non-zero nanos + if nanoseconds != 0 { + if seconds == 0 && nanoseconds.is_negative() { + s.serialize_str(&format!("-0.{}s", nanoseconds.abs())) + } else { + s.serialize_str(&format!("{}.{}s", seconds, nanoseconds.abs())) + } + } else { + s.serialize_str(&format!("{}s", seconds)) + } + } + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s: Option<&str> = Deserialize::deserialize(deserializer)?; + match s.map(|s| parse_duration(s)) { + None => Ok(None), + Some(Ok(d)) => Ok(Some(d)), + Some(Err(e)) => Err(serde::de::Error::custom(e)), + } + } +} + +pub mod urlsafe_base64 { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(x: &Option>, s: S) -> Result + where + S: Serializer, + { + match x { + None => s.serialize_none(), + Some(x) => s.serialize_some(&base64::encode_config(x, base64::URL_SAFE)) + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let s: Option<&str> = Deserialize::deserialize(deserializer)?; + // TODO: Map error + match s.map(|s| base64::decode_config(s, base64::URL_SAFE)) { + None => Ok(None), + Some(Ok(d)) => Ok(Some(d)), + Some(Err(e)) => Err(serde::de::Error::custom(e)), + } + } + + // TODO: https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask + // "google-fieldmask" +} diff --git a/src/generator/templates/api/lib/schema.mako b/src/generator/templates/api/lib/schema.mako index 55590f8a76..9f9cb5bfd9 100644 --- a/src/generator/templates/api/lib/schema.mako +++ b/src/generator/templates/api/lib/schema.mako @@ -18,9 +18,9 @@ ${struct} { #[serde(rename="${pn}")] % endif % if p.get("format", None) == "byte": - #[serde(serialize_with = "client::types::to_urlsafe_base64", deserialize_with = "client::types::from_urlsafe_base64")] + #[serde(with = "client::serde::urlsafe_base64")] % elif p.get("format", None) == "google-duration": - #[serde(serialize_with = "client::types::to_duration_str", deserialize_with = "client::types::from_duration_str")] + #[serde(with = "client::serde::duration")] % endif pub ${mangle_ident(pn)}: ${to_rust_type(schemas, s.id, pn, p, allow_optionals=allow_optionals)}, % endfor