Skip to content

1837 daps refactor common library to use proto3#1842

Open
JoshuaSBrown wants to merge 13 commits intodevelfrom
1837-DAPS-refactor-common-library-to-use-proto3
Open

1837 daps refactor common library to use proto3#1842
JoshuaSBrown wants to merge 13 commits intodevelfrom
1837-DAPS-refactor-common-library-to-use-proto3

Conversation

@JoshuaSBrown
Copy link
Collaborator

@JoshuaSBrown JoshuaSBrown commented Feb 4, 2026

Description

This is part of a larger refactor that will be implemented in stages this PR works with the common library and it's tests.

How Has This Been Tested?

Artifacts (if appropriate):

Tasks

  • - A description of the PR has been provided, and a diagram included if it is a new feature.
  • - Formatter has been run
  • - CHANGELOG comment has been added
  • - Labels have been assigned to the pr
  • - A reviwer has been added
  • - A user has been assigned to work on the pr
  • - If new feature a unit test has been added

Summary by Sourcery

Refactor the common library to use the new proto3-based Envelope schema for message typing and transport, simplifying framing and message mapping while updating tests and build configuration accordingly.

Enhancements:

  • Replace protocol/message ID–based typing with a single 16-bit msg_type derived from proto3 Envelope field numbers throughout framing, mapping, and message factory code.
  • Introduce Envelope-based wrap/unwrap logic in ProtoBufMap and adjust ZeroMQ send/receive paths to serialize and deserialize Envelope-wrapped messages at the wire boundary.
  • Simplify ProtoBufFactory to construct messages using generated proto3 descriptors and remove protocol registration and global MessageFactory state.
  • Update IMessageMapper and its ProtoBufMap implementation to look up message types by name and expose an auth requirement check instead of protocol ID APIs.

Build:

  • Switch CMake configuration to generate C++ code from the new proto3/common tree with a 1-1-1 proto layout and adjust proto copying for the web server.
  • Remove legacy proto-based unit test wiring and targets tied to the previous SDMS_Anon/SDMS_Auth schema.

Tests:

  • Update unit, integration, and security tests to use proto3 SDMS messages and Envelope types, align with the new msg_type framing, and relax timeouts/add retry logic for more robust ZMQ-based tests.

@JoshuaSBrown JoshuaSBrown self-assigned this Feb 4, 2026
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 4, 2026

Reviewer's Guide

Refactors the common library to use a new proto3-based, envelope-centric messaging scheme, simplifying Frame metadata, centralizing message type lookup on the Envelope descriptor, and updating ZeroMQ send/receive paths and tests to operate on inner messages wrapped in SDMS::Envelope instead of legacy Anon/Auth protocol IDs.

Sequence diagram for sending a message with proto3 Envelope over ZeroMQ

sequenceDiagram
    participant App as ClientApp
    participant GMsg as GoogleProtoMessage
    participant FFactory as FrameFactory
    participant PMap as ProtoBufMap
    participant ZMQ as ZeroMQCommunicator
    participant Sock as ZeroMQSocket
    participant Buf as Buffer
    participant Env as Envelope

    App->>GMsg: setPayload(unique_ptr<Message> inner)
    activate GMsg
    GMsg->>FFactory: create(inner, PMap)
    activate FFactory
    FFactory->>PMap: getMessageType(inner)
    activate PMap
    PMap-->>FFactory: uint16_t msg_type
    deactivate PMap
    FFactory-->>GMsg: Frame frame(msg_type, size=inner.ByteSizeLong())
    deactivate FFactory
    GMsg->>PMap: wrapInEnvelope(inner)
    activate PMap
    PMap-->>GMsg: unique_ptr<Envelope> temp_envelope
    deactivate PMap
    GMsg->>GMsg: m_dyn_attributes[FRAME_SIZE] = temp_envelope.ByteSizeLong()
    GMsg->>GMsg: m_dyn_attributes[MSG_TYPE] = frame.msg_type
    deactivate GMsg

    App->>ZMQ: send(msg)
    activate ZMQ
    ZMQ->>ZMQ: sendFrame(msg, Sock)
    ZMQ->>FFactory: create(msg)
    activate FFactory
    FFactory-->>ZMQ: Frame frame(msg_type, size)
    deactivate FFactory
    ZMQ->>Sock: zmq_msg_send(frame header)

    ZMQ->>ZMQ: sendBody(msg, Buf, Sock)
    ZMQ->>GMsg: getPayload()
    GMsg-->>ZMQ: proto::Message* inner
    ZMQ->>PMap: wrapInEnvelope(*inner)
    activate PMap
    PMap-->>ZMQ: unique_ptr<Envelope> Env
    deactivate PMap
    ZMQ->>Buf: copyToBuffer(Env, size)
    ZMQ->>Sock: zmq_msg_send(body bytes)
    deactivate ZMQ
Loading

Sequence diagram for receiving a proto3 Envelope message over ZeroMQ

