use crate::error::{ClientError, Result};
use crate::proto::Uuid as ProtoUuid;
use chrono::{DateTime, Utc};
use prost::Name;
use prost_types::{Any, Timestamp};
use uuid::Uuid;
pub const TYPE_URL_PREFIX: &str = "type.googleapis.com";
pub fn type_url(type_name: &str) -> String {
format!("{}/{}", TYPE_URL_PREFIX, type_name)
}
pub fn type_name_from_url(type_url: &str) -> &str {
type_url.rsplit('/').next().unwrap_or(type_url)
}
pub fn type_url_matches_exact(type_url: &str, full_type_name: &str) -> bool {
type_url == format!("{}/{}", TYPE_URL_PREFIX, full_type_name)
}
pub fn type_matches<T: prost::Message + Name>(any: &Any) -> bool {
let expected = format!("{}/{}", TYPE_URL_PREFIX, T::full_name());
any.type_url == expected
}
pub fn try_unpack<T: prost::Message + Default + Name>(any: &Any) -> Option<T> {
if type_matches::<T>(any) {
T::decode(any.value.as_slice()).ok()
} else {
None
}
}
pub fn unpack<T: prost::Message + Default + Name>(any: &Any) -> Result<T> {
let expected = format!("{}/{}", TYPE_URL_PREFIX, T::full_name());
if any.type_url != expected {
return Err(ClientError::InvalidArgument {
msg: format!("type mismatch: expected {}, got {}", expected, any.type_url),
});
}
T::decode(any.value.as_slice()).map_err(|e| ClientError::InvalidArgument {
msg: format!("decode error: {}", e),
})
}
pub fn full_type_url<T: Name>() -> String {
format!("{}/{}", TYPE_URL_PREFIX, T::full_name())
}
pub fn full_type_name<T: Name>() -> String {
T::full_name()
}
pub fn uuid_to_proto(uuid: Uuid) -> ProtoUuid {
ProtoUuid {
value: uuid.as_bytes().to_vec(),
}
}
pub fn proto_to_uuid(proto: &ProtoUuid) -> Result<Uuid> {
Uuid::from_slice(&proto.value).map_err(|e| ClientError::InvalidArgument {
msg: format!("invalid UUID: {}", e),
})
}
pub fn parse_timestamp(rfc3339: &str) -> Result<Timestamp> {
let dt: DateTime<Utc> = rfc3339.parse().map_err(|e| ClientError::InvalidTimestamp {
msg: format!("{}: {}", rfc3339, e),
})?;
Ok(Timestamp {
seconds: dt.timestamp(),
nanos: dt.timestamp_subsec_nanos() as i32,
})
}
pub fn now() -> Timestamp {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time before unix epoch");
Timestamp {
seconds: now.as_secs() as i64,
nanos: now.subsec_nanos() as i32,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_type_url() {
assert_eq!(
type_url("examples.AddItemToCart"),
"type.googleapis.com/examples.AddItemToCart"
);
}
#[test]
fn test_type_name_from_url() {
assert_eq!(
type_name_from_url("type.googleapis.com/examples.AddItemToCart"),
"examples.AddItemToCart"
);
assert_eq!(type_name_from_url("AddItemToCart"), "AddItemToCart");
}
#[test]
fn test_type_url_matches_exact() {
assert!(type_url_matches_exact(
"type.googleapis.com/examples.AddItemToCart",
"examples.AddItemToCart"
));
assert!(!type_url_matches_exact(
"type.googleapis.com/examples.AddItemToCart",
"examples.RemoveItem"
));
assert!(!type_url_matches_exact(
"type.googleapis.com/examples.AddItemToCart",
"AddItemToCart"
));
}
#[test]
fn test_uuid_conversion() {
let uuid = Uuid::new_v4();
let proto = uuid_to_proto(uuid);
let back = proto_to_uuid(&proto).unwrap();
assert_eq!(uuid, back);
}
#[test]
fn test_parse_timestamp() {
let ts = parse_timestamp("2024-01-15T10:30:00Z").unwrap();
assert_eq!(ts.seconds, 1705314600);
assert_eq!(ts.nanos, 0);
}
#[test]
fn test_parse_timestamp_with_nanos() {
let ts = parse_timestamp("2024-01-15T10:30:00.123456789Z").unwrap();
assert_eq!(ts.seconds, 1705314600);
assert_eq!(ts.nanos, 123456789);
}
#[test]
fn test_parse_timestamp_invalid() {
assert!(parse_timestamp("not a timestamp").is_err());
}
}