pw_protobuf: Don't write empty nested messages

When using the message-struct based API, do not write out a zero-sized
field for a nested struct that has all default fields.

Change-Id: I56f40ebf710b776371693e4ce2b27312244f2c6c
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/97900
Reviewed-by: Armando Montanez <amontanez@google.com>
Commit-Queue: Scott James Remnant <keybuk@google.com>
diff --git a/pw_protobuf/codegen_message_test.cc b/pw_protobuf/codegen_message_test.cc
index 4fee12c..de6111a 100644
--- a/pw_protobuf/codegen_message_test.cc
+++ b/pw_protobuf/codegen_message_test.cc
@@ -1286,12 +1286,6 @@
 
   // clang-format off
   constexpr uint8_t expected_proto[] = {
-    // pigweed.pigweed (empty)
-    0x3a, 0x00,
-    // pigweed.proto
-    0x4a, 0x02,
-    // pigweed.proto.meta (empty)
-    0x2a, 0x00,
     // pigweed.bytes (default)
     0x5a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   };
@@ -1476,12 +1470,6 @@
 
   // clang-format off
   constexpr uint8_t expected_proto[] = {
-    // pigweed.pigweed (empty)
-    0x3a, 0x00,
-    // pigweed.proto (default)
-    0x4a, 0x02,
-    // pigweed.proto.meta (empty)
-    0x2a, 0x00,
     // pigweed.bytes (default)
     0x5a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     // pigweed.description
@@ -1520,12 +1508,6 @@
 
   // clang-format off
   constexpr uint8_t expected_proto[] = {
-    // pigweed.pigweed (empty)
-    0x3a, 0x00,
-    // pigweed.proto (default)
-    0x4a, 0x02,
-    // pigweed.proto.meta (empty)
-    0x2a, 0x00,
     // pigweed.bytes (default)
     0x5a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     // pigweed.special_property
@@ -1687,12 +1669,6 @@
     0x0a, 0x04, 'c', 'h', 'i', 'p',
     // pigweed.device_info.attributes[1].value
     0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c',
-    // pigweed.pigweed (empty)
-    0x3a, 0x00,
-    // pigweed.proto (default)
-    0x4a, 0x02,
-    // pigweed.proto.meta (empty)
-    0x2a, 0x00,
     // pigweed.bytes (default)
     0x5a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   };
diff --git a/pw_protobuf/encoder.cc b/pw_protobuf/encoder.cc
index e41e934..b367766 100644
--- a/pw_protobuf/encoder.cc
+++ b/pw_protobuf/encoder.cc
@@ -35,7 +35,8 @@
 
 namespace pw::protobuf {
 
-StreamEncoder StreamEncoder::GetNestedEncoder(uint32_t field_number) {
+StreamEncoder StreamEncoder::GetNestedEncoder(uint32_t field_number,
+                                              bool write_when_empty) {
   PW_CHECK(!nested_encoder_open());
   PW_CHECK(ValidFieldNumber(field_number));
 
@@ -62,7 +63,7 @@
   } else {
     nested_buffer = ByteSpan();
   }
-  return StreamEncoder(*this, nested_buffer);
+  return StreamEncoder(*this, nested_buffer, write_when_empty);
 }
 
 StreamEncoder::~StreamEncoder() {
@@ -109,6 +110,10 @@
     return;
   }
 
+  if (!nested.memory_writer_.bytes_written() && !nested.write_when_empty_) {
+    return;
+  }
+
   status_ = WriteLengthDelimitedField(temp_field_number,
                                       nested.memory_writer_.WrittenData());
 }
@@ -511,7 +516,8 @@
           // Nested Message. Struct member is an embedded struct for the
           // nested field. Obtain a nested encoder and recursively call Write()
           // using the fields table pointer from this field.
-          auto nested_encoder = GetNestedEncoder(field.field_number());
+          auto nested_encoder = GetNestedEncoder(field.field_number(),
+                                                 /*write_when_empty=*/false);
           PW_TRY(nested_encoder.Write(values, *field.nested_message_fields()));
         } else if (field.is_fixed_size()) {
           // Fixed-length bytes field. Struct member is a std::array<std::byte>.
diff --git a/pw_protobuf/public/pw_protobuf/encoder.h b/pw_protobuf/public/pw_protobuf/encoder.h
index c05c9db..fc724fb 100644
--- a/pw_protobuf/public/pw_protobuf/encoder.h
+++ b/pw_protobuf/public/pw_protobuf/encoder.h
@@ -118,6 +118,7 @@
   // provide a zero-length scratch buffer.
   constexpr StreamEncoder(stream::Writer& writer, ByteSpan scratch_buffer)
       : status_(OkStatus()),
+        write_when_empty_(true),
         parent_(nullptr),
         nested_field_number_(0),
         memory_writer_(scratch_buffer),
@@ -154,7 +155,9 @@
   //
   // Postcondition: Until the nested child encoder has been destroyed, this
   //     encoder cannot be used.
-  StreamEncoder GetNestedEncoder(uint32_t field_number);
+  StreamEncoder GetNestedEncoder(uint32_t field_number) {
+    return GetNestedEncoder(field_number, /*write_when_empty=*/true);
+  }
 
   // Returns the current encoder's status.
   //
@@ -628,6 +631,7 @@
   //     acts like a parent encoder with an active child encoder.
   constexpr StreamEncoder(StreamEncoder&& other)
       : status_(other.status_),
+        write_when_empty_(true),
         parent_(other.parent_),
         nested_field_number_(other.nested_field_number_),
         memory_writer_(std::move(other.memory_writer_)),
@@ -649,12 +653,21 @@
   Status Write(std::span<const std::byte> message,
                std::span<const MessageField> table);
 
+  // Protected method to create a nested encoder, specifying whether the field
+  // should be written when no fields were added to the nested encoder. Not
+  // part of the public API since callers can simply not create a nested encoder
+  // in those situations.
+  StreamEncoder GetNestedEncoder(uint32_t field_number, bool write_when_empty);
+
  private:
   friend class MemoryEncoder;
 
-  constexpr StreamEncoder(StreamEncoder& parent, ByteSpan scratch_buffer)
+  constexpr StreamEncoder(StreamEncoder& parent,
+                          ByteSpan scratch_buffer,
+                          bool write_when_empty = true)
       : status_(scratch_buffer.empty() ? Status::ResourceExhausted()
                                        : OkStatus()),
+        write_when_empty_(write_when_empty),
         parent_(&parent),
         nested_field_number_(0),
         memory_writer_(scratch_buffer),
@@ -769,6 +782,10 @@
   // encoder enters an error state.
   Status status_;
 
+  // Checked by the parent when the nested encoder is closed, and if no bytes
+  // were written, the field is not written.
+  bool write_when_empty_;
+
   // If this is a nested encoder, this points to the encoder that created it.
   // For user-created MemoryEncoders, parent_ points to this object as an
   // optimization for the MemoryEncoder and nested encoders to use the same