sequenceDiagram
    participant Sock as ZeroMQSocket
    participant ZMQ as ZeroMQCommunicator
    participant Buf as Buffer
    participant FFactory as FrameFactory
    participant PMap as ProtoBufMap
    participant Env as Envelope
    participant PFac as ProtoBufFactory
    participant GMsg as GoogleProtoMessage

    Sock-->>ZMQ: zmq_msg_recv(frame header)
    activate ZMQ
    ZMQ->>FFactory: create(zmq_msg)
    activate FFactory
    FFactory-->>ZMQ: Frame frame(size, msg_type)
    deactivate FFactory
    ZMQ->>GMsg: set(FRAME_SIZE, frame.size)
    ZMQ->>GMsg: set(MSG_TYPE, frame.msg_type)

    alt frame.size > 0
      Sock-->>ZMQ: zmq_msg_recv(body)
      ZMQ->>Buf: copyToBuffer(buffer, data, frame.size)
      ZMQ->>Env: copyFromBuffer(&envelope, buffer)
      ZMQ->>PMap: unwrapFromEnvelope(envelope)
      activate PMap
      PMap-->>ZMQ: unique_ptr<Message> inner
      deactivate PMap
      ZMQ->>GMsg: setPayload(std::move(inner))
    else frame.size == 0
      ZMQ->>GMsg: get(MSG_TYPE) -> msg_type
      ZMQ->>PFac: create(msg_type)
      activate PFac
      PFac-->>ZMQ: unique_ptr<Message> empty_inner
      deactivate PFac
      ZMQ->>GMsg: setPayload(std::move(empty_inner))
    end
    deactivate ZMQ
Loading

Class diagram for updated proto3 envelope messaging components

classDiagram
    namespace SDMS {
      class IMessageMapper {
        <<interface>>
        +uint16_t getMessageType(string message_name)
        +bool requiresAuth(string msg_type)
      }

      class ProtoBufMap {
        +ProtoBufMap()
        +const Descriptor* getDescriptorType(uint16_t message_type) const
        +bool exists(uint16_t message_type) const
        +uint16_t getMessageType(Message msg) const
        +uint16_t getMessageType(string message_name) const
        +string toString(uint16_t message_type) const
        +Envelope* wrapInEnvelope(Message inner) const
        +Message* unwrapFromEnvelope(Envelope envelope) const
        +bool requiresAuth(string msg_type) const
        -FileDescriptorMap m_file_descriptor_map
        -DescriptorMap m_descriptor_map
        -MsgTypeMap m_msg_type_map
      }

      class ProtoBufFactory {
        +ProtoBufFactory()
        +Message* create(uint16_t desc_type)
        +Message* create(Descriptor msg_descriptor)
        -ProtoBufMap m_proto_map
      }

      class Frame {
        +uint32_t size
        +uint16_t msg_type
        +uint16_t context
        +void clear()
      }

      class FrameConverter {
        +enum CopyDirection
        +void copy(CopyDirection direction, zmq_msg_t zmq_msg, Frame frame)
        +void copy(CopyDirection direction, IMessage msg, Frame frame)
      }

      class FrameFactory {
        +Frame create(Message msg, ProtoBufMap proto_map)
        +Frame create(IMessage msg)
        +Frame create(zmq_msg_t zmq_msg)
      }

      class IMessage {
        <<interface>>
        +bool exists(string key) const
        +void set(string key, uint32_t value)
        +void set(string key, uint16_t value)
        +variant get(string key) const
        +void setPayload(variant payload)
        +variant getPayload() const
      }

      class GoogleProtoMessage {
        +GoogleProtoMessage()
        +bool exists(string attribute_type) const
        +void set(string attribute_type, uint32_t value)
        +void set(string attribute_type, uint16_t value)
        +variant get(string attribute_type) const
        +void setPayload(variant payload)
        +variant getPayload() const
        -unordered_map<string, variant> m_dyn_attributes
        -ProtoBufMap m_proto_map
        -unique_ptr<Message> m_payload
      }

      class Envelope {
        +static const Descriptor* descriptor()
        +const Descriptor* GetDescriptor() const
        +const Reflection* GetReflection() const
      }
    }

    SDMS.ProtoBufMap ..|> SDMS.IMessageMapper
    SDMS.ProtoBufFactory --> SDMS.ProtoBufMap
    SDMS.GoogleProtoMessage ..|> SDMS.IMessage
    SDMS.GoogleProtoMessage --> SDMS.FrameFactory
    SDMS.GoogleProtoMessage --> SDMS.ProtoBufMap
    SDMS.FrameFactory --> SDMS.ProtoBufMap
    SDMS.FrameConverter --> SDMS.Frame
    SDMS.FrameConverter --> SDMS.IMessage
    SDMS.ProtoBufMap --> SDMS.Envelope
    SDMS.ProtoBufFactory --> SDMS.Envelope
Loading

File-Level Changes

