firebase_rs_sdk/storage/metadata/
serde.rs

1use 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}