Skip to content

Add a ToString function to create human-readable debug descriptions #2384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 15, 2019
4 changes: 4 additions & 0 deletions Firestore/Example/Firestore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -522,6 +523,7 @@
B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = create_noop_connectivity_monitor.cc; sourceTree = "<group>"; };
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>"; };
B68B1E002213A764008977EF /* to_string_apple_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = to_string_apple_test.mm; sourceTree = "<group>"; };
B68FC0E421F6848700A7055C /* watch_change_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = watch_change_test.mm; sourceTree = "<group>"; };
B69CF05A219B9105004C434D /* FIRFirestore+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FIRFirestore+Testing.h"; sourceTree = "<group>"; };
B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = grpc_stream_test.cc; sourceTree = "<group>"; };
Expand Down Expand Up @@ -717,6 +719,7 @@
54740A561FC913EB00713A1A /* util */ = {
isa = PBXGroup;
children = (
B68B1E002213A764008977EF /* to_string_apple_test.mm */,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you run scripts/sync_project.rb does this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */,
B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */,
B6FB467B208E9A8200554BA2 /* async_queue_test.cc */,
Expand Down Expand Up @@ -2002,6 +2005,7 @@
5492E09D2021552D00B64F25 /* FSTLocalStoreTests.mm in Sources */,
5CC9650520A0E9BD00A2D6A1 /* FSTMemoryLRUGarbageCollectorTests.mm in Sources */,
5492E0A12021552D00B64F25 /* FSTMemoryLocalStoreTests.mm in Sources */,
B68B1E012213A765008977EF /* to_string_apple_test.mm in Sources */,
5492E0AD2021552D00B64F25 /* FSTMemoryMutationQueueTests.mm in Sources */,
5492E0A42021552D00B64F25 /* FSTMemoryQueryCacheTests.mm in Sources */,
5492E0A52021552D00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions Firestore/core/src/firebase/firestore/immutable/sorted_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_H_
#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_H_

#include <type_traits>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to include util/type_traits.h?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover, sorry. I tried adding some static_asserts, but that didn't turn out to be very useful, because compilation attempts proceed after hitting a static_assert, drowning it in a pool of useless noise.

#include <utility>

#include "Firestore/core/src/firebase/firestore/immutable/array_sorted_map.h"
Expand All @@ -38,6 +39,8 @@ namespace immutable {
template <typename K, typename V, typename C = util::Comparator<K>>
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<K, V>;
using array_type = impl::ArraySortedMap<K, V, C>;
Expand Down
202 changes: 202 additions & 0 deletions Firestore/core/src/firebase/firestore/util/to_string.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* 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 !defined(__OBJC__)
#error "This header only supports Objective-C++"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could split this into to_string.h and to_string_apple.h (or do some ifdef magic around the objc specificities). There's no real urgent need to do so, but it'd make this useful from the pure c++ world too.

Optional. If you do decide to do this, doing this as a separate PR would be reasonable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't bother splitting. Just add #if defined(__OBJC__) around the relevant parts.

Using a mechanism like the one using FormatChoice in string_format.h makes this easier to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with ifdefs.

#endif // !defined(__OBJC__)

#import <Foundation/Foundation.h>

#include <algorithm>
#include <numeric>
#include <string>
#include <type_traits>
#include <utility>

#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 {

template <typename T>
std::string ToString(const T& value);

namespace impl {

// Checks whether the given type `T` defines a member function `ToString`
template <typename T, typename = absl::void_t<>>
struct has_to_string : std::false_type {};
template <typename T>
struct has_to_string<T, absl::void_t<decltype(std::declval<T>().ToString())>>
: std::true_type {};

// Fallback

template <typename T>
std::string ToStringDefault(const T& value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're mixing "ToString" as a prefix and as a suffix in these member names. Shouldn't these be DefaultToString and CustomToString to match the others below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return std::to_string(value);
}

// Member function `ToString`

template <typename T>
std::string ToStringCustom(const T& value, std::false_type) {
return ToStringDefault(value);
}

template <typename T>
std::string ToStringCustom(const T& value, std::true_type) {
return value.ToString();
}

// Objective-C class

template <typename T>
std::string ObjCToString(const T& value, std::false_type) {
return ToStringCustom(value, has_to_string<T>{});
}

template <typename T>
std::string ObjCToString(const T& value, std::true_type) {
return MakeString([value description]);
}

// Container

template <typename T>
std::string ContainerToString(const T& value, std::false_type) {
return ObjCToString(value, is_objective_c_pointer<T>{});
}

template <typename T>
std::string ContainerToString(const T& value, std::true_type) {
std::string contents = absl::StrJoin(
value, ", ", [](std::string* out, const typename T::value_type& element) {
out->append(ToString(element));
});
return std::string{"["} + contents + "]";
}

// std::string

template <typename T>
std::string StringToString(const T& value, std::false_type) {
return ContainerToString(value, is_iterable<T>{});
}

template <typename T>
std::string StringToString(const T& value, std::true_type) {
return value;
}

// Associative container

template <typename T>
std::string MapToString(const T& value, std::false_type) {
return StringToString(value, std::is_convertible<T, std::string>{});
}

template <typename T>
std::string MapToString(const T& value, std::true_type) {
std::string contents = absl::StrJoin(
value, ", ", [](std::string* out, const typename T::value_type& kv) {
out->append(
StringFormat("%s: %s", ToString(kv.first), ToString(kv.second)));
});
return std::string{"{"} + contents + "}";
}

} // namespace impl

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation comments are usually better off on the declaration than the definition when they're separate.

* 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<DocumentKey> v{
* DocumentKey({"foo/bar"}),
* DocumentKey({"this/that"})
* };
* assert(ToString(v) == "[foo/bar, this/that]");
*
* std::map<int, std::string> m{
{1, "foo"},
{2, "bar"}
* };
* assert(ToString(m) == "{1: foo, 2: bar}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unordered map might be interesting... (Or at least difficult to test. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (sort of).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha. That's a pretty good way to dodge the issue. :)

*
* The following algorithm is used:
* - 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 (convertible to) `std::string`, it's used as is;
*
* - 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, if `value` is an Objective-C class, the description is created
* by calling `[value description]`and converting the result to an
* `std::string`;
*
* - otherwise, if `value` defines a member function called `ToString`, the
* description is created by invoking the function;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a type is both a (custom) container and defines ToString, then this priority list implies that ToString won't be called. It would seem like ToString() should be given the highest priority rather than the lowest?

Regardless of where you end up on the above question, if you care about this ordering, consider adding a test for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The ordering is now:

  • ToString
  • description
  • conversion to std::string;
  • assoc. container;
  • container;
  • std::to_string.

What do you think?

Test -- added a simple test. I mostly care about the ordering between associative and simple containers, because it's the only one that will always "conflict", but it's implicitly tested already.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New ordering seems reasonable. And yeah, the ordering between assoc/simple containers is definitely already tested. The one I was referring to, was the ToString + container ordering, which you've now added. ltgm.

*
* - 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_convertible<T, std::string>` as the tag.
*/

template <typename T>
std::string ToString(const T& value) {
return impl::MapToString(value, is_associative_container<T>{});
}

} // namespace util
} // namespace firestore
} // namespace firebase

#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_TO_STRING_H_
52 changes: 18 additions & 34 deletions Firestore/core/src/firebase/firestore/util/type_traits.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

#include <type_traits>

#include "absl/meta/type_traits.h"

namespace firebase {
namespace firestore {
namespace util {
Expand All @@ -45,43 +47,25 @@ namespace util {
* is_objective_c_pointer<std::unique_ptr<int>>::value == false
*/
template <typename T>
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) };
struct is_objective_c_pointer : std::is_convertible<T, id> {};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is equivalent to the previous implementation (and the tests pass), but I'll run it by Gil before merging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If TEST(TypeTraitsTest, IsObjectiveCPointer) passes I'm all for having less code.

Could this be a using statement? Something like using is_objective_c_pointer = std::is_convertible<T, id>?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, good idea.


constexpr operator bool() const {
return value;
}

constexpr bool operator()() const {
return value;
}
};
#endif // __OBJC__

// Hard-code the answer for `void` because you can't pass arguments of type
// `void` to another function.
template <>
struct is_objective_c_pointer<void> : public std::false_type {};
template <typename T, typename = absl::void_t<>>
struct is_iterable : std::false_type {};
template <typename T>
struct is_iterable<
T,
absl::void_t<decltype(std::declval<T>().begin(), std::declval<T>().end())>>
: std::true_type {};

#endif // __OBJC__
template <typename T, typename = absl::void_t<>>
struct is_associative_container : std::false_type {};
template <typename T>
struct is_associative_container<
T,
absl::void_t<decltype(std::declval<typename T::mapped_type>())>>
: std::true_type {};

} // namespace util
} // namespace firestore
Expand Down
Loading