From b31fe35eb7a1301e1e6c5d3381b9f3d8054734dd Mon Sep 17 00:00:00 2001 From: Konstantin Varlamov Date: Tue, 6 Feb 2018 19:15:54 -0500 Subject: C++ port: port FSTFieldPath and FSTResourcePath to C++ (#749) Similar to Objective-C, FieldPath and ResourcePath share most of their interface (and implementation) by deriving from BasePath (using CRTP, so that factory methods in BasePath can return an instance of the derived class). --- .../Example/Firestore.xcodeproj/project.pbxproj | 8 + Firestore/Example/Tests/Model/FSTPathTests.mm | 4 + .../src/firebase/firestore/model/CMakeLists.txt | 3 + .../core/src/firebase/firestore/model/base_path.h | 181 ++++++++++++++ .../src/firebase/firestore/model/field_path.cc | 176 +++++++++++++ .../core/src/firebase/firestore/model/field_path.h | 94 +++++++ .../src/firebase/firestore/model/resource_path.cc | 57 +++++ .../src/firebase/firestore/model/resource_path.h | 84 +++++++ .../src/firebase/firestore/util/firebase_assert.h | 4 + .../test/firebase/firestore/model/CMakeLists.txt | 2 + .../firebase/firestore/model/field_path_test.cc | 277 +++++++++++++++++++++ .../firebase/firestore/model/resource_path_test.cc | 105 ++++++++ 12 files changed, 995 insertions(+) create mode 100644 Firestore/core/src/firebase/firestore/model/base_path.h create mode 100644 Firestore/core/src/firebase/firestore/model/field_path.cc create mode 100644 Firestore/core/src/firebase/firestore/model/field_path.h create mode 100644 Firestore/core/src/firebase/firestore/model/resource_path.cc create mode 100644 Firestore/core/src/firebase/firestore/model/resource_path.h create mode 100644 Firestore/core/test/firebase/firestore/model/field_path_test.cc create mode 100644 Firestore/core/test/firebase/firestore/model/resource_path_test.cc 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 = ""; }; ABF6506B201131F8005F2C74 /* timestamp_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = timestamp_test.cc; sourceTree = ""; }; 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 = ""; }; + B686F2B02024FFD70028D6BE /* resource_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = resource_path_test.cc; sourceTree = ""; }; 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 = ""; }; D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; D5B259DAA9149B80D6245B57 /* FSTTestDispatchQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTTestDispatchQueue.h; sourceTree = ""; }; @@ -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 +#include +#include +#include +#include +#include + +#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). + */ +template +class BasePath { + protected: + using SegmentsT = std::vector; + + 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 + BasePath(const IterT begin, const IterT end) : segments_{begin, end} { + } + BasePath(std::initializer_list 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 +#include + +#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 +#include +#include + +#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 { + public: + FieldPath() = default; + /** Constructs the path from segments. */ + template + FieldPath(const IterT begin, const IterT end) : BasePath{begin, end} { + } + FieldPath(std::initializer_list 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 +#include + +#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 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 +#include + +#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 { + public: + ResourcePath() = default; + /** Constructs the path from segments. */ + template + ResourcePath(const IterT begin, const IterT end) : BasePath{begin, end} { + } + ResourcePath(std::initializer_list 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 +#include +#include + +#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 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 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 +#include +#include + +#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 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 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 -- cgit v1.2.3