origincloud_core/
protobuf.rs1pub use pbjson_types::*;
11
12pub use crate::rpc::Any;
13
14use chrono::{DateTime, SecondsFormat, Utc};
15use serde::de::Error as _;
16use serde::ser::Error as _;
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18
19#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Message)]
23pub struct Timestamp {
24 #[prost(int64, tag = "1")]
26 pub seconds: i64,
27 #[prost(int32, tag = "2")]
29 pub nanos: i32,
30}
31
32impl From<DateTime<Utc>> for Timestamp {
33 fn from(t: DateTime<Utc>) -> Self {
34 Self {
35 seconds: t.timestamp(),
36 nanos: i32::try_from(t.timestamp_subsec_nanos()).unwrap_or(999_999_999),
37 }
38 }
39}
40
41impl From<std::time::SystemTime> for Timestamp {
42 fn from(t: std::time::SystemTime) -> Self {
43 DateTime::<Utc>::from(t).into()
44 }
45}
46
47impl TryFrom<Timestamp> for DateTime<Utc> {
48 type Error = String;
49
50 fn try_from(t: Timestamp) -> Result<Self, String> {
51 u32::try_from(t.nanos)
52 .ok()
53 .and_then(|nanos| DateTime::from_timestamp(t.seconds, nanos))
54 .ok_or_else(|| {
55 format!(
56 "{}s {}ns is outside the times a timestamp holds",
57 t.seconds, t.nanos
58 )
59 })
60 }
61}
62
63impl Serialize for Timestamp {
64 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
65 let t = DateTime::<Utc>::try_from(*self).map_err(S::Error::custom)?;
66 serializer.serialize_str(&t.to_rfc3339_opts(SecondsFormat::AutoSi, true))
67 }
68}
69
70impl<'de> Deserialize<'de> for Timestamp {
71 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
72 let text = String::deserialize(deserializer)?;
73 let t = DateTime::parse_from_rfc3339(&text).map_err(D::Error::custom)?;
74 Ok(t.with_timezone(&Utc).into())
75 }
76}
77
78#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
83pub struct FieldMask {
84 #[prost(string, repeated, tag = "1")]
86 pub paths: Vec<String>,
87}
88
89impl FieldMask {
90 pub fn new<I, S>(paths: I) -> Self
92 where
93 I: IntoIterator<Item = S>,
94 S: Into<String>,
95 {
96 Self {
97 paths: paths.into_iter().map(Into::into).collect(),
98 }
99 }
100}
101
102impl Serialize for FieldMask {
103 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
104 let mut joined = String::new();
105 for (i, path) in self.paths.iter().enumerate() {
106 if i > 0 {
107 joined.push(',');
108 }
109 for (j, part) in path.split('.').enumerate() {
110 if j > 0 {
111 joined.push('.');
112 }
113 joined.push_str(&to_lower_camel(part));
114 }
115 }
116 serializer.serialize_str(&joined)
117 }
118}
119
120impl<'de> Deserialize<'de> for FieldMask {
121 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
122 let joined = String::deserialize(deserializer)?;
123 let mut paths = Vec::new();
124 for path in joined.split(',').filter(|p| !p.is_empty()) {
125 if path.contains('_') {
126 return Err(D::Error::custom(format!(
127 "the field mask path {path:?} isn't in camel case"
128 )));
129 }
130 let parts: Vec<String> = path.split('.').map(to_snake).collect();
131 paths.push(parts.join("."));
132 }
133 Ok(Self { paths })
134 }
135}
136
137fn to_lower_camel(name: &str) -> String {
138 let mut out = String::with_capacity(name.len());
139 let mut upper = false;
140 for c in name.chars() {
141 if c == '_' {
142 upper = true;
143 } else if upper {
144 out.extend(c.to_uppercase());
145 upper = false;
146 } else {
147 out.push(c);
148 }
149 }
150 out
151}
152
153fn to_snake(name: &str) -> String {
154 let mut out = String::with_capacity(name.len() + 4);
155 for c in name.chars() {
156 if c.is_ascii_uppercase() {
157 out.push('_');
158 out.push(c.to_ascii_lowercase());
159 } else {
160 out.push(c);
161 }
162 }
163 out
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn a_timestamp_is_rfc_3339_in_utc_with_a_z() {
172 for (seconds, nanos, json) in [
173 (1_793_361_600, 0, r#""2026-10-30T12:00:00Z""#),
174 (1_793_361_600, 500_000_000, r#""2026-10-30T12:00:00.500Z""#),
175 (0, 1, r#""1970-01-01T00:00:00.000000001Z""#),
176 ] {
177 let t = Timestamp { seconds, nanos };
178 assert_eq!(serde_json::to_string(&t).unwrap(), json);
179 assert_eq!(serde_json::from_str::<Timestamp>(json).unwrap(), t);
180 }
181 let offset: Timestamp = serde_json::from_str(r#""2026-10-30T14:00:00+02:00""#).unwrap();
182 assert_eq!(offset.seconds, 1_793_361_600);
183 assert!(
184 serde_json::to_string(&Timestamp {
185 seconds: 0,
186 nanos: -1
187 })
188 .is_err()
189 );
190 }
191
192 #[test]
193 fn a_field_mask_is_one_string_of_camel_case_paths() {
194 let mask = FieldMask::new(["size_gib", "tags", "volume.storage_class"]);
195 let json = serde_json::to_string(&mask).unwrap();
196 assert_eq!(json, r#""sizeGib,tags,volume.storageClass""#);
197 assert_eq!(serde_json::from_str::<FieldMask>(&json).unwrap(), mask);
198 assert_eq!(
199 serde_json::from_str::<FieldMask>(r#""""#).unwrap(),
200 FieldMask::default()
201 );
202 assert!(serde_json::from_str::<FieldMask>(r#""size_gib""#).is_err());
203 }
204}