firebase_rs_sdk/storage/metadata/
serde.rs1use serde::de::Error as DeError;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use serde_json::Value;
4use std::collections::BTreeMap;
5
6#[derive(Clone, Debug, Default, Deserialize, Serialize)]
7#[serde(rename_all = "camelCase")]
8pub struct ObjectMetadata {
9 #[serde(default)]
10 pub bucket: Option<String>,
11 #[serde(default)]
12 pub name: Option<String>,
13 #[serde(default)]
14 pub full_path: Option<String>,
15 #[serde(default)]
16 pub generation: Option<String>,
17 #[serde(default)]
18 pub metageneration: Option<String>,
19 #[serde(
20 default,
21 deserialize_with = "deserialize_option_u64_from_string",
22 serialize_with = "serialize_option_u64_as_string"
23 )]
24 pub size: Option<u64>,
25 #[serde(default)]
26 pub time_created: Option<String>,
27 #[serde(default)]
28 pub updated: Option<String>,
29 #[serde(default)]
30 pub content_type: Option<String>,
31 #[serde(default)]
32 pub cache_control: Option<String>,
33 #[serde(default)]
34 pub content_disposition: Option<String>,
35 #[serde(default)]
36 pub content_language: Option<String>,
37 #[serde(default)]
38 pub content_encoding: Option<String>,
39 #[serde(default)]
40 pub md5_hash: Option<String>,
41 #[serde(default)]
42 pub crc32c: Option<String>,
43 #[serde(default)]
44 pub etag: Option<String>,
45 #[serde(default, rename = "metadata")]
46 pub custom_metadata: Option<BTreeMap<String, String>>,
47 #[serde(
48 default,
49 deserialize_with = "deserialize_download_tokens",
50 serialize_with = "serialize_download_tokens"
51 )]
52 pub download_tokens: Option<Vec<String>>,
53 #[serde(skip)]
54 pub raw: Value,
55}
56
57impl ObjectMetadata {
58 pub fn from_value(value: Value) -> Self {
59 let mut metadata: ObjectMetadata =
60 serde_json::from_value(value.clone()).unwrap_or_default();
61 metadata.raw = value;
62 metadata
63 }
64
65 pub fn raw(&self) -> &Value {
66 &self.raw
67 }
68
69 pub fn size_bytes(&self) -> Option<u64> {
70 self.size
71 }
72
73 pub fn download_tokens(&self) -> Option<&[String]> {
74 self.download_tokens.as_deref()
75 }
76}
77
78#[derive(Clone, Debug, Default, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub struct SetMetadataRequest {
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub cache_control: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub content_disposition: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub content_encoding: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub content_language: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub content_type: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none", rename = "metadata")]
92 pub custom_metadata: Option<BTreeMap<String, String>>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub md5_hash: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub crc32c: Option<String>,
97}
98
99pub type SettableMetadata = SetMetadataRequest;
100pub type UploadMetadata = SetMetadataRequest;
101
102impl SetMetadataRequest {
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub fn with_cache_control(mut self, value: impl Into<String>) -> Self {
108 self.cache_control = Some(value.into());
109 self
110 }
111
112 pub fn with_content_disposition(mut self, value: impl Into<String>) -> Self {
113 self.content_disposition = Some(value.into());
114 self
115 }
116
117 pub fn with_content_encoding(mut self, value: impl Into<String>) -> Self {
118 self.content_encoding = Some(value.into());
119 self
120 }
121
122 pub fn with_content_language(mut self, value: impl Into<String>) -> Self {
123 self.content_language = Some(value.into());
124 self
125 }
126
127 pub fn with_content_type(mut self, value: impl Into<String>) -> Self {
128 self.content_type = Some(value.into());
129 self
130 }
131
132 pub fn with_md5_hash(mut self, value: impl Into<String>) -> Self {
133 self.md5_hash = Some(value.into());
134 self
135 }
136
137 pub fn with_crc32c(mut self, value: impl Into<String>) -> Self {
138 self.crc32c = Some(value.into());
139 self
140 }
141
142 pub fn insert_custom_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
143 self.custom_metadata
144 .get_or_insert_with(BTreeMap::new)
145 .insert(key.into(), value.into());
146 }
147
148 pub fn custom_metadata(&self) -> Option<&BTreeMap<String, String>> {
149 self.custom_metadata.as_ref()
150 }
151}
152
153fn deserialize_option_u64_from_string<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
154where
155 D: Deserializer<'de>,
156{
157 let option: Option<Value> = Option::deserialize(deserializer)?;
158 match option {
159 None | Some(Value::Null) => Ok(None),
160 Some(Value::Number(number)) => Ok(number.as_u64()),
161 Some(Value::String(s)) => {
162 if s.trim().is_empty() {
163 Ok(None)
164 } else {
165 s.parse::<u64>()
166 .map(Some)
167 .map_err(|err| D::Error::custom(format!("invalid u64 value '{s}': {err}")))
168 }
169 }
170 Some(other) => Err(D::Error::custom(format!(
171 "unexpected numeric value: {other}"
172 ))),
173 }
174}
175
176fn serialize_option_u64_as_string<S>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
177where
178 S: Serializer,
179{
180 match value {
181 Some(v) => serializer.serialize_str(&v.to_string()),
182 None => serializer.serialize_none(),
183 }
184}
185
186fn deserialize_download_tokens<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
187where
188 D: Deserializer<'de>,
189{
190 let option: Option<Value> = Option::deserialize(deserializer)?;
191 let tokens = match option {
192 None | Some(Value::Null) => None,
193 Some(Value::String(s)) => {
194 let values: Vec<String> = s
195 .split(',')
196 .map(|token| token.trim())
197 .filter(|token| !token.is_empty())
198 .map(|token| token.to_string())
199 .collect();
200 if values.is_empty() {
201 None
202 } else {
203 Some(values)
204 }
205 }
206 Some(Value::Array(entries)) => {
207 let values: Vec<String> = entries
208 .into_iter()
209 .filter_map(|entry| match entry {
210 Value::String(s) if !s.is_empty() => Some(s),
211 _ => None,
212 })
213 .collect();
214 if values.is_empty() {
215 None
216 } else {
217 Some(values)
218 }
219 }
220 Some(other) => {
221 return Err(D::Error::custom(format!(
222 "unexpected downloadTokens format: {other}"
223 )))
224 }
225 };
226 Ok(tokens)
227}
228
229fn serialize_download_tokens<S>(
230 tokens: &Option<Vec<String>>,
231 serializer: S,
232) -> Result<S::Ok, S::Error>
233where
234 S: Serializer,
235{
236 match tokens {
237 Some(values) => serializer.serialize_str(&values.join(",")),
238 None => serializer.serialize_none(),
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn parses_metadata_from_value() {
248 let json = serde_json::json!({
249 "bucket": "my-bucket",
250 "name": "photos/cat.jpg",
251 "fullPath": "photos/cat.jpg",
252 "generation": "1",
253 "metageneration": "2",
254 "size": "42",
255 "timeCreated": "2023-01-01T00:00:00Z",
256 "updated": "2023-01-01T01:00:00Z",
257 "contentType": "image/jpeg",
258 "metadata": {"env": "test"},
259 "downloadTokens": "token1,token2",
260 "md5Hash": "abc",
261 "crc32c": "def",
262 "etag": "ghi"
263 });
264
265 let metadata = ObjectMetadata::from_value(json);
266 assert_eq!(metadata.bucket.as_deref(), Some("my-bucket"));
267 assert_eq!(metadata.name.as_deref(), Some("photos/cat.jpg"));
268 assert_eq!(metadata.size_bytes(), Some(42));
269 assert_eq!(metadata.download_tokens().unwrap(), ["token1", "token2"]);
270 assert_eq!(metadata.md5_hash.as_deref(), Some("abc"));
271 assert_eq!(metadata.crc32c.as_deref(), Some("def"));
272 assert_eq!(metadata.etag.as_deref(), Some("ghi"));
273 }
274
275 #[test]
276 fn serializes_set_metadata_request() {
277 let mut request = SetMetadataRequest::new()
278 .with_cache_control("max-age=60")
279 .with_content_type("image/jpeg")
280 .with_md5_hash("abc");
281 request.insert_custom_metadata("env", "prod");
282
283 let value = serde_json::to_value(&request).unwrap();
284 assert_eq!(value["cacheControl"], "max-age=60");
285 assert_eq!(value["contentType"], "image/jpeg");
286 assert_eq!(value["md5Hash"], "abc");
287 assert_eq!(value["metadata"]["env"], "prod");
288 }
289}