firebase_rs_sdk/firestore/remote/
serializer.rs

1use std::collections::BTreeMap;
2use std::str::FromStr;
3
4use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
5use base64::Engine;
6use chrono::{DateTime, SecondsFormat, TimeZone, Utc};
7use serde_json::{json, Value as JsonValue};
8
9use crate::firestore::error::{invalid_argument, FirestoreResult};
10use crate::firestore::model::{DatabaseId, DocumentKey, GeoPoint, Timestamp};
11use crate::firestore::value::{BytesValue, FirestoreValue, MapValue, ValueKind};
12
13#[derive(Clone, Debug)]
14pub struct JsonProtoSerializer {
15    database_id: DatabaseId,
16}
17
18impl JsonProtoSerializer {
19    pub fn new(database_id: DatabaseId) -> Self {
20        Self { database_id }
21    }
22
23    pub fn database_id(&self) -> &DatabaseId {
24        &self.database_id
25    }
26
27    pub fn database_name(&self) -> String {
28        format!(
29            "projects/{}/databases/{}",
30            self.database_id.project_id(),
31            self.database_id.database()
32        )
33    }
34
35    pub fn document_name(&self, key: &DocumentKey) -> String {
36        format!(
37            "{}/documents/{}",
38            self.database_name(),
39            key.path().canonical_string()
40        )
41    }
42
43    pub fn encode_document_fields(&self, map: &MapValue) -> JsonValue {
44        json!({
45            "fields": encode_map_fields(map)
46        })
47    }
48
49    pub fn encode_commit_body(&self, key: &DocumentKey, map: &MapValue) -> JsonValue {
50        let name = self.document_name(key);
51        json!({
52            "writes": [
53                {
54                    "update": {
55                        "name": name,
56                        "fields": encode_map_fields(map)
57                    }
58                }
59            ]
60        })
61    }
62
63    pub fn decode_document_fields(&self, value: &JsonValue) -> FirestoreResult<Option<MapValue>> {
64        if value.get("fields").is_some() {
65            decode_map_value(value).map(Some)
66        } else {
67            // Document exists but has no user fields.
68            Ok(Some(MapValue::new(BTreeMap::new())))
69        }
70    }
71
72    pub fn decode_map_value(&self, value: &JsonValue) -> FirestoreResult<MapValue> {
73        decode_map_value(value)
74    }
75
76    pub fn encode_value(&self, value: &FirestoreValue) -> JsonValue {
77        encode_value(value)
78    }
79}
80
81fn encode_map_fields(map: &MapValue) -> JsonValue {
82    let mut fields = serde_json::Map::new();
83    for (key, value) in map.fields() {
84        fields.insert(key.clone(), encode_value(value));
85    }
86    JsonValue::Object(fields)
87}
88
89fn encode_value(value: &FirestoreValue) -> JsonValue {
90    match value.kind() {
91        ValueKind::Null => json!({ "nullValue": JsonValue::Null }),
92        ValueKind::Boolean(boolean) => json!({ "booleanValue": boolean }),
93        ValueKind::Integer(integer) => json!({ "integerValue": integer.to_string() }),
94        ValueKind::Double(double) => json!({ "doubleValue": double }),
95        ValueKind::Timestamp(timestamp) => json!({ "timestampValue": encode_timestamp(timestamp) }),
96        ValueKind::String(string) => json!({ "stringValue": string }),
97        ValueKind::Bytes(bytes) => {
98            json!({ "bytesValue": BASE64_STANDARD.encode(bytes.as_slice()) })
99        }
100        ValueKind::Reference(reference) => json!({ "referenceValue": reference }),
101        ValueKind::GeoPoint(point) => json!({
102            "geoPointValue": {
103                "latitude": point.latitude(),
104                "longitude": point.longitude(),
105            }
106        }),
107        ValueKind::Array(array) => {
108            let values = array.values().iter().map(encode_value).collect::<Vec<_>>();
109            json!({ "arrayValue": { "values": values } })
110        }
111        ValueKind::Map(map) => json!({
112            "mapValue": {
113                "fields": encode_map_fields(map)
114            }
115        }),
116    }
117}
118
119fn decode_map_value(value: &JsonValue) -> FirestoreResult<MapValue> {
120    let map = value
121        .as_object()
122        .ok_or_else(|| invalid_argument("Expected object for map value"))?;
123    let fields_object = match map.get("fields") {
124        Some(fields_value) => fields_value
125            .as_object()
126            .ok_or_else(|| invalid_argument("Expected 'fields' to be an object"))?,
127        None => return Ok(MapValue::new(BTreeMap::new())),
128    };
129
130    let mut fields = BTreeMap::new();
131    for (key, value) in fields_object {
132        fields.insert(key.clone(), decode_value(value)?);
133    }
134    Ok(MapValue::new(fields))
135}
136
137fn decode_value(value: &JsonValue) -> FirestoreResult<FirestoreValue> {
138    let object = value
139        .as_object()
140        .ok_or_else(|| invalid_argument("Expected Firestore value object"))?;
141    if let Some(null_value) = object.get("nullValue") {
142        if null_value.is_null() {
143            return Ok(FirestoreValue::null());
144        }
145    }
146    if let Some(bool_value) = object.get("booleanValue") {
147        let value = bool_value
148            .as_bool()
149            .ok_or_else(|| invalid_argument("booleanValue must be bool"))?;
150        return Ok(FirestoreValue::from_bool(value));
151    }
152    if let Some(integer_value) = object.get("integerValue") {
153        let parsed = match integer_value {
154            JsonValue::String(value) => i64::from_str(value)
155                .map_err(|err| invalid_argument(format!("Invalid integerValue: {err}")))?,
156            JsonValue::Number(number) => number
157                .as_i64()
158                .ok_or_else(|| invalid_argument("Integer out of range"))?,
159            _ => return Err(invalid_argument("integerValue must be a string or number")),
160        };
161        return Ok(FirestoreValue::from_integer(parsed));
162    }
163    if let Some(double_value) = object.get("doubleValue") {
164        let parsed = match double_value {
165            JsonValue::Number(number) => number
166                .as_f64()
167                .ok_or_else(|| invalid_argument("Invalid doubleValue"))?,
168            JsonValue::String(value) => value
169                .parse::<f64>()
170                .map_err(|err| invalid_argument(format!("Invalid doubleValue: {err}")))?,
171            _ => return Err(invalid_argument("doubleValue must be a number or string")),
172        };
173        return Ok(FirestoreValue::from_double(parsed));
174    }
175    if let Some(timestamp_value) = object.get("timestampValue") {
176        let timestamp_str = timestamp_value
177            .as_str()
178            .ok_or_else(|| invalid_argument("timestampValue must be string"))?;
179        return Ok(FirestoreValue::from_timestamp(parse_timestamp(
180            timestamp_str,
181        )?));
182    }
183    if let Some(string_value) = object.get("stringValue") {
184        let str_value = string_value
185            .as_str()
186            .ok_or_else(|| invalid_argument("stringValue must be string"))?;
187        return Ok(FirestoreValue::from_string(str_value));
188    }
189    if let Some(bytes_value) = object.get("bytesValue") {
190        let str_value = bytes_value
191            .as_str()
192            .ok_or_else(|| invalid_argument("bytesValue must be base64 string"))?;
193        let decoded = BASE64_STANDARD
194            .decode(str_value)
195            .map_err(|err| invalid_argument(format!("Invalid bytesValue: {err}")))?;
196        return Ok(FirestoreValue::from_bytes(BytesValue::from(decoded)));
197    }
198    if let Some(reference_value) = object.get("referenceValue") {
199        let str_value = reference_value
200            .as_str()
201            .ok_or_else(|| invalid_argument("referenceValue must be string"))?;
202        return Ok(FirestoreValue::from_reference(str_value));
203    }
204    if let Some(geo_point) = object.get("geoPointValue") {
205        let latitude = geo_point
206            .get("latitude")
207            .and_then(|value| value.as_f64())
208            .ok_or_else(|| invalid_argument("geoPointValue.latitude must be f64"))?;
209        let longitude = geo_point
210            .get("longitude")
211            .and_then(|value| value.as_f64())
212            .ok_or_else(|| invalid_argument("geoPointValue.longitude must be f64"))?;
213        return Ok(FirestoreValue::from_geo_point(GeoPoint::new(
214            latitude, longitude,
215        )?));
216    }
217    if let Some(array_value) = object.get("arrayValue") {
218        let decoded = if let Some(values) = array_value.get("values") {
219            match values.as_array() {
220                Some(entries) => entries
221                    .iter()
222                    .map(decode_value)
223                    .collect::<FirestoreResult<Vec<_>>>()?,
224                None => Vec::new(),
225            }
226        } else {
227            Vec::new()
228        };
229        return Ok(FirestoreValue::from_array(decoded));
230    }
231    if let Some(map_value) = object.get("mapValue") {
232        let map = decode_map_value(map_value)?;
233        return Ok(FirestoreValue::from_map(map.fields().clone()));
234    }
235
236    Err(invalid_argument("Unknown Firestore value type"))
237}
238
239fn encode_timestamp(timestamp: &Timestamp) -> String {
240    Utc.timestamp_opt(timestamp.seconds, timestamp.nanos as u32)
241        .single()
242        .unwrap_or_else(|| Utc.timestamp_opt(0, 0).single().expect("zero timestamp"))
243        .to_rfc3339_opts(SecondsFormat::Nanos, true)
244}
245
246fn parse_timestamp(value: &str) -> FirestoreResult<Timestamp> {
247    let datetime = DateTime::parse_from_rfc3339(value)
248        .map_err(|err| invalid_argument(format!("Invalid timestamp: {err}")))?;
249    let datetime_utc = datetime.with_timezone(&Utc);
250    Ok(Timestamp::new(
251        datetime_utc.timestamp(),
252        datetime_utc.timestamp_subsec_nanos() as i32,
253    ))
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn encode_decode_roundtrip() {
262        let mut map = BTreeMap::new();
263        map.insert("name".to_string(), FirestoreValue::from_string("Ada"));
264        map.insert("age".to_string(), FirestoreValue::from_integer(42));
265        map.insert(
266            "nested".to_string(),
267            FirestoreValue::from_map({
268                let mut inner = BTreeMap::new();
269                inner.insert("flag".to_string(), FirestoreValue::from_bool(true));
270                inner
271            }),
272        );
273        let map = MapValue::new(map);
274        let serializer = JsonProtoSerializer::new(DatabaseId::default("project"));
275        let encoded = serializer.encode_document_fields(&map);
276        let decoded = serializer.decode_document_fields(&encoded).unwrap();
277        assert!(decoded.is_some());
278        let decoded_map = decoded.unwrap();
279        assert_eq!(
280            decoded_map.fields().get("name"),
281            Some(&FirestoreValue::from_string("Ada"))
282        );
283        assert_eq!(
284            decoded_map.fields().get("age"),
285            Some(&FirestoreValue::from_integer(42))
286        );
287    }
288}