pw_string: ToString function

ToString allows writing string representations of arbitrary types to a
buffer.

Change-Id: Ie0e327560847509b03c651bd1697be8bc084b360
diff --git a/pw_string/BUILD.gn b/pw_string/BUILD.gn
index 0963511..d84b1d7 100644
--- a/pw_string/BUILD.gn
+++ b/pw_string/BUILD.gn
@@ -24,6 +24,7 @@
     ":default_config",
   ]
   public = [
+    "public/pw_string/to_string.h",
     "public/pw_string/type_to_string.h",
   ]
   sources = [
@@ -37,6 +38,16 @@
   ]
 }
 
+pw_test("to_string_test") {
+  deps = [
+    ":pw_string",
+    "$dir_pw_unit_test:main",
+  ]
+  sources = [
+    "to_string_test.cc",
+  ]
+}
+
 pw_test("type_to_string_test") {
   deps = [
     ":pw_string",
diff --git a/pw_string/public/pw_string/to_string.h b/pw_string/public/pw_string/to_string.h
new file mode 100644
index 0000000..50605df
--- /dev/null
+++ b/pw_string/public/pw_string/to_string.h
@@ -0,0 +1,92 @@
+// Copyright 2019 The Pigweed Authors
+//
+// 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
+//
+//     https://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.
+#pragma once
+
+// Provides the ToString function, which outputs string representations of
+// arbitrary types to a buffer.
+//
+// ToString returns the number of characters written, excluding the null
+// terminator, and a status. A null terminator is always written if the output
+// buffer has room.
+//
+// ToString functions may be defined for any type. This is done by providing a
+// ToString overload in the pw namespace. The overload must follow ToString's
+// semantics:
+//
+//   1. Always null terminate if the output buffer has room.
+//   2. Return the number of characters written, excluding the null terminator,
+//      as a StatusWithSize.
+//   3. If the buffer is too small to fit the output, return a StatusWithSize
+//      with the number of characters written and a status of
+//      RESOURCE_EXHAUSTED. Other status codes may be used for different errors.
+//
+// For example, providing the following overload would allow ToString, and any
+// classes that use it, to print instances of a custom type:
+//
+//   namespace pw {
+//
+//   inline StatusWithSize ToString(const SomeCustomType& value,
+//                                  const std::span<char>& buffer) {
+//     return /* ... implementation ... */;
+//   }
+//
+//   }  // namespace pw
+//
+// Note that none of the functions in this module use std::snprintf. ToString
+// overloads may use snprintf if needed, but the ToString semantics must be
+// maintained.
+//
+// ToString is a low-level function. To write complex objects to string, a
+// StringBuilder may be easier to work with. StringBuilder's operator<< may be
+// overloaded for custom types.
+
+#include <string_view>
+#include <type_traits>
+
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_string/type_to_string.h"
+
+namespace pw {
+
+// This function provides string printing numeric types, enums, and anything
+// that convertible to a std::string_view, such as std::string.
+template <typename T>
+StatusWithSize ToString(const T& value, const span<char>& buffer) {
+  if constexpr (std::is_same_v<std::decay_t<T>, bool>) {
+    return string::BoolToString(value, buffer);
+  } else if constexpr (std::is_same_v<std::decay_t<T>, char>) {
+    return string::CopyEntireString(std::string_view(&value, 1), buffer);
+  } else if constexpr (std::is_integral_v<T>) {
+    return string::IntToString(value, buffer);
+  } else if constexpr (std::is_enum_v<T>) {
+    return string::IntToString<std::underlying_type_t<T>>(value, buffer);
+  } else if constexpr (std::is_floating_point_v<T>) {
+    return string::FloatAsIntToString(value, buffer);
+  } else if constexpr (std::is_pointer_v<T> || std::is_null_pointer_v<T>) {
+    return string::PointerToString(value, buffer);
+  } else if constexpr (std::is_convertible_v<T, std::string_view>) {
+    return string::CopyString(value, buffer);
+  } else {
+    // By default, no definition of UnknownTypeToString is provided.
+    return string::UnknownTypeToString(value, buffer);
+  }
+}
+
+// ToString overloads for custom types may be provided.
+inline StatusWithSize ToString(Status status, const span<char>& buffer) {
+  return string::CopyString(status.str(), buffer);
+}
+
+}  // namespace pw
diff --git a/pw_string/public/pw_string/type_to_string.h b/pw_string/public/pw_string/type_to_string.h
index 7a994c0..7ad737a 100644
--- a/pw_string/public/pw_string/type_to_string.h
+++ b/pw_string/public/pw_string/type_to_string.h
@@ -83,4 +83,37 @@
 //
 StatusWithSize FloatAsIntToString(float value, const span<char>& buffer);
 
+// Writes a bool as "true" or "false". Semantics match CopyEntireString.
+StatusWithSize BoolToString(bool value, const span<char>& buffer);
+
+// Writes the pointer's address or "null". Semantics match CopyEntireString.
+StatusWithSize PointerToString(const void* pointer, const span<char>& buffer);
+
+// Copies the string to the buffer, truncating if the full string does not fit.
+// Always null terminates if buffer.size() > 0.
+//
+// Returns the number of characters written, excluding the null terminator. If
+// the string is truncated, the status is RESOURCE_EXHAUSTED.
+StatusWithSize CopyString(const std::string_view& value,
+                          const span<char>& buffer);
+
+// Copies the string to the buffer, if the entire string fits. Always null
+// terminates if buffer.size() > 0.
+//
+// Returns the number of characters written, excluding the null terminator. If
+// the full string does not fit, only a null terminator is written and the
+// status is RESOURCE_EXHAUSTED.
+StatusWithSize CopyEntireString(const std::string_view& value,
+                                const span<char>& buffer);
+
+// This function is a fallback that is called if by ToString if no overload
+// matches. No definition is provided, so attempting to print an unsupported
+// type causes a linker error.
+//
+// Applications may define pw::string::UnknownTypeToString to support generic
+// printing for unknown types, if desired. Implementations must follow the
+// ToString semantics.
+template <typename T>
+StatusWithSize UnknownTypeToString(const T& value, const span<char>& buffer);
+
 }  // namespace pw::string
diff --git a/pw_string/to_string_test.cc b/pw_string/to_string_test.cc
new file mode 100644
index 0000000..e34f0ec
--- /dev/null
+++ b/pw_string/to_string_test.cc
@@ -0,0 +1,221 @@
+// Copyright 2019 The Pigweed Authors
+//
+// 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
+//
+//     https://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 "pw_string/to_string.h"
+
+#include <array>
+#include <cinttypes>
+#include <cmath>
+#include <cstring>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "pw_status/status.h"
+
+namespace pw {
+
+struct CustomType {
+  unsigned a;
+  unsigned b;
+
+  static constexpr const char* kToString = "This is a CustomType";
+
+  CustomType() = default;
+
+  // Non-copyable to verify that ToString doesn't copy it.
+  CustomType(const CustomType&) = delete;
+  CustomType& operator=(const CustomType&) = delete;
+};
+
+StatusWithSize ToString(const CustomType&, const span<char>& buffer) {
+  int result =
+      std::snprintf(buffer.data(), buffer.size(), CustomType::kToString);
+  if (result < 0) {
+    return StatusWithSize(Status::UNKNOWN, 0);
+  }
+  if (static_cast<size_t>(result) < buffer.size()) {
+    return StatusWithSize(result);
+  }
+  return StatusWithSize(Status::RESOURCE_EXHAUSTED,
+                        buffer.empty() ? 0u : buffer.size() - 1);
+}
+
+namespace {
+
+char buffer[128] = {};
+char expected[128] = {};
+
+TEST(ToString, Bool) {
+  const volatile bool b = true;
+  EXPECT_EQ(4u, ToString(b, buffer).size());
+  EXPECT_STREQ("true", buffer);
+  EXPECT_EQ(5u, ToString(false, buffer).size());
+  EXPECT_STREQ("false", buffer);
+}
+
+TEST(ToString, Char) {
+  EXPECT_EQ(1u, ToString('%', buffer).size());
+  EXPECT_STREQ("%", buffer);
+}
+
+template <typename T>
+constexpr T kInteger = 127;
+
+TEST(ToString, Integer_AllTypesAreSupported) {
+  EXPECT_EQ(3u, ToString(kInteger<unsigned char>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<signed char>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<unsigned short>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<signed short>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<unsigned int>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<signed int>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<unsigned long>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<signed long>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<unsigned long long>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+  EXPECT_EQ(3u, ToString(kInteger<signed long long>, buffer).size());
+  EXPECT_STREQ("127", buffer);
+}
+
+TEST(ToString, Integer_BufferTooSmall_WritesNullTerminator) {
+  auto result = ToString(-1234, span(buffer, 5));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("", buffer);
+}
+
+TEST(ToString, Float) {
+  EXPECT_EQ(1u, ToString(0.0f, buffer).size());
+  EXPECT_STREQ("0", buffer);
+  EXPECT_EQ(3u, ToString(INFINITY, buffer).size());
+  EXPECT_STREQ("inf", buffer);
+  EXPECT_EQ(4u, ToString(-NAN, buffer).size());
+  EXPECT_STREQ("-NaN", buffer);
+}
+
+TEST(ToString, Pointer_NonNull_WritesValue) {
+  CustomType custom;
+  const size_t length = std::snprintf(expected,
+                                      sizeof(expected),
+                                      "%" PRIdPTR,
+                                      reinterpret_cast<intptr_t>(&custom));
+
+  EXPECT_EQ(length, ToString(&custom, buffer).size());
+  EXPECT_STREQ(expected, buffer);
+  EXPECT_EQ(length, ToString(static_cast<void*>(&custom), buffer).size());
+  EXPECT_STREQ(expected, buffer);
+  EXPECT_EQ(1u, ToString(reinterpret_cast<int*>(4), buffer).size());
+  EXPECT_STREQ("4", buffer);
+}
+
+TEST(ToString, Pointer_Null_WritesNull) {
+  EXPECT_EQ(4u, ToString(nullptr, buffer).size());
+  EXPECT_STREQ("null", buffer);
+  EXPECT_EQ(4u,
+            ToString(static_cast<const CustomType*>(nullptr), buffer).size());
+  EXPECT_STREQ("null", buffer);
+}
+
+TEST(ToString, String_Literal) {
+  EXPECT_EQ(0u, ToString("", buffer).size());
+  EXPECT_STREQ("", buffer);
+  EXPECT_EQ(5u, ToString("hello", buffer).size());
+  EXPECT_STREQ("hello", buffer);
+}
+
+TEST(ToString, String_MutableBuffer) {
+  char chars[] = {'C', 'o', 'o', 'l', '\0'};
+  EXPECT_EQ(sizeof(chars) - 1, ToString(chars, buffer).size());
+  EXPECT_STREQ("Cool", buffer);
+}
+
+TEST(ToString, Object) {
+  CustomType custom;
+  EXPECT_EQ(std::strlen(CustomType::kToString),
+            ToString(custom, buffer).size());
+  EXPECT_STREQ(CustomType::kToString, buffer);
+}
+
+enum Foo : uint8_t {
+  BAR = 32,
+  BAZ = 100,
+};
+
+TEST(ToString, Enum) {
+  EXPECT_EQ(2u, ToString(Foo::BAR, buffer).size());
+  EXPECT_STREQ("32", buffer);
+
+  EXPECT_EQ(3u, ToString(Foo::BAZ, buffer).size());
+  EXPECT_STREQ("100", buffer);
+}
+
+TEST(ToString, Status) {
+  EXPECT_EQ(2u, ToString(Status(), buffer).size());
+  EXPECT_STREQ(Status().str(), buffer);
+}
+
+TEST(ToString, StatusCode) {
+  EXPECT_EQ(2u, ToString(Status::UNAVAILABLE, buffer).size());
+  EXPECT_STREQ("14", buffer);
+}
+
+TEST(ToString, StdArrayAsBuffer) {
+  std::array<char, 128> test_buffer;
+  EXPECT_EQ(5u, ToString(false, test_buffer).size());
+  EXPECT_STREQ("false", test_buffer.data());
+  EXPECT_EQ(2u, ToString("Hi", test_buffer).size());
+  EXPECT_STREQ("Hi", test_buffer.data());
+  EXPECT_EQ(4u, ToString(static_cast<void*>(nullptr), test_buffer).size());
+  EXPECT_STREQ("null", test_buffer.data());
+}
+
+TEST(ToString, StringView) {
+  std::string_view view = "cool";
+  EXPECT_EQ(4u, ToString(view, buffer).size());
+  EXPECT_STREQ("cool", buffer);
+}
+
+TEST(ToString, StringView_TooSmall_Truncates) {
+  std::string_view view = "kale!";
+  EXPECT_EQ(3u, ToString(view, span(buffer, 4)).size());
+  EXPECT_STREQ("kal", buffer);
+}
+
+TEST(ToString, StringView_EmptyBuffer_WritesNothing) {
+  constexpr char kOriginal[] = {'@', '#', '$', '%'};
+  char test_buffer[sizeof(kOriginal)];
+  std::memcpy(test_buffer, kOriginal, sizeof(kOriginal));
+
+  EXPECT_EQ(0u,
+            ToString(std::string_view("Hello!"), span(test_buffer, 0)).size());
+  ASSERT_EQ(0, std::memcmp(kOriginal, test_buffer, sizeof(kOriginal)));
+}
+
+TEST(ToString, StdString) {
+  EXPECT_EQ(5u, ToString(std::string("Whoa!"), buffer).size());
+  EXPECT_STREQ("Whoa!", buffer);
+
+  EXPECT_EQ(0u, ToString(std::string(), buffer).size());
+  EXPECT_STREQ("", buffer);
+}
+
+}  // namespace
+}  // namespace pw
diff --git a/pw_string/type_to_string.cc b/pw_string/type_to_string.cc
index 6997e1e..829dfdd 100644
--- a/pw_string/type_to_string.cc
+++ b/pw_string/type_to_string.cc
@@ -119,7 +119,9 @@
   if (result.ok()) {
     buffer[0] = '-';
     return StatusWithSize(result.size() + 1);
-  } else if (!buffer.empty()) {
+  }
+
+  if (!buffer.empty()) {
     buffer[0] = '\0';
   }
   return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
@@ -148,8 +150,46 @@
   if (!buffer.empty()) {
     buffer[0] = '\0';
   }
-
   return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
 }
 
+StatusWithSize BoolToString(bool value, const span<char>& buffer) {
+  return CopyEntireString(value ? "true" : "false", buffer);
+}
+
+StatusWithSize PointerToString(const void* pointer, const span<char>& buffer) {
+  if (pointer == nullptr) {
+    return CopyEntireString("null", buffer);
+  }
+  // TODO(hepler): Add support for hexadecimal output.
+  return IntToString(reinterpret_cast<uintptr_t>(pointer), buffer);
+}
+
+StatusWithSize CopyString(const std::string_view& value,
+                          const span<char>& buffer) {
+  if (buffer.empty()) {
+    return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
+  }
+
+  const size_t copied = value.copy(buffer.data(), buffer.size() - 1);
+  buffer[copied] = '\0';
+
+  return StatusWithSize(
+      copied == value.size() ? Status::OK : Status::RESOURCE_EXHAUSTED, copied);
+}
+
+StatusWithSize CopyEntireString(const std::string_view& value,
+                                const span<char>& buffer) {
+  if (value.size() >= buffer.size()) {
+    if (!buffer.empty()) {
+      buffer[0] = '\0';
+    }
+    return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
+  }
+
+  std::memcpy(buffer.data(), value.data(), value.size());
+  buffer[value.size()] = '\0';
+  return StatusWithSize(value.size());
+}
+
 }  // namespace pw::string
diff --git a/pw_string/type_to_string_test.cc b/pw_string/type_to_string_test.cc
index e7473b9..389f9f6 100644
--- a/pw_string/type_to_string_test.cc
+++ b/pw_string/type_to_string_test.cc
@@ -17,6 +17,7 @@
 #include <cmath>
 #include <cstring>
 #include <limits>
+#include <string_view>
 
 #include "gtest/gtest.h"
 
@@ -316,5 +317,118 @@
   EXPECT_STREQ("", buffer_);
 }
 
