From e12b997c6147663c692233a7e29a7433f4aee6f6 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Fri, 4 May 2018 15:21:22 -0400 Subject: [De]serialize DocumentKeys via the remote Serializer (#1212) --- .../src/firebase/firestore/remote/serializer.cc | 75 ++++++++++++++++++++++ .../src/firebase/firestore/remote/serializer.h | 39 ++++++----- .../firebase/firestore/remote/serializer_test.cc | 42 +++++++++++- 3 files changed, 140 insertions(+), 16 deletions(-) (limited to 'Firestore/core') diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.cc b/Firestore/core/src/firebase/firestore/remote/serializer.cc index 2a89515..e81ea2d 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.cc +++ b/Firestore/core/src/firebase/firestore/remote/serializer.cc @@ -25,14 +25,18 @@ #include #include "Firestore/Protos/nanopb/google/firestore/v1beta1/document.pb.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" namespace firebase { namespace firestore { namespace remote { +using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldValue; using firebase::firestore::model::ObjectValue; +using firebase::firestore::model::ResourcePath; using firebase::firestore::util::Status; using firebase::firestore::util::StatusOr; @@ -719,6 +723,64 @@ ObjectValue::Map DecodeObject(Reader* reader) { }); } +/** + * Creates the prefix for a fully qualified resource path, without a local path + * on the end. + */ +ResourcePath EncodeDatabaseId(const DatabaseId& database_id) { + return ResourcePath{"projects", database_id.project_id(), "databases", + database_id.database_id()}; +} + +/** + * Encodes a databaseId and resource path into the following form: + * /projects/$projectId/database/$databaseId/documents/$path + */ +std::string EncodeResourceName(const DatabaseId& database_id, + const ResourcePath& path) { + return EncodeDatabaseId(database_id) + .Append("documents") + .Append(path) + .CanonicalString(); +} + +/** + * Validates that a path has a prefix that looks like a valid encoded + * databaseId. + */ +bool IsValidResourceName(const ResourcePath& path) { + // Resource names have at least 4 components (project ID, database ID) + // and commonly the (root) resource type, e.g. documents + return path.size() >= 4 && path[0] == "projects" && path[2] == "databases"; +} + +/** + * Decodes a fully qualified resource name into a resource path and validates + * that there is a project and database encoded in the path. There are no + * guarantees that a local path is also encoded in this resource name. + */ +ResourcePath DecodeResourceName(absl::string_view encoded) { + ResourcePath resource = ResourcePath::FromString(encoded); + FIREBASE_ASSERT_MESSAGE(IsValidResourceName(resource), + "Tried to deserialize invalid key %s", + resource.CanonicalString().c_str()); + return resource; +} + +/** + * Decodes a fully qualified resource name into a resource path and validates + * that there is a project and database encoded in the path along with a local + * path. + */ +ResourcePath ExtractLocalPathFromResourceName( + const ResourcePath& resource_name) { + FIREBASE_ASSERT_MESSAGE( + resource_name.size() > 4 && resource_name[4] == "documents", + "Tried to deserialize invalid key %s", + resource_name.CanonicalString().c_str()); + return resource_name.PopFirst(5); +} + } // namespace Status Serializer::EncodeFieldValue(const FieldValue& field_value, @@ -739,6 +801,19 @@ StatusOr Serializer::DecodeFieldValue(const uint8_t* bytes, } } +std::string Serializer::EncodeKey(const DocumentKey& key) const { + return EncodeResourceName(database_id_, key.path()); +} + +DocumentKey Serializer::DecodeKey(absl::string_view name) const { + ResourcePath resource = DecodeResourceName(name); + FIREBASE_ASSERT_MESSAGE(resource[1] == database_id_.project_id(), + "Tried to deserialize key from different project."); + FIREBASE_ASSERT_MESSAGE(resource[3] == database_id_.database_id(), + "Tried to deserialize key from different database."); + return DocumentKey{ExtractLocalPathFromResourceName(resource)}; +} + } // namespace remote } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.h b/Firestore/core/src/firebase/firestore/remote/serializer.h index 7d13583..86aa6e2 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.h +++ b/Firestore/core/src/firebase/firestore/remote/serializer.h @@ -19,13 +19,17 @@ #include #include +#include #include +#include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "absl/base/attributes.h" +#include "absl/strings/string_view.h" namespace firebase { namespace firestore { @@ -46,20 +50,13 @@ namespace remote { // interpret." Adjust for C++. class Serializer { public: - Serializer() { + /** + * @param database_id Must remain valid for the lifetime of this Serializer + * object. + */ + explicit Serializer(const firebase::firestore::model::DatabaseId& database_id) + : database_id_(database_id) { } - // TODO(rsgowman): We eventually need the DatabaseId, but can't add it just - // yet since it's not used yet (which travis complains about). So for now, - // we'll create a parameterless ctor (above) that likely won't exist in the - // final version of this class. - ///** - // * @param database_id Must remain valid for the lifetime of this Serializer - // * object. - // */ - // explicit Serializer(const firebase::firestore::model::DatabaseId& - // database_id) - // : database_id_(database_id) { - //} /** * Converts the FieldValue model passed into bytes. @@ -101,9 +98,21 @@ class Serializer { return DecodeFieldValue(bytes.data(), bytes.size()); } + /** + * Encodes the given document key as a fully qualified name. This includes the + * databaseId associated with this Serializer and the key path. + */ + std::string EncodeKey( + const firebase::firestore::model::DocumentKey& key) const; + + /** + * Decodes the given document key from a fully qualified name. + */ + firebase::firestore::model::DocumentKey DecodeKey( + absl::string_view name) const; + private: - // TODO(rsgowman): We don't need the database_id_ yet (but will eventually). - // model::DatabaseId* database_id_; + const firebase::firestore::model::DatabaseId& database_id_; }; } // namespace remote diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc index ecfbf32..1dded05 100644 --- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc @@ -37,14 +37,17 @@ #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "google/protobuf/stubs/common.h" #include "google/protobuf/util/message_differencer.h" #include "gtest/gtest.h" using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::model::DatabaseId; using firebase::firestore::model::FieldValue; using firebase::firestore::model::ObjectValue; using firebase::firestore::remote::Serializer; +using firebase::firestore::testutil::Key; using firebase::firestore::util::Status; using firebase::firestore::util::StatusOr; using google::protobuf::util::MessageDifferencer; @@ -65,8 +68,10 @@ TEST(Serializer, CanLinkToNanopb) { // Fixture for running serializer tests. class SerializerTest : public ::testing::Test { public: - SerializerTest() : serializer(/*DatabaseId("p", "d")*/) { + SerializerTest() : serializer(kDatabaseId) { } + + const DatabaseId kDatabaseId{"p", "d"}; Serializer serializer; void ExpectRoundTrip(const FieldValue& model, @@ -425,5 +430,40 @@ TEST_F(SerializerTest, IncompleteTag) { Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } +TEST_F(SerializerTest, EncodesKey) { + EXPECT_EQ("projects/p/databases/d/documents", serializer.EncodeKey(Key(""))); + EXPECT_EQ("projects/p/databases/d/documents/one/two/three/four", + serializer.EncodeKey(Key("one/two/three/four"))); +} + +TEST_F(SerializerTest, DecodesKey) { + EXPECT_EQ(Key(""), serializer.DecodeKey("projects/p/databases/d/documents")); + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "projects/p/databases/d/documents/one/two/three/four")); + // Same, but with a leading slash + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "/projects/p/databases/d/documents/one/two/three/four")); +} + +TEST_F(SerializerTest, BadKey) { + std::vector bad_cases{ + "", // empty (and too short) + "projects/p", // too short + "projects/p/databases/d", // too short + "projects/p/databases/d/documents/odd_number_of_local_elements", + "projects_spelled_wrong/p/databases/d/documents", + "projects/p/databases_spelled_wrong/d/documents", + "projects/not_project_p/databases/d/documents", + "projects/p/databases/not_database_d/documents", + "projects/p/databases/d/not_documents", + }; + + for (const std::string& bad_key : bad_cases) { + EXPECT_ANY_THROW(serializer.DecodeKey(bad_key)); + } +} + // TODO(rsgowman): Test [en|de]coding multiple protos into the same output // vector. -- cgit v1.2.3