Skip to content

Commit 7223a1c

Browse files
authored
Merge pull request #6 from firebase/blog
Add blogs code to public repo
2 parents 7c0ddc1 + b5ef01a commit 7223a1c

File tree

3 files changed

+760
-0
lines changed

3 files changed

+760
-0
lines changed

blogs/sept-2021/converter.cc

+390
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "converter.h"
18+
19+
#include <cassert>
20+
#include <exception>
21+
#include <string>
22+
#include <utility>
23+
24+
#include "firebase/firestore.h"
25+
26+
namespace firebase {
27+
namespace firestore {
28+
29+
namespace {
30+
31+
// Helper functions to get values from maps of `Variant`s. Return
32+
// a default-constructed value if the required key was not found.
33+
34+
bool TryGetBoolean(const std::map<Variant, Variant>& from, const char* key) {
35+
auto it = from.find(Variant(key));
36+
if (it == from.end() || !it->second.is_bool()) {
37+
return false;
38+
}
39+
40+
return it->second.bool_value();
41+
}
42+
43+
int64_t TryGetInteger(const std::map<Variant, Variant>& from, const char* key) {
44+
auto it = from.find(Variant(key));
45+
if (it == from.end() || !it->second.is_int64()) {
46+
return 0;
47+
}
48+
49+
return it->second.int64_value();
50+
}
51+
52+
double TryGetDouble(const std::map<Variant, Variant>& from, const char* key) {
53+
auto it = from.find(Variant(key));
54+
if (it == from.end() || !it->second.is_double()) {
55+
return 0.0;
56+
}
57+
58+
return it->second.double_value();
59+
}
60+
61+
std::string TryGetString(const std::map<Variant, Variant>& from,
62+
const char* key) {
63+
auto it = from.find(Variant(key));
64+
if (it == from.end() || !it->second.is_string()) {
65+
return "";
66+
}
67+
68+
return it->second.string_value();
69+
}
70+
71+
// Helper functions to get values from maps of `FieldValue`s. Return
72+
// a default-constructed value if the required key was not found.
73+
74+
bool TryGetBoolean(const MapFieldValue& from, const std::string& key) {
75+
auto it = from.find(key);
76+
if (it == from.end() || !it->second.is_boolean()) {
77+
return false;
78+
}
79+
80+
return it->second.boolean_value();
81+
}
82+
83+
std::string TryGetString(const MapFieldValue& from, const std::string& key) {
84+
auto it = from.find(key);
85+
if (it == from.end() || !it->second.is_string()) {
86+
return "";
87+
}
88+
89+
return it->second.string_value();
90+
}
91+
92+
std::vector<FieldValue> TryGetArray(const MapFieldValue& from,
93+
const std::string& key) {
94+
auto it = from.find(key);
95+
if (it == from.end() || !it->second.is_array()) {
96+
return {};
97+
}
98+
99+
return it->second.array_value();
100+
}
101+
102+
} // namespace
103+
104+
// `Variant` -> `FieldValue`
105+
106+
FieldValue Converter::ConvertAny(const Variant& from, bool within_array) const {
107+
switch (from.type()) {
108+
// Primitives -- one-to-one mapping.
109+
110+
case Variant::Type::kTypeNull:
111+
return FieldValue::Null();
112+
case Variant::Type::kTypeBool:
113+
return FieldValue::Boolean(from.bool_value());
114+
case Variant::Type::kTypeInt64:
115+
return FieldValue::Integer(from.int64_value());
116+
case Variant::Type::kTypeDouble:
117+
return FieldValue::Double(from.double_value());
118+
119+
// Firestore does not have a distinction between static and mutable
120+
// strings and blobs. In all cases, the resulting `FieldValue` has
121+
// ownership of the underlying string or blob.
122+
123+
case Variant::Type::kTypeStaticString:
124+
case Variant::Type::kTypeMutableString:
125+
return FieldValue::String(from.string_value());
126+
127+
case Variant::Type::kTypeStaticBlob:
128+
case Variant::Type::kTypeMutableBlob:
129+
return FieldValue::Blob(from.blob_data(), from.blob_size());
130+
131+
// Containers are converted recursively.
132+
133+
case Variant::Type::kTypeVector:
134+
return ConvertArray(from.vector(), within_array);
135+
case Variant::Type::kTypeMap:
136+
return ConvertMap(from.map());
137+
138+
default:
139+
assert(false && "Unknown Variant type");
140+
std::terminate();
141+
}
142+
}
143+
144+
FieldValue Converter::ConvertArray(const std::vector<Variant>& from,
145+
bool within_array) const {
146+
if (!within_array) {
147+
return ConvertRegularArray(from);
148+
149+
} else {
150+
// Firestore doesn't support nested arrays. As a workaround, create an
151+
// intermediate map to contain the nested array.
152+
return FieldValue::Map({
153+
{"special", FieldValue::Boolean(true)},
154+
{"type", FieldValue::String("nested_array")},
155+
{"value", ConvertRegularArray(from)},
156+
});
157+
}
158+
}
159+
160+
FieldValue Converter::ConvertRegularArray(
161+
const std::vector<Variant>& from) const {
162+
std::vector<FieldValue> result;
163+
result.reserve(from.size());
164+
165+
for (const auto& v : from) {
166+
result.push_back(ConvertAny(v, /*within_array=*/true));
167+
}
168+
169+
return FieldValue::Array(std::move(result));
170+
}
171+
172+
FieldValue Converter::ConvertMap(const std::map<Variant, Variant>& from) const {
173+
// Check if the map represents a special value (e.g. a `Timestamp`) that
174+
// should round-trip.
175+
bool is_special = TryGetBoolean(from, "special");
176+
if (is_special) {
177+
return ConvertSpecialValue(from);
178+
} else {
179+
return ConvertRegularMap(from);
180+
}
181+
}
182+
183+
FieldValue Converter::ConvertRegularMap(
184+
const std::map<Variant, Variant>& from) const {
185+
MapFieldValue result;
186+
187+
for (const auto& kv : from) {
188+
// Note: Firestore only supports string keys. If it's possible for the map
189+
// to contain non-string keys, you would have to convert them to a string
190+
// representation or skip them.
191+
assert(kv.first.is_string());
192+
result[kv.first.string_value()] = ConvertVariantToFieldValue(kv.second);
193+
}
194+
195+
return FieldValue::Map(std::move(result));
196+
}
197+
198+
FieldValue Converter::ConvertSpecialValue(
199+
const std::map<Variant, Variant>& from) const {
200+
// Special values are Firestore entities encoded as maps because they are not
201+
// directly supported by `Variant`. The assumption is that the map contains
202+
// a boolean field "special" set to true and a string field "type" indicating
203+
// which kind of an entity it contains.
204+
205+
std::string type = TryGetString(from, "type");
206+
if (type.empty()) {
207+
return {};
208+
}
209+
210+
if (type == "timestamp") {
211+
Timestamp result(TryGetInteger(from, "seconds"),
212+
TryGetInteger(from, "nanoseconds"));
213+
return FieldValue::Timestamp(result);
214+
215+
} else if (type == "geo_point") {
216+
GeoPoint result(TryGetDouble(from, "latitude"),
217+
TryGetDouble(from, "longitude"));
218+
return FieldValue::GeoPoint(result);
219+
220+
} else if (type == "document_reference") {
221+
DocumentReference result =
222+
firestore_->Document(TryGetString(from, "document_path"));
223+
return FieldValue::Reference(result);
224+
225+
} else if (type == "delete") {
226+
return FieldValue::Delete();
227+
228+
} else if (type == "server_timestamp") {
229+
return FieldValue::ServerTimestamp();
230+
}
231+
232+
return {};
233+
}
234+
235+
// `FieldValue` -> `Variant`
236+
237+
Variant Converter::ConvertFieldValueToVariant(const FieldValue& from) const {
238+
switch (from.type()) {
239+
// Primitives -- one-to-one mapping.
240+
241+
case FieldValue::Type::kNull:
242+
return Variant::Null();
243+
case FieldValue::Type::kBoolean:
244+
return Variant(from.boolean_value());
245+
case FieldValue::Type::kInteger:
246+
return Variant(from.integer_value());
247+
case FieldValue::Type::kDouble:
248+
return Variant(from.double_value());
249+
250+
// `FieldValue` always owns the underlying string or blob, so the safest
251+
// approach is to copy the value to a `Variant` that will assume
252+
// ownership.
253+
254+
case FieldValue::Type::kString:
255+
return Variant(from.string_value());
256+
case FieldValue::Type::kBlob:
257+
return Variant::FromMutableBlob(from.blob_value(), from.blob_size());
258+
259+
// Containers are converted recursively.
260+
261+
case FieldValue::Type::kArray:
262+
return ConvertArray(from.array_value());
263+
case FieldValue::Type::kMap:
264+
return ConvertMap(from.map_value());
265+
266+
// Firestore-specific structs are encoded as maps with a boolean field
267+
// "special" set to true and a string field "type" indicating the original
268+
// type.
269+
270+
case FieldValue::Type::kTimestamp: {
271+
Timestamp ts = from.timestamp_value();
272+
MapFieldValue as_map = {
273+
{"special", FieldValue::Boolean(true)},
274+
{"type", FieldValue::String("timestamp")},
275+
{"seconds", FieldValue::Integer(ts.seconds())},
276+
{"nanoseconds", FieldValue::Integer(ts.nanoseconds())}};
277+
return ConvertRegularMap(as_map);
278+
// Note: if using the resulting `Variant` with RTDB, you might want to
279+
// convert timestamps to the number of milliseconds since Unix epoch:
280+
// Timestamp ts = from.timestamp_value();
281+
// int64_t millis = ts.seconds() * 1000 + ts.nanoseconds() / (1000*1000);
282+
// return Variant(millis);
283+
}
284+
285+
case FieldValue::Type::kGeoPoint: {
286+
GeoPoint gp = from.geo_point_value();
287+
MapFieldValue as_map = {
288+
{"special", FieldValue::Boolean(true)},
289+
{"type", FieldValue::String("geo_point")},
290+
{"latitude", FieldValue::Double(gp.latitude())},
291+
{"longitude", FieldValue::Double(gp.longitude())}};
292+
return ConvertRegularMap(as_map);
293+
}
294+
295+
case FieldValue::Type::kReference: {
296+
DocumentReference ref = from.reference_value();
297+
std::string path = ref.path();
298+
MapFieldValue as_map = {
299+
{"special", FieldValue::Boolean(true)},
300+
{"type", FieldValue::String("document_reference")},
301+
{"document_path", FieldValue::String(path)}};
302+
return ConvertRegularMap(as_map);
303+
}
304+
305+
// Firestore-specific sentinel values can also be encoded as maps.
306+
307+
case FieldValue::Type::kDelete: {
308+
// Note: if using the resulting `Variant` with RTDB, you might want to
309+
// convert a `delete` sentinel to null:
310+
// return Variant::Null();
311+
MapFieldValue as_map = {{"special", FieldValue::Boolean(true)},
312+
{"type", FieldValue::String("delete")}};
313+
return ConvertRegularMap(as_map);
314+
}
315+
316+
case FieldValue::Type::kServerTimestamp: {
317+
MapFieldValue as_map = {{"special", FieldValue::Boolean(true)},
318+
{"type", FieldValue::String("server_timestamp")}};
319+
// Note: if using the resulting `Variant` with RTDB, you might want to
320+
// convert the server timestamp to the representation used by RTDB:
321+
// MapFieldValue as_map = {{".sv", FieldValue::String("timestamp")}};
322+
return ConvertRegularMap(as_map);
323+
}
324+
325+
// Several Firestore sentinel values cannot be converted losslessly
326+
// (because they don't allow accessing the underlying value(s)). In this
327+
// example, we will simply assert that these values should never be given
328+
// to the converter.
329+
case FieldValue::Type::kArrayUnion:
330+
case FieldValue::Type::kArrayRemove:
331+
case FieldValue::Type::kIncrementInteger:
332+
case FieldValue::Type::kIncrementDouble:
333+
assert(false && "Unsupported FieldValue type");
334+
std::terminate();
335+
336+
default:
337+
assert(false && "Unknown FieldValue type");
338+
std::terminate();
339+
}
340+
}
341+
342+
Variant Converter::ConvertArray(const std::vector<FieldValue>& from) const {
343+
std::vector<Variant> result;
344+
result.reserve(from.size());
345+
346+
for (const auto& v : from) {
347+
result.push_back(ConvertFieldValueToVariant(v));
348+
}
349+
350+
return Variant(result);
351+
}
352+
353+
Variant Converter::ConvertMap(const MapFieldValue& from) const {
354+
// Firestore doesn't support nested arrays, so nested arrays are instead
355+
// encoded as an "array-map-array" structure. Make sure nested arrays
356+
// round-trip.
357+
bool is_special = TryGetBoolean(from, "special");
358+
if (is_special) {
359+
return ConvertSpecialValue(from);
360+
} else {
361+
return ConvertRegularMap(from);
362+
}
363+
}
364+
365+
Variant Converter::ConvertRegularMap(const MapFieldValue& from) const {
366+
std::map<Variant, Variant> result;
367+
368+
for (const auto& kv : from) {
369+
result[Variant(kv.first)] = ConvertFieldValueToVariant(kv.second);
370+
}
371+
372+
return Variant(result);
373+
}
374+
375+
Variant Converter::ConvertSpecialValue(const MapFieldValue& from) const {
376+
std::string type = TryGetString(from, "type");
377+
if (type.empty()) {
378+
return Variant();
379+
}
380+
381+
if (type == "nested_array") {
382+
// Unnest the array.
383+
return ConvertArray(TryGetArray(from, "value"));
384+
}
385+
386+
return Variant();
387+
}
388+
389+
} // namespace firestore
390+
} // namespace firebase

0 commit comments

Comments
 (0)