hubcio commented on code in PR #3610:
URL: https://github.com/apache/iggy/pull/3610#discussion_r3598487630


##########
foreign/cpp/src/type_conversion.rs:
##########
@@ -188,6 +194,119 @@ impl From<RustStats> for ffi::Stats {
     }
 }
 
+impl From<RustTransportEndpoints> for ffi::TransportEndpoints {
+    fn from(endpoints: RustTransportEndpoints) -> Self {
+        ffi::TransportEndpoints {
+            tcp: endpoints.tcp,
+            quic: endpoints.quic,
+            http: endpoints.http,
+            websocket: endpoints.websocket,
+        }
+    }
+}
+
+impl From<RustClusterNode> for ffi::ClusterNode {
+    fn from(node: RustClusterNode) -> Self {
+        ffi::ClusterNode {
+            name: node.name,
+            ip: node.ip,
+            endpoints: ffi::TransportEndpoints::from(node.endpoints),
+            role: node.role.to_string(),
+            status: node.status.to_string(),
+        }
+    }
+}
+
+impl From<RustClusterMetadata> for ffi::ClusterMetadata {
+    fn from(metadata: RustClusterMetadata) -> Self {
+        ffi::ClusterMetadata {
+            name: metadata.name,
+            nodes: metadata
+                .nodes
+                .into_iter()
+                .map(ffi::ClusterNode::from)
+                .collect(),
+        }
+    }
+}
+
+impl From<ffi::GlobalPermissions> for RustGlobalPermissions {
+    fn from(permissions: ffi::GlobalPermissions) -> Self {
+        RustGlobalPermissions {
+            manage_servers: permissions.manage_servers,
+            read_servers: permissions.read_servers,
+            manage_users: permissions.manage_users,
+            read_users: permissions.read_users,
+            manage_streams: permissions.manage_streams,
+            read_streams: permissions.read_streams,
+            manage_topics: permissions.manage_topics,
+            read_topics: permissions.read_topics,
+            poll_messages: permissions.poll_messages,
+            send_messages: permissions.send_messages,
+        }
+    }
+}
+
+impl From<ffi::TopicPermissions> for RustTopicPermissions {
+    fn from(permissions: ffi::TopicPermissions) -> Self {
+        RustTopicPermissions {
+            manage_topic: permissions.manage_topic,
+            read_topic: permissions.read_topic,
+            poll_messages: permissions.poll_messages,
+            send_messages: permissions.send_messages,
+        }
+    }
+}
+
+impl TryFrom<ffi::StreamPermissions> for RustStreamPermissions {
+    type Error = String;
+
+    fn try_from(permissions: ffi::StreamPermissions) -> Result<Self, 
Self::Error> {
+        let mut topics = BTreeMap::new();
+        for entry in permissions.topics {
+            let topic_id = entry.topic_id as usize;
+            if topics
+                .insert(topic_id, 
RustTopicPermissions::from(entry.permissions))
+                .is_some()
+            {
+                return Err(format!("duplicate topic permission ID: 
{topic_id}"));
+            }
+        }
+        let topics = (!topics.is_empty()).then_some(topics);
+
+        Ok(RustStreamPermissions {
+            manage_stream: permissions.manage_stream,
+            read_stream: permissions.read_stream,
+            manage_topics: permissions.manage_topics,
+            read_topics: permissions.read_topics,
+            poll_messages: permissions.poll_messages,
+            send_messages: permissions.send_messages,
+            topics,
+        })
+    }
+}
+
+impl TryFrom<ffi::Permissions> for RustPermissions {

Review Comment:
   this whole permissions conversion chain (`TryFrom<ffi::Permissions>`, 
`TryFrom<ffi::StreamPermissions>`, and the `From` impls above) plus 
`update_permissions` ship with no test. it's an authz path and the 
`has_permissions` false -> `None` vs true + all-false -> `Some` distinction is 
load-bearing, so it's worth a direct unit test on `TryFrom<ffi::Permissions>` - 
duplicate-id rejection, empty -> `None`, and the None/Some cases. doesn't need 
`create_user` to exist yet since the conversion is pure.



##########
foreign/cpp/src/lib.rs:
##########
@@ -88,11 +88,40 @@ mod ffi {
         topics_count: u32,
     }
 
+    #[repr(u8)]
+    enum HeaderKind {
+        Raw = 1,
+        String = 2,
+        Bool = 3,
+        Int8 = 4,
+        Int16 = 5,
+        Int32 = 6,
+        Int64 = 7,
+        Int128 = 8,
+        UInt8 = 9,

Review Comment:
   the header kind variants are named `UInt8..UInt128` here, but every other 
SDK and the Rust core use `Uint8..Uint128` (lowercase i) - see `HeaderKind` in 
iggy_common, plus the C# and Java SDKs. the numeric codes match so there's no 
wire impact, but this is the name C++ users type, and renaming a public enum 
after release is breaking. cheap to align to `Uint8` now.



##########
foreign/cpp/src/type_conversion.rs:
##########
@@ -314,12 +433,36 @@ impl From<RustConsumerGroupDetails> for 
ffi::ConsumerGroupDetails {
     }
 }
 
-impl From<RustIggyMessage> for ffi::IggyMessagePolled {
-    fn from(message: RustIggyMessage) -> Self {
+impl TryFrom<RustIggyMessage> for ffi::IggyMessagePolled {
+    type Error = String;
+
+    fn try_from(message: RustIggyMessage) -> Result<Self, Self::Error> {
         let id_bytes = message.header.id.to_le_bytes();
         let id_lo = u64::from_le_bytes(id_bytes[0..8].try_into().unwrap());
         let id_hi = u64::from_le_bytes(id_bytes[8..16].try_into().unwrap());
-        ffi::IggyMessagePolled {
+        let user_headers = match message.user_headers {

Review Comment:
   poll decode here is structural-only (via `WireUserHeaders`), so it accepts 
unknown kind codes 16-255 and wrong-width fixed types, whereas the Rust SDK 
poll is strict (`user_headers_from_wire` checks kind + width). same stored 
bytes read differently across SDKs. looks intentional for forward-compat - 
worth a comment saying so.



##########
foreign/cpp/src/type_conversion.rs:
##########
@@ -330,45 +473,352 @@ impl From<RustIggyMessage> for ffi::IggyMessagePolled {
             payload_length: message.header.payload_length,
             reserved: message.header.reserved,
             payload: message.payload.to_vec(),
-            user_headers: message
-                .user_headers
-                .map(|headers| headers.to_vec())
-                .unwrap_or_default(),
+            user_headers,
+        })
+    }
+}
+
+impl From<RustHeaderEntry> for ffi::HeaderEntry {
+    fn from(entry: RustHeaderEntry) -> Self {
+        ffi::HeaderEntry {
+            key: ffi::HeaderField {
+                kind: entry.key.kind().as_code(),
+                value: entry.key.as_bytes().to_vec(),
+            },
+            value: ffi::HeaderField {
+                kind: entry.value.kind().as_code(),
+                value: entry.value.as_bytes().to_vec(),
+            },
         }
     }
 }
 
+impl TryFrom<ffi::HeaderEntry> for RustHeaderEntry {
+    type Error = String;
+
+    fn try_from(entry: ffi::HeaderEntry) -> Result<Self, Self::Error> {
+        Ok(RustHeaderEntry {
+            key: {
+                let kind = RustHeaderKind::from_code(entry.key.kind)
+                    .map_err(|error| format!("Could not convert header field: 
{error}"))?;
+
+                match kind {
+                    RustHeaderKind::Raw => 
RustHeaderKey::try_from(entry.key.value)
+                        .map_err(|error| format!("Could not convert header 
field: {error}"))?,
+                    RustHeaderKind::String => {
+                        let value = 
String::from_utf8(entry.key.value).map_err(|_| {
+                            "Could not convert header field: invalid UTF-8 
string".to_string()
+                        })?;
+                        RustHeaderKey::try_from(value)
+                            .map_err(|error| format!("Could not convert header 
field: {error}"))?
+                    }
+                    RustHeaderKind::Bool => match entry.key.value.as_slice() {
+                        [0] => RustHeaderKey::from(false),
+                        [1] => RustHeaderKey::from(true),
+                        _ => {
+                            return Err(
+                                "Could not convert header field: bool values 
must encode as 0 or 1"
+                                    .to_string(),
+                            );
+                        }
+                    },
+                    RustHeaderKind::Int8 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int16 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int32 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int64 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int128 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(i128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int128 values 
require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint8 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(u8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint16 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(u16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint32 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(u32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint64 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(u64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint128 => match 
entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(u128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint128 
values require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float32 => match 
entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(f32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float32 
values require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float64 => match 
entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(f64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float64 
values require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                }
+            },
+            value: {
+                let kind = RustHeaderKind::from_code(entry.value.kind)
+                    .map_err(|error| format!("Could not convert header field: 
{error}"))?;
+
+                match kind {
+                    RustHeaderKind::Raw => 
RustHeaderValue::try_from(entry.value.value)
+                        .map_err(|error| format!("Could not convert header 
field: {error}"))?,
+                    RustHeaderKind::String => {
+                        let value = 
String::from_utf8(entry.value.value).map_err(|_| {
+                            "Could not convert header field: invalid UTF-8 
string".to_string()
+                        })?;
+                        RustHeaderValue::try_from(value)
+                            .map_err(|error| format!("Could not convert header 
field: {error}"))?
+                    }
+                    RustHeaderKind::Bool => match entry.value.value.as_slice() 
{
+                        [0] => RustHeaderValue::from(false),
+                        [1] => RustHeaderValue::from(true),
+                        _ => {
+                            return Err(
+                                "Could not convert header field: bool values 
must encode as 0 or 1"
+                                    .to_string(),
+                            );
+                        }
+                    },
+                    RustHeaderKind::Int8 => match entry.value.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderValue::from(i8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int16 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int32 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int64 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int128 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int128 values 
require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint8 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint16 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint32 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint64 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint128 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint128 
values require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float32 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(f32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float32 
values require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float64 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(f64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float64 
values require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                }
+            },
+        })
+    }
+}
+
 impl TryFrom<ffi::IggyMessageToSend> for RustIggyMessage {
     type Error = String;
 
     fn try_from(message: ffi::IggyMessageToSend) -> Result<Self, Self::Error> {
-        if !message.user_headers.is_empty() {
-            return Err(
-                "Could not convert message: user_headers are not yet supported 
in the C++ SDK"
-                    .to_string(),
-            );
+        let mut user_headers = BTreeMap::new();
+        for entry in message.user_headers {
+            let header_entry = RustHeaderEntry::try_from(entry)
+                .map_err(|error| format!("Could not convert message user 
headers: {error}"))?;
+            if user_headers
+                .insert(header_entry.key, header_entry.value)
+                .is_some()
+            {
+                return Err(
+                    "Could not convert message user headers: duplicate header 
key".to_string(),
+                );
+            }
         }
         let id = ((message.id_hi as u128) << 64) | (message.id_lo as u128);
         let payload = Bytes::from(message.payload);
-        RustIggyMessage::builder()
-            .id(id)
-            .payload(payload)
-            .build()
-            .map_err(|error| format!("Could not convert message: {error}"))
+        if user_headers.is_empty() {

Review Comment:
   the empty/non-empty branch here duplicates the whole builder chain just to 
conditionally add headers. the builder is bon and `user_headers` is an 
`Option`, so it generates `maybe_user_headers` - this collapses to one chain: 
`.maybe_user_headers((!user_headers.is_empty()).then_some(user_headers))`. same 
result, empty map stays `None`.



##########
foreign/cpp/src/type_conversion.rs:
##########
@@ -314,12 +433,36 @@ impl From<RustConsumerGroupDetails> for 
ffi::ConsumerGroupDetails {
     }
 }
 
-impl From<RustIggyMessage> for ffi::IggyMessagePolled {
-    fn from(message: RustIggyMessage) -> Self {
+impl TryFrom<RustIggyMessage> for ffi::IggyMessagePolled {
+    type Error = String;
+
+    fn try_from(message: RustIggyMessage) -> Result<Self, Self::Error> {
         let id_bytes = message.header.id.to_le_bytes();
         let id_lo = u64::from_le_bytes(id_bytes[0..8].try_into().unwrap());
         let id_hi = u64::from_le_bytes(id_bytes[8..16].try_into().unwrap());
-        ffi::IggyMessagePolled {
+        let user_headers = match message.user_headers {
+            Some(raw_headers) => {
+                let wire_headers = 
WireUserHeaders::from_bytes(raw_headers).map_err(|error| {

Review Comment:
   poll decode propagates a `WireUserHeaders::from_bytes` error here, and 
`TryFrom<RustPolledMessages>` collects with `collect::<Result<Vec,_>>()?` (line 
821), so one message with a malformed user-header TLV fails the entire poll 
batch - every good message in that batch is dropped with it. the server does 
not validate user-header TLV on the produce path (no `validate_user_headers` / 
`WireUserHeaders` call anywhere in core/server), so a non-first-party or buggy 
producer can store a header that then breaks C++ polls of that range. with 
offset-based polling the consumer re-fetches the same range and keeps failing 
(stuck); with auto_commit the batch gets skipped and its good messages are 
lost. the reference Rust SDK handles this gracefully - `user_headers_map()` 
warns and returns `Ok(None)`, keeping the message readable. suggest mirroring 
that: match `from_bytes`, on error warn and emit empty headers, keep the 
message.



##########
foreign/cpp/src/type_conversion.rs:
##########
@@ -330,45 +473,352 @@ impl From<RustIggyMessage> for ffi::IggyMessagePolled {
             payload_length: message.header.payload_length,
             reserved: message.header.reserved,
             payload: message.payload.to_vec(),
-            user_headers: message
-                .user_headers
-                .map(|headers| headers.to_vec())
-                .unwrap_or_default(),
+            user_headers,
+        })
+    }
+}
+
+impl From<RustHeaderEntry> for ffi::HeaderEntry {
+    fn from(entry: RustHeaderEntry) -> Self {
+        ffi::HeaderEntry {
+            key: ffi::HeaderField {
+                kind: entry.key.kind().as_code(),
+                value: entry.key.as_bytes().to_vec(),
+            },
+            value: ffi::HeaderField {
+                kind: entry.value.kind().as_code(),
+                value: entry.value.as_bytes().to_vec(),
+            },
         }
     }
 }
 
+impl TryFrom<ffi::HeaderEntry> for RustHeaderEntry {

Review Comment:
   the key block and value block here are two nearly identical 15-arm matches 
that differ only in producing `HeaderKey` vs `HeaderValue` - and both are 
`HeaderField<T>` with all the `From` / `TryFrom` impls generic over the marker. 
this collapses to one generic helper `fn decode_field<T>(kind: u8, value: 
Vec<u8>) -> Result<HeaderField<T>, String>` called for key and value, cutting 
around 135 lines and folding the duplicated error strings into one spot.



##########
foreign/cpp/src/type_conversion.rs:
##########
@@ -330,45 +473,352 @@ impl From<RustIggyMessage> for ffi::IggyMessagePolled {
             payload_length: message.header.payload_length,
             reserved: message.header.reserved,
             payload: message.payload.to_vec(),
-            user_headers: message
-                .user_headers
-                .map(|headers| headers.to_vec())
-                .unwrap_or_default(),
+            user_headers,
+        })
+    }
+}
+
+impl From<RustHeaderEntry> for ffi::HeaderEntry {

Review Comment:
   this `From<RustHeaderEntry> for ffi::HeaderEntry` has no callers - the poll 
path builds `ffi::HeaderEntry` inline from the `WireUserHeaders` iterator, and 
`RustIggyMessage` stores headers as `Option<Bytes>`, never 
`Vec<RustHeaderEntry>`. dead, can be deleted.



##########
foreign/cpp/src/type_conversion.rs:
##########
@@ -330,45 +473,352 @@ impl From<RustIggyMessage> for ffi::IggyMessagePolled {
             payload_length: message.header.payload_length,
             reserved: message.header.reserved,
             payload: message.payload.to_vec(),
-            user_headers: message
-                .user_headers
-                .map(|headers| headers.to_vec())
-                .unwrap_or_default(),
+            user_headers,
+        })
+    }
+}
+
+impl From<RustHeaderEntry> for ffi::HeaderEntry {
+    fn from(entry: RustHeaderEntry) -> Self {
+        ffi::HeaderEntry {
+            key: ffi::HeaderField {
+                kind: entry.key.kind().as_code(),
+                value: entry.key.as_bytes().to_vec(),
+            },
+            value: ffi::HeaderField {
+                kind: entry.value.kind().as_code(),
+                value: entry.value.as_bytes().to_vec(),
+            },
         }
     }
 }
 
+impl TryFrom<ffi::HeaderEntry> for RustHeaderEntry {
+    type Error = String;
+
+    fn try_from(entry: ffi::HeaderEntry) -> Result<Self, Self::Error> {
+        Ok(RustHeaderEntry {
+            key: {
+                let kind = RustHeaderKind::from_code(entry.key.kind)
+                    .map_err(|error| format!("Could not convert header field: 
{error}"))?;
+
+                match kind {
+                    RustHeaderKind::Raw => 
RustHeaderKey::try_from(entry.key.value)
+                        .map_err(|error| format!("Could not convert header 
field: {error}"))?,
+                    RustHeaderKind::String => {
+                        let value = 
String::from_utf8(entry.key.value).map_err(|_| {
+                            "Could not convert header field: invalid UTF-8 
string".to_string()
+                        })?;
+                        RustHeaderKey::try_from(value)
+                            .map_err(|error| format!("Could not convert header 
field: {error}"))?
+                    }
+                    RustHeaderKind::Bool => match entry.key.value.as_slice() {
+                        [0] => RustHeaderKey::from(false),
+                        [1] => RustHeaderKey::from(true),
+                        _ => {
+                            return Err(
+                                "Could not convert header field: bool values 
must encode as 0 or 1"
+                                    .to_string(),
+                            );
+                        }
+                    },
+                    RustHeaderKind::Int8 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int16 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int32 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int64 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(i64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int128 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(i128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int128 values 
require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint8 => match entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(u8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint16 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(u16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint32 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(u32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint64 => match entry.key.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderKey::from(u64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint128 => match 
entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(u128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint128 
values require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float32 => match 
entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(f32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float32 
values require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float64 => match 
entry.key.value.try_into() {
+                        Ok(bytes) => 
RustHeaderKey::from(f64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float64 
values require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                }
+            },
+            value: {
+                let kind = RustHeaderKind::from_code(entry.value.kind)
+                    .map_err(|error| format!("Could not convert header field: 
{error}"))?;
+
+                match kind {
+                    RustHeaderKind::Raw => 
RustHeaderValue::try_from(entry.value.value)
+                        .map_err(|error| format!("Could not convert header 
field: {error}"))?,
+                    RustHeaderKind::String => {
+                        let value = 
String::from_utf8(entry.value.value).map_err(|_| {
+                            "Could not convert header field: invalid UTF-8 
string".to_string()
+                        })?;
+                        RustHeaderValue::try_from(value)
+                            .map_err(|error| format!("Could not convert header 
field: {error}"))?
+                    }
+                    RustHeaderKind::Bool => match entry.value.value.as_slice() 
{
+                        [0] => RustHeaderValue::from(false),
+                        [1] => RustHeaderValue::from(true),
+                        _ => {
+                            return Err(
+                                "Could not convert header field: bool values 
must encode as 0 or 1"
+                                    .to_string(),
+                            );
+                        }
+                    },
+                    RustHeaderKind::Int8 => match entry.value.value.try_into() 
{
+                        Ok(bytes) => 
RustHeaderValue::from(i8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int16 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int32 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int64 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Int128 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(i128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: int128 values 
require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint8 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u8::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint8 values 
require exactly 1 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint16 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u16::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint16 values 
require exactly 2 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint32 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint32 values 
require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint64 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint64 values 
require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Uint128 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(u128::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: uint128 
values require exactly 16 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float32 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(f32::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float32 
values require exactly 4 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                    RustHeaderKind::Float64 => match 
entry.value.value.try_into() {
+                        Ok(bytes) => 
RustHeaderValue::from(f64::from_le_bytes(bytes)),
+                        Err(value) => {
+                            return Err(format!(
+                                "Could not convert header field: float64 
values require exactly 8 bytes, got {}",
+                                value.len()
+                            ));
+                        }
+                    },
+                }
+            },
+        })
+    }
+}
+
 impl TryFrom<ffi::IggyMessageToSend> for RustIggyMessage {
     type Error = String;
 
     fn try_from(message: ffi::IggyMessageToSend) -> Result<Self, Self::Error> {
-        if !message.user_headers.is_empty() {
-            return Err(
-                "Could not convert message: user_headers are not yet supported 
in the C++ SDK"
-                    .to_string(),
-            );
+        let mut user_headers = BTreeMap::new();

Review Comment:
   worth a note that user headers are unordered and keys unique: they go into a 
`BTreeMap`, so the C++ `Vec<HeaderEntry>` insertion order is not preserved on 
send or poll (sorted by kind+value), and duplicate keys are rejected. the `Vec` 
shape reads like it keeps order.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to