pw_protobuf: Generate enum-to-string functions

Generate an <EnumName>ToString() function alongside the enum and the
IsValid<EnumName>() function.

Change-Id: I7b9a8c3cb9e24321072a32821430f8ec9aff9aeb
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/126683
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
diff --git a/pw_protobuf/codegen_encoder_test.cc b/pw_protobuf/codegen_encoder_test.cc
index e6645a9..de0a581 100644
--- a/pw_protobuf/codegen_encoder_test.cc
+++ b/pw_protobuf/codegen_encoder_test.cc
@@ -551,5 +551,26 @@
             0);
 }
 
+TEST(Codegen, EnumToString) {
+  EXPECT_STREQ(test::pwpb::BoolToString(test::pwpb::Bool::kTrue), "TRUE");
+  EXPECT_STREQ(test::pwpb::BoolToString(test::pwpb::Bool::kFalse), "FALSE");
+  EXPECT_STREQ(test::pwpb::BoolToString(test::pwpb::Bool::kFileNotFound),
+               "FILE_NOT_FOUND");
+  EXPECT_STREQ(test::pwpb::BoolToString(static_cast<test::pwpb::Bool>(12893)),
+               "");
+}
+
+TEST(Codegen, NestedEnumToString) {
+  EXPECT_STREQ(test::pwpb::Pigweed::Pigweed::BinaryToString(
+                   test::pwpb::Pigweed::Pigweed::Binary::kZero),
+               "ZERO");
+  EXPECT_STREQ(test::pwpb::Pigweed::Pigweed::BinaryToString(
+                   test::pwpb::Pigweed::Pigweed::Binary::kOne),
+               "ONE");
+  EXPECT_STREQ(test::pwpb::Pigweed::Pigweed::BinaryToString(
+                   static_cast<test::pwpb::Pigweed::Pigweed::Binary>(12893)),
+               "");
+}
+
 }  // namespace
 }  // namespace pw::protobuf
diff --git a/pw_protobuf/docs.rst b/pw_protobuf/docs.rst
index 787f150..7ebfb14 100644
--- a/pw_protobuf/docs.rst
+++ b/pw_protobuf/docs.rst
@@ -1723,15 +1723,24 @@
 
 Enumerations
 ============
-Enumerations are read using code generated ``ReadEnum`` methods that return the
-code generated enumeration as the appropriate type.
+``pw_protobuf`` generates a few functions for working with enumerations.
+Most importantly, enumerations are read using generated ``ReadEnum`` methods
+that return the enumeration as the appropriate generated type.
 
 .. cpp:function:: Result<MyProto::Enum> MyProto::StreamDecoder::ReadEnum()
 
-To validate the value encoded in the wire format against the known set of
-enumerates, a function is generated that you can use:
+   Decodes an enum from the stream.
 
-.. cpp:function:: bool MyProto::IsValidEnum(MyProto::Enum)
+.. cpp:function:: constexpr bool MyProto::IsValidEnum(MyProto::Enum value)
+
+  Validates the value encoded in the wire format against the known set of
+  enumerates.
+
+.. cpp:function:: constexpr const char* MyProto::EnumToString(MyProto::Enum value)
+
+  Returns the string representation of the enum value. For example,
+  ``FooToString(Foo::kBarBaz)`` returns ``"BAR_BAZ"``. Returns the empty string
+  if the value is not a valid value.
 
 To read enumerations with the lower-level API, you would need to cast the
 retured value from the ``uint32_t``.
@@ -1739,14 +1748,14 @@
 The following two code blocks are equivalent, where the first is using the code
 generated API, and the second implemented by hand.
 
-.. code:: c++
+.. code-block:: c++
 
   pw::Result<MyProto::Award> award = my_proto_decoder.ReadAward();
   if (!MyProto::IsValidAward(award)) {
     PW_LOG_DBG("Unknown award");
   }
 
-.. code:: c++
+.. code-block:: c++
 
   PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
       static_cast<uint32_t>(MyProto::Fields::AWARD));
diff --git a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
index 3945dbc..2a3346b 100644
--- a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
+++ b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
@@ -2273,7 +2273,7 @@
 def generate_function_for_enum(
     proto_enum: ProtoEnum, root: ProtoNode, output: OutputFile
 ) -> None:
-    """Creates a C++ validation function for for a proto enum."""
+    """Creates a C++ validation function for a proto enum."""
     assert proto_enum.type() == ProtoNode.Type.ENUM
 
     enum_name = proto_enum.cpp_namespace(root=root)
@@ -2290,6 +2290,30 @@
     output.write_line('}')
 
 
+def generate_to_string_for_enum(
+    proto_enum: ProtoEnum, root: ProtoNode, output: OutputFile
+) -> None:
+    """Creates a C++ to string function for a proto enum."""
+    assert proto_enum.type() == ProtoNode.Type.ENUM
+
+    enum_name = proto_enum.cpp_namespace(root=root)
+    output.write_line(
+        f'// Returns string names for {enum_name}; '
+        'returns "" for invalid enum values.'
+    )
+    output.write_line(
+        f'constexpr const char* {enum_name}ToString({enum_name} value) {{'
+    )
+    with output.indent():
+        output.write_line('switch (value) {')
+        with output.indent():
+            for name, _ in proto_enum.values():
+                output.write_line(f'case {enum_name}::{name}: return "{name}";')
+            output.write_line('default: return "";')
+        output.write_line('}')
+    output.write_line('}')
+
+
 def forward_declare(
     node: ProtoMessage, root: ProtoNode, output: OutputFile
 ) -> None:
@@ -2325,6 +2349,8 @@
             generate_code_for_enum(cast(ProtoEnum, child), node, output)
             output.write_line()
             generate_function_for_enum(cast(ProtoEnum, child), node, output)
+            output.write_line()
+            generate_to_string_for_enum(cast(ProtoEnum, child), node, output)
 
     output.write_line(f'}}  // namespace {namespace}')
 
@@ -2576,6 +2602,8 @@
             generate_code_for_enum(cast(ProtoEnum, node), package, output)
             output.write_line()
             generate_function_for_enum(cast(ProtoEnum, node), package, output)
+            output.write_line()
+            generate_to_string_for_enum(cast(ProtoEnum, node), package, output)
 
     # Run through all messages, generating structs and classes for each.
     messages = []