diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 29a0ab8dd64..f69fe1d848f 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -201,7 +201,9 @@ B67BF449216EB43000CA9097 /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */; }; 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 */; }; + B68B1E012213A765008977EF /* to_string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68B1E002213A764008977EF /* to_string_apple_test.mm */; }; B68FC0E521F6848700A7055C /* watch_change_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68FC0E421F6848700A7055C /* watch_change_test.mm */; }; + B696858E2214B53900271095 /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; }; B6BBE43121262CF400C6A53E /* grpc_stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */; }; B6D1B68520E2AB1B00B35856 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; }; B6D9649121544D4F00EB9CFB /* grpc_connection_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */; }; @@ -522,7 +524,9 @@ B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = create_noop_connectivity_monitor.cc; sourceTree = ""; }; 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 = ""; }; + B68B1E002213A764008977EF /* to_string_apple_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = to_string_apple_test.mm; sourceTree = ""; }; B68FC0E421F6848700A7055C /* watch_change_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = watch_change_test.mm; sourceTree = ""; }; + B696858D2214B53900271095 /* to_string_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = to_string_test.cc; sourceTree = ""; }; B69CF05A219B9105004C434D /* FIRFirestore+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FIRFirestore+Testing.h"; sourceTree = ""; }; B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = grpc_stream_test.cc; sourceTree = ""; }; B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = exponential_backoff_test.cc; sourceTree = ""; }; @@ -701,7 +705,6 @@ 546854A720A3681B004BDBD5 /* remote */ = { isa = PBXGroup; children = ( - B68FC0E421F6848700A7055C /* watch_change_test.mm */, 546854A820A36867004BDBD5 /* datastore_test.mm */, B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */, B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */, @@ -710,6 +713,7 @@ B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */, 61F72C5520BC48FD001A68CB /* serializer_test.cc */, B66D8995213609EE0086DA0C /* stream_test.mm */, + B68FC0E421F6848700A7055C /* watch_change_test.mm */, ); path = remote; sourceTree = ""; @@ -752,6 +756,8 @@ 54131E9620ADE678001DF3FF /* string_format_test.cc */, AB380CFC201A2EE200D97691 /* string_util_test.cc */, 79507DF8378D3C42F5B36268 /* string_win_test.cc */, + B68B1E002213A764008977EF /* to_string_apple_test.mm */, + B696858D2214B53900271095 /* to_string_test.cc */, 2A0CF41BA5AED6049B0BEB2C /* type_traits_apple_test.mm */, ); path = util; @@ -1678,8 +1684,6 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-macOS_example/Pods-macOS_example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC-macOS/openssl_grpc.framework", @@ -1692,8 +1696,6 @@ "${BUILT_PRODUCTS_DIR}/nanopb-macOS/nanopb.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl_grpc.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", @@ -1769,15 +1771,11 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-macOS_example-checkManifestLockResult.txt", ); @@ -1981,7 +1979,6 @@ 5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */, 5492E0BD2021555100B64F25 /* FSTDocumentTests.mm in Sources */, 5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */, - B68FC0E521F6848700A7055C /* watch_change_test.mm in Sources */, 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */, 5492E0BF2021555100B64F25 /* FSTFieldValueTests.mm in Sources */, 54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */, @@ -2099,11 +2096,14 @@ AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */, 54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */, ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, + B68B1E012213A765008977EF /* to_string_apple_test.mm in Sources */, + B696858E2214B53900271095 /* to_string_test.cc in Sources */, ABC1D7E12023A40C00BA84F0 /* token_test.cc in Sources */, 54A0352720A3AED0003E0143 /* transform_operations_test.mm in Sources */, 549CCA5120A36DBC00BCEB75 /* tree_sorted_map_test.cc in Sources */, C80B10E79CDD7EF7843C321E /* type_traits_apple_test.mm in Sources */, ABC1D7DE2023A05300BA84F0 /* user_test.cc in Sources */, + B68FC0E521F6848700A7055C /* watch_change_test.mm in Sources */, 544129DE21C2DDC800EFB9CC /* write.pb.cc in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.mm b/Firestore/Example/Tests/Model/FSTFieldValueTests.mm index 7fb501d83f1..45a07f2c194 100644 --- a/Firestore/Example/Tests/Model/FSTFieldValueTests.mm +++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.mm @@ -54,8 +54,9 @@ } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) { // We directly convert these here so that the databaseIDs can be different. FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value; - wrappedValue = [FSTReferenceValue referenceValue:reference.key - databaseID:reference.databaseID]; + wrappedValue = + [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:reference.key] + databaseID:reference.databaseID]; } else { wrappedValue = FSTTestFieldValue(value); } @@ -258,7 +259,7 @@ - (void)testWrapResourceNames { for (FSTDocumentKeyReference *value in values) { FSTFieldValue *wrapped = FSTTestFieldValue(value); XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]); - XCTAssertEqualObjects([wrapped value], value.key); + XCTAssertEqualObjects([wrapped value], [FSTDocumentKey keyWithDocumentKey:value.key]); XCTAssertTrue(*((FSTReferenceValue *)wrapped).databaseID == *(const DatabaseId *)(value.databaseID)); } @@ -459,7 +460,9 @@ - (void)testValueEquality { ], @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ], @[ - [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1") databaseID:&database_id], + [FSTReferenceValue + referenceValue:[FSTDocumentKey keyWithDocumentKey:FSTTestDocKey(@"coll/doc1")] + databaseID:&database_id], FSTTestFieldValue(FSTTestRef("project", DatabaseId::kDefault, @"coll/doc1")) ], @[ FSTTestRef("project", "(default)", @"coll/doc2") ], diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index e4d2f7066cd..624628cb753 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -470,11 +470,13 @@ - (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator "a valid document ID, but it was an empty string."); } ResourcePath path = self.query.path.Append([documentKey UTF8String]); - fieldValue = [FSTReferenceValue referenceValue:DocumentKey{path} - databaseID:self.firestore.databaseID]; + fieldValue = + [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:DocumentKey{path}] + databaseID:self.firestore.databaseID]; } else if ([value isKindOfClass:[FIRDocumentReference class]]) { FIRDocumentReference *ref = (FIRDocumentReference *)value; - fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; + fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:ref.key] + databaseID:self.firestore.databaseID]; } else { FSTThrowInvalidArgument(@"Invalid query. When querying by document ID you must provide a " "valid string or DocumentReference, but it was of type: %@", @@ -568,8 +570,9 @@ - (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)i // orders), multiple documents could match the position, yielding duplicate results. for (FSTSortOrder *sortOrder in self.query.sortOrders) { if (sortOrder.field == FieldPath::KeyFieldPath()) { - [components addObject:[FSTReferenceValue referenceValue:document.key - databaseID:self.firestore.databaseID]]; + [components addObject:[FSTReferenceValue + referenceValue:[FSTDocumentKey keyWithDocumentKey:document.key] + databaseID:self.firestore.databaseID]]; } else { FSTFieldValue *value = [document fieldForPath:sortOrder.field]; if (value != nil) { @@ -610,8 +613,9 @@ - (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isB @"Invalid query. Document ID '%@' contains a slash.", documentID); } const DocumentKey key{self.query.path.Append([documentID UTF8String])}; - [components addObject:[FSTReferenceValue referenceValue:key - databaseID:self.firestore.databaseID]]; + [components + addObject:[FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:key] + databaseID:self.firestore.databaseID]]; } else { FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; [components addObject:fieldValue]; diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm index 5b981f645f5..4b885c69769 100644 --- a/Firestore/Source/API/FSTUserDataConverter.mm +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -455,7 +455,8 @@ - (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(ParseCo self.databaseID->project_id().c_str(), self.databaseID->database_id().c_str(), context.FieldDescription().c_str()); } - return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; + return [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:reference.key] + databaseID:self.databaseID]; } else { FSTThrowInvalidArgument(@"Unsupported type: %@%s", NSStringFromClass([input class]), diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm index 86b9cde8303..4cf43322778 100644 --- a/Firestore/Source/Core/FSTView.mm +++ b/Firestore/Source/Core/FSTView.mm @@ -118,7 +118,7 @@ - (BOOL)isEqual:(id)other { - (NSUInteger)hash { NSUInteger hash = self.type; - hash = hash * 31u + [self.key hash]; + hash = hash * 31u + self.key.Hash(); return hash; } @@ -241,8 +241,8 @@ - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap newDoc = (FSTDocument *)maybeNewDoc; } if (newDoc) { - HARD_ASSERT(key == newDoc.key, "Mismatching key in document changes: %s != %s", key, - newDoc.key.ToString()); + HARD_ASSERT(key == newDoc.key, "Mismatching key in document changes: %s != %s", + key.ToString(), newDoc.key.ToString()); if (![self.query matchesDocument:newDoc]) { newDoc = nil; } diff --git a/Firestore/Source/Core/FSTViewSnapshot.mm b/Firestore/Source/Core/FSTViewSnapshot.mm index 6e7e59979b9..07c38b81441 100644 --- a/Firestore/Source/Core/FSTViewSnapshot.mm +++ b/Firestore/Source/Core/FSTViewSnapshot.mm @@ -97,7 +97,7 @@ - (NSString *)description { std::string result = absl::StrJoin( _changeMap, ",", [](std::string *out, const std::pair &kv) { - out->append(StringFormat("%s: %s", kv.first, kv.second)); + out->append(StringFormat("%s: %s", kv.first.ToString(), kv.second)); }); return WrapNSString(std::string{"{"} + result + "}"); } diff --git a/Firestore/Source/Model/FSTDocument.mm b/Firestore/Source/Model/FSTDocument.mm index 40f5666f641..5158ac7716e 100644 --- a/Firestore/Source/Model/FSTDocument.mm +++ b/Firestore/Source/Model/FSTDocument.mm @@ -153,7 +153,7 @@ - (BOOL)isEqual:(id)other { } - (NSUInteger)hash { - NSUInteger result = [self.key hash]; + NSUInteger result = self.key.Hash(); result = result * 31 + self.version.Hash(); result = result * 31 + [self.data hash]; result = result * 31 + _documentState; @@ -212,7 +212,7 @@ - (BOOL)isEqual:(id)other { } - (NSUInteger)hash { - NSUInteger result = [self.key hash]; + NSUInteger result = self.key.Hash(); result = result * 31 + self.version.Hash(); result = result * 31 + (_hasCommittedMutations ? 1 : 0); return result; @@ -250,7 +250,7 @@ - (BOOL)isEqual:(id)other { } - (NSUInteger)hash { - NSUInteger result = [self.key hash]; + NSUInteger result = self.key.Hash(); result = result * 31 + self.version.Hash(); return result; } diff --git a/Firestore/Source/Remote/FSTSerializerBeta.mm b/Firestore/Source/Remote/FSTSerializerBeta.mm index 93e6f115035..3e9b34d4912 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.mm +++ b/Firestore/Source/Remote/FSTSerializerBeta.mm @@ -355,7 +355,8 @@ - (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName { HARD_ASSERT(database_id == *self.databaseID, "Database %s:%s cannot encode reference from %s:%s", self.databaseID->project_id(), self.databaseID->database_id(), database_id.project_id(), database_id.database_id()); - return [FSTReferenceValue referenceValue:key databaseID:self.databaseID]; + return [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:key] + databaseID:self.databaseID]; } - (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue { diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_map.h b/Firestore/core/src/firebase/firestore/immutable/sorted_map.h index 21dcfd40ef3..7960f6588d3 100644 --- a/Firestore/core/src/firebase/firestore/immutable/sorted_map.h +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_map.h @@ -38,6 +38,8 @@ namespace immutable { template > class SortedMap : public impl::SortedMapBase { public: + using key_type = K; + using mapped_type = V; /** The type of the entries stored in the map. */ using value_type = std::pair; using array_type = impl::ArraySortedMap; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm index 9592cd724c6..9bfce73c0c4 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm @@ -30,6 +30,7 @@ #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/string_util.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" #include "absl/strings/match.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Firestore/core/src/firebase/firestore/model/document_key.h b/Firestore/core/src/firebase/firestore/model/document_key.h index a795e3ea90a..ab4e3373648 100644 --- a/Firestore/core/src/firebase/firestore/model/document_key.h +++ b/Firestore/core/src/firebase/firestore/model/document_key.h @@ -51,10 +51,6 @@ class DocumentKey { explicit DocumentKey(ResourcePath&& path); #if defined(__OBJC__) - operator FSTDocumentKey*() const { - return [FSTDocumentKey keyWithDocumentKey:*this]; - } - NSUInteger Hash() const { return util::Hash(ToString()); } diff --git a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt index 21180afa26d..d14b0dc283d 100644 --- a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt @@ -252,6 +252,7 @@ cc_library( range.h string_util.cc string_util.h + to_string.h type_traits.h DEPENDS absl_base diff --git a/Firestore/core/src/firebase/firestore/util/string_format.h b/Firestore/core/src/firebase/firestore/util/string_format.h index 01776a9de19..e76aeaeafee 100644 --- a/Firestore/core/src/firebase/firestore/util/string_format.h +++ b/Firestore/core/src/firebase/firestore/util/string_format.h @@ -53,7 +53,7 @@ struct FormatChoice<5> {}; * * Chooses a conversion to a text form in this order: * * If the value is exactly of `bool` type (without implicit conversions) - * the text will the "true" or "false". + * the text will be "true" or "false". * * If the value is of type `const char*`, the text will be the value * interpreted as a C string. To show the address of a single char or to * show the `const char*` as an address, cast to `void*`. diff --git a/Firestore/core/src/firebase/firestore/util/string_util.h b/Firestore/core/src/firebase/firestore/util/string_util.h index 46462d89c68..86acc56b241 100644 --- a/Firestore/core/src/firebase/firestore/util/string_util.h +++ b/Firestore/core/src/firebase/firestore/util/string_util.h @@ -65,23 +65,6 @@ std::string PrefixSuccessor(absl::string_view prefix); */ std::string ImmediateSuccessor(absl::string_view s); -/** - * Returns a string description of the contents of the given collection. - */ -template -std::string ToString(const Container& container) { - std::string result; - result.append("["); - const char* sep = ""; - for (auto&& item : container) { - result.append(sep); - result.append(item); - sep = ", "; - } - result.append("]"); - return result; -} - } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/to_string.h b/Firestore/core/src/firebase/firestore/util/to_string.h new file mode 100644 index 00000000000..9a989375fdb --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/to_string.h @@ -0,0 +1,187 @@ +/* + * Copyright 2019 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_UTIL_TO_STRING_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_TO_STRING_H_ + +#if __OBJC__ +#import +#endif // __OBJC__ + +#include +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "Firestore/core/src/firebase/firestore/util/string_format.h" +#include "Firestore/core/src/firebase/firestore/util/type_traits.h" +#include "absl/meta/type_traits.h" +#include "absl/strings/str_join.h" + +namespace firebase { +namespace firestore { +namespace util { + +/** + * Creates a human-readable description of the given `value`. The representation + * is loosely inspired by Python. + * + * The general idea is to create the description by using the most specific + * available function that creates a string representation of the class; for + * containers, do this recursively, adding some minimal container formatting to + * the output. + * + * Example: + * + * std::vector v{ + * DocumentKey({"foo/bar"}), + * DocumentKey({"this/that"}) + * }; + * assert(ToString(v) == "[foo/bar, this/that]"); + * + * std::map m{ + {1, "foo"}, + {2, "bar"} + * }; + * assert(ToString(m) == "{1: foo, 2: bar}"); + * + * The following algorithm is used: + * + * - if `value` defines a member function called `ToString`, the description is + * created by invoking the function; + * + * - (in Objective-C++ only) otherwise, if `value` is an Objective-C object, + * the description is created by calling `[value description]`and converting + * the result to an `std::string`; + * + * - otherwise, if `value` is an `std::string`, it's used as is; + * + * - otherwise, if `value` is an associative container (`std::map`, + * `std::unordered_map`, `f:f:immutable::SortedMap`, etc.), the description is + * of the form: + * + * {key1: value1, key2: value2} + * + * where the description of each key and value is created by running + * `ToString` recursively; + * + * - otherwise, if `value` is a container, the description is of the form: + * + * [element1, element2] + * + * where the description of each element is created by running `ToString` + * recursively; + * + * - otherwise, `std::to_string` is used as a fallback. If `std::to_string` is + * not defined for the class, a compilation error will be produced. + * + * Implementation notes: to rank different choices and avoid clashes (e.g., + * a type that is an associative container is also a (simple) container), tag + * dispatch is used. Each function in the chain either is tagged by + * `std::true_type` and can process the value, or is tagged by `std::false_type` + * and passes the value to the next function by the rank. When passing to the + * next function, some trait corresponding to the function is given in place of + * the tag; for example, `StringToString`, which can handle `std::string`s, is + * invoked with `std::is_same` as the tag. + */ +template +std::string ToString(const T& value); + +namespace impl { + +// Checks whether the given type `T` defines a member function `ToString` + +template > +struct has_to_string : std::false_type {}; + +template +struct has_to_string().ToString())>> + : std::true_type {}; + +template +struct ToStringChoice : ToStringChoice {}; + +template <> +struct ToStringChoice<5> {}; + +#if __OBJC__ + +// Objective-C class +template ::value>> +std::string ToStringImpl(T value, ToStringChoice<0>) { + return MakeString([value description]); +} + +#endif // __OBJC__ + +// Has `ToString` member function +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<1>) { + return value.ToString(); +} + +// `std::string` +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<2>) { + return value; +} + +// Associative container +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<3>) { + std::string contents = absl::StrJoin( + value, ", ", [](std::string* out, const typename T::value_type& kv) { + out->append(ToString(kv.first)); + out->append(": "); + out->append(ToString(kv.second)); + }); + return std::string{"{"} + contents + "}"; // NOLINT(whitespace/braces) +} + +// Container +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<4>) { + std::string contents = absl::StrJoin( + value, ", ", [](std::string* out, const typename T::value_type& element) { + out->append(ToString(element)); + }); + return std::string{"["} + contents + "]"; // NOLINT(whitespace/braces) +} + +// Fallback +template +std::string ToStringImpl(const T& value, ToStringChoice<5>) { + FormatArg arg{value}; + return std::string{arg.data(), arg.data() + arg.size()}; +} + +} // namespace impl + +template +std::string ToString(const T& value) { + return impl::ToStringImpl(value, impl::ToStringChoice<0>{}); +} + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_TO_STRING_H_ diff --git a/Firestore/core/src/firebase/firestore/util/type_traits.h b/Firestore/core/src/firebase/firestore/util/type_traits.h index 52feb6be377..fbec4179aaf 100644 --- a/Firestore/core/src/firebase/firestore/util/type_traits.h +++ b/Firestore/core/src/firebase/firestore/util/type_traits.h @@ -22,6 +22,9 @@ #endif #include +#include + +#include "absl/meta/type_traits.h" namespace firebase { namespace firestore { @@ -45,44 +48,32 @@ namespace util { * is_objective_c_pointer>::value == false */ template -struct is_objective_c_pointer { - private: - using yes_type = char (&)[10]; - using no_type = char (&)[1]; - - /** - * A non-existent function declared to produce a pointer to type T (which is - * consistent with the way Objective-C objects are referenced). - * - * Note that there is no definition for this function but that's okay because - * we only need it to reason about the function's type at compile type. - */ - static T Pointer(); - - static yes_type Choose(id value); - static no_type Choose(...); - - public: - using value_type = bool; - - enum { value = sizeof(Choose(Pointer())) == sizeof(yes_type) }; - - constexpr operator bool() const { - return value; - } - - constexpr bool operator()() const { - return value; - } -}; - -// Hard-code the answer for `void` because you can't pass arguments of type -// `void` to another function. -template <> -struct is_objective_c_pointer : public std::false_type {}; +using is_objective_c_pointer = std::is_convertible; #endif // __OBJC__ +// is_iterable + +template > +struct is_iterable : std::false_type {}; + +template +struct is_iterable< + T, + absl::void_t().begin(), std::declval().end())>> + : std::true_type {}; + +// is_associative_container + +template > +struct is_associative_container : std::false_type {}; + +template +struct is_associative_container< + T, + absl::void_t())>> + : std::true_type {}; + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/to_string_apple_test.mm b/Firestore/core/test/firebase/firestore/util/to_string_apple_test.mm new file mode 100644 index 00000000000..9d047825157 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/to_string_apple_test.mm @@ -0,0 +1,70 @@ +/* + * Copyright 2019 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. + */ + +#import + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/util/to_string.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +TEST(ToStringAppleTest, ObjCTypes) { + EXPECT_EQ(ToString(@123), "123"); + EXPECT_EQ(ToString(@"foo"), "foo"); + + NSArray* objc_array = @[ @1, @2, @3 ]; + EXPECT_EQ(ToString(objc_array), "(\n 1,\n 2,\n 3\n)"); +} + +TEST(ToStringAppleTest, Nested) { + using Nested = std::map*>; + Nested foo1{ + {100, @[ @1, @2, @3 ]}, + {200, @[ @4, @5, @6 ]}, + }; + Nested foo2{ + {300, @[ @3, @2, @1 ]}, + }; + std::map> nested{ + {"bar", std::vector{foo1}}, + {"baz", std::vector{foo2}}, + }; + std::string expected = R"!({bar: [{100: ( + 1, + 2, + 3 +), 200: ( + 4, + 5, + 6 +)}], baz: [{300: ( + 3, + 2, + 1 +)}]})!"; + EXPECT_EQ(ToString(nested), expected); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/to_string_test.cc b/Firestore/core/test/firebase/firestore/util/to_string_test.cc new file mode 100644 index 00000000000..5a030fe2139 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/to_string_test.cc @@ -0,0 +1,158 @@ +/* + * Copyright 2019 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 +#include +#include +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +using immutable::SortedMap; +using immutable::SortedSet; +using model::DocumentKey; + +TEST(ToStringTest, SimpleTypes) { + EXPECT_EQ(ToString(123), "123"); + EXPECT_EQ(ToString(1.5), "1.5"); + + EXPECT_EQ(ToString("foo"), "foo"); + EXPECT_EQ(ToString(std::string{"foo"}), "foo"); + + EXPECT_EQ(ToString(true), "true"); + + EXPECT_EQ(ToString(nullptr), "null"); + + void* ptr = reinterpret_cast(0xBAAAAAAD); + EXPECT_EQ(ToString(ptr), "baaaaaad"); +} + +TEST(ToStringTest, CustomToString) { + DocumentKey key({"rooms", "firestore"}); + EXPECT_EQ(ToString(key), "rooms/firestore"); +} + +TEST(ToStringTest, Container) { + std::vector keys{ + DocumentKey({"foo", "bar"}), + DocumentKey({"foo", "baz"}), + }; + EXPECT_EQ(ToString(keys), "[foo/bar, foo/baz]"); +} + +TEST(ToStringTest, StdMap) { + std::map key_map{ + {1, DocumentKey({"foo", "bar"})}, + {2, DocumentKey({"foo", "baz"})}, + }; + EXPECT_EQ(ToString(key_map), "{1: foo/bar, 2: foo/baz}"); +} + +TEST(ToStringTest, CustomMap) { + using MapT = SortedMap; + MapT sorted_map = MapT{}.insert(1, "foo").insert(2, "bar"); + EXPECT_EQ(ToString(sorted_map), "{1: foo, 2: bar}"); +} + +TEST(ToStringTest, CustomSet) { + using SetT = SortedSet; + SetT sorted_set = SetT{}.insert("foo").insert("bar"); + EXPECT_EQ(ToString(sorted_set), "[bar, foo]"); +} + +TEST(ToStringTest, MoreStdContainers) { + std::deque d{1, 2, 3, 4}; + EXPECT_EQ(ToString(d), "[1, 2, 3, 4]"); + + std::set s{5, 6, 7}; + EXPECT_EQ(ToString(s), "[5, 6, 7]"); + + // Multimap with the same duplicate element twice to avoid dealing with order. + std::unordered_multimap mm{{3, "abc"}, {3, "abc"}}; + EXPECT_EQ(ToString(mm), "{3: abc, 3: abc}"); +} + +TEST(ToStringTest, Nested) { + using Nested = std::map>; + Nested foo1{ + {100, {1, 2, 3}}, + {200, {4, 5, 6}}, + }; + Nested foo2{ + {300, {3, 2, 1}}, + }; + std::map> nested{ + {"bar", std::vector{foo1}}, + {"baz", std::vector{foo2}}, + }; + std::string expected = + "{bar: [{100: [1, 2, 3], 200: [4, 5, 6]}], " + "baz: [{300: [3, 2, 1]}]}"; + EXPECT_EQ(ToString(nested), expected); +} + +class Foo {}; +std::string ToString(const Foo&) { + return "Foo"; +} + +TEST(ToStringTest, FreeFunctionToStringIsConsidered) { + EXPECT_EQ(ToString(Foo{}), "Foo"); +} + +TEST(ToStringTest, Ordering) { + struct Container { + using value_type = int; + + explicit Container(std::vector&& v) : v{std::move(v)} { + } + + std::vector::const_iterator begin() const { + return v.begin(); + } + std::vector::const_iterator end() const { + return v.end(); + } + + std::vector v; + }; + + struct CustomToString : public Container { + using Container::Container; + std::string ToString() const { + return "CustomToString"; + } + }; + + EXPECT_EQ(ToString(Container{{1, 2, 3}}), "[1, 2, 3]"); + EXPECT_EQ(ToString(CustomToString{{1, 2, 3}}), "CustomToString"); +} + +} // namespace util +} // namespace firestore +} // namespace firebase