firebase_rs_sdk/firestore/remote/
serializer.rs1use 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 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}