aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Firestore/Example/Firestore.xcodeproj/project.pbxproj8
-rw-r--r--Firestore/Example/Tests/Model/FSTPathTests.mm4
-rw-r--r--Firestore/core/src/firebase/firestore/model/CMakeLists.txt3
-rw-r--r--Firestore/core/src/firebase/firestore/model/base_path.h181
-rw-r--r--Firestore/core/src/firebase/firestore/model/field_path.cc176
-rw-r--r--Firestore/core/src/firebase/firestore/model/field_path.h94
-rw-r--r--Firestore/core/src/firebase/firestore/model/resource_path.cc57
-rw-r--r--Firestore/core/src/firebase/firestore/model/resource_path.h84
-rw-r--r--Firestore/core/src/firebase/firestore/util/firebase_assert.h4
-rw-r--r--Firestore/core/test/firebase/firestore/model/CMakeLists.txt2
-rw-r--r--Firestore/core/test/firebase/firestore/model/field_path_test.cc277
-rw-r--r--Firestore/core/test/firebase/firestore/model/resource_path_test.cc105
12 files changed, 995 insertions, 0 deletions
diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
index b475963..8d99eb6 100644
--- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj
+++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
@@ -144,6 +144,8 @@
ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; };
ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; };
AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; };
+ B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; };
+ B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; };
C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; };
DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; };
DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
@@ -338,6 +340,8 @@
AB7BAB332012B519001E0872 /* geo_point_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = geo_point_test.cc; path = ../../core/test/firebase/firestore/geo_point_test.cc; sourceTree = "<group>"; };
ABF6506B201131F8005F2C74 /* timestamp_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = timestamp_test.cc; sourceTree = "<group>"; };
B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ B686F2AD2023DDB20028D6BE /* field_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = field_path_test.cc; sourceTree = "<group>"; };
+ B686F2B02024FFD70028D6BE /* resource_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = resource_path_test.cc; sourceTree = "<group>"; };
CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = "<group>"; };
D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
D5B259DAA9149B80D6245B57 /* FSTTestDispatchQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTTestDispatchQueue.h; sourceTree = "<group>"; };
@@ -565,6 +569,8 @@
AB356EF5200E9D1A0089B766 /* model */ = {
isa = PBXGroup;
children = (
+ B686F2B02024FFD70028D6BE /* resource_path_test.cc */,
+ B686F2AD2023DDB20028D6BE /* field_path_test.cc */,
AB71064B201FA60300344F18 /* database_id_test.cc */,
AB356EF6200EA5EB0089B766 /* field_value_test.cc */,
ABF6506B201131F8005F2C74 /* timestamp_test.cc */,
@@ -1238,6 +1244,7 @@
5492E0A42021552D00B64F25 /* FSTMemoryQueryCacheTests.mm in Sources */,
5492E0A92021552D00B64F25 /* FSTRemoteDocumentChangeBufferTests.mm in Sources */,
54C2294F1FECABAE007D065B /* log_test.cc in Sources */,
+ B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */,
5492E0CA2021557E00B64F25 /* FSTWatchChangeTests.mm in Sources */,
5492E063202154B900B64F25 /* FSTViewSnapshotTest.mm in Sources */,
5492E0BC2021555100B64F25 /* FSTPathTests.mm in Sources */,
@@ -1247,6 +1254,7 @@
5492E0A82021552D00B64F25 /* FSTLevelDBLocalStoreTests.mm in Sources */,
5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */,
DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */,
+ B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */,
5492E03120213FFC00B64F25 /* FSTLevelDBSpecTests.mm in Sources */,
5492E0B12021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm in Sources */,
5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */,
diff --git a/Firestore/Example/Tests/Model/FSTPathTests.mm b/Firestore/Example/Tests/Model/FSTPathTests.mm
index b8529e5..68bcc44 100644
--- a/Firestore/Example/Tests/Model/FSTPathTests.mm
+++ b/Firestore/Example/Tests/Model/FSTPathTests.mm
@@ -173,6 +173,10 @@ NS_ASSUME_NONNULL_BEGIN
ASSERT_ROUND_TRIP(@"`.foo\\\\`", 1);
ASSERT_ROUND_TRIP(@"`.foo\\\\`.`.foo`", 2);
ASSERT_ROUND_TRIP(@"foo.`\\``.bar", 3);
+
+ FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:@"foo\\.bar"];
+ XCTAssertEqualObjects([path canonicalString], @"`foo.bar`");
+ XCTAssertEqual(path.length, 1);
}
#undef ASSERT_ROUND_TRIP
diff --git a/Firestore/core/src/firebase/firestore/model/CMakeLists.txt b/Firestore/core/src/firebase/firestore/model/CMakeLists.txt
index aee0d86..8bdbe18 100644
--- a/Firestore/core/src/firebase/firestore/model/CMakeLists.txt
+++ b/Firestore/core/src/firebase/firestore/model/CMakeLists.txt
@@ -15,8 +15,11 @@
cc_library(
firebase_firestore_model
SOURCES
+ base_path.h
database_id.cc
database_id.h
+ field_path.cc
+ field_path.h
field_value.cc
field_value.h
timestamp.cc
diff --git a/Firestore/core/src/firebase/firestore/model/base_path.h b/Firestore/core/src/firebase/firestore/model/base_path.h
new file mode 100644
index 0000000..f5a8ab7
--- /dev/null
+++ b/Firestore/core/src/firebase/firestore/model/base_path.h
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2018 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_BASE_PATH_H_
+#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_BASE_PATH_H_
+
+#include <algorithm>
+#include <cctype>
+#include <initializer_list>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+namespace impl {
+
+/**
+ * BasePath represents a path sequence in the Firestore database. It is composed
+ * of an ordered sequence of string segments.
+ *
+ * BasePath is reassignable and movable. Apart from those, all other mutating
+ * operations return new independent instances.
+ *
+ * ## Subclassing Notes
+ *
+ * BasePath is strictly meant as a base class for concrete implementations. It
+ * doesn't contain a single virtual method, can't be instantiated, and should
+ * never be used in any polymorphic way. BasePath is templated to allow static
+ * factory methods to return objects of the derived class (the expected
+ * inheritance would use CRTP: struct Derived : BasePath<Derived>).
+ */
+template <typename T>
+class BasePath {
+ protected:
+ using SegmentsT = std::vector<std::string>;
+
+ public:
+ using const_iterator = SegmentsT::const_iterator;
+
+ /** Returns i-th segment of the path. */
+ const std::string& operator[](const size_t i) const {
+ FIREBASE_ASSERT_MESSAGE(i < segments_.size(), "index %u out of range", i);
+ return segments_[i];
+ }
+
+ /** Returns the first segment of the path. */
+ const std::string& first_segment() const {
+ FIREBASE_ASSERT_MESSAGE(!empty(),
+ "Cannot call first_segment on empty path");
+ return segments_[0];
+ }
+ /** Returns the last segment of the path. */
+ const std::string& last_segment() const {
+ FIREBASE_ASSERT_MESSAGE(!empty(), "Cannot call last_segment on empty path");
+ return segments_[size() - 1];
+ }
+
+ size_t size() const {
+ return segments_.size();
+ }
+ bool empty() const {
+ return segments_.empty();
+ }
+
+ const_iterator begin() const {
+ return segments_.begin();
+ }
+ const_iterator end() const {
+ return segments_.end();
+ }
+
+ /**
+ * Returns a new path which is the result of concatenating this path with an
+ * additional segment.
+ */
+ T Append(const std::string& segment) const {
+ auto appended = segments_;
+ appended.push_back(segment);
+ return T{std::move(appended)};
+ }
+ T Append(std::string&& segment) const {
+ auto appended = segments_;
+ appended.push_back(std::move(segment));
+ return T{std::move(appended)};
+ }
+
+ /**
+ * Returns a new path which is the result of concatenating this path with an
+ * another path.
+ */
+ T Append(const T& path) const {
+ auto appended = segments_;
+ appended.insert(appended.end(), path.begin(), path.end());
+ return T{std::move(appended)};
+ }
+
+ /**
+ * Returns a new path which is the result of omitting the first n segments of
+ * this path.
+ */
+ T PopFirst(const size_t n = 1) const {
+ FIREBASE_ASSERT_MESSAGE(n <= size(),
+ "Cannot call PopFirst(%u) on path of length %u", n,
+ size());
+ return T{begin() + n, end()};
+ }
+
+ /**
+ * Returns a new path which is the result of omitting the last segment of
+ * this path.
+ */
+ T PopLast() const {
+ FIREBASE_ASSERT_MESSAGE(!empty(), "Cannot call PopLast() on empty path");
+ return T{begin(), end() - 1};
+ }
+
+ /**
+ * Returns true if this path is a prefix of the given path.
+ *
+ * Empty path is a prefix of any path. Any path is a prefix of itself.
+ */
+ bool IsPrefixOf(const T& rhs) const {
+ return size() <= rhs.size() && std::equal(begin(), end(), rhs.begin());
+ }
+
+ bool operator==(const BasePath& rhs) const {
+ return segments_ == rhs.segments_;
+ }
+ bool operator!=(const BasePath& rhs) const {
+ return segments_ != rhs.segments_;
+ }
+ bool operator<(const BasePath& rhs) const {
+ return segments_ < rhs.segments_;
+ }
+ bool operator>(const BasePath& rhs) const {
+ return segments_ > rhs.segments_;
+ }
+ bool operator<=(const BasePath& rhs) const {
+ return segments_ <= rhs.segments_;
+ }
+ bool operator>=(const BasePath& rhs) const {
+ return segments_ >= rhs.segments_;
+ }
+
+ protected:
+ BasePath() = default;
+ template <typename IterT>
+ BasePath(const IterT begin, const IterT end) : segments_{begin, end} {
+ }
+ BasePath(std::initializer_list<std::string> list) : segments_{list} {
+ }
+ BasePath(SegmentsT&& segments) : segments_{std::move(segments)} {
+ }
+
+ private:
+ SegmentsT segments_;
+};
+
+} // namespace impl
+} // namespace model
+} // namespace firestore
+} // namespace firebase
+
+#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_BASE_PATH_H_
diff --git a/Firestore/core/src/firebase/firestore/model/field_path.cc b/Firestore/core/src/firebase/firestore/model/field_path.cc
new file mode 100644
index 0000000..6c40600
--- /dev/null
+++ b/Firestore/core/src/firebase/firestore/model/field_path.cc
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2018 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Firestore/core/src/firebase/firestore/model/field_path.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/str_replace.h"
+#include "absl/strings/str_split.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+namespace {
+
+// TODO(varconst): move to C++ equivalent of FSTDocumentKey.{h,cc}
+const char* const kDocumentKeyPath = "__name__";
+
+/**
+ * True if the string could be used as a segment in a field path without
+ * escaping. Valid identifies follow the regex [a-zA-Z_][a-zA-Z0-9_]*
+ */
+bool IsValidIdentifier(const std::string& segment) {
+ if (segment.empty()) {
+ return false;
+ }
+
+ // Note: strictly speaking, only digits are guaranteed by the Standard to
+ // be a contiguous range, while alphabetic characters may have gaps. Ignoring
+ // this peculiarity, because it doesn't affect the platforms that Firestore
+ // supports.
+ const unsigned char first = segment.front();
+ if (first != '_' && (first < 'a' || first > 'z') &&
+ (first < 'A' || first > 'Z')) {
+ return false;
+ }
+ for (int i = 1; i != segment.size(); ++i) {
+ const unsigned char c = segment[i];
+ if (c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') &&
+ (c < '0' || c > '9')) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace
+
+FieldPath FieldPath::FromServerFormat(const absl::string_view path) {
+ // TODO(b/37244157): Once we move to v1beta1, we should make this more
+ // strict. Right now, it allows non-identifier path components, even if they
+ // aren't escaped. Technically, this will mangle paths with backticks in
+ // them used in v1alpha1, but that's fine.
+
+ SegmentsT segments;
+ std::string segment;
+ segment.reserve(path.size());
+
+ // string_view doesn't have a c_str() method, because it might not be
+ // null-terminated. Assertions expect C strings, so construct std::string on
+ // the fly, so that c_str() might be called on it.
+ const auto to_string = [](const absl::string_view view) {
+ return std::string{view.data(), view.data() + view.size()};
+ };
+ const auto finish_segment = [&segments, &segment, &path, &to_string] {
+ FIREBASE_ASSERT_MESSAGE(
+ !segment.empty(),
+ "Invalid field path (%s). Paths must not be empty, begin with "
+ "'.', end with '.', or contain '..'",
+ to_string(path).c_str());
+ // Move operation will clear segment, but capacity will remain the same
+ // (not, strictly speaking, required by the standard, but true in practice).
+ segments.push_back(std::move(segment));
+ };
+
+ // Inside backticks, dots are treated literally.
+ bool inside_backticks = false;
+ int i = 0;
+ while (i < path.size()) {
+ const char c = path[i];
+ // std::string (and string_view) may contain embedded nulls. For full
+ // compatibility with Objective C behavior, finish upon encountering the
+ // first terminating null.
+ if (c == '\0') {
+ break;
+ }
+
+ switch (c) {
+ case '.':
+ if (!inside_backticks) {
+ finish_segment();
+ } else {
+ segment += c;
+ }
+ break;
+
+ case '`':
+ inside_backticks = !inside_backticks;
+ break;
+
+ case '\\':
+ // TODO(b/37244157): Make this a user-facing exception once we
+ // finalize field escaping.
+ FIREBASE_ASSERT_MESSAGE(i + 1 != path.size(),
+ "Trailing escape characters not allowed in %s",
+ to_string(path).c_str());
+ ++i;
+ segment += path[i];
+ break;
+
+ default:
+ segment += c;
+ break;
+ }
+ ++i;
+ }
+ finish_segment();
+
+ FIREBASE_ASSERT_MESSAGE(!inside_backticks, "Unterminated ` in path %s",
+ to_string(path).c_str());
+
+ return FieldPath{std::move(segments)};
+}
+
+const FieldPath& FieldPath::EmptyPath() {
+ static const FieldPath empty_path;
+ return empty_path;
+}
+
+const FieldPath& FieldPath::KeyFieldPath() {
+ static const FieldPath key_field_path{kDocumentKeyPath};
+ return key_field_path;
+}
+
+bool FieldPath::IsKeyFieldPath() const {
+ return size() == 1 && first_segment() == kDocumentKeyPath;
+}
+
+std::string FieldPath::CanonicalString() const {
+ const auto escaped_segment = [](const std::string& segment) {
+ auto escaped = absl::StrReplaceAll(segment, {{"\\", "\\\\"}, {"`", "\\`"}});
+ const bool needs_escaping = !IsValidIdentifier(escaped);
+ if (needs_escaping) {
+ escaped.insert(escaped.begin(), '`');
+ escaped.push_back('`');
+ }
+ return escaped;
+ };
+ return absl::StrJoin(
+ begin(), end(), ".",
+ [escaped_segment](std::string* out, const std::string& segment) {
+ out->append(escaped_segment(segment));
+ });
+}
+
+} // namespace model
+} // namespace firestore
+} // namespace firebase
diff --git a/Firestore/core/src/firebase/firestore/model/field_path.h b/Firestore/core/src/firebase/firestore/model/field_path.h
new file mode 100644
index 0000000..a8b147e
--- /dev/null
+++ b/Firestore/core/src/firebase/firestore/model/field_path.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_FIELD_PATH_H_
+#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_FIELD_PATH_H_
+
+#include <initializer_list>
+#include <string>
+#include <utility>
+
+#include "Firestore/core/src/firebase/firestore/model/base_path.h"
+#include "absl/strings/string_view.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+/**
+ * A dot-separated path for navigating sub-objects within a document.
+ *
+ * Immutable; all instances are fully independent.
+ */
+class FieldPath : public impl::BasePath<FieldPath> {
+ public:
+ FieldPath() = default;
+ /** Constructs the path from segments. */
+ template <typename IterT>
+ FieldPath(const IterT begin, const IterT end) : BasePath{begin, end} {
+ }
+ FieldPath(std::initializer_list<std::string> list) : BasePath{list} {
+ }
+
+ /**
+ * Creates and returns a new path from the server formatted field-path string,
+ * where path segments are separated by a dot "." and optionally encoded using
+ * backticks.
+ */
+ static FieldPath FromServerFormat(absl::string_view path);
+ /** Returns a field path that represents an empty path. */
+ static const FieldPath& EmptyPath();
+ /** Returns a field path that represents a document key. */
+ static const FieldPath& KeyFieldPath();
+
+ /** Returns a standardized string representation of this path. */
+ std::string CanonicalString() const;
+ /** True if this FieldPath represents a document key. */
+ bool IsKeyFieldPath() const;
+
+ bool operator==(const FieldPath& rhs) const {
+ return BasePath::operator==(rhs);
+ }
+ bool operator!=(const FieldPath& rhs) const {
+ return BasePath::operator!=(rhs);
+ }
+ bool operator<(const FieldPath& rhs) const {
+ return BasePath::operator<(rhs);
+ }
+ bool operator>(const FieldPath& rhs) const {
+ return BasePath::operator>(rhs);
+ }
+ bool operator<=(const FieldPath& rhs) const {
+ return BasePath::operator<=(rhs);
+ }
+ bool operator>=(const FieldPath& rhs) const {
+ return BasePath::operator>=(rhs);
+ }
+
+ private:
+ FieldPath(SegmentsT&& segments) : BasePath{std::move(segments)} {
+ }
+
+ // So that methods of base can construct FieldPath using the private
+ // constructor.
+ friend class BasePath;
+};
+
+} // namespace model
+} // namespace firestore
+} // namespace firebase
+
+#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_FIELD_PATH_H_
diff --git a/Firestore/core/src/firebase/firestore/model/resource_path.cc b/Firestore/core/src/firebase/firestore/model/resource_path.cc
new file mode 100644
index 0000000..36218e9
--- /dev/null
+++ b/Firestore/core/src/firebase/firestore/model/resource_path.cc
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/str_split.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+ResourcePath ResourcePath::Parse(const absl::string_view path) {
+ // NOTE: The client is ignorant of any path segments containing escape
+ // sequences (e.g. __id123__) and just passes them through raw (they exist
+ // for legacy reasons and should not be used frequently).
+
+ FIREBASE_ASSERT_MESSAGE(
+ path.find("//") == std::string::npos,
+ "Invalid path (%s). Paths must not contain // in them.",
+ std::string{path.data(), path.data() + path.size()}.c_str());
+
+ // SkipEmpty because we may still have an empty segment at the beginning or
+ // end if they had a leading or trailing slash (which we allow).
+ std::vector<std::string> segments =
+ absl::StrSplit(path, '/', absl::SkipEmpty());
+ return ResourcePath{std::move(segments)};
+}
+
+std::string ResourcePath::CanonicalString() const {
+ // NOTE: The client is ignorant of any path segments containing escape
+ // sequences (e.g. __id123__) and just passes them through raw (they exist
+ // for legacy reasons and should not be used frequently).
+
+ return absl::StrJoin(begin(), end(), "/");
+}
+
+} // namespace model
+} // namespace firestore
+} // namespace firebase
diff --git a/Firestore/core/src/firebase/firestore/model/resource_path.h b/Firestore/core/src/firebase/firestore/model/resource_path.h
new file mode 100644
index 0000000..481d32f
--- /dev/null
+++ b/Firestore/core/src/firebase/firestore/model/resource_path.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_RESOURCE_PATH_H_
+#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_RESOURCE_PATH_H_
+
+#include <initializer_list>
+#include <string>
+
+#include "Firestore/core/src/firebase/firestore/model/base_path.h"
+#include "absl/strings/string_view.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+/**
+ * A slash-separated path for navigating resources (documents and collections)
+ * within Firestore. Immutable; all instances are fully independent.
+ */
+class ResourcePath : public impl::BasePath<ResourcePath> {
+ public:
+ ResourcePath() = default;
+ /** Constructs the path from segments. */
+ template <typename IterT>
+ ResourcePath(const IterT begin, const IterT end) : BasePath{begin, end} {
+ }
+ ResourcePath(std::initializer_list<std::string> list) : BasePath{list} {
+ }
+ /**
+ * Creates and returns a new path from the given resource-path string, where
+ * the path segments are separated by a slash "/".
+ */
+ static ResourcePath Parse(absl::string_view path);
+
+ /** Returns a standardized string representation of this path. */
+ std::string CanonicalString() const;
+
+ bool operator==(const ResourcePath& rhs) const {
+ return BasePath::operator==(rhs);
+ }
+ bool operator!=(const ResourcePath& rhs) const {
+ return BasePath::operator!=(rhs);
+ }
+ bool operator<(const ResourcePath& rhs) const {
+ return BasePath::operator<(rhs);
+ }
+ bool operator>(const ResourcePath& rhs) const {
+ return BasePath::operator>(rhs);
+ }
+ bool operator<=(const ResourcePath& rhs) const {
+ return BasePath::operator<=(rhs);
+ }
+ bool operator>=(const ResourcePath& rhs) const {
+ return BasePath::operator>=(rhs);
+ }
+
+ private:
+ ResourcePath(SegmentsT&& segments) : BasePath{std::move(segments)} {
+ }
+
+ // So that methods of base can construct ResourcePath using the private
+ // constructor.
+ friend class BasePath;
+};
+
+} // namespace model
+} // namespace firestore
+} // namespace firebase
+
+#endif
diff --git a/Firestore/core/src/firebase/firestore/util/firebase_assert.h b/Firestore/core/src/firebase/firestore/util/firebase_assert.h
index cff550a..993f27a 100644
--- a/Firestore/core/src/firebase/firestore/util/firebase_assert.h
+++ b/Firestore/core/src/firebase/firestore/util/firebase_assert.h
@@ -75,6 +75,10 @@
} \
} while (0)
+// Assert with custom message that is not compiled out in release builds.
+#define FIREBASE_ASSERT_MESSAGE(expression, ...) \
+ FIREBASE_ASSERT_MESSAGE_WITH_EXPRESSION(expression, expression, __VA_ARGS__)
+
// Assert condition is true otherwise display the specified expression,
// message and abort. Compiled out of release builds.
#if defined(NDEBUG)
diff --git a/Firestore/core/test/firebase/firestore/model/CMakeLists.txt b/Firestore/core/test/firebase/firestore/model/CMakeLists.txt
index 0f83bf2..63ed813 100644
--- a/Firestore/core/test/firebase/firestore/model/CMakeLists.txt
+++ b/Firestore/core/test/firebase/firestore/model/CMakeLists.txt
@@ -16,8 +16,10 @@ cc_test(
firebase_firestore_model_test
SOURCES
database_id_test.cc
+ field_path_test.cc
field_value_test.cc
timestamp_test.cc
+ resource_path_test.cc
DEPENDS
firebase_firestore_model
)
diff --git a/Firestore/core/test/firebase/firestore/model/field_path_test.cc b/Firestore/core/test/firebase/firestore/model/field_path_test.cc
new file mode 100644
index 0000000..7c7e0a3
--- /dev/null
+++ b/Firestore/core/test/firebase/firestore/model/field_path_test.cc
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2018 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Firestore/core/src/firebase/firestore/model/field_path.h"
+
+#include <initializer_list>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+TEST(FieldPath, Constructors) {
+ const FieldPath empty_path;
+ EXPECT_TRUE(empty_path.empty());
+ EXPECT_EQ(0, empty_path.size());
+ EXPECT_TRUE(empty_path.begin() == empty_path.end());
+
+ const FieldPath path_from_list = {"rooms", "Eros", "messages"};
+ EXPECT_FALSE(path_from_list.empty());
+ EXPECT_EQ(3, path_from_list.size());
+ EXPECT_TRUE(path_from_list.begin() + 3 == path_from_list.end());
+
+ std::vector<std::string> segments{"rooms", "Eros", "messages"};
+ const FieldPath path_from_segments{segments.begin(), segments.end()};
+ EXPECT_FALSE(path_from_segments.empty());
+ EXPECT_EQ(3, path_from_segments.size());
+ EXPECT_TRUE(path_from_segments.begin() + 3 == path_from_segments.end());
+
+ FieldPath copied = path_from_list;
+ EXPECT_EQ(path_from_list, copied);
+ const FieldPath moved = std::move(copied);
+ EXPECT_EQ(path_from_list, moved);
+ EXPECT_NE(copied, moved);
+ EXPECT_EQ(empty_path, copied);
+}
+
+TEST(FieldPath, Indexing) {
+ const FieldPath path{"rooms", "Eros", "messages"};
+
+ EXPECT_EQ(path.first_segment(), "rooms");
+ EXPECT_EQ(path[0], "rooms");
+
+ EXPECT_EQ(path[1], "Eros");
+
+ EXPECT_EQ(path[2], "messages");
+ EXPECT_EQ(path.last_segment(), "messages");
+}
+
+TEST(FieldPath, PopFirst) {
+ const FieldPath abc{"rooms", "Eros", "messages"};
+ const FieldPath bc{"Eros", "messages"};
+ const FieldPath c{"messages"};
+ const FieldPath empty;
+ const FieldPath abc_dupl{"rooms", "Eros", "messages"};
+
+ EXPECT_NE(empty, c);
+ EXPECT_NE(c, bc);
+ EXPECT_NE(bc, abc);
+
+ EXPECT_EQ(bc, abc.PopFirst());
+ EXPECT_EQ(c, abc.PopFirst(2));
+ EXPECT_EQ(empty, abc.PopFirst(3));
+ EXPECT_EQ(abc_dupl, abc);
+}
+
+TEST(FieldPath, PopLast) {
+ const FieldPath abc{"rooms", "Eros", "messages"};
+ const FieldPath ab{"rooms", "Eros"};
+ const FieldPath a{"rooms"};
+ const FieldPath empty;
+ const FieldPath abc_dupl{"rooms", "Eros", "messages"};
+
+ EXPECT_EQ(ab, abc.PopLast());
+ EXPECT_EQ(a, abc.PopLast().PopLast());
+ EXPECT_EQ(empty, abc.PopLast().PopLast().PopLast());
+}
+
+TEST(FieldPath, Concatenation) {
+ const FieldPath path;
+ const FieldPath a{"rooms"};
+ const FieldPath ab{"rooms", "Eros"};
+ const FieldPath abc{"rooms", "Eros", "messages"};
+
+ EXPECT_EQ(a, path.Append("rooms"));
+ EXPECT_EQ(ab, path.Append("rooms").Append("Eros"));
+ EXPECT_EQ(abc, path.Append("rooms").Append("Eros").Append("messages"));
+ EXPECT_EQ(abc, path.Append(FieldPath{"rooms", "Eros", "messages"}));
+ EXPECT_EQ(abc, path.Append({"rooms", "Eros", "messages"}));
+
+ const FieldPath bcd{"Eros", "messages", "this_week"};
+ EXPECT_EQ(bcd, abc.PopFirst().Append("this_week"));
+}
+
+TEST(FieldPath, Comparison) {
+ const FieldPath abc{"a", "b", "c"};
+ const FieldPath abc2{"a", "b", "c"};
+ const FieldPath xyz{"x", "y", "z"};
+ EXPECT_EQ(abc, abc2);
+ EXPECT_NE(abc, xyz);
+
+ const FieldPath empty;
+ const FieldPath a{"a"};
+ const FieldPath b{"b"};
+ const FieldPath ab{"a", "b"};
+
+ EXPECT_TRUE(empty < a);
+ EXPECT_TRUE(a < b);
+ EXPECT_TRUE(a < ab);
+
+ EXPECT_TRUE(a > empty);
+ EXPECT_TRUE(b > a);
+ EXPECT_TRUE(ab > a);
+}
+
+TEST(FieldPath, IsPrefixOf) {
+ const FieldPath empty;
+ const FieldPath a{"a"};
+ const FieldPath ab{"a", "b"};
+ const FieldPath abc{"a", "b", "c"};
+ const FieldPath b{"b"};
+ const FieldPath ba{"b", "a"};
+
+ EXPECT_TRUE(empty.IsPrefixOf(empty));
+ EXPECT_TRUE(empty.IsPrefixOf(a));
+ EXPECT_TRUE(empty.IsPrefixOf(ab));
+ EXPECT_TRUE(empty.IsPrefixOf(abc));
+ EXPECT_TRUE(empty.IsPrefixOf(b));
+ EXPECT_TRUE(empty.IsPrefixOf(ba));
+
+ EXPECT_FALSE(a.IsPrefixOf(empty));
+ EXPECT_TRUE(a.IsPrefixOf(a));
+ EXPECT_TRUE(a.IsPrefixOf(ab));
+ EXPECT_TRUE(a.IsPrefixOf(abc));
+ EXPECT_FALSE(a.IsPrefixOf(b));
+ EXPECT_FALSE(a.IsPrefixOf(ba));
+
+ EXPECT_FALSE(ab.IsPrefixOf(empty));
+ EXPECT_FALSE(ab.IsPrefixOf(a));
+ EXPECT_TRUE(ab.IsPrefixOf(ab));
+ EXPECT_TRUE(ab.IsPrefixOf(abc));
+ EXPECT_FALSE(ab.IsPrefixOf(b));
+ EXPECT_FALSE(ab.IsPrefixOf(ba));
+
+ EXPECT_FALSE(abc.IsPrefixOf(empty));
+ EXPECT_FALSE(abc.IsPrefixOf(a));
+ EXPECT_FALSE(abc.IsPrefixOf(ab));
+ EXPECT_TRUE(abc.IsPrefixOf(abc));
+ EXPECT_FALSE(abc.IsPrefixOf(b));
+ EXPECT_FALSE(abc.IsPrefixOf(ba));
+}
+
+TEST(FieldPath, AccessFailures) {
+ const FieldPath path;
+ ASSERT_DEATH_IF_SUPPORTED(path.first_segment(), "");
+ ASSERT_DEATH_IF_SUPPORTED(path.last_segment(), "");
+ ASSERT_DEATH_IF_SUPPORTED(path[0], "");
+ ASSERT_DEATH_IF_SUPPORTED(path[1], "");
+ ASSERT_DEATH_IF_SUPPORTED(path.PopFirst(), "");
+ ASSERT_DEATH_IF_SUPPORTED(path.PopFirst(2), "");
+ ASSERT_DEATH_IF_SUPPORTED(path.PopLast(), "");
+}
+
+TEST(FieldPath, Parsing) {
+ const auto parse = [](const std::pair<std::string, size_t> expected) {
+ const auto path = FieldPath::FromServerFormat(expected.first);
+ return std::make_pair(path.CanonicalString(), path.size());
+ };
+ const auto make_expected = [](const std::string& str, const size_t size) {
+ return std::make_pair(str, size);
+ };
+
+ auto expected = make_expected("foo", 1);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected("foo.bar", 2);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected("foo.bar.baz", 3);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected(R"(`.foo\\`)", 1);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected(R"(`.foo\\`.`.foo`)", 2);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected(R"(foo.`\``.bar)", 3);
+ EXPECT_EQ(expected, parse(expected));
+
+ const auto path_with_dot = FieldPath::FromServerFormat(R"(foo\.bar)");
+ EXPECT_EQ(path_with_dot.CanonicalString(), "`foo.bar`");
+ EXPECT_EQ(path_with_dot.size(), 1);
+}
+
+// This is a special case in C++: std::string may contain embedded nulls. To
+// fully mimic behavior of Objective-C code, parsing must terminate upon
+// encountering the first null terminator in the string.
+TEST(FieldPath, ParseEmbeddedNull) {
+ std::string str{"foo"};
+ str += '\0';
+ str += ".bar";
+
+ const auto path = FieldPath::FromServerFormat(str);
+ EXPECT_EQ(path.size(), 1);
+ EXPECT_EQ(path.CanonicalString(), "foo");
+}
+
+TEST(FieldPath, ParseFailures) {
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat(""), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat("."), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat(".."), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat("foo."), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat(".bar"), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat("foo..bar"), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat(R"(foo\)"), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat(R"(foo.\)"), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat("foo`"), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat("foo```"), "");
+ ASSERT_DEATH_IF_SUPPORTED(FieldPath::FromServerFormat("`foo"), "");
+}
+
+TEST(FieldPath, CanonicalStringOfSubstring) {
+ const auto path = FieldPath::FromServerFormat("foo.bar.baz");
+ EXPECT_EQ(path.CanonicalString(), "foo.bar.baz");
+ EXPECT_EQ(path.PopFirst().CanonicalString(), "bar.baz");
+ EXPECT_EQ(path.PopLast().CanonicalString(), "foo.bar");
+ EXPECT_EQ(path.PopFirst().PopLast().CanonicalString(), "bar");
+ EXPECT_EQ(path.PopFirst().PopLast().CanonicalString(), "bar");
+ EXPECT_EQ(path.PopLast().PopFirst().PopLast().CanonicalString(), "");
+}
+
+TEST(FieldPath, CanonicalStringEscaping) {
+ // Should be escaped
+ EXPECT_EQ(FieldPath::FromServerFormat("1").CanonicalString(), "`1`");
+ EXPECT_EQ(FieldPath::FromServerFormat("1ab").CanonicalString(), "`1ab`");
+ EXPECT_EQ(FieldPath::FromServerFormat("ab!").CanonicalString(), "`ab!`");
+ EXPECT_EQ(FieldPath::FromServerFormat("/ab").CanonicalString(), "`/ab`");
+ EXPECT_EQ(FieldPath::FromServerFormat("a#b").CanonicalString(), "`a#b`");
+
+ // Should not be escaped
+ EXPECT_EQ(FieldPath::FromServerFormat("_ab").CanonicalString(), "_ab");
+ EXPECT_EQ(FieldPath::FromServerFormat("a1").CanonicalString(), "a1");
+ EXPECT_EQ(FieldPath::FromServerFormat("a_").CanonicalString(), "a_");
+}
+
+TEST(FieldPath, EmptyPath) {
+ const auto& empty_path = FieldPath::EmptyPath();
+ EXPECT_EQ(empty_path, FieldPath{empty_path});
+ EXPECT_EQ(empty_path, FieldPath{});
+ EXPECT_EQ(&empty_path, &FieldPath::EmptyPath());
+}
+
+TEST(FieldPath, KeyFieldPath) {
+ const auto& key_field_path = FieldPath::KeyFieldPath();
+ EXPECT_EQ(key_field_path, FieldPath{key_field_path});
+ EXPECT_EQ(key_field_path,
+ FieldPath::FromServerFormat(key_field_path.CanonicalString()));
+ EXPECT_EQ(&key_field_path, &FieldPath::KeyFieldPath());
+ EXPECT_NE(key_field_path, FieldPath::FromServerFormat(
+ key_field_path.CanonicalString().substr(1)));
+}
+
+} // namespace model
+} // namespace firestore
+} // namespace firebase
diff --git a/Firestore/core/test/firebase/firestore/model/resource_path_test.cc b/Firestore/core/test/firebase/firestore/model/resource_path_test.cc
new file mode 100644
index 0000000..317a1db
--- /dev/null
+++ b/Firestore/core/test/firebase/firestore/model/resource_path_test.cc
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2018 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Firestore/core/src/firebase/firestore/model/resource_path.h"
+
+#include <initializer_list>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+namespace firebase {
+namespace firestore {
+namespace model {
+
+TEST(ResourcePath, Constructor) {
+ const ResourcePath empty_path;
+ EXPECT_TRUE(empty_path.empty());
+ EXPECT_EQ(0, empty_path.size());
+ EXPECT_TRUE(empty_path.begin() == empty_path.end());
+
+ const ResourcePath path_from_list{{"rooms", "Eros", "messages"}};
+ EXPECT_FALSE(path_from_list.empty());
+ EXPECT_EQ(3, path_from_list.size());
+ EXPECT_TRUE(path_from_list.begin() + 3 == path_from_list.end());
+
+ std::vector<std::string> segments{"rooms", "Eros", "messages"};
+ const ResourcePath path_from_segments{segments.begin(), segments.end()};
+ EXPECT_FALSE(path_from_segments.empty());
+ EXPECT_EQ(3, path_from_segments.size());
+ EXPECT_TRUE(path_from_segments.begin() + 3 == path_from_segments.end());
+
+ ResourcePath copied = path_from_list;
+ EXPECT_EQ(path_from_list, copied);
+ const ResourcePath moved = std::move(copied);
+ EXPECT_EQ(path_from_list, moved);
+ EXPECT_NE(copied, moved);
+ EXPECT_EQ(empty_path, copied);
+}
+
+TEST(ResourcePath, Comparison) {
+ const ResourcePath abc{"a", "b", "c"};
+ const ResourcePath abc2{"a", "b", "c"};
+ const ResourcePath xyz{"x", "y", "z"};
+ EXPECT_EQ(abc, abc2);
+ EXPECT_NE(abc, xyz);
+
+ const ResourcePath empty;
+ const ResourcePath a{"a"};
+ const ResourcePath b{"b"};
+ const ResourcePath ab{"a", "b"};
+
+ EXPECT_TRUE(empty < a);
+ EXPECT_TRUE(a < b);
+ EXPECT_TRUE(a < ab);
+
+ EXPECT_TRUE(a > empty);
+ EXPECT_TRUE(b > a);
+ EXPECT_TRUE(ab > a);
+}
+
+TEST(ResourcePath, Parsing) {
+ const auto parse = [](const std::pair<std::string, size_t> expected) {
+ const auto path = ResourcePath::Parse(expected.first);
+ return std::make_pair(path.CanonicalString(), path.size());
+ };
+ const auto make_expected = [](const std::string& str, const size_t size) {
+ return std::make_pair(str, size);
+ };
+
+ auto expected = make_expected("", 0);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected("foo", 1);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected("foo/bar", 2);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected("foo/bar/baz", 3);
+ EXPECT_EQ(expected, parse(expected));
+ expected = make_expected(R"(foo/__!?#@..`..\`/baz)", 3);
+ EXPECT_EQ(expected, parse(expected));
+
+ EXPECT_EQ(ResourcePath::Parse("/foo/").CanonicalString(), "foo");
+}
+
+TEST(ResourcePath, ParseFailures) {
+ ASSERT_DEATH_IF_SUPPORTED(ResourcePath::Parse("//"), "");
+ ASSERT_DEATH_IF_SUPPORTED(ResourcePath::Parse("foo//bar"), "");
+}
+
+} // namespace model
+} // namespace firestore
+} // namespace firebase