+class CopyStringTest : public TestWithBuffer {};
+
+using namespace std::literals::string_view_literals;
+
+TEST_F(CopyStringTest, EmptyStringView_WritesNullTerminator) {
+  EXPECT_EQ(0u, CopyString("", buffer_).size());
+  EXPECT_EQ('\0', buffer_[0]);
+}
+
+TEST_F(CopyStringTest, EmptyBuffer_WritesNothing) {
+  auto result = CopyString("Hello", span(buffer_, 0));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ(kStartingString, buffer_);
+}
+
+TEST_F(CopyStringTest, TooSmall_Truncates) {
+  auto result = CopyString("Hi!", span(buffer_, 3));
+  EXPECT_EQ(2u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("Hi", buffer_);
+}
+
+TEST_F(CopyStringTest, ExactFit) {
+  auto result = CopyString("Hi!", span(buffer_, 4));
+  EXPECT_EQ(3u, result.size());
+  EXPECT_TRUE(result.ok());
+  EXPECT_STREQ("Hi!", buffer_);
+}
+
+TEST_F(CopyStringTest, NullTerminatorsInString) {
+  ASSERT_EQ(4u, CopyString("\0!\0\0"sv, span(buffer_, 5)).size());
+  EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
+}
+
+class CopyEntireStringTest : public TestWithBuffer {};
+
+TEST_F(CopyEntireStringTest, EmptyStringView_WritesNullTerminator) {
+  EXPECT_EQ(0u, CopyEntireString("", buffer_).size());
+  EXPECT_EQ('\0', buffer_[0]);
+}
+
+TEST_F(CopyEntireStringTest, EmptyBuffer_WritesNothing) {
+  auto result = CopyEntireString("Hello", span(buffer_, 0));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ(kStartingString, buffer_);
+}
+
+TEST_F(CopyEntireStringTest, TooSmall_WritesNothing) {
+  auto result = CopyEntireString("Hi!", span(buffer_, 3));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("", buffer_);
+}
+
+TEST_F(CopyEntireStringTest, ExactFit) {
+  auto result = CopyEntireString("Hi!", span(buffer_, 4));
+  EXPECT_EQ(3u, result.size());
+  EXPECT_TRUE(result.ok());
+  EXPECT_STREQ("Hi!", buffer_);
+}
+
+TEST_F(CopyEntireStringTest, NullTerminatorsInString) {
+  ASSERT_EQ(4u, CopyEntireString("\0!\0\0"sv, span(buffer_, 5)).size());
+  EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
+}
+
+class PointerToStringTest : public TestWithBuffer {};
+
+TEST_F(PointerToStringTest, Nullptr_WritesNull) {
+  EXPECT_EQ(4u, PointerToString(nullptr, span(buffer_, 5)).size());
+  EXPECT_STREQ("null", buffer_);
+}
+
+TEST_F(PointerToStringTest, WritesAddress) {
+  const void* pointer = reinterpret_cast<void*>(321);
+  EXPECT_EQ(3u, PointerToString(pointer, buffer_).size());
+  EXPECT_STREQ("321", buffer_);
+}
+
+class BoolToStringTest : public TestWithBuffer {};
+
+TEST_F(BoolToStringTest, ExactFit) {
+  EXPECT_EQ(4u, BoolToString(true, span(buffer_, 5)).size());
+  EXPECT_STREQ("true", buffer_);
+
+  EXPECT_EQ(5u, BoolToString(false, span(buffer_, 6)).size());
+  EXPECT_STREQ("false", buffer_);
+}
+
+TEST_F(BoolToStringTest, True_TooSmall_WritesNullTerminator) {
+  auto result = BoolToString(true, span(buffer_, 4));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("", buffer_);
+}
+
+TEST_F(BoolToStringTest, False_TooSmall_WritesNullTerminator) {
+  auto result = BoolToString(false, span(buffer_, 5));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("", buffer_);
+}
+
+TEST_F(BoolToStringTest, EmptyBuffer_WritesNothing) {
+  EXPECT_EQ(0u, BoolToString(true, span(buffer_, 0)).size());
+  EXPECT_STREQ(kStartingString, buffer_);
+
+  EXPECT_EQ(0u, BoolToString(false, span(buffer_, 0)).size());
+  EXPECT_STREQ(kStartingString, buffer_);
+}
+
 }  // namespace
 }  // namespace pw::string