Change Details Files
Replace legacy Anon/Auth protocol ID-based mapping with Envelope-based message type resolution and wrapping/unwrapping.
  • Removed ProtoBufMap’s registration of Anon/Auth Protocol enums and protocol ID maps, and now derive message type from SDMS::Envelope field numbers.
  • Added wrapInEnvelope/unwrapFromEnvelope helpers to ProtoBufMap to serialize inner messages inside an SDMS::Envelope at the wire boundary.
  • Changed getMessageType/toString/exists to work off the Envelope descriptor and message names rather than protocol IDs and per-file descriptor maps.
  • Extended IMessageMapper interface to expose getMessageType(message_name) and requiresAuth(message_type), with ProtoBufMap implementing requiresAuth via a hard-coded anon allowlist.
common/source/ProtoBufMap.cpp
common/include/common/ProtoBufMap.hpp
common/include/common/IMessageMapper.hpp
Simplify Frame structure to use a single uint16_t msg_type, and propagate this change through converters, factories, messages, and tests.
  • Replaced separate proto_id/msg_id fields in Frame with a single msg_type field, updated clear() semantics, and removed getMsgType().
  • Updated FrameConverter to pack/unpack msg_type as a 16-bit field in the frame header and to only set MSG_TYPE (no PROTO_ID/MSG_ID) on IMessage.
  • Adjusted FrameFactory to compute msg_type via ProtoBufMap::getMessageType and to consume MSG_TYPE instead of PROTO_ID/MSG_ID when building Frames from IMessage.
  • Removed google::PROTO_ID/MSG_ID dynamic attributes from GoogleProtoMessage and IMessage constants; tests now assert on msg_type only.
common/source/Frame.cpp
common/source/Frame.hpp
common/source/messages/GoogleProtoMessage.cpp
common/include/common/IMessage.hpp
common/tests/unit/test_Frame.cpp
Update ZeroMQ communicator send/receive logic to deal with Envelope-wrapped payloads and the new msg_type semantics.
  • sendFrame() unchanged logically but now works with Frames that carry only msg_type, not proto_id/msg_id.
  • receiveBody() now reads raw bytes into an SDMS::Envelope, then unwraps to the inner message via ProtoBufMap::unwrapFromEnvelope, falling back to creating a message by msg_type for zero-size frames.
  • sendBody() now wraps the inner protobuf message in an Envelope using ProtoBufMap::wrapInEnvelope before serialization, and validates frame_size against the Envelope size.
  • ZeroMQCommunicatorSecure and includes were cleaned up to rely on ProtoBufFactory via the header only.
common/source/communicators/ZeroMQCommunicator.cpp
common/source/communicators/ZeroMQCommunicator.hpp
common/source/communicators/ZeroMQCommunicatorSecure.cpp
Rework ProtoBufFactory to construct messages directly from the generated factory using Envelope-based descriptors.
  • Simplified ProtoBufFactory to drop cached MessageFactory pointer and Anon/Auth Protocol_descriptor calls.
  • create(uint16_t) now asks ProtoBufMap for the descriptor associated with a field number (Envelope field) and defers to create(const Descriptor*).
  • create(const Descriptor*) now uses google::protobuf::MessageFactory::generated_factory() on demand.
  • Removed the old ProtoBufFactory unit test and updated ProtoBufMap tests to use SDMS::VersionRequest from the new proto3 package.
common/source/ProtoBufFactory.cpp
common/source/ProtoBufFactory.hpp
common/tests/unit/test_ProtoBufMap.cpp
common/tests/unit/CMakeLists.txt
common/tests/unit/test_ProtoBufFactory.cpp
Align IMessage/GoogleProtoMessage behavior and tests with the envelope-based wire format and new proto3 message namespaces.
  • GoogleProtoMessage::setPayload now computes msg_type from the inner message, then builds a temporary Envelope to set FRAME_SIZE equal to the envelope’s ByteSizeLong, while storing only the inner message as payload.
  • All unit tests and security/functional tests that used SDMS::Anon or SDMS::Auth types were updated to the flat SDMS::* proto3 messages and to the new ErrorCode enum values (e.g., SERVICE_ERROR vs ID_SERVICE_ERROR).
  • Tests that reason about sizes (e.g., FrameFactory2) now use ProtoBufMap::wrapInEnvelope to compute the expected frame size based on the Envelope.
  • ZeroMQ/Proxy and CommunicatorFactory tests adjusted timeouts and message casting to work with Envelope-based payload flow in the TCP secure client/server examples.
common/source/messages/GoogleProtoMessage.cpp
common/tests/unit/test_MessageFactory.cpp
common/tests/unit/test_Buffer.cpp
common/tests/unit/test_Proxy.cpp
common/tests/unit/test_ProxyBasicZMQ.cpp
common/tests/unit/test_CommunicatorFactory.cpp
common/tests/security/tcp_secure/test_tcp_secure_client.cpp
common/tests/security/tcp_secure/test_tcp_secure_server.cpp
Switch build system and proto input paths from legacy proto/common to proto3/common and generate/ship envelope + new message layout.
  • Updated top-level CMakeLists and common/CMakeLists to add_subdirectory(common/proto3/common) instead of proto/common and to glob proto3/common/.proto for generation.
  • common/proto3/common/CMakeLists now explicitly globs enums/messages/anon/auth/.proto plus envelope.proto for cpp generation.
  • Adjusted web build proto copying to copy from common/proto/common to web/proto/ (likely transitional) and to use the new ProtoFiles set.
  • Added/modified numerous proto3 files (enums, anon, auth, messages, envelope) consistent with the 1-1-1 proto3 layout; the diff lists them but not the content here.
CMakeLists.txt
common/CMakeLists.txt
common/proto3/common/CMakeLists.txt
common/proto3/common/envelope.proto
common/proto3/common/enums/*.proto
common/proto3/common/anon/*.proto
common/proto3/common/auth/*.proto
common/proto3/common/messages/*.proto

Assessment against linked issues

Issue Objective Addressed Explanation
#1837 Refactor the common library code to use the new proto3 message definitions (e.g., envelope-based messages) instead of the legacy SDMS_Anon/SDMS_Auth protobufs.
#1837 Update the common library’s protobuf handling (mapping/factories/framing and ZeroMQ communication) to the new proto3 design, including envelope wrapping/unwrapping of messages.
#1837 Update common-library-related build configuration and tests to use the new proto3 protos and keep the common test suite passing.

Possibly linked issues

  • #unknown: The PR implements the common-library proto3 refactor requested in the issue, updating mappings, frames, factories, and tests.
  • #[Refactor] - Rework Protobuf files following protobuf design Principles: PR implements proto3, new envelope message, and reorganized common protos toward the requested 1-1-1 structure.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • ProtoBufMap still carries unused state and commented-out methods (e.g., m_file_descriptor_map, m_msg_type_map, protocol ID mapping); consider removing these members and dead code now that everything routes through Envelope to avoid confusion about how message types are actually resolved.
  • In ZeroMQCommunicator::receiveBody you now unconditionally construct an SDMS::Envelope and no longer use the desc_type/ProtoBufFactory for instantiation; if this is the new invariant, consider simplifying the interface and removing or repurposing the unused factory/desc_type plumbing to make the data flow clearer.
  • In tcp_secure_client.cpp the comment // Changed: SERVICE_ERROR -> ID_SERVICE_ERROR contradicts the code, which checks against ErrorCode::SERVICE_ERROR; update or remove this comment to reflect the current behavior and prevent confusion about which enum value is expected.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- ProtoBufMap still carries unused state and commented-out methods (e.g., m_file_descriptor_map, m_msg_type_map, protocol ID mapping); consider removing these members and dead code now that everything routes through Envelope to avoid confusion about how message types are actually resolved.
- In ZeroMQCommunicator::receiveBody you now unconditionally construct an SDMS::Envelope and no longer use the desc_type/ProtoBufFactory for instantiation; if this is the new invariant, consider simplifying the interface and removing or repurposing the unused factory/desc_type plumbing to make the data flow clearer.
- In tcp_secure_client.cpp the comment `// Changed: SERVICE_ERROR -> ID_SERVICE_ERROR` contradicts the code, which checks against ErrorCode::SERVICE_ERROR; update or remove this comment to reflect the current behavior and prevent confusion about which enum value is expected.

## Individual Comments

### Comment 1
<location> `common/source/ProtoBufMap.cpp:60-66` </location>
<code_context>
-                                       << "\" is not registered with protocol "
-                                       << a_proto_id);
-  }
+const proto::Descriptor* ProtoBufMap::getDescriptorType(uint16_t field_number) const {
+    const auto* envelope_desc = SDMS::Envelope::descriptor();
+    const auto* field = envelope_desc->FindFieldByNumber(field_number);
+    if (field && field->type() == proto::FieldDescriptor::TYPE_MESSAGE) {
+        return field->message_type();
+    }
+    return nullptr;
+}

</code_context>

<issue_to_address>
**suggestion (bug_risk):** getDescriptorType silently returns nullptr instead of signaling unregistered field numbers.

This changes the previous behavior, where unknown message types triggered `EXCEPT_PARAM(EC_PROTO_INIT, ...)`, to silently returning `nullptr`. That pushes error handling onto every caller and risks missed checks and harder-to-diagnose failures.

If strict validation of field numbers is still desired, consider either preserving the exception behavior in `getDescriptorType`, or splitting the API into a strict `getDescriptorType` (throws on failure) and a `tryGetDescriptorType` (returns `nullptr`), and using the strict variant in correctness-critical paths.

Suggested implementation:

```cpp
const proto::Descriptor* ProtoBufMap::tryGetDescriptorType(uint16_t field_number) const {
    const auto* envelope_desc = SDMS::Envelope::descriptor();
    const auto* field = envelope_desc->FindFieldByNumber(field_number);
    if (field && field->type() == proto::FieldDescriptor::TYPE_MESSAGE) {
        return field->message_type();
    }
    return nullptr;
}

const proto::Descriptor* ProtoBufMap::getDescriptorType(uint16_t field_number) const {
    const auto* desc = tryGetDescriptorType(field_number);
    if (!desc) {
        EXCEPT_PARAM(EC_PROTO_INIT,
                     "Field number " << field_number
                                     << " is not registered in SDMS::Envelope");
    }
    return desc;
}

```

1. In `common/include/common/ProtoBufMap.hpp` (or the corresponding header), declare the new API:
   - Add `const proto::Descriptor* tryGetDescriptorType(uint16_t field_number) const;` to the `ProtoBufMap` class.
2. Update all existing callers of `getDescriptorType`:
   - For correctness-critical paths that previously relied on the exception, keep using `getDescriptorType` (no change needed to retain strict behavior).
   - For optional/defensive checks where a missing field is acceptable, switch to `tryGetDescriptorType` and handle a `nullptr` return explicitly.
3. If you have unit tests that previously asserted on `EXCEPT_PARAM(EC_PROTO_INIT, ...)` for unknown field numbers, ensure they now call `getDescriptorType` (not `tryGetDescriptorType`) so the strict behavior is still validated.
</issue_to_address>

### Comment 2
<location> `common/source/communicators/ZeroMQCommunicator.cpp:389-391` </location>
<code_context>
       copyToBuffer(buffer, zmq_msg_data(&zmq_msg), frame_size);
       uint16_t desc_type = std::get<uint16_t>(msg.get(MSG_TYPE));
-      std::unique_ptr<proto::Message> payload = factory.create(desc_type);
+      std::unique_ptr<proto::Message> payload = std::make_unique<SDMS::Envelope>();
+        
+        //factory.create(desc_type);
       if (payload == nullptr) {
         zmq_msg_close(&zmq_msg);
</code_context>

<issue_to_address>
**issue:** By hard-coding payload creation to SDMS::Envelope, desc_type and ProtoBufFactory are effectively ignored, reducing flexibility.

Previously we relied on `factory.create(desc_type)` to select the payload type from `desc_type`; now we always build `SDMS::Envelope` and leave `desc_type` unused. If the protocol still uses multiple payload types (or might in future), this removes that flexibility.

If the protocol is now Envelope-only, either drop `desc_type` from the metadata and simplify, or keep using `ProtoBufFactory` (e.g., for the inner message) while fixing only the outer type to `Envelope`. Right now the presence of `desc_type` without using it is misleading and risks metadata/payload mismatches.
</issue_to_address>

### Comment 3
<location> `common/tests/unit/test_ProtoBufMap.cpp:47-56` </location>
<code_context>
             << " and " << proto_map.getMessageType(*msg) << std::endl;
 }

 BOOST_AUTO_TEST_CASE(testing_ProtoBufMap_toString) {
   ProtoBufMap proto_map;
-  SDMS::Anon::VersionRequest version_request;
-  uint16_t msg_type = proto_map.getMessageType(version_request);
+
+  // Changed: wrap in envelope
+  SDMS::Envelope envelope;
+  envelope.mutable_version_request();
+
+  uint16_t msg_type = proto_map.getMessageType(envelope);
   auto name = proto_map.toString(msg_type);
+
   BOOST_CHECK(name.compare("VersionRequest") == 0);
 }

</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for new ProtoBufMap APIs (getMessageType(string), exists, unknown field_number handling)

The refactor significantly changed ProtoBufMap’s public API (string-based `getMessageType`, nullable `getDescriptorType`, `exists`, and `toString` returning `"Unknown(<n>)"` instead of throwing), but the current test only covers a known `Envelope` type.

Please add unit tests to exercise the new behaviors, e.g.:
- `getMessageType("VersionRequest")` matches `getMessageType(envelope_with_version_request)`.
- `getMessageType("does_not_exist")` throws the expected `EXCEPT_PARAM`.
- `exists(field_number)` is `true` for a known field and `false` for an invalid one.
- `toString(unknown_field_number)` returns the `"Unknown(<number>)"` string and does not throw.

This will lock in the new contract and make future changes safer.

Suggested implementation:

```cpp
#define BOOST_TEST_MODULE protobuffactory
#include <boost/filesystem.hpp>
#include <boost/test/unit_test.hpp>
// Local public includes
#include "common/ProtoBufMap.hpp"
#include "common/SDMS_Exceptions.hpp"

// Proto file includes
#include "common/envelope.pb.h"  // Changed

// Standard includes

```

```cpp
  std::cout << "VersionRequest msg_type of VersionRequest, " << msg_type
            << " and " << proto_map.getMessageType(*msg) << std::endl;
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_getMessageType_by_name_matches_envelope) {
  ProtoBufMap proto_map;

  // Known message via envelope
  SDMS::Envelope envelope;
  envelope.mutable_version_request();

  const uint16_t type_from_envelope = proto_map.getMessageType(envelope);

  // Same message type via string-based API
  const uint16_t type_from_name = proto_map.getMessageType("VersionRequest");

  BOOST_CHECK_EQUAL(type_from_envelope, type_from_name);
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_getMessageType_unknown_name_throws) {
  ProtoBufMap proto_map;

  // Ensure unknown names throw the expected SDMS exception
  BOOST_CHECK_THROW(proto_map.getMessageType("does_not_exist"), SDMS::Exception);
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_exists_known_and_unknown_field_numbers) {
  ProtoBufMap proto_map;

  // For the Envelope proto, "version_request" should be a known field.
  // In typical Envelope schemas this is field number 1.
  const uint16_t known_field_number = 1;
  const uint16_t unknown_field_number = 9999;

  BOOST_CHECK(proto_map.exists(known_field_number));
  BOOST_CHECK(!proto_map.exists(unknown_field_number));
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_toString_unknown_field_number) {
  ProtoBufMap proto_map;

  const uint16_t unknown_field_number = 65535;
  const auto name = proto_map.toString(unknown_field_number);

  std::ostringstream expected;
  expected << "Unknown(" << unknown_field_number << ")";

  BOOST_CHECK_EQUAL(name, expected.str());
}

```

1. If the SDMS exception type or header name differ in your codebase, adjust:
   - The include `"common/SDMS_Exceptions.hpp"` to the actual header that declares the exception thrown by `ProtoBufMap::getMessageType(std::string)`.
   - The exception type in `BOOST_CHECK_THROW(..., SDMS::Exception)` to whatever `EXCEPT_PARAM` (or equivalent) actually throws (e.g., `SDMS::Core::Exception` or a derived type).
2. The known field number for `version_request` is assumed to be `1`. If your `Envelope` proto uses a different field number, update `known_field_number` accordingly.
</issue_to_address>

### Comment 4
<location> `common/tests/unit/test_ProxyBasicZMQ.cpp:292-298` </location>
<code_context>
               .compare(key) == 0);

+      // Changed: cast to Envelope, access inner message
       auto google_msg = std::get<::google::protobuf::Message *>(
           response.message->getPayload());
-      auto new_auth_by_pass_req =
-          dynamic_cast<SDMS::Anon::AuthenticateByTokenRequest *>(google_msg);
+      auto recv_envelope =
+          dynamic_cast<SDMS::Envelope *>(google_msg);

-      BOOST_CHECK(new_auth_by_pass_req->token().compare(token) == 0);
+      BOOST_CHECK(recv_envelope->has_authenticate_by_token_request());
+      BOOST_CHECK(recv_envelope->authenticate_by_token_request().token().compare(token) == 0);
     } // Server receive
     proxy_thread->join();
</code_context>

<issue_to_address>
**suggestion (testing):** Add a null check (BOOST_REQUIRE) after dynamic_cast to SDMS::Envelope to avoid undefined behavior in test failure cases

Here `dynamic_cast<SDMS::Envelope *>(google_msg)` may return `nullptr` if the payload is not an `SDMS::Envelope`, and the subsequent `BOOST_CHECK` calls will then dereference a null pointer. Please add `BOOST_REQUIRE(recv_envelope != nullptr)` before using `recv_envelope`, and mirror this pattern in the other similar blocks in this file, as done in `test_Proxy.cpp`.

Suggested implementation:

```cpp
      // Changed: cast to Envelope, access inner message
      auto google_msg = std::get<::google::protobuf::Message *>(
          response.message->getPayload());
      auto recv_envelope =
          dynamic_cast<SDMS::Envelope *>(google_msg);

      BOOST_REQUIRE(recv_envelope != nullptr);
      BOOST_CHECK(recv_envelope->has_authenticate_by_token_request());
      BOOST_CHECK(recv_envelope->authenticate_by_token_request().token().compare(token) == 0);

```

To fully implement your review comment, you should also:
1. Search in `common/tests/unit/test_ProxyBasicZMQ.cpp` for all occurrences of `dynamic_cast<SDMS::Envelope *>(` (or equivalent casts to `SDMS::Envelope*`) and insert a `BOOST_REQUIRE(<var_name> != nullptr);` immediately after each `dynamic_cast`, before any dereference or method call on the pointer.
2. Ensure the pattern matches what is done in `test_Proxy.cpp` (i.e., use `BOOST_REQUIRE` rather than `BOOST_CHECK` for the null check so the test aborts before dereferencing a null pointer).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +60 to +66
const proto::Descriptor* ProtoBufMap::getDescriptorType(uint16_t field_number) const {
const auto* envelope_desc = SDMS::Envelope::descriptor();
const auto* field = envelope_desc->FindFieldByNumber(field_number);
if (field && field->type() == proto::FieldDescriptor::TYPE_MESSAGE) {
return field->message_type();
}
return nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): getDescriptorType silently returns nullptr instead of signaling unregistered field numbers.

This changes the previous behavior, where unknown message types triggered EXCEPT_PARAM(EC_PROTO_INIT, ...), to silently returning nullptr. That pushes error handling onto every caller and risks missed checks and harder-to-diagnose failures.

If strict validation of field numbers is still desired, consider either preserving the exception behavior in getDescriptorType, or splitting the API into a strict getDescriptorType (throws on failure) and a tryGetDescriptorType (returns nullptr), and using the strict variant in correctness-critical paths.

Suggested implementation:

const proto::Descriptor* ProtoBufMap::tryGetDescriptorType(uint16_t field_number) const {
    const auto* envelope_desc = SDMS::Envelope::descriptor();
    const auto* field = envelope_desc->FindFieldByNumber(field_number);
    if (field && field->type() == proto::FieldDescriptor::TYPE_MESSAGE) {
        return field->message_type();
    }
    return nullptr;
}

const proto::Descriptor* ProtoBufMap::getDescriptorType(uint16_t field_number) const {
    const auto* desc = tryGetDescriptorType(field_number);
    if (!desc) {
        EXCEPT_PARAM(EC_PROTO_INIT,
                     "Field number " << field_number
                                     << " is not registered in SDMS::Envelope");
    }
    return desc;
}
  1. In common/include/common/ProtoBufMap.hpp (or the corresponding header), declare the new API:
    • Add const proto::Descriptor* tryGetDescriptorType(uint16_t field_number) const; to the ProtoBufMap class.
  2. Update all existing callers of getDescriptorType:
    • For correctness-critical paths that previously relied on the exception, keep using getDescriptorType (no change needed to retain strict behavior).
    • For optional/defensive checks where a missing field is acceptable, switch to tryGetDescriptorType and handle a nullptr return explicitly.
  3. If you have unit tests that previously asserted on EXCEPT_PARAM(EC_PROTO_INIT, ...) for unknown field numbers, ensure they now call getDescriptorType (not tryGetDescriptorType) so the strict behavior is still validated.

Comment on lines 47 to 56
BOOST_AUTO_TEST_CASE(testing_ProtoBufMap_toString) {
ProtoBufMap proto_map;
SDMS::Anon::VersionRequest version_request;
uint16_t msg_type = proto_map.getMessageType(version_request);

// Changed: wrap in envelope
SDMS::Envelope envelope;
envelope.mutable_version_request();

uint16_t msg_type = proto_map.getMessageType(envelope);
auto name = proto_map.toString(msg_type);

Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add tests for new ProtoBufMap APIs (getMessageType(string), exists, unknown field_number handling)

The refactor significantly changed ProtoBufMap’s public API (string-based getMessageType, nullable getDescriptorType, exists, and toString returning "Unknown(<n>)" instead of throwing), but the current test only covers a known Envelope type.

Please add unit tests to exercise the new behaviors, e.g.:

  • getMessageType("VersionRequest") matches getMessageType(envelope_with_version_request).
  • getMessageType("does_not_exist") throws the expected EXCEPT_PARAM.
  • exists(field_number) is true for a known field and false for an invalid one.
  • toString(unknown_field_number) returns the "Unknown(<number>)" string and does not throw.

This will lock in the new contract and make future changes safer.

Suggested implementation:

#define BOOST_TEST_MODULE protobuffactory
#include <boost/filesystem.hpp>
#include <boost/test/unit_test.hpp>
// Local public includes
#include "common/ProtoBufMap.hpp"
#include "common/SDMS_Exceptions.hpp"

// Proto file includes
#include "common/envelope.pb.h"  // Changed

// Standard includes
  std::cout << "VersionRequest msg_type of VersionRequest, " << msg_type
            << " and " << proto_map.getMessageType(*msg) << std::endl;
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_getMessageType_by_name_matches_envelope) {
  ProtoBufMap proto_map;

  // Known message via envelope
  SDMS::Envelope envelope;
  envelope.mutable_version_request();

  const uint16_t type_from_envelope = proto_map.getMessageType(envelope);

  // Same message type via string-based API
  const uint16_t type_from_name = proto_map.getMessageType("VersionRequest");

  BOOST_CHECK_EQUAL(type_from_envelope, type_from_name);
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_getMessageType_unknown_name_throws) {
  ProtoBufMap proto_map;

  // Ensure unknown names throw the expected SDMS exception
  BOOST_CHECK_THROW(proto_map.getMessageType("does_not_exist"), SDMS::Exception);
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_exists_known_and_unknown_field_numbers) {
  ProtoBufMap proto_map;

  // For the Envelope proto, "version_request" should be a known field.
  // In typical Envelope schemas this is field number 1.
  const uint16_t known_field_number = 1;
  const uint16_t unknown_field_number = 9999;

  BOOST_CHECK(proto_map.exists(known_field_number));
  BOOST_CHECK(!proto_map.exists(unknown_field_number));
}

BOOST_AUTO_TEST_CASE(test_ProtoBufMap_toString_unknown_field_number) {
  ProtoBufMap proto_map;

  const uint16_t unknown_field_number = 65535;
  const auto name = proto_map.toString(unknown_field_number);

  std::ostringstream expected;
  expected << "Unknown(" << unknown_field_number << ")";

  BOOST_CHECK_EQUAL(name, expected.str());
}
  1. If the SDMS exception type or header name differ in your codebase, adjust:
    • The include "common/SDMS_Exceptions.hpp" to the actual header that declares the exception thrown by ProtoBufMap::getMessageType(std::string).
    • The exception type in BOOST_CHECK_THROW(..., SDMS::Exception) to whatever EXCEPT_PARAM (or equivalent) actually throws (e.g., SDMS::Core::Exception or a derived type).
  2. The known field number for version_request is assumed to be 1. If your Envelope proto uses a different field number, update known_field_number accordingly.

Comment on lines 292 to 298
auto google_msg = std::get<::google::protobuf::Message *>(
response.message->getPayload());
auto new_auth_by_pass_req =
dynamic_cast<SDMS::Anon::AuthenticateByTokenRequest *>(google_msg);
auto recv_envelope =
dynamic_cast<SDMS::Envelope *>(google_msg);

BOOST_CHECK(new_auth_by_pass_req->token().compare(token) == 0);
BOOST_CHECK(recv_envelope->has_authenticate_by_token_request());
BOOST_CHECK(recv_envelope->authenticate_by_token_request().token().compare(token) == 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add a null check (BOOST_REQUIRE) after dynamic_cast to SDMS::Envelope to avoid undefined behavior in test failure cases

Here dynamic_cast<SDMS::Envelope *>(google_msg) may return nullptr if the payload is not an SDMS::Envelope, and the subsequent BOOST_CHECK calls will then dereference a null pointer. Please add BOOST_REQUIRE(recv_envelope != nullptr) before using recv_envelope, and mirror this pattern in the other similar blocks in this file, as done in test_Proxy.cpp.

Suggested implementation:

      // Changed: cast to Envelope, access inner message
      auto google_msg = std::get<::google::protobuf::Message *>(
          response.message->getPayload());
      auto recv_envelope =
          dynamic_cast<SDMS::Envelope *>(google_msg);

      BOOST_REQUIRE(recv_envelope != nullptr);
      BOOST_CHECK(recv_envelope->has_authenticate_by_token_request());
      BOOST_CHECK(recv_envelope->authenticate_by_token_request().token().compare(token) == 0);

To fully implement your review comment, you should also:

  1. Search in common/tests/unit/test_ProxyBasicZMQ.cpp for all occurrences of dynamic_cast<SDMS::Envelope *>( (or equivalent casts to SDMS::Envelope*) and insert a BOOST_REQUIRE(<var_name> != nullptr); immediately after each dynamic_cast, before any dereference or method call on the pointer.
  2. Ensure the pattern matches what is done in test_Proxy.cpp (i.e., use BOOST_REQUIRE rather than BOOST_CHECK for the null check so the test aborts before dereferencing a null pointer).

@JoshuaSBrown
Copy link
Collaborator Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In ZeroMQCommunicator::sendBody, wrapInEnvelope is now applied to whatever msg.getPayload() returns; callers like test_tcp_secure_client/server are now passing an Envelope as the payload, which will cause an envelope-inside-envelope mismatch (field type vs Envelope message) — either GoogleProtoMessage/clients should always pass the inner message, or wrapInEnvelope should explicitly detect and skip wrapping an Envelope.
  • ProtoBufMap::requiresAuth and getMessageType(const std::string&) rely on hard‑coded message name strings; consider deriving these from the Envelope descriptor (or a generated map) instead so that adding/renaming messages in the proto3 definitions doesn’t silently desync this logic.
  • Several test cases now use much larger timeouts (e.g., 30–400 ms → 800–2000 ms) and a manual send‑retry loop; it may be worth tightening these again or centralizing the retry/timeout behavior to avoid unnecessarily slowing the test suite and to keep timing behavior consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ZeroMQCommunicator::sendBody`, `wrapInEnvelope` is now applied to whatever `msg.getPayload()` returns; callers like `test_tcp_secure_client`/`server` are now passing an `Envelope` as the payload, which will cause an envelope-inside-envelope mismatch (field type vs `Envelope` message) — either `GoogleProtoMessage`/clients should always pass the inner message, or `wrapInEnvelope` should explicitly detect and skip wrapping an `Envelope`.
- `ProtoBufMap::requiresAuth` and `getMessageType(const std::string&)` rely on hard‑coded message name strings; consider deriving these from the `Envelope` descriptor (or a generated map) instead so that adding/renaming messages in the proto3 definitions doesn’t silently desync this logic.
- Several test cases now use much larger timeouts (e.g., 30–400 ms → 800–2000 ms) and a manual send‑retry loop; it may be worth tightening these again or centralizing the retry/timeout behavior to avoid unnecessarily slowing the test suite and to keep timing behavior consistent.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@JoshuaSBrown JoshuaSBrown requested a review from nedvedba February 5, 2026 21:49
@JoshuaSBrown JoshuaSBrown added Component: Common Priority: Medium Above average priority Type: Refactor Imlplementation change, same functionality labels Feb 5, 2026
Copy link
Collaborator

@nedvedba nedvedba left a comment

Choose a reason for hiding this comment

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

Looks good to me. Much cleaner and readable C++ code. Was the solution to backwards compatibility just making all the fields optional in proto files?

* [DAPS-1845] - refactor repo server to use proto3 (#1846)
* [DAPS-1847] - refactor: python package to be compatible with proto3 envelope (#1849)
* [DAPS-1848] - refactor: update authz files to use proto3. (#1851)
* [DAPS-1850] - refactor web server proto3 (#1852)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Component: Common Priority: Medium Above average priority Type: Refactor Imlplementation change, same functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] - Common library to use new proto3

2 participants