firestore_db_and_auth/
firebase_rest_to_rust.rs

1//! # Low Level API to convert between rust types and the Firebase REST API
2//! Low level API to convert between generated rust types (see [`crate::dto`]) and
3//! the data types of the Firebase REST API. Those are 1:1 translations of the grpc API
4//! and deeply nested and wrapped.
5
6use bytes::Bytes;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11use super::dto;
12use super::errors::{FirebaseError, Result};
13
14#[derive(Debug, Serialize, Deserialize)]
15struct Wrapper {
16    #[serde(flatten)]
17    extra: HashMap<String, Value>,
18}
19
20use serde_json::{map::Map, Number};
21
22/// Converts a firebase google-rpc-api inspired heavily nested and wrapped response value
23/// of the Firebase REST API into a flattened serde json value.
24///
25/// This is a low level API. You probably want to use [`crate::documents`] instead.
26///
27/// This method works recursively!
28pub(crate) fn firebase_value_to_serde_value(v: &dto::Value) -> serde_json::Value {
29    if let Some(timestamp_value) = v.timestamp_value.as_ref() {
30        return Value::String(timestamp_value.clone());
31    } else if let Some(integer_value) = v.integer_value.as_ref() {
32        if let Ok(four) = integer_value.parse::<i64>() {
33            return Value::Number(four.into());
34        }
35    } else if let Some(double_value) = v.double_value {
36        if let Some(dd) = Number::from_f64(double_value) {
37            return Value::Number(dd);
38        }
39    } else if let Some(map_value) = v.map_value.as_ref() {
40        let mut map: Map<String, serde_json::value::Value> = Map::new();
41        if let Some(map_fields) = &map_value.fields {
42            for (map_key, map_v) in map_fields {
43                map.insert(map_key.clone(), firebase_value_to_serde_value(&map_v));
44            }
45        }
46        return Value::Object(map);
47    } else if let Some(string_value) = v.string_value.as_ref() {
48        return Value::String(string_value.clone());
49    } else if let Some(boolean_value) = v.boolean_value {
50        return Value::Bool(boolean_value);
51    } else if let Some(array_value) = v.array_value.as_ref() {
52        let mut vec: Vec<Value> = Vec::new();
53        if let Some(values) = &array_value.values {
54            for k in values {
55                vec.push(firebase_value_to_serde_value(&k));
56            }
57        }
58        return Value::Array(vec);
59    }
60    Value::Null
61}
62
63/// Converts a flat serde json value into a firebase google-rpc-api inspired heavily nested and wrapped type
64/// to be consumed by the Firebase REST API.
65///
66/// This is a low level API. You probably want to use [`crate::documents`] instead.
67///
68/// This method works recursively!
69pub(crate) fn serde_value_to_firebase_value(v: &serde_json::Value) -> dto::Value {
70    if v.is_f64() {
71        return dto::Value {
72            double_value: Some(v.as_f64().unwrap()),
73            ..Default::default()
74        };
75    } else if let Some(integer_value) = v.as_i64() {
76        return dto::Value {
77            integer_value: Some(integer_value.to_string()),
78            ..Default::default()
79        };
80    } else if let Some(map_value) = v.as_object() {
81        let mut map: HashMap<String, dto::Value> = HashMap::new();
82        for (map_key, map_v) in map_value {
83            map.insert(map_key.to_owned(), serde_value_to_firebase_value(&map_v));
84        }
85        return dto::Value {
86            map_value: Some(dto::MapValue { fields: Some(map) }),
87            ..Default::default()
88        };
89    } else if let Some(string_value) = v.as_str() {
90        return dto::Value {
91            string_value: Some(string_value.to_owned()),
92            ..Default::default()
93        };
94    } else if let Some(boolean_value) = v.as_bool() {
95        return dto::Value {
96            boolean_value: Some(boolean_value),
97            ..Default::default()
98        };
99    } else if let Some(array_value) = v.as_array() {
100        let mut vec: Vec<dto::Value> = Vec::new();
101        for k in array_value {
102            vec.push(serde_value_to_firebase_value(&k));
103        }
104        return dto::Value {
105            array_value: Some(dto::ArrayValue { values: Some(vec) }),
106            ..Default::default()
107        };
108    }
109    Default::default()
110}
111
112/// Converts a firebase google-rpc-api inspired heavily nested and wrapped response document
113/// of the Firebase REST API into a given custom type.
114///
115/// This is a low level API. You probably want to use [`crate::documents`] instead.
116///
117/// Arguments:
118/// * document: The document to convert
119/// * input_doc: Optional. The input bytes. Those will be part of the result in case of a parsing error.
120///
121/// Internals:
122///
123/// This method uses recursion to decode the given firebase type.
124pub fn document_to_pod<T>(document: &dto::Document, input_doc: Option<&Bytes>) -> Result<T>
125where
126    for<'de> T: Deserialize<'de>,
127{
128    // The firebase document has a field called "fields" that contain all top-level fields.
129    // We want those to be flattened to our custom data structure. To not reinvent the wheel,
130    // perform the firebase-value to serde-values conversion for all fields first and wrap those
131    // Wrapper struct with a HashMap. Use #[serde(flatten)] on that map.
132    let r = Wrapper {
133        extra: document
134            .fields
135            .as_ref()
136            .unwrap()
137            .iter()
138            .map(|(k, v)| {
139                return (k.to_owned(), firebase_value_to_serde_value(&v));
140            })
141            .collect(),
142    };
143
144    let v = serde_json::to_value(r)?;
145    let r: T = serde_json::from_value(v).map_err(|e| FirebaseError::SerdeVerbose {
146        doc: Some(document.name.clone()),
147        input_doc: String::from_utf8_lossy(input_doc.unwrap_or(&Bytes::new()))
148            .replace("\n", " ")
149            .to_string(),
150        ser: e,
151    })?;
152    Ok(r)
153}
154
155/// Converts a custom data type into a firebase google-rpc-api inspired heavily nested and wrapped type
156/// to be consumed by the Firebase REST API.
157///
158/// This is a low level API. You probably want to use [`crate::documents`] instead.
159///
160/// Internals:
161///
162/// This method uses recursion to decode the given firebase type.
163pub fn pod_to_document<T>(pod: &T) -> Result<dto::Document>
164where
165    T: Serialize,
166{
167    let v = serde_json::to_value(pod)?;
168    Ok(dto::Document {
169        fields: serde_value_to_firebase_value(&v).map_value.unwrap().fields,
170        ..Default::default()
171    })
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    use super::Result;
179    use serde::{Deserialize, Serialize};
180    use std::collections::HashMap;
181
182    #[derive(Serialize, Deserialize)]
183    struct DemoPod {
184        integer_test: u32,
185        boolean_test: bool,
186        string_test: String,
187    }
188
189    #[test]
190    fn test_document_to_pod() -> Result<()> {
191        let mut map: HashMap<String, dto::Value> = HashMap::new();
192        map.insert(
193            "integer_test".to_owned(),
194            dto::Value {
195                integer_value: Some("12".to_owned()),
196                ..Default::default()
197            },
198        );
199        map.insert(
200            "boolean_test".to_owned(),
201            dto::Value {
202                boolean_value: Some(true),
203                ..Default::default()
204            },
205        );
206        map.insert(
207            "string_test".to_owned(),
208            dto::Value {
209                string_value: Some("abc".to_owned()),
210                ..Default::default()
211            },
212        );
213        let t = dto::Document {
214            fields: Some(map),
215            ..Default::default()
216        };
217        let firebase_doc: DemoPod = document_to_pod(&t, None)?;
218        assert_eq!(firebase_doc.string_test, "abc");
219        assert_eq!(firebase_doc.integer_test, 12);
220        assert_eq!(firebase_doc.boolean_test, true);
221
222        Ok(())
223    }
224
225    #[test]
226    fn test_pod_to_document() -> Result<()> {
227        let t = DemoPod {
228            integer_test: 12,
229            boolean_test: true,
230            string_test: "abc".to_owned(),
231        };
232        let firebase_doc = pod_to_document(&t)?;
233        let map = firebase_doc.fields;
234        assert_eq!(
235            map.unwrap()
236                .get("integer_test")
237                .expect("a value in the map for integer_test")
238                .integer_value
239                .as_ref()
240                .expect("an integer value"),
241            "12"
242        );
243
244        Ok(())
245    }
246}