Skip to main content

agntcy_slim_proto/
impls.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::fmt::Display;
6use std::hash::{Hash, Hasher};
7use std::time::Duration;
8
9use slim_version::version;
10use thiserror::Error;
11use twox_hash::XxHash64;
12
13use crate::dataplane::proto::v1::command_payload::CommandPayloadType;
14use crate::dataplane::proto::v1::content::ContentType;
15use crate::dataplane::proto::v1::link::LinkType as ProtoLinkType;
16use crate::dataplane::proto::v1::message::MessageType;
17use crate::dataplane::proto::v1::message::MessageType::Link as ProtoLinkMessageType;
18use crate::dataplane::proto::v1::message::MessageType::Publish as ProtoPublishType;
19use crate::dataplane::proto::v1::message::MessageType::Subscribe as ProtoSubscribeType;
20use crate::dataplane::proto::v1::message::MessageType::SubscriptionAck as ProtoSubscriptionAckType;
21use crate::dataplane::proto::v1::message::MessageType::Unsubscribe as ProtoUnsubscribeType;
22use crate::dataplane::proto::v1::{
23    ApplicationPayload, CommandPayload, Content, DiscoveryReplyPayload, DiscoveryRequestPayload,
24    EncodedName, GroupAckPayload, GroupAddPayload, GroupClosePayload, GroupNackPayload,
25    GroupProposalPayload, GroupRemovePayload, GroupWelcomePayload, HeartbeatPayload,
26    JoinReplyPayload, JoinRequestPayload, LeaveReplyPayload, LeaveRequestPayload,
27    Link as ProtoLink, LinkConnectionType, LinkNegotiationPayload, Message as ProtoMessage,
28    MlsPayload, MlsSettings as ProtoMlsSettings, Name as ProtoName, NameId, Participant,
29    ParticipantSettings, ParticipantState, Publish as ProtoPublish, SessionHeader,
30    SessionMessageType, SessionType as ProtoSessionType, SlimHeader, StringName,
31    Subscribe as ProtoSubscribe, SubscriptionAck as ProtoSubscriptionAck, TimerSettings,
32    Unsubscribe as ProtoUnsubscribe, UpdateParticipantStatePayload,
33};
34
35fn calculate_hash<T: Hash + ?Sized>(t: &T) -> u64 {
36    let mut hasher = XxHash64::default();
37    t.hash(&mut hasher);
38    hasher.finish()
39}
40
41#[derive(Error, Debug, Clone, PartialEq, Eq)]
42pub enum NameError {
43    #[error("invalid name id format: {0}")]
44    InvalidNameIdFormat(String),
45    #[error("invalid name format: {0}")]
46    InvalidNameFormat(String),
47}
48
49/// DELETE_GROUP indicates that the entire group is being closed.
50/// The moderator sets this metadata on the leave message sent to all participants
51/// when a channel deletion is requested.
52pub const DELETE_GROUP: &str = "DELETE_GROUP";
53
54/// PUBLISH_TO indicates that a message should bypass normal sequencing and be delivered directly to the specified endpoint.
55/// This is used in group sessions when the application API `publish_to` is used instead of `publish`.
56/// The value is set to `TRUE_VAL` for direct delivery without buffering.
57pub const PUBLISH_TO: &str = "PUBLISH_TO";
58
59/// LEAVE_REPLY_SENT indicates that a leave reply was already sent to the participant.
60/// This is used internally by the moderator when a LEAVING_SESSION leave request is queued
61/// because the moderator is busy. The metadata is swapped from LEAVING_SESSION to LEAVE_REPLY_SENT
62/// so that on re-processing, the leave reply is not sent twice.
63/// The value is set to `TRUE_VAL`.
64pub const LEAVE_REPLY_SENT: &str = "LEAVE_REPLY_SENT";
65
66/// LEAVING_SESSION indicates that a participant is gracefully leaving the session.
67/// This is used in the leave request message sent by a participant closing the session to the moderator.
68/// The value is set to `TRUE_VAL` for graceful departure.
69pub const LEAVING_SESSION: &str = "LEAVING_SESSION";
70
71/// Standard string value representing a boolean "true" in message metadata.
72pub const TRUE_VAL: &str = "TRUE";
73
74/// Standard string value representing a boolean "false" in message metadata.
75pub const FALSE_VAL: &str = "FALSE";
76
77/// Maximum message ID for normal sequenced messages.
78/// Messages with IDs in the range [0, MAX_PUBLISH_ID] follow normal sequencing.
79/// Messages with IDs > MAX_PUBLISH_ID (used for `PUBLISH_TO` messages) bypass sequencing.
80/// Value: Half of u32::MAX to allow a separate ID space for out-of-band messages.
81pub const MAX_PUBLISH_ID: u32 = u32::MAX / 2;
82
83/// Default TTL value for messages that do not have an explicit TTL set.
84pub const DEFAULT_TTL: u32 = 8;
85
86#[derive(Error, Debug, PartialEq)]
87pub enum MessageError {
88    #[error("SLIM header not found")]
89    SlimHeaderNotFound,
90    #[error("source not found")]
91    SourceNotFound,
92    #[error("source encoded name not found")]
93    SourceEncodedNameNotFound,
94    #[error("destination not found")]
95    DestinationNotFound,
96    #[error("destination encoded name not found")]
97    DestinationEncodedNameNotFound,
98    #[error("session header not found")]
99    SessionHeaderNotFound,
100    #[error("message type not found")]
101    MessageTypeNotFound,
102    #[error("incoming connection not found")]
103    IncomingConnectionNotFound,
104    #[error("content type is not set")]
105    ContentTypeNotSet,
106    #[error("content is not an application payload")]
107    NotApplicationPayload,
108    #[error("content is not a command payload")]
109    NotCommandPayload,
110    #[error("link type is not set")]
111    LinkTypeNotSet,
112    #[error("invalid command payload type: expected {expected}, got {got}")]
113    InvalidCommandPayloadType {
114        expected: Box<String>,
115        got: Box<String>,
116    },
117    #[error("builder error: source is required")]
118    BuilderErrorSourceRequired,
119    #[error("builder error: destination is required")]
120    BuilderErrorDestinationRequired,
121    #[error("participant name not found")]
122    ParticipantNameNotFound,
123    #[error("participant settings not found")]
124    ParticipantSettingsNotFound,
125}
126
127/// Struct grouping the SLIM header flags for convenience.
128#[derive(Debug, Clone)]
129pub struct SlimHeaderFlags {
130    pub fanout: u32,
131    pub recv_from: Option<u64>,
132    pub forward_to: Option<u64>,
133    pub incoming_conn: Option<u64>,
134    pub error: Option<bool>,
135    pub ttl: u32,
136}
137
138impl Default for SlimHeaderFlags {
139    fn default() -> Self {
140        Self {
141            fanout: 1,
142            recv_from: None,
143            forward_to: None,
144            incoming_conn: None,
145            error: None,
146            ttl: DEFAULT_TTL,
147        }
148    }
149}
150
151impl Display for SlimHeaderFlags {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(
154            f,
155            "fanout: {}, recv_from: {:?}, forward_to: {:?}, incoming_conn: {:?}, error: {:?}, ttl: {:?}",
156            self.fanout, self.recv_from, self.forward_to, self.incoming_conn, self.error, self.ttl
157        )
158    }
159}
160
161impl SlimHeaderFlags {
162    pub fn new(
163        fanout: u32,
164        recv_from: Option<u64>,
165        forward_to: Option<u64>,
166        incoming_conn: Option<u64>,
167        error: Option<bool>,
168    ) -> Self {
169        Self {
170            fanout,
171            recv_from,
172            forward_to,
173            incoming_conn,
174            error,
175            ttl: DEFAULT_TTL,
176        }
177    }
178
179    pub fn with_fanout(self, fanout: u32) -> Self {
180        Self { fanout, ..self }
181    }
182
183    pub fn with_recv_from(self, recv_from: u64) -> Self {
184        Self {
185            recv_from: Some(recv_from),
186            ..self
187        }
188    }
189
190    pub fn with_forward_to(self, forward_to: u64) -> Self {
191        Self {
192            forward_to: Some(forward_to),
193            ..self
194        }
195    }
196
197    pub fn with_incoming_conn(self, incoming_conn: u64) -> Self {
198        Self {
199            incoming_conn: Some(incoming_conn),
200            ..self
201        }
202    }
203
204    pub fn with_error(self, error: bool) -> Self {
205        Self {
206            error: Some(error),
207            ..self
208        }
209    }
210
211    pub fn with_ttl(self, ttl: u32) -> Self {
212        Self { ttl, ..self }
213    }
214}
215
216impl NameId {
217    pub const NULL_COMPONENT: u128 = u128::MAX;
218    pub const RESERVED_IDS: u128 = 50;
219
220    pub const fn is_reserved_id(id: u128) -> bool {
221        id >= (u128::MAX - Self::RESERVED_IDS)
222    }
223}
224
225impl std::fmt::Display for NameId {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        let s: String = (*self).into();
228        write!(f, "{}", s)
229    }
230}
231
232impl From<u128> for NameId {
233    fn from(id: u128) -> Self {
234        NameId {
235            id_0: (id >> 64) as u64,
236            id_1: (id & 0xFFFFFFFFFFFFFFFF) as u64,
237        }
238    }
239}
240
241impl From<NameId> for u128 {
242    fn from(name_id: NameId) -> Self {
243        (name_id.id_0 as u128) << 64 | (name_id.id_1 as u128)
244    }
245}
246
247impl TryFrom<String> for NameId {
248    type Error = NameError;
249
250    fn try_from(s: String) -> Result<Self, Self::Error> {
251        match s.as_str() {
252            "NULL_COMPONENT" => Ok(Self::from(Self::NULL_COMPONENT)),
253            _ => {
254                if let Ok(uuid) = uuid::Uuid::parse_str(&s) {
255                    return Ok(Self::from(uuid.as_u128()));
256                }
257                Err(NameError::InvalidNameIdFormat(s))
258            }
259        }
260    }
261}
262
263impl From<NameId> for String {
264    fn from(name_id: NameId) -> String {
265        let val: u128 = name_id.into();
266        match val {
267            NameId::NULL_COMPONENT => "NULL_COMPONENT".to_string(),
268            id => uuid::Uuid::from_u128(id).to_string(),
269        }
270    }
271}
272
273impl EncodedName {
274    pub fn id(&self) -> u128 {
275        self.name_id
276            .as_ref()
277            .map_or(NameId::NULL_COMPONENT, |nid| (*nid).into())
278    }
279
280    pub fn string_id(&self) -> String {
281        self.name_id
282            .as_ref()
283            .map_or("NULL_COMPONENT".to_string(), |nid| (*nid).into())
284    }
285}
286
287impl ProtoName {
288    pub fn from_strings(components: [impl Into<String>; 3]) -> Self {
289        let [s0, s1, s2] = components.map(Into::into);
290        Self {
291            name: Some(EncodedName {
292                component_0: calculate_hash(&s0),
293                component_1: calculate_hash(&s1),
294                component_2: calculate_hash(&s2),
295                name_id: Some(NameId::from(NameId::NULL_COMPONENT)),
296            }),
297            str_name: Some(StringName {
298                str_component_0: s0,
299                str_component_1: s1,
300                str_component_2: s2,
301            }),
302        }
303    }
304
305    pub fn with_id(mut self, id: u128) -> Self {
306        self.name.as_mut().expect("encoded name missing").name_id = Some(NameId::from(id));
307        self
308    }
309
310    pub fn id(&self) -> u128 {
311        self.name.as_ref().expect("encoded name missing").id()
312    }
313
314    pub fn string_id(&self) -> String {
315        self.name
316            .as_ref()
317            .expect("encoded name missing")
318            .string_id()
319    }
320
321    pub fn name_id(&self) -> Option<NameId> {
322        self.name.as_ref().expect("encoded name missing").name_id
323    }
324
325    pub fn has_id(&self) -> bool {
326        self.id() != NameId::NULL_COMPONENT
327    }
328
329    pub fn set_id(&mut self, id: u128) {
330        self.name.as_mut().expect("encoded name missing").name_id = Some(NameId::from(id));
331    }
332
333    pub fn reset_id(&mut self) {
334        self.name.as_mut().expect("encoded name missing").name_id =
335            Some(NameId::from(NameId::NULL_COMPONENT));
336    }
337
338    pub fn match_prefix(&self, other: &ProtoName) -> bool {
339        let a = self.name.as_ref().expect("encoded name missing");
340        let b = other.name.as_ref().expect("encoded name missing");
341        a.component_0 == b.component_0
342            && a.component_1 == b.component_1
343            && a.component_2 == b.component_2
344    }
345
346    pub fn str_components(&self) -> (&str, &str, &str) {
347        let s = self.str_name.as_ref().expect("string name missing");
348        (&s.str_component_0, &s.str_component_1, &s.str_component_2)
349    }
350
351    pub fn parse_name(s: &str) -> Result<ProtoName, NameError> {
352        let trimmed = s.trim();
353        if trimmed.is_empty() {
354            return Err(NameError::InvalidNameFormat(s.to_string()));
355        }
356        let parts: Vec<&str> = trimmed.split('/').map(str::trim).collect();
357        if parts.len() != 3 || parts.iter().any(|p| p.is_empty()) {
358            return Err(NameError::InvalidNameFormat(s.to_string()));
359        }
360        Ok(ProtoName::from_strings([parts[0], parts[1], parts[2]]))
361    }
362}
363
364impl std::fmt::Display for ProtoName {
365    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366        if let Some(s) = &self.str_name {
367            write!(
368                f,
369                "{}/{}/{}/{}",
370                s.str_component_0,
371                s.str_component_1,
372                s.str_component_2,
373                self.name
374                    .as_ref()
375                    .and_then(|n| n.name_id)
376                    .map_or("NULL_COMPONENT".to_string(), |id| id.to_string())
377            )
378        } else if let Some(enc) = &self.name {
379            write!(
380                f,
381                "{}/{}/{}/{}",
382                enc.component_0,
383                enc.component_1,
384                enc.component_2,
385                enc.name_id
386                    .as_ref()
387                    .map_or("NULL_COMPONENT".to_string(), |id| id.to_string())
388            )
389        } else {
390            write!(f, "<empty>")
391        }
392    }
393}
394
395impl ParticipantSettings {
396    pub fn bidirectional() -> Self {
397        Self {
398            sends_data: true,
399            receives_data: true,
400        }
401    }
402
403    pub fn send_only() -> Self {
404        Self {
405            sends_data: true,
406            receives_data: false,
407        }
408    }
409
410    pub fn receive_only() -> Self {
411        Self {
412            sends_data: false,
413            receives_data: true,
414        }
415    }
416
417    pub fn is_sender(&self) -> bool {
418        self.sends_data
419    }
420
421    pub fn is_receiver(&self) -> bool {
422        self.receives_data
423    }
424}
425
426impl Participant {
427    pub fn new(name: ProtoName, settings: ParticipantSettings) -> Self {
428        Self {
429            name: Some(name),
430            settings: Some(settings),
431        }
432    }
433
434    pub fn get_name(&self) -> Result<ProtoName, MessageError> {
435        match &self.name {
436            Some(name) => Ok(name.clone()),
437            None => Err(MessageError::ParticipantNameNotFound),
438        }
439    }
440
441    pub fn get_settings(&self) -> Result<&ParticipantSettings, MessageError> {
442        match &self.settings {
443            Some(settings) => Ok(settings),
444            None => Err(MessageError::ParticipantSettingsNotFound),
445        }
446    }
447}
448
449impl Display for MessageType {
450    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451        match self {
452            MessageType::Publish(_) => write!(f, "publish"),
453            MessageType::Subscribe(_) => write!(f, "subscribe"),
454            MessageType::Unsubscribe(_) => write!(f, "unsubscribe"),
455            MessageType::Link(_) => write!(f, "link"),
456            MessageType::SubscriptionAck(_) => write!(f, "subscription_ack"),
457        }
458    }
459}
460
461impl SlimHeader {
462    pub fn new(
463        source: ProtoName,
464        destination: ProtoName,
465        identity: &str,
466        flags: Option<SlimHeaderFlags>,
467    ) -> Self {
468        let flags = flags.unwrap_or_default();
469        Self {
470            source: Some(source),
471            destination: Some(destination),
472            identity: identity.to_string(),
473            fanout: flags.fanout,
474            version: version().to_string(),
475            recv_from: flags.recv_from,
476            forward_to: flags.forward_to,
477            incoming_conn: flags.incoming_conn,
478            error: flags.error,
479            header_mac: None,
480            ttl: flags.ttl,
481            e2e_header_sig: None,
482        }
483    }
484
485    pub fn clear_flags(&mut self) {
486        self.recv_from = None;
487        self.forward_to = None;
488    }
489
490    pub fn get_fanout(&self) -> u32 {
491        self.fanout
492    }
493
494    pub fn get_recv_from(&self) -> Option<u64> {
495        self.recv_from
496    }
497
498    pub fn get_forward_to(&self) -> Option<u64> {
499        self.forward_to
500    }
501
502    pub fn get_incoming_conn(&self) -> Option<u64> {
503        self.incoming_conn
504    }
505
506    pub fn get_error(&self) -> Option<bool> {
507        self.error
508    }
509
510    pub fn get_source(&self) -> ProtoName {
511        self.source.clone().expect("source not found")
512    }
513
514    pub fn get_encoded_source(&self) -> EncodedName {
515        self.source
516            .as_ref()
517            .expect("source not found")
518            .name
519            .expect("source encoded name not found")
520    }
521
522    pub fn get_dst(&self) -> ProtoName {
523        self.destination.clone().expect("destination not found")
524    }
525
526    pub fn get_encoded_dst(&self) -> EncodedName {
527        self.destination
528            .as_ref()
529            .expect("destination not found")
530            .name
531            .expect("destination encoded name not found")
532    }
533
534    pub fn get_identity(&self) -> String {
535        self.identity.clone()
536    }
537
538    pub fn get_version(&self) -> String {
539        self.version.clone()
540    }
541
542    pub fn set_source(&mut self, source: ProtoName) {
543        self.source = Some(source);
544    }
545
546    pub fn set_destination(&mut self, dst: ProtoName) {
547        self.destination = Some(dst);
548    }
549
550    pub fn set_identity(&mut self, identity: String) {
551        self.identity = identity;
552    }
553
554    pub fn set_fanout(&mut self, fanout: u32) {
555        self.fanout = fanout;
556    }
557
558    pub fn set_recv_from(&mut self, recv_from: Option<u64>) {
559        self.recv_from = recv_from;
560    }
561
562    pub fn set_forward_to(&mut self, forward_to: Option<u64>) {
563        self.forward_to = forward_to;
564    }
565
566    pub fn set_error(&mut self, error: Option<bool>) {
567        self.error = error;
568    }
569
570    pub fn set_incoming_conn(&mut self, incoming_conn: Option<u64>) {
571        self.incoming_conn = incoming_conn;
572    }
573
574    pub fn set_error_flag(&mut self, error: Option<bool>) {
575        self.error = error;
576    }
577
578    pub fn get_ttl(&self) -> u32 {
579        self.ttl
580    }
581
582    pub fn set_ttl(&mut self, ttl: u32) {
583        self.ttl = ttl;
584    }
585
586    pub fn decrement_ttl(&mut self) -> u32 {
587        self.ttl = self.ttl.saturating_sub(1);
588        self.ttl
589    }
590
591    pub fn get_connections(&self) -> (u64, Option<u64>, Option<u64>) {
592        let incoming = self
593            .get_incoming_conn()
594            .expect("incoming connection not found");
595        (incoming, self.get_recv_from(), self.get_forward_to())
596    }
597}
598
599impl SessionHeader {
600    pub fn new(
601        session_type: i32,
602        session_message_type: i32,
603        session_id: u32,
604        message_id: u32,
605    ) -> Self {
606        Self {
607            session_type,
608            session_message_type,
609            session_id,
610            message_id,
611        }
612    }
613
614    pub fn get_session_id(&self) -> u32 {
615        self.session_id
616    }
617
618    pub fn get_message_id(&self) -> u32 {
619        self.message_id
620    }
621
622    pub fn set_session_id(&mut self, session_id: u32) {
623        self.session_id = session_id;
624    }
625
626    pub fn set_message_id(&mut self, message_id: u32) {
627        self.message_id = message_id;
628    }
629
630    pub fn clear(&mut self) {
631        self.session_id = 0;
632        self.message_id = 0;
633    }
634}
635
636impl SessionMessageType {
637    pub fn is_command_message(&self) -> bool {
638        matches!(
639            self,
640            SessionMessageType::DiscoveryRequest
641                | SessionMessageType::DiscoveryReply
642                | SessionMessageType::JoinRequest
643                | SessionMessageType::JoinReply
644                | SessionMessageType::LeaveRequest
645                | SessionMessageType::LeaveReply
646                | SessionMessageType::GroupAdd
647                | SessionMessageType::GroupRemove
648                | SessionMessageType::GroupWelcome
649                | SessionMessageType::GroupClose
650                | SessionMessageType::GroupProposal
651                | SessionMessageType::GroupAck
652                | SessionMessageType::GroupNack
653                | SessionMessageType::Heartbeat
654                | SessionMessageType::UpdateParticipantState
655        )
656    }
657
658    pub fn is_post_session_control(&self) -> bool {
659        matches!(
660            self,
661            SessionMessageType::LeaveRequest
662                | SessionMessageType::LeaveReply
663                | SessionMessageType::GroupAdd
664                | SessionMessageType::GroupRemove
665                | SessionMessageType::GroupClose
666                | SessionMessageType::GroupProposal
667                | SessionMessageType::GroupAck
668                | SessionMessageType::GroupNack
669                | SessionMessageType::Heartbeat
670                | SessionMessageType::UpdateParticipantState
671        )
672    }
673}
674
675impl ProtoSubscribe {
676    fn new(
677        source: ProtoName,
678        dst: ProtoName,
679        identity: Option<&str>,
680        flags: Option<SlimHeaderFlags>,
681    ) -> Self {
682        let id = identity.unwrap_or("");
683        let header = Some(SlimHeader::new(source, dst, id, flags));
684        ProtoSubscribe {
685            header,
686            subscription_id: 0,
687        }
688    }
689}
690
691impl From<ProtoMessage> for ProtoSubscribe {
692    fn from(message: ProtoMessage) -> Self {
693        match message.message_type {
694            Some(ProtoSubscribeType(s)) => s,
695            _ => panic!("message type is not subscribe"),
696        }
697    }
698}
699
700impl ProtoUnsubscribe {
701    fn new(
702        source: ProtoName,
703        dst: ProtoName,
704        identity: Option<&str>,
705        flags: Option<SlimHeaderFlags>,
706    ) -> Self {
707        let id = identity.unwrap_or("");
708        let header = Some(SlimHeader::new(source, dst, id, flags));
709        ProtoUnsubscribe {
710            header,
711            subscription_id: 0,
712        }
713    }
714}
715
716impl From<ProtoMessage> for ProtoUnsubscribe {
717    fn from(message: ProtoMessage) -> Self {
718        match message.message_type {
719            Some(ProtoUnsubscribeType(u)) => u,
720            _ => panic!("message type is not unsubscribe"),
721        }
722    }
723}
724
725impl ProtoPublish {
726    fn with_header(
727        header: Option<SlimHeader>,
728        session: Option<SessionHeader>,
729        payload: Option<Content>,
730    ) -> Self {
731        ProtoPublish {
732            header,
733            session,
734            msg: payload,
735        }
736    }
737
738    pub fn get_slim_header(&self) -> &SlimHeader {
739        self.header.as_ref().expect("SLIM header missing")
740    }
741
742    pub fn get_session_header(&self) -> &SessionHeader {
743        self.session.as_ref().expect("session header missing")
744    }
745
746    pub fn get_slim_header_as_mut(&mut self) -> &mut SlimHeader {
747        self.header.as_mut().expect("SLIM header missing")
748    }
749
750    pub fn get_session_header_as_mut(&mut self) -> &mut SessionHeader {
751        self.session.as_mut().expect("session header missing")
752    }
753
754    pub fn get_payload(&self) -> &Content {
755        self.msg.as_ref().expect("payload missing")
756    }
757
758    pub fn set_payload(&mut self, payload: Content) {
759        self.msg = Some(payload);
760    }
761
762    pub fn is_command(&self) -> bool {
763        match &self
764            .get_payload()
765            .content_type
766            .as_ref()
767            .expect("content missing")
768        {
769            ContentType::AppPayload(_) => false,
770            ContentType::CommandPayload(_) => true,
771        }
772    }
773
774    pub fn get_application_payload(&self) -> &ApplicationPayload {
775        match self
776            .get_payload()
777            .content_type
778            .as_ref()
779            .expect("content missing")
780        {
781            ContentType::AppPayload(application_payload) => application_payload,
782            ContentType::CommandPayload(_) => panic!("the payload is not an application payload"),
783        }
784    }
785
786    pub fn get_command_payload(&self) -> &CommandPayload {
787        match self
788            .get_payload()
789            .content_type
790            .as_ref()
791            .expect("content missing")
792        {
793            ContentType::AppPayload(_) => panic!("the payload is not a command payload"),
794            ContentType::CommandPayload(command_payload) => command_payload,
795        }
796    }
797}
798
799impl From<ProtoMessage> for ProtoPublish {
800    fn from(message: ProtoMessage) -> Self {
801        match message.message_type {
802            Some(ProtoPublishType(p)) => p,
803            _ => panic!("message type is not publish"),
804        }
805    }
806}
807
808macro_rules! impl_payload_extractors {
809    ($($method_name:ident => $getter_method:ident($payload_type:ty)),* $(,)?) => {
810        $(
811            pub fn $method_name(&self) -> Result<&$payload_type, MessageError> {
812                self.extract_command_payload()?.$getter_method()
813            }
814        )*
815    };
816}
817
818impl ProtoMessage {
819    fn new(metadata: HashMap<String, String>, message_type: MessageType) -> Self {
820        ProtoMessage {
821            metadata,
822            message_type: Some(message_type),
823        }
824    }
825
826    fn validate_link(link: &ProtoLink) -> Result<(), MessageError> {
827        if link.link_type.is_none() {
828            return Err(MessageError::LinkTypeNotSet);
829        }
830        Ok(())
831    }
832
833    fn validate_routed_header(slim_header: &SlimHeader) -> Result<(), MessageError> {
834        match &slim_header.source {
835            None => return Err(MessageError::SourceNotFound),
836            Some(src) if src.name.is_none() => return Err(MessageError::SourceEncodedNameNotFound),
837            _ => {}
838        }
839        match &slim_header.destination {
840            None => return Err(MessageError::DestinationNotFound),
841            Some(dst) if dst.name.is_none() => {
842                return Err(MessageError::DestinationEncodedNameNotFound);
843            }
844            _ => {}
845        }
846        Ok(())
847    }
848
849    fn validate_publish(p: &ProtoPublish) -> Result<(), MessageError> {
850        let hdr = p.header.as_ref().ok_or(MessageError::SlimHeaderNotFound)?;
851        Self::validate_routed_header(hdr)?;
852        if p.session.is_none() {
853            return Err(MessageError::SessionHeaderNotFound);
854        }
855        Ok(())
856    }
857
858    fn validate_subscribe(s: &ProtoSubscribe) -> Result<(), MessageError> {
859        let hdr = s.header.as_ref().ok_or(MessageError::SlimHeaderNotFound)?;
860        Self::validate_routed_header(hdr)
861    }
862
863    fn validate_unsubscribe(u: &ProtoUnsubscribe) -> Result<(), MessageError> {
864        let hdr = u.header.as_ref().ok_or(MessageError::SlimHeaderNotFound)?;
865        Self::validate_routed_header(hdr)
866    }
867
868    pub fn validate(&self) -> Result<(), MessageError> {
869        match &self.message_type {
870            None => Err(MessageError::MessageTypeNotFound),
871            Some(ProtoLinkMessageType(link)) => Self::validate_link(link),
872            Some(ProtoPublishType(p)) => Self::validate_publish(p),
873            Some(ProtoSubscribeType(s)) => Self::validate_subscribe(s),
874            Some(ProtoUnsubscribeType(u)) => Self::validate_unsubscribe(u),
875            Some(ProtoSubscriptionAckType(_)) => Ok(()),
876        }
877    }
878
879    pub fn insert_metadata(&mut self, key: String, val: String) {
880        self.metadata.insert(key, val);
881    }
882
883    pub fn remove_metadata(&mut self, key: &str) -> Option<String> {
884        self.metadata.remove(key)
885    }
886
887    pub fn contains_metadata(&self, key: &str) -> bool {
888        self.metadata.contains_key(key)
889    }
890
891    pub fn get_metadata(&self, key: &str) -> Option<&String> {
892        self.metadata.get(key)
893    }
894
895    pub fn get_metadata_map(&self) -> HashMap<String, String> {
896        self.metadata.clone()
897    }
898
899    pub fn set_metadata_map(&mut self, map: HashMap<String, String>) {
900        for (k, v) in &map {
901            self.insert_metadata(k.to_string(), v.to_string());
902        }
903    }
904
905    pub fn get_slim_header(&self) -> &SlimHeader {
906        match &self.message_type {
907            Some(ProtoPublishType(publish)) => {
908                publish.header.as_ref().expect("SLIM header missing")
909            }
910            Some(ProtoSubscribeType(sub)) => sub.header.as_ref().expect("SLIM header missing"),
911            Some(ProtoUnsubscribeType(unsub)) => {
912                unsub.header.as_ref().expect("SLIM header missing")
913            }
914            Some(ProtoLinkMessageType(_)) | Some(ProtoSubscriptionAckType(_)) | None => {
915                panic!("SLIM header not found")
916            }
917        }
918    }
919
920    pub fn get_slim_header_mut(&mut self) -> &mut SlimHeader {
921        match &mut self.message_type {
922            Some(ProtoPublishType(publish)) => {
923                publish.header.as_mut().expect("SLIM header missing")
924            }
925            Some(ProtoSubscribeType(sub)) => sub.header.as_mut().expect("SLIM header missing"),
926            Some(ProtoUnsubscribeType(unsub)) => {
927                unsub.header.as_mut().expect("SLIM header missing")
928            }
929            Some(ProtoLinkMessageType(_)) | Some(ProtoSubscriptionAckType(_)) | None => {
930                panic!("SLIM header not found")
931            }
932        }
933    }
934
935    pub fn try_get_slim_header(&self) -> Option<&SlimHeader> {
936        match &self.message_type {
937            Some(ProtoPublishType(publish)) => publish.header.as_ref(),
938            Some(ProtoSubscribeType(sub)) => sub.header.as_ref(),
939            Some(ProtoUnsubscribeType(unsub)) => unsub.header.as_ref(),
940            Some(ProtoLinkMessageType(_)) | Some(ProtoSubscriptionAckType(_)) | None => None,
941        }
942    }
943
944    pub fn get_session_header(&self) -> &SessionHeader {
945        match &self.message_type {
946            Some(ProtoPublishType(publish)) => {
947                publish.session.as_ref().expect("session header missing")
948            }
949            Some(ProtoSubscribeType(_))
950            | Some(ProtoUnsubscribeType(_))
951            | Some(ProtoLinkMessageType(_))
952            | Some(ProtoSubscriptionAckType(_))
953            | None => panic!("session header not found"),
954        }
955    }
956
957    pub fn get_session_header_mut(&mut self) -> &mut SessionHeader {
958        match &mut self.message_type {
959            Some(ProtoPublishType(publish)) => {
960                publish.session.as_mut().expect("session header missing")
961            }
962            Some(ProtoSubscribeType(_))
963            | Some(ProtoUnsubscribeType(_))
964            | Some(ProtoLinkMessageType(_))
965            | Some(ProtoSubscriptionAckType(_))
966            | None => panic!("session header not found"),
967        }
968    }
969
970    pub fn try_get_session_header(&self) -> Option<&SessionHeader> {
971        match &self.message_type {
972            Some(ProtoPublishType(publish)) => publish.session.as_ref(),
973            Some(ProtoSubscribeType(_))
974            | Some(ProtoUnsubscribeType(_))
975            | Some(ProtoLinkMessageType(_))
976            | Some(ProtoSubscriptionAckType(_))
977            | None => None,
978        }
979    }
980
981    pub fn try_get_session_header_mut(&mut self) -> Option<&mut SessionHeader> {
982        match &mut self.message_type {
983            Some(ProtoPublishType(publish)) => publish.session.as_mut(),
984            Some(ProtoSubscribeType(_))
985            | Some(ProtoUnsubscribeType(_))
986            | Some(ProtoLinkMessageType(_))
987            | Some(ProtoSubscriptionAckType(_))
988            | None => None,
989        }
990    }
991
992    pub fn get_id(&self) -> u32 {
993        self.get_session_header().get_message_id()
994    }
995
996    pub fn get_source(&self) -> ProtoName {
997        self.get_slim_header().get_source()
998    }
999
1000    pub fn get_encoded_source(&self) -> EncodedName {
1001        self.get_slim_header().get_encoded_source()
1002    }
1003
1004    pub fn get_dst(&self) -> ProtoName {
1005        self.get_slim_header().get_dst()
1006    }
1007
1008    pub fn get_encoded_dst(&self) -> EncodedName {
1009        self.get_slim_header().get_encoded_dst()
1010    }
1011
1012    pub fn get_identity(&self) -> String {
1013        self.get_slim_header().get_identity()
1014    }
1015
1016    pub fn get_fanout(&self) -> u32 {
1017        self.get_slim_header().get_fanout()
1018    }
1019
1020    pub fn get_recv_from(&self) -> Option<u64> {
1021        self.get_slim_header().get_recv_from()
1022    }
1023
1024    pub fn get_forward_to(&self) -> Option<u64> {
1025        self.get_slim_header().get_forward_to()
1026    }
1027
1028    pub fn get_error(&self) -> Option<bool> {
1029        self.get_slim_header().get_error()
1030    }
1031
1032    pub fn get_incoming_conn(&self) -> u64 {
1033        self.get_slim_header()
1034            .get_incoming_conn()
1035            .expect("incoming connection not found")
1036    }
1037
1038    pub fn try_get_incoming_conn(&self) -> Option<u64> {
1039        self.get_slim_header().get_incoming_conn()
1040    }
1041
1042    pub fn get_type(&self) -> &MessageType {
1043        match &self.message_type {
1044            Some(t) => t,
1045            None => panic!("message type not found"),
1046        }
1047    }
1048
1049    pub fn get_payload(&self) -> Option<&Content> {
1050        match &self.message_type {
1051            Some(ProtoPublishType(p)) => p.msg.as_ref(),
1052            Some(ProtoSubscribeType(_))
1053            | Some(ProtoUnsubscribeType(_))
1054            | Some(ProtoLinkMessageType(_))
1055            | Some(ProtoSubscriptionAckType(_))
1056            | None => panic!("payload not found"),
1057        }
1058    }
1059
1060    pub fn set_payload(&mut self, payload: Content) {
1061        match &mut self.message_type {
1062            Some(ProtoPublishType(p)) => p.set_payload(payload),
1063            Some(ProtoSubscribeType(_))
1064            | Some(ProtoUnsubscribeType(_))
1065            | Some(ProtoLinkMessageType(_))
1066            | Some(ProtoSubscriptionAckType(_))
1067            | None => panic!("no payload allowed"),
1068        }
1069    }
1070
1071    pub fn get_session_message_type(&self) -> SessionMessageType {
1072        self.get_session_header().session_message_type()
1073    }
1074
1075    pub fn clear_slim_header(&mut self) {
1076        if self.is_link() || self.is_subscription_ack() {
1077            return;
1078        }
1079        self.get_slim_header_mut().clear_flags();
1080    }
1081
1082    pub fn set_recv_from(&mut self, recv_from: Option<u64>) {
1083        self.get_slim_header_mut().set_recv_from(recv_from);
1084    }
1085
1086    pub fn set_forward_to(&mut self, forward_to: Option<u64>) {
1087        self.get_slim_header_mut().set_forward_to(forward_to);
1088    }
1089
1090    pub fn set_error(&mut self, error: Option<bool>) {
1091        self.get_slim_header_mut().set_error(error);
1092    }
1093
1094    pub fn set_fanout(&mut self, fanout: u32) {
1095        self.get_slim_header_mut().set_fanout(fanout);
1096    }
1097
1098    pub fn set_incoming_conn(&mut self, incoming_conn: Option<u64>) {
1099        self.get_slim_header_mut().set_incoming_conn(incoming_conn);
1100    }
1101
1102    pub fn set_error_flag(&mut self, error: Option<bool>) {
1103        self.get_slim_header_mut().set_error_flag(error);
1104    }
1105
1106    pub fn get_ttl(&self) -> u32 {
1107        self.get_slim_header().get_ttl()
1108    }
1109
1110    pub fn set_ttl(&mut self, ttl: u32) {
1111        self.get_slim_header_mut().set_ttl(ttl);
1112    }
1113
1114    pub fn decrement_ttl(&mut self) -> u32 {
1115        self.get_slim_header_mut().decrement_ttl()
1116    }
1117
1118    pub fn set_session_message_type(&mut self, message_type: SessionMessageType) {
1119        self.get_session_header_mut()
1120            .set_session_message_type(message_type);
1121    }
1122
1123    pub fn set_session_type(&mut self, session_type: ProtoSessionType) {
1124        self.get_session_header_mut().set_session_type(session_type);
1125    }
1126
1127    pub fn get_session_type(&self) -> ProtoSessionType {
1128        self.get_session_header().session_type()
1129    }
1130
1131    pub fn set_message_id(&mut self, message_id: u32) {
1132        self.get_session_header_mut().set_message_id(message_id);
1133    }
1134
1135    pub fn is_publish(&self) -> bool {
1136        matches!(self.get_type(), MessageType::Publish(_))
1137    }
1138
1139    pub fn is_subscribe(&self) -> bool {
1140        matches!(self.get_type(), MessageType::Subscribe(_))
1141    }
1142
1143    pub fn is_unsubscribe(&self) -> bool {
1144        matches!(self.get_type(), MessageType::Unsubscribe(_))
1145    }
1146
1147    pub fn is_link(&self) -> bool {
1148        matches!(self.get_type(), MessageType::Link(_))
1149    }
1150
1151    pub fn get_link_negotiation_payload(&self) -> Option<LinkNegotiationPayload> {
1152        match &self.message_type {
1153            Some(ProtoLinkMessageType(link)) => match &link.link_type {
1154                Some(ProtoLinkType::LinkNegotiation(payload)) => Some(payload.clone()),
1155                _ => None,
1156            },
1157            _ => None,
1158        }
1159    }
1160
1161    pub fn is_subscription_ack(&self) -> bool {
1162        matches!(self.get_type(), MessageType::SubscriptionAck(_))
1163    }
1164
1165    pub fn is_traceable(&self) -> bool {
1166        !self.is_link() && !self.is_subscription_ack()
1167    }
1168
1169    pub fn get_subscription_ack(&self) -> &ProtoSubscriptionAck {
1170        match &self.message_type {
1171            Some(ProtoSubscriptionAckType(ack)) => ack,
1172            _ => panic!("message type is not subscription_ack"),
1173        }
1174    }
1175
1176    pub fn get_subscription_id(&self) -> Option<u64> {
1177        match &self.message_type {
1178            Some(ProtoSubscribeType(s)) if s.subscription_id != 0 => Some(s.subscription_id),
1179            Some(ProtoUnsubscribeType(u)) if u.subscription_id != 0 => Some(u.subscription_id),
1180            _ => None,
1181        }
1182    }
1183
1184    pub fn take_subscription_id(&mut self) -> Option<u64> {
1185        match &mut self.message_type {
1186            Some(ProtoSubscribeType(s)) if s.subscription_id != 0 => {
1187                Some(std::mem::take(&mut s.subscription_id))
1188            }
1189            Some(ProtoUnsubscribeType(u)) if u.subscription_id != 0 => {
1190                Some(std::mem::take(&mut u.subscription_id))
1191            }
1192            _ => None,
1193        }
1194    }
1195
1196    pub fn set_subscription_id(&mut self, subscription_id: u64) {
1197        match &mut self.message_type {
1198            Some(ProtoSubscribeType(s)) => s.subscription_id = subscription_id,
1199            Some(ProtoUnsubscribeType(u)) => u.subscription_id = subscription_id,
1200            _ => {}
1201        }
1202    }
1203
1204    pub fn extract_command_payload(&self) -> Result<&CommandPayload, MessageError> {
1205        self.get_payload()
1206            .ok_or(MessageError::ContentTypeNotSet)?
1207            .as_command_payload()
1208    }
1209
1210    impl_payload_extractors! {
1211        extract_discovery_request => as_discovery_request_payload(DiscoveryRequestPayload),
1212        extract_discovery_reply => as_discovery_reply_payload(DiscoveryReplyPayload),
1213        extract_join_request => as_join_request_payload(JoinRequestPayload),
1214        extract_join_reply => as_join_reply_payload(JoinReplyPayload),
1215        extract_leave_request => as_leave_request_payload(LeaveRequestPayload),
1216        extract_leave_reply => as_leave_reply_payload(LeaveReplyPayload),
1217        extract_group_add => as_group_add_payload(GroupAddPayload),
1218        extract_group_remove => as_group_remove_payload(GroupRemovePayload),
1219        extract_group_welcome => as_welcome_payload(GroupWelcomePayload),
1220        extract_group_close => as_group_close_payload(GroupClosePayload),
1221        extract_group_proposal => as_group_proposal_payload(GroupProposalPayload),
1222        extract_group_ack => as_group_ack_payload(GroupAckPayload),
1223        extract_group_nack => as_group_nack_payload(GroupNackPayload),
1224        extract_heartbeat => as_heartbeat_payload(HeartbeatPayload),
1225        extract_update_participant_state => as_update_participant_state_payload(UpdateParticipantStatePayload),
1226    }
1227
1228    pub fn builder() -> ProtoMessageBuilder {
1229        ProtoMessageBuilder::new()
1230    }
1231}
1232
1233impl Content {
1234    pub fn as_application_payload(&self) -> Result<&ApplicationPayload, MessageError> {
1235        match &self.content_type {
1236            Some(ContentType::AppPayload(app_payload)) => Ok(app_payload),
1237            Some(ContentType::CommandPayload(_)) => Err(MessageError::NotApplicationPayload),
1238            None => Err(MessageError::ContentTypeNotSet),
1239        }
1240    }
1241
1242    pub fn as_command_payload(&self) -> Result<&CommandPayload, MessageError> {
1243        match &self.content_type {
1244            Some(ContentType::AppPayload(_)) => Err(MessageError::NotCommandPayload),
1245            Some(ContentType::CommandPayload(comm_payload)) => Ok(comm_payload),
1246            None => Err(MessageError::ContentTypeNotSet),
1247        }
1248    }
1249}
1250
1251impl ApplicationPayload {
1252    pub fn new(payload_type: &str, blob: Vec<u8>) -> Self {
1253        Self {
1254            payload_type: payload_type.to_string(),
1255            blob,
1256        }
1257    }
1258
1259    pub fn as_content(&self) -> Content {
1260        Content {
1261            content_type: Some(ContentType::AppPayload(self.clone())),
1262        }
1263    }
1264}
1265
1266macro_rules! impl_command_payload_getters {
1267    ($($method_name:ident => $variant:ident($payload_type:ty)),* $(,)?) => {
1268        $(
1269            pub fn $method_name(&self) -> Result<&$payload_type, MessageError> {
1270                match &self.command_payload_type {
1271                    Some(CommandPayloadType::$variant(payload)) => Ok(payload),
1272                    Some(other) => Err(MessageError::InvalidCommandPayloadType {
1273                        expected: Box::new(stringify!($variant).to_string()),
1274                        got: Box::new(format!("{:?}", other)),
1275                    }),
1276                    None => Err(MessageError::InvalidCommandPayloadType {
1277                        expected: Box::new(stringify!($variant).to_string()),
1278                        got: Box::new("None".to_string()),
1279                    }),
1280                }
1281            }
1282        )*
1283    };
1284}
1285
1286impl CommandPayload {
1287    pub fn as_content(self) -> Content {
1288        Content {
1289            content_type: Some(ContentType::CommandPayload(self)),
1290        }
1291    }
1292
1293    impl_command_payload_getters! {
1294        as_discovery_request_payload => DiscoveryRequest(DiscoveryRequestPayload),
1295        as_discovery_reply_payload => DiscoveryReply(DiscoveryReplyPayload),
1296        as_join_request_payload => JoinRequest(JoinRequestPayload),
1297        as_join_reply_payload => JoinReply(JoinReplyPayload),
1298        as_leave_request_payload => LeaveRequest(LeaveRequestPayload),
1299        as_leave_reply_payload => LeaveReply(LeaveReplyPayload),
1300        as_group_add_payload => GroupAdd(GroupAddPayload),
1301        as_group_remove_payload => GroupRemove(GroupRemovePayload),
1302        as_welcome_payload => GroupWelcome(GroupWelcomePayload),
1303        as_group_close_payload => GroupClose(GroupClosePayload),
1304        as_group_proposal_payload => GroupProposal(GroupProposalPayload),
1305        as_group_ack_payload => GroupAck(GroupAckPayload),
1306        as_group_nack_payload => GroupNack(GroupNackPayload),
1307        as_heartbeat_payload => Heartbeat(HeartbeatPayload),
1308        as_update_participant_state_payload => UpdateParticipantState(UpdateParticipantStatePayload),
1309    }
1310
1311    pub fn builder() -> CommandPayloadBuilder {
1312        CommandPayloadBuilder::new()
1313    }
1314}
1315
1316impl AsRef<ProtoPublish> for ProtoMessage {
1317    fn as_ref(&self) -> &ProtoPublish {
1318        match &self.message_type {
1319            Some(ProtoPublishType(p)) => p,
1320            _ => panic!("message type is not publish"),
1321        }
1322    }
1323}
1324
1325pub struct CommandPayloadBuilder;
1326
1327impl CommandPayloadBuilder {
1328    pub fn new() -> Self {
1329        Self
1330    }
1331
1332    pub fn discovery_request(self) -> CommandPayload {
1333        let payload = DiscoveryRequestPayload {};
1334        CommandPayload {
1335            command_payload_type: Some(CommandPayloadType::DiscoveryRequest(payload)),
1336        }
1337    }
1338
1339    pub fn discovery_reply(self) -> CommandPayload {
1340        let payload = DiscoveryReplyPayload {};
1341        CommandPayload {
1342            command_payload_type: Some(CommandPayloadType::DiscoveryReply(payload)),
1343        }
1344    }
1345
1346    #[allow(deprecated)]
1347    pub fn join_request(
1348        self,
1349        max_retries: Option<u32>,
1350        timer_duration: Option<Duration>,
1351        data_channel: Option<ProtoName>,
1352        control_channel: Option<ProtoName>,
1353        mls_settings: Option<ProtoMlsSettings>,
1354    ) -> CommandPayload {
1355        let timer_settings = match (timer_duration, max_retries) {
1356            (Some(t), Some(m)) => Some(TimerSettings {
1357                timeout: t.as_millis() as u32,
1358                max_retries: m,
1359            }),
1360            _ => None,
1361        };
1362
1363        let payload = JoinRequestPayload {
1364            timer_settings,
1365            channel: data_channel,
1366            control: control_channel,
1367            mls_settings,
1368        };
1369        CommandPayload {
1370            command_payload_type: Some(CommandPayloadType::JoinRequest(payload)),
1371        }
1372    }
1373
1374    pub fn join_reply(
1375        self,
1376        key_package: Option<Vec<u8>>,
1377        participant: Participant,
1378    ) -> CommandPayload {
1379        let payload = JoinReplyPayload {
1380            key_package,
1381            participant: Some(participant),
1382        };
1383        CommandPayload {
1384            command_payload_type: Some(CommandPayloadType::JoinReply(payload)),
1385        }
1386    }
1387
1388    pub fn leave_request(self) -> CommandPayload {
1389        let payload = LeaveRequestPayload {};
1390        CommandPayload {
1391            command_payload_type: Some(CommandPayloadType::LeaveRequest(payload)),
1392        }
1393    }
1394
1395    pub fn leave_reply(self) -> CommandPayload {
1396        let payload = LeaveReplyPayload {};
1397        CommandPayload {
1398            command_payload_type: Some(CommandPayloadType::LeaveReply(payload)),
1399        }
1400    }
1401
1402    pub fn group_add(
1403        self,
1404        new_participant: Participant,
1405        participants: Vec<Participant>,
1406        mls: Option<MlsPayload>,
1407    ) -> CommandPayload {
1408        let payload = GroupAddPayload {
1409            new_participant: Some(new_participant),
1410            participants,
1411            mls,
1412        };
1413        CommandPayload {
1414            command_payload_type: Some(CommandPayloadType::GroupAdd(payload)),
1415        }
1416    }
1417
1418    pub fn group_remove(
1419        self,
1420        removed_participant: ProtoName,
1421        participants: Vec<ProtoName>,
1422        mls: Option<MlsPayload>,
1423    ) -> CommandPayload {
1424        let payload = GroupRemovePayload {
1425            removed_participant: Some(removed_participant),
1426            participants,
1427            mls,
1428        };
1429        CommandPayload {
1430            command_payload_type: Some(CommandPayloadType::GroupRemove(payload)),
1431        }
1432    }
1433
1434    pub fn group_welcome(
1435        self,
1436        participants: Vec<Participant>,
1437        mls: Option<MlsPayload>,
1438    ) -> CommandPayload {
1439        let payload = GroupWelcomePayload { participants, mls };
1440        CommandPayload {
1441            command_payload_type: Some(CommandPayloadType::GroupWelcome(payload)),
1442        }
1443    }
1444
1445    pub fn group_close(self, participants: Vec<ProtoName>) -> CommandPayload {
1446        let payload = GroupClosePayload { participants };
1447        CommandPayload {
1448            command_payload_type: Some(CommandPayloadType::GroupClose(payload)),
1449        }
1450    }
1451
1452    pub fn group_proposal(
1453        self,
1454        source: Option<ProtoName>,
1455        mls_proposal: Vec<u8>,
1456    ) -> CommandPayload {
1457        let payload = GroupProposalPayload {
1458            source,
1459            mls_proposal,
1460        };
1461        CommandPayload {
1462            command_payload_type: Some(CommandPayloadType::GroupProposal(payload)),
1463        }
1464    }
1465
1466    pub fn group_ack(self) -> CommandPayload {
1467        let payload = GroupAckPayload {};
1468        CommandPayload {
1469            command_payload_type: Some(CommandPayloadType::GroupAck(payload)),
1470        }
1471    }
1472
1473    pub fn group_nack(self) -> CommandPayload {
1474        let payload = GroupNackPayload {};
1475        CommandPayload {
1476            command_payload_type: Some(CommandPayloadType::GroupNack(payload)),
1477        }
1478    }
1479
1480    pub fn heartbeat(self, epoch: u64) -> CommandPayload {
1481        let payload = HeartbeatPayload { epoch };
1482        CommandPayload {
1483            command_payload_type: Some(CommandPayloadType::Heartbeat(payload)),
1484        }
1485    }
1486
1487    pub fn update_participant_state(
1488        self,
1489        participant: ProtoName,
1490        new_state: ParticipantState,
1491        epoch: u64,
1492    ) -> CommandPayload {
1493        let payload = UpdateParticipantStatePayload {
1494            participant: Some(participant),
1495            new_state: new_state as i32,
1496            epoch,
1497        };
1498        CommandPayload {
1499            command_payload_type: Some(CommandPayloadType::UpdateParticipantState(payload)),
1500        }
1501    }
1502}
1503
1504impl Default for CommandPayloadBuilder {
1505    fn default() -> Self {
1506        Self::new()
1507    }
1508}
1509
1510pub struct ProtoMessageBuilder {
1511    source: Option<ProtoName>,
1512    destination: Option<ProtoName>,
1513    identity: Option<String>,
1514    flags: Option<SlimHeaderFlags>,
1515    session_type: Option<ProtoSessionType>,
1516    session_message_type: Option<SessionMessageType>,
1517    session_id: Option<u32>,
1518    message_id: Option<u32>,
1519    payload: Option<Content>,
1520    metadata: HashMap<String, String>,
1521    subscription_id: Option<u64>,
1522}
1523
1524impl ProtoMessageBuilder {
1525    pub fn new() -> Self {
1526        Self {
1527            source: None,
1528            destination: None,
1529            identity: None,
1530            flags: None,
1531            session_type: None,
1532            session_message_type: None,
1533            session_id: None,
1534            message_id: None,
1535            payload: None,
1536            metadata: HashMap::new(),
1537            subscription_id: None,
1538        }
1539    }
1540
1541    pub fn source(mut self, source: ProtoName) -> Self {
1542        self.source = Some(source);
1543        self
1544    }
1545
1546    pub fn destination(mut self, destination: ProtoName) -> Self {
1547        self.destination = Some(destination);
1548        self
1549    }
1550
1551    pub fn identity(mut self, identity: impl Into<String>) -> Self {
1552        self.identity = Some(identity.into());
1553        self
1554    }
1555
1556    pub fn flags(mut self, flags: SlimHeaderFlags) -> Self {
1557        self.flags = Some(flags);
1558        self
1559    }
1560
1561    pub fn fanout(mut self, fanout: u32) -> Self {
1562        self.flags.get_or_insert_default().fanout = fanout;
1563        self
1564    }
1565
1566    pub fn recv_from(mut self, recv_from: u64) -> Self {
1567        self.flags.get_or_insert_default().recv_from = Some(recv_from);
1568        self
1569    }
1570
1571    pub fn forward_to(mut self, forward_to: u64) -> Self {
1572        self.flags.get_or_insert_default().forward_to = Some(forward_to);
1573        self
1574    }
1575
1576    pub fn incoming_conn(mut self, incoming_conn: u64) -> Self {
1577        self.flags.get_or_insert_default().incoming_conn = Some(incoming_conn);
1578        self
1579    }
1580
1581    pub fn error(mut self, error: bool) -> Self {
1582        self.flags.get_or_insert_default().error = Some(error);
1583        self
1584    }
1585
1586    pub fn ttl(mut self, ttl: u32) -> Self {
1587        self.flags.get_or_insert_default().ttl = ttl;
1588        self
1589    }
1590
1591    pub fn session_type(mut self, session_type: ProtoSessionType) -> Self {
1592        self.session_type = Some(session_type);
1593        self
1594    }
1595
1596    pub fn session_message_type(mut self, session_message_type: SessionMessageType) -> Self {
1597        self.session_message_type = Some(session_message_type);
1598        self
1599    }
1600
1601    pub fn session_id(mut self, session_id: u32) -> Self {
1602        self.session_id = Some(session_id);
1603        self
1604    }
1605
1606    pub fn message_id(mut self, message_id: u32) -> Self {
1607        self.message_id = Some(message_id);
1608        self
1609    }
1610
1611    pub fn payload(mut self, payload: Content) -> Self {
1612        self.payload = Some(payload);
1613        self
1614    }
1615
1616    pub fn application_payload(mut self, payload_type: &str, blob: Vec<u8>) -> Self {
1617        let app_payload = ApplicationPayload::new(payload_type, blob);
1618        self.payload = Some(app_payload.as_content());
1619        self
1620    }
1621
1622    pub fn command_payload(mut self, payload: CommandPayload) -> Self {
1623        self.payload = Some(payload.as_content());
1624        self
1625    }
1626
1627    pub fn with_slim_header(mut self, header: SlimHeader) -> Self {
1628        if let Some(src) = header.source.clone() {
1629            self.source = Some(src);
1630        }
1631        if let Some(dst) = header.destination.clone() {
1632            self.destination = Some(dst);
1633        }
1634        if !header.identity.is_empty() {
1635            self.identity = Some(header.identity.clone());
1636        }
1637
1638        self.flags = Some(SlimHeaderFlags {
1639            fanout: header.fanout,
1640            recv_from: header.recv_from,
1641            forward_to: header.forward_to,
1642            incoming_conn: header.incoming_conn,
1643            error: header.error,
1644            ttl: header.ttl,
1645        });
1646        self
1647    }
1648
1649    pub fn with_session_header(mut self, header: SessionHeader) -> Self {
1650        self.session_type = Some(header.session_type());
1651        self.session_message_type = Some(header.session_message_type());
1652        self.session_id = Some(header.session_id);
1653        self.message_id = Some(header.message_id);
1654        self
1655    }
1656
1657    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1658        self.metadata.insert(key.into(), value.into());
1659        self
1660    }
1661
1662    pub fn metadata_map(mut self, map: HashMap<String, String>) -> Self {
1663        self.metadata.extend(map);
1664        self
1665    }
1666
1667    pub fn subscription_id(mut self, id: u64) -> Self {
1668        self.subscription_id = Some(id);
1669        self
1670    }
1671
1672    pub fn build_publish(self) -> Result<ProtoMessage, MessageError> {
1673        let source = self
1674            .source
1675            .ok_or(MessageError::BuilderErrorSourceRequired)?;
1676        let destination = self
1677            .destination
1678            .ok_or(MessageError::BuilderErrorDestinationRequired)?;
1679
1680        let slim_header = Some(SlimHeader::new(
1681            source,
1682            destination,
1683            self.identity.as_deref().unwrap_or(""),
1684            self.flags,
1685        ));
1686
1687        let session_header = if self.session_type.is_some() || self.session_message_type.is_some() {
1688            Some(SessionHeader::new(
1689                self.session_type
1690                    .unwrap_or(ProtoSessionType::PointToPoint)
1691                    .into(),
1692                self.session_message_type
1693                    .unwrap_or(SessionMessageType::Msg)
1694                    .into(),
1695                self.session_id.unwrap_or(0),
1696                self.message_id.unwrap_or_else(rand::random),
1697            ))
1698        } else {
1699            Some(SessionHeader::default())
1700        };
1701
1702        let publish = ProtoPublish::with_header(slim_header, session_header, self.payload);
1703        Ok(ProtoMessage::new(self.metadata, ProtoPublishType(publish)))
1704    }
1705
1706    pub fn build_subscribe(self) -> Result<ProtoMessage, MessageError> {
1707        let source = self
1708            .source
1709            .ok_or(MessageError::BuilderErrorSourceRequired)?;
1710        let destination = self
1711            .destination
1712            .ok_or(MessageError::BuilderErrorDestinationRequired)?;
1713
1714        let mut subscribe =
1715            ProtoSubscribe::new(source, destination, self.identity.as_deref(), self.flags);
1716        subscribe.subscription_id = self.subscription_id.unwrap_or_default();
1717
1718        Ok(ProtoMessage::new(
1719            self.metadata,
1720            ProtoSubscribeType(subscribe),
1721        ))
1722    }
1723
1724    pub fn build_unsubscribe(self) -> Result<ProtoMessage, MessageError> {
1725        let source = self
1726            .source
1727            .ok_or(MessageError::BuilderErrorSourceRequired)?;
1728        let destination = self
1729            .destination
1730            .ok_or(MessageError::BuilderErrorDestinationRequired)?;
1731
1732        let mut unsubscribe =
1733            ProtoUnsubscribe::new(source, destination, self.identity.as_deref(), self.flags);
1734        unsubscribe.subscription_id = self.subscription_id.unwrap_or_default();
1735
1736        Ok(ProtoMessage::new(
1737            self.metadata,
1738            ProtoUnsubscribeType(unsubscribe),
1739        ))
1740    }
1741
1742    pub fn build_subscription_ack(
1743        self,
1744        subscription_id: u64,
1745        success: bool,
1746        error: impl Into<String>,
1747    ) -> ProtoMessage {
1748        let ack = ProtoSubscriptionAck {
1749            subscription_id,
1750            success,
1751            error: error.into(),
1752        };
1753        ProtoMessage::new(self.metadata, ProtoSubscriptionAckType(ack))
1754    }
1755
1756    #[allow(clippy::too_many_arguments)]
1757    pub fn build_link_negotiation(
1758        self,
1759        link_id: impl Into<String>,
1760        slim_version: impl Into<String>,
1761        is_reply: bool,
1762        link_ecdh_public_key: Option<Vec<u8>>,
1763        connection_type: LinkConnectionType,
1764        node_id: impl Into<String>,
1765        deployment_name: impl Into<String>,
1766    ) -> ProtoMessage {
1767        let link = ProtoLink {
1768            link_type: Some(ProtoLinkType::LinkNegotiation(LinkNegotiationPayload {
1769                link_id: link_id.into(),
1770                slim_version: slim_version.into(),
1771                is_reply,
1772                link_ecdh_public_key: link_ecdh_public_key.unwrap_or_default(),
1773                connection_type: connection_type.into(),
1774                node_id: node_id.into(),
1775                deployment_name: deployment_name.into(),
1776            })),
1777        };
1778        ProtoMessage::new(self.metadata, ProtoLinkMessageType(link))
1779    }
1780}
1781
1782impl Default for ProtoMessageBuilder {
1783    fn default() -> Self {
1784        Self::new()
1785    }
1786}
1787
1788#[cfg(test)]
1789mod name_tests {
1790    use super::*;
1791
1792    #[test]
1793    fn test_proto_name_from_strings() {
1794        let n1 = ProtoName::from_strings(["Org", "Default", "App"]);
1795        let n2 = ProtoName::from_strings(["Org", "Default", "App"]);
1796        assert_eq!(n1, n2);
1797        let n3 = ProtoName::from_strings(["Other", "Default", "App"]);
1798        assert_ne!(n1, n3);
1799    }
1800
1801    #[test]
1802    fn test_proto_name_with_id() {
1803        let n = ProtoName::from_strings(["a", "b", "c"]).with_id(42);
1804        assert_eq!(n.id(), 42);
1805        assert!(n.has_id());
1806    }
1807
1808    #[test]
1809    fn test_proto_name_reset_id() {
1810        let mut n = ProtoName::from_strings(["a", "b", "c"]).with_id(42);
1811        n.reset_id();
1812        assert_eq!(n.id(), NameId::NULL_COMPONENT);
1813        assert!(!n.has_id());
1814    }
1815
1816    #[test]
1817    fn test_proto_name_match_prefix() {
1818        let n1 = ProtoName::from_strings(["Org", "Default", "App"]).with_id(1);
1819        let n2 = ProtoName::from_strings(["Org", "Default", "App"]).with_id(999);
1820        assert!(n1.match_prefix(&n2));
1821
1822        let n3 = ProtoName::from_strings(["Other", "Default", "App"]).with_id(1);
1823        assert!(!n1.match_prefix(&n3));
1824    }
1825
1826    #[test]
1827    fn test_proto_name_str_components() {
1828        let n = ProtoName::from_strings(["org", "ns", "app"]);
1829        let (a, b, c) = n.str_components();
1830        assert_eq!(a, "org");
1831        assert_eq!(b, "ns");
1832        assert_eq!(c, "app");
1833    }
1834
1835    #[test]
1836    fn test_proto_name_hash_stability() {
1837        let proto = ProtoName::from_strings(["Org", "Default", "App"]).with_id(7);
1838        let enc = proto.name.unwrap();
1839        let proto2 = ProtoName::from_strings(["Org", "Default", "App"]).with_id(7);
1840        let enc2 = proto2.name.unwrap();
1841        assert_eq!(enc, enc2);
1842        assert_eq!(enc.id(), 7);
1843    }
1844
1845    #[test]
1846    fn test_name_id_roundtrip() {
1847        let values = [0u128, 1, 42, u128::MAX / 2, u128::MAX - 4];
1848        for v in values {
1849            let nid = NameId::from(v);
1850            let result: u128 = nid.into();
1851            assert_eq!(result, v, "roundtrip failed for {v}");
1852        }
1853    }
1854
1855    #[test]
1856    fn test_name_id_from_string_valid_uuid() {
1857        let nid = NameId::try_from("00000000-0000-0000-0000-00000000002a".to_string())
1858            .expect("valid UUID string should parse");
1859        let result: u128 = nid.into();
1860        assert_eq!(result, 42);
1861    }
1862
1863    #[test]
1864    fn test_name_id_from_string_invalid() {
1865        assert!(NameId::try_from("not-a-uuid".to_string()).is_err());
1866        assert!(NameId::try_from("".to_string()).is_err());
1867    }
1868
1869    #[test]
1870    fn test_name_id_display_uuid() {
1871        let nid = NameId::from(42);
1872        assert_eq!(nid.to_string(), "00000000-0000-0000-0000-00000000002a");
1873    }
1874
1875    #[test]
1876    fn test_name_id_display_reserved() {
1877        let str_nid: String = NameId::from(NameId::NULL_COMPONENT).into();
1878        assert_eq!(str_nid, "NULL_COMPONENT");
1879    }
1880
1881    #[test]
1882    fn test_name_id_is_reserved() {
1883        assert!(NameId::is_reserved_id(NameId::NULL_COMPONENT));
1884        assert!(!NameId::is_reserved_id(0));
1885        assert!(!NameId::is_reserved_id(42));
1886    }
1887
1888    #[test]
1889    fn test_proto_name_default_id_is_null_component() {
1890        let n = ProtoName::from_strings(["a", "b", "c"]);
1891        assert_eq!(n.id(), NameId::NULL_COMPONENT);
1892        assert!(!n.has_id());
1893    }
1894
1895    #[test]
1896    fn test_proto_name_set_id() {
1897        let mut n = ProtoName::from_strings(["a", "b", "c"]);
1898        n.set_id(123);
1899        assert_eq!(n.id(), 123);
1900        assert!(n.has_id());
1901    }
1902
1903    #[test]
1904    fn test_proto_name_string_id() {
1905        let n = ProtoName::from_strings(["a", "b", "c"]).with_id(42);
1906        assert_eq!(n.string_id(), "00000000-0000-0000-0000-00000000002a");
1907    }
1908
1909    #[test]
1910    fn test_proto_name_display_with_id() {
1911        let n = ProtoName::from_strings(["org", "ns", "app"]).with_id(1);
1912        let s = format!("{}", n);
1913        assert_eq!(s, "org/ns/app/00000000-0000-0000-0000-000000000001");
1914    }
1915
1916    #[test]
1917    fn test_proto_name_display_null_component() {
1918        let n = ProtoName::from_strings(["org", "ns", "app"]);
1919        let s = format!("{}", n);
1920        assert_eq!(s, "org/ns/app/NULL_COMPONENT");
1921    }
1922
1923    #[test]
1924    fn test_encoded_name_id_when_name_id_none() {
1925        let enc = EncodedName {
1926            component_0: 0,
1927            component_1: 0,
1928            component_2: 0,
1929            name_id: None,
1930        };
1931        assert_eq!(enc.id(), NameId::NULL_COMPONENT);
1932        assert_eq!(enc.string_id(), "NULL_COMPONENT");
1933    }
1934
1935    #[test]
1936    fn test_parse_name_valid() {
1937        let n = ProtoName::parse_name("org/ns/app").unwrap();
1938        let (a, b, c) = n.str_components();
1939        assert_eq!(a, "org");
1940        assert_eq!(b, "ns");
1941        assert_eq!(c, "app");
1942    }
1943
1944    #[test]
1945    fn test_parse_name_trims_whitespace() {
1946        let n = ProtoName::parse_name(" org / ns / app ").unwrap();
1947        let (a, b, c) = n.str_components();
1948        assert_eq!(a, "org");
1949        assert_eq!(b, "ns");
1950        assert_eq!(c, "app");
1951    }
1952
1953    #[test]
1954    fn test_parse_name_empty_string() {
1955        assert!(ProtoName::parse_name("").is_err());
1956        assert!(ProtoName::parse_name("   ").is_err());
1957    }
1958
1959    #[test]
1960    fn test_parse_name_too_few_parts() {
1961        assert!(ProtoName::parse_name("org/ns").is_err());
1962        assert!(ProtoName::parse_name("org").is_err());
1963    }
1964
1965    #[test]
1966    fn test_parse_name_too_many_parts() {
1967        assert!(ProtoName::parse_name("a/b/c/d").is_err());
1968    }
1969
1970    #[test]
1971    fn test_parse_name_empty_component() {
1972        assert!(ProtoName::parse_name("org//app").is_err());
1973        assert!(ProtoName::parse_name("/ns/app").is_err());
1974        assert!(ProtoName::parse_name("org/ns/").is_err());
1975    }
1976}
1977
1978#[cfg(test)]
1979mod message_tests {
1980    use super::*;
1981
1982    fn test_subscription_template(
1983        subscription: bool,
1984        source: ProtoName,
1985        dst: ProtoName,
1986        identity: Option<&str>,
1987        flags: Option<SlimHeaderFlags>,
1988    ) {
1989        let sub = {
1990            let mut builder = ProtoMessage::builder()
1991                .source(source.clone())
1992                .destination(dst.clone());
1993
1994            if let Some(id) = identity {
1995                builder = builder.identity(id);
1996            }
1997
1998            if let Some(f) = flags.clone() {
1999                builder = builder.flags(f);
2000            }
2001
2002            if subscription {
2003                builder.build_subscribe().unwrap()
2004            } else {
2005                builder.build_unsubscribe().unwrap()
2006            }
2007        };
2008
2009        let flags = if flags.is_none() {
2010            Some(SlimHeaderFlags::default())
2011        } else {
2012            flags
2013        };
2014
2015        assert!(!sub.is_publish());
2016        assert_eq!(sub.is_subscribe(), subscription);
2017        assert_eq!(sub.is_unsubscribe(), !subscription);
2018        assert_eq!(flags.as_ref().unwrap().recv_from, sub.get_recv_from());
2019        assert_eq!(flags.as_ref().unwrap().forward_to, sub.get_forward_to());
2020        assert_eq!(None, sub.try_get_incoming_conn());
2021        assert_eq!(source, sub.get_source());
2022        assert_eq!(dst, sub.get_dst());
2023    }
2024
2025    fn test_publish_template(
2026        source: ProtoName,
2027        dst: ProtoName,
2028        identity: Option<&str>,
2029        flags: Option<SlimHeaderFlags>,
2030    ) {
2031        let mut builder = ProtoMessage::builder()
2032            .source(source.clone())
2033            .destination(dst.clone())
2034            .application_payload("str", b"this is the content of the message".to_vec());
2035
2036        if let Some(id) = identity {
2037            builder = builder.identity(id);
2038        }
2039
2040        if let Some(f) = flags.clone() {
2041            builder = builder.flags(f);
2042        }
2043
2044        let pub_msg = builder.build_publish().unwrap();
2045        let flags = flags.or_else(|| Some(SlimHeaderFlags::default()));
2046
2047        assert!(pub_msg.is_publish());
2048        assert!(!pub_msg.is_subscribe());
2049        assert!(!pub_msg.is_unsubscribe());
2050        assert_eq!(flags.as_ref().unwrap().recv_from, pub_msg.get_recv_from());
2051        assert_eq!(flags.as_ref().unwrap().forward_to, pub_msg.get_forward_to());
2052        assert_eq!(None, pub_msg.try_get_incoming_conn());
2053        assert_eq!(source, pub_msg.get_source());
2054        assert_eq!(dst, pub_msg.get_dst());
2055        assert_eq!(flags.as_ref().unwrap().fanout, pub_msg.get_fanout());
2056    }
2057
2058    #[test]
2059    fn test_subscription() {
2060        let source = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2061        let dst = ProtoName::from_strings(["org", "ns", "type"]).with_id(2);
2062        test_subscription_template(true, source.clone(), dst.clone(), None, None);
2063        test_subscription_template(
2064            true,
2065            source.clone(),
2066            dst.clone(),
2067            None,
2068            Some(SlimHeaderFlags::default().with_recv_from(50)),
2069        );
2070        test_subscription_template(
2071            true,
2072            source,
2073            dst,
2074            None,
2075            Some(SlimHeaderFlags::default().with_forward_to(30)),
2076        );
2077    }
2078
2079    #[test]
2080    fn test_unsubscription() {
2081        let source = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2082        let dst = ProtoName::from_strings(["org", "ns", "type"]).with_id(2);
2083        test_subscription_template(false, source.clone(), dst.clone(), None, None);
2084        test_subscription_template(
2085            false,
2086            source.clone(),
2087            dst.clone(),
2088            None,
2089            Some(SlimHeaderFlags::default().with_recv_from(50)),
2090        );
2091        test_subscription_template(
2092            false,
2093            source,
2094            dst,
2095            None,
2096            Some(SlimHeaderFlags::default().with_forward_to(30)),
2097        );
2098    }
2099
2100    #[test]
2101    fn test_publish() {
2102        let source = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2103        let mut dst = ProtoName::from_strings(["org", "ns", "type"]);
2104        test_publish_template(
2105            source.clone(),
2106            dst.clone(),
2107            None,
2108            Some(SlimHeaderFlags::default()),
2109        );
2110        dst.set_id(2);
2111        test_publish_template(
2112            source.clone(),
2113            dst.clone(),
2114            None,
2115            Some(SlimHeaderFlags::default()),
2116        );
2117        dst.reset_id();
2118        test_publish_template(
2119            source.clone(),
2120            dst.clone(),
2121            None,
2122            Some(SlimHeaderFlags::default().with_recv_from(50)),
2123        );
2124        test_publish_template(
2125            source,
2126            dst,
2127            None,
2128            Some(SlimHeaderFlags::default().with_forward_to(30)),
2129        );
2130    }
2131
2132    #[test]
2133    fn test_conversions() {
2134        let name = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2135        let dst = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2136        let proto_subscribe = ProtoMessage::builder()
2137            .source(name.clone())
2138            .destination(dst.clone())
2139            .flags(
2140                SlimHeaderFlags::default()
2141                    .with_recv_from(2)
2142                    .with_forward_to(3),
2143            )
2144            .build_subscribe()
2145            .unwrap();
2146        let proto_subscribe = ProtoSubscribe::from(proto_subscribe);
2147        assert_eq!(proto_subscribe.header.as_ref().unwrap().get_source(), name);
2148        assert_eq!(proto_subscribe.header.as_ref().unwrap().get_dst(), dst);
2149
2150        let proto_unsubscribe = ProtoMessage::builder()
2151            .source(name.clone())
2152            .destination(dst.clone())
2153            .flags(
2154                SlimHeaderFlags::default()
2155                    .with_recv_from(2)
2156                    .with_forward_to(3),
2157            )
2158            .build_unsubscribe()
2159            .unwrap();
2160        let proto_unsubscribe = ProtoUnsubscribe::from(proto_unsubscribe);
2161        assert_eq!(
2162            proto_unsubscribe.header.as_ref().unwrap().get_source(),
2163            name
2164        );
2165        assert_eq!(proto_unsubscribe.header.as_ref().unwrap().get_dst(), dst);
2166
2167        let proto_publish = ProtoMessage::builder()
2168            .source(name.clone())
2169            .destination(dst.clone())
2170            .flags(
2171                SlimHeaderFlags::default()
2172                    .with_recv_from(2)
2173                    .with_forward_to(3),
2174            )
2175            .application_payload("str", b"this is the content of the message".to_vec())
2176            .build_publish()
2177            .unwrap();
2178        let proto_publish = ProtoPublish::from(proto_publish);
2179        assert_eq!(proto_publish.header.as_ref().unwrap().get_source(), name);
2180        assert_eq!(proto_publish.header.as_ref().unwrap().get_dst(), dst);
2181    }
2182
2183    #[test]
2184    fn test_panic() {
2185        let source = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2186        let dst = ProtoName::from_strings(["org", "ns", "type"]).with_id(2);
2187        let msg = ProtoMessage::builder()
2188            .source(source)
2189            .destination(dst)
2190            .flags(
2191                SlimHeaderFlags::default()
2192                    .with_recv_from(2)
2193                    .with_forward_to(3),
2194            )
2195            .build_subscribe()
2196            .unwrap();
2197
2198        assert!(std::panic::catch_unwind(|| ProtoUnsubscribe::from(msg.clone())).is_err());
2199        assert!(std::panic::catch_unwind(|| ProtoPublish::from(msg.clone())).is_err());
2200        assert!(std::panic::catch_unwind(|| ProtoSubscribe::from(msg)).is_ok());
2201    }
2202
2203    #[test]
2204    fn test_panic_header() {
2205        let header = SlimHeader {
2206            source: None,
2207            destination: None,
2208            identity: String::new(),
2209            fanout: 0,
2210            version: version().to_string(),
2211            recv_from: None,
2212            forward_to: None,
2213            incoming_conn: None,
2214            error: None,
2215            header_mac: None,
2216            ttl: DEFAULT_TTL,
2217            e2e_header_sig: None,
2218        };
2219
2220        assert!(std::panic::catch_unwind(|| header.get_source()).is_err());
2221        assert!(std::panic::catch_unwind(|| header.get_dst()).is_err());
2222        assert!(std::panic::catch_unwind(|| header.get_recv_from()).is_ok());
2223        assert!(std::panic::catch_unwind(|| header.get_forward_to()).is_ok());
2224        assert!(std::panic::catch_unwind(|| header.get_incoming_conn()).is_ok());
2225        assert!(std::panic::catch_unwind(|| header.get_error()).is_ok());
2226    }
2227
2228    #[test]
2229    fn test_panic_session_header() {
2230        let header = SessionHeader::new(0, 0, 0, 0);
2231        assert!(std::panic::catch_unwind(|| header.get_session_id()).is_ok());
2232        assert!(std::panic::catch_unwind(|| header.get_message_id()).is_ok());
2233    }
2234
2235    #[test]
2236    fn test_panic_proto_message() {
2237        let message = ProtoMessage {
2238            metadata: HashMap::new(),
2239            message_type: None,
2240        };
2241        assert!(std::panic::catch_unwind(|| message.get_slim_header()).is_err());
2242        assert!(std::panic::catch_unwind(|| message.get_type()).is_err());
2243        assert!(std::panic::catch_unwind(|| message.get_source()).is_err());
2244        assert!(std::panic::catch_unwind(|| message.get_dst()).is_err());
2245        assert!(std::panic::catch_unwind(|| message.get_recv_from()).is_err());
2246        assert!(std::panic::catch_unwind(|| message.get_forward_to()).is_err());
2247        assert!(std::panic::catch_unwind(|| message.get_incoming_conn()).is_err());
2248        assert!(std::panic::catch_unwind(|| message.get_fanout()).is_err());
2249    }
2250
2251    #[test]
2252    fn test_service_type_to_int() {
2253        let total_service_types = SessionMessageType::UpdateParticipantState as i32;
2254        for i in 0..total_service_types {
2255            let service_type =
2256                SessionMessageType::try_from(i).expect("failed to convert int to service type");
2257            assert_eq!(i32::from(service_type), i32::from(service_type));
2258        }
2259        assert!(SessionMessageType::try_from(total_service_types + 1).is_err());
2260    }
2261
2262    #[test]
2263    fn test_proto_message_builder() {
2264        let source = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2265        let dest = ProtoName::from_strings(["org", "ns", "app"]).with_id(2);
2266
2267        let msg = ProtoMessage::builder()
2268            .source(source.clone())
2269            .destination(dest.clone())
2270            .application_payload("test", b"hello world".to_vec())
2271            .build_publish()
2272            .unwrap();
2273        assert!(msg.is_publish());
2274        assert_eq!(msg.get_source(), source);
2275        assert_eq!(msg.get_dst(), dest);
2276
2277        let msg = ProtoMessage::builder()
2278            .source(source.clone())
2279            .destination(dest.clone())
2280            .session_type(ProtoSessionType::Multicast)
2281            .session_message_type(SessionMessageType::Msg)
2282            .session_id(42)
2283            .message_id(100)
2284            .fanout(256)
2285            .application_payload("test", b"broadcast".to_vec())
2286            .build_publish()
2287            .unwrap();
2288        assert_eq!(msg.get_session_type(), ProtoSessionType::Multicast);
2289        assert_eq!(msg.get_id(), 100);
2290        assert_eq!(msg.get_fanout(), 256);
2291
2292        let msg = ProtoMessage::builder()
2293            .source(source.clone())
2294            .destination(dest.clone())
2295            .metadata("key1", "value1")
2296            .metadata("key2", "value2")
2297            .application_payload("test", vec![1, 2, 3])
2298            .build_publish()
2299            .unwrap();
2300        assert_eq!(msg.get_metadata("key1"), Some(&"value1".to_string()));
2301        assert_eq!(msg.get_metadata("key2"), Some(&"value2".to_string()));
2302
2303        let msg = ProtoMessage::builder()
2304            .source(source.clone())
2305            .destination(dest.clone())
2306            .recv_from(10)
2307            .build_subscribe()
2308            .unwrap();
2309        assert!(msg.is_subscribe());
2310        assert_eq!(msg.get_recv_from(), Some(10));
2311
2312        let msg = ProtoMessage::builder()
2313            .source(source)
2314            .destination(dest)
2315            .forward_to(20)
2316            .build_unsubscribe()
2317            .unwrap();
2318        assert!(msg.is_unsubscribe());
2319        assert_eq!(msg.get_forward_to(), Some(20));
2320    }
2321
2322    #[test]
2323    fn test_command_payload_builder() {
2324        let dest = ProtoName::from_strings(["org", "ns", "app"]);
2325
2326        assert!(CommandPayload::builder()
2327            .discovery_request()
2328            .as_discovery_request_payload()
2329            .is_ok());
2330        assert!(CommandPayload::builder()
2331            .discovery_reply()
2332            .as_discovery_reply_payload()
2333            .is_ok());
2334
2335        let payload = CommandPayload::builder().join_request(
2336            Some(5),
2337            Some(Duration::from_secs(10)),
2338            Some(dest.clone()),
2339            None,
2340            Some(ProtoMlsSettings::default()),
2341        );
2342        let extracted = payload.as_join_request_payload().unwrap();
2343        assert!(extracted.mls_settings.is_some());
2344        assert!(extracted.timer_settings.is_some());
2345
2346        let participant = Participant::new(dest.clone(), ParticipantSettings::bidirectional());
2347        let payload =
2348            CommandPayload::builder().join_reply(Some(vec![1, 2, 3]), participant.clone());
2349        let extracted = payload.as_join_reply_payload().unwrap();
2350        assert_eq!(extracted.key_package, Some(vec![1, 2, 3]));
2351        assert_eq!(extracted.participant, Some(participant.clone()));
2352
2353        assert!(CommandPayload::builder()
2354            .leave_request()
2355            .as_leave_request_payload()
2356            .is_ok());
2357        assert!(CommandPayload::builder()
2358            .leave_reply()
2359            .as_leave_reply_payload()
2360            .is_ok());
2361
2362        let participants = vec![participant.clone()];
2363        let payload =
2364            CommandPayload::builder().group_add(participant.clone(), participants.clone(), None);
2365        let extracted = payload.as_group_add_payload().unwrap();
2366        assert_eq!(extracted.new_participant, Some(participant));
2367        assert_eq!(extracted.participants, participants);
2368
2369        let payload =
2370            CommandPayload::builder().group_remove(dest.clone(), vec![dest.clone()], None);
2371        assert!(payload
2372            .as_group_remove_payload()
2373            .unwrap()
2374            .removed_participant
2375            .is_some());
2376        assert!(!CommandPayload::builder()
2377            .group_welcome(
2378                vec![Participant::new(
2379                    dest.clone(),
2380                    ParticipantSettings::bidirectional()
2381                )],
2382                None
2383            )
2384            .as_welcome_payload()
2385            .unwrap()
2386            .participants
2387            .is_empty());
2388        assert_eq!(
2389            CommandPayload::builder()
2390                .group_proposal(Some(dest.clone()), vec![4, 5, 6])
2391                .as_group_proposal_payload()
2392                .unwrap()
2393                .mls_proposal,
2394            vec![4, 5, 6]
2395        );
2396        assert!(CommandPayload::builder()
2397            .group_ack()
2398            .as_group_ack_payload()
2399            .is_ok());
2400        assert!(CommandPayload::builder()
2401            .group_nack()
2402            .as_group_nack_payload()
2403            .is_ok());
2404        assert!(CommandPayload::builder()
2405            .heartbeat(0)
2406            .as_heartbeat_payload()
2407            .is_ok());
2408        assert!(CommandPayload::builder()
2409            .update_participant_state(
2410                ProtoName::from_strings(["org", "ns", "test"]),
2411                ParticipantState::Offline,
2412                42,
2413            )
2414            .as_update_participant_state_payload()
2415            .is_ok());
2416    }
2417
2418    #[test]
2419    fn test_builder_with_command_payload() {
2420        let source = ProtoName::from_strings(["org", "ns", "type"]).with_id(1);
2421        let dest = ProtoName::from_strings(["org", "ns", "app"]).with_id(2);
2422        let cmd_payload = CommandPayload::builder().discovery_request();
2423
2424        let msg = ProtoMessage::builder()
2425            .source(source)
2426            .destination(dest)
2427            .session_type(ProtoSessionType::PointToPoint)
2428            .session_message_type(SessionMessageType::DiscoveryRequest)
2429            .session_id(1)
2430            .command_payload(cmd_payload)
2431            .build_publish()
2432            .unwrap();
2433
2434        assert!(msg.is_publish());
2435        assert_eq!(
2436            msg.get_session_message_type(),
2437            SessionMessageType::DiscoveryRequest
2438        );
2439    }
2440
2441    #[test]
2442    fn test_validate_link_without_link_type() {
2443        let link = ProtoLink { link_type: None };
2444        let msg = ProtoMessage::new(HashMap::new(), ProtoLinkMessageType(link));
2445        assert!(matches!(msg.validate(), Err(MessageError::LinkTypeNotSet)));
2446    }
2447
2448    #[test]
2449    fn test_validate_link_with_link_type() {
2450        let link = ProtoLink {
2451            link_type: Some(ProtoLinkType::LinkNegotiation(LinkNegotiationPayload {
2452                link_id: "abc".into(),
2453                slim_version: "1.0.0".into(),
2454                is_reply: false,
2455                link_ecdh_public_key: vec![],
2456                connection_type: 0,
2457                node_id: String::new(),
2458                deployment_name: String::new(),
2459            })),
2460        };
2461        let msg = ProtoMessage::new(HashMap::new(), ProtoLinkMessageType(link));
2462        assert!(msg.validate().is_ok());
2463    }
2464
2465    #[test]
2466    fn test_build_link_negotiation_request() {
2467        let msg = ProtoMessage::builder().build_link_negotiation(
2468            "my-id",
2469            "1.2.3",
2470            false,
2471            None,
2472            LinkConnectionType::Remote,
2473            "",
2474            "",
2475        );
2476        assert!(msg.is_link());
2477        assert!(!msg.is_publish());
2478        assert!(!msg.is_subscribe());
2479        assert!(msg.validate().is_ok());
2480    }
2481
2482    #[test]
2483    fn test_build_link_negotiation_reply() {
2484        let msg = ProtoMessage::builder().build_link_negotiation(
2485            "my-id",
2486            "1.2.3",
2487            true,
2488            None,
2489            LinkConnectionType::Remote,
2490            "",
2491            "",
2492        );
2493        assert!(msg.is_link());
2494        assert!(msg.validate().is_ok());
2495    }
2496
2497    #[test]
2498    fn test_validate_subscribe_missing_source_encoded_name() {
2499        let valid = ProtoName::from_strings(["org", "ns", "agent"]);
2500        let hdr = SlimHeader {
2501            source: Some(ProtoName {
2502                name: None,
2503                str_name: None,
2504            }),
2505            destination: Some(valid),
2506            ..Default::default()
2507        };
2508        let msg = ProtoMessage::new(
2509            HashMap::new(),
2510            ProtoSubscribeType(ProtoSubscribe {
2511                header: Some(hdr),
2512                ..Default::default()
2513            }),
2514        );
2515        assert!(matches!(
2516            msg.validate(),
2517            Err(MessageError::SourceEncodedNameNotFound)
2518        ));
2519    }
2520
2521    #[test]
2522    fn test_validate_subscribe_missing_destination_encoded_name() {
2523        let valid = ProtoName::from_strings(["org", "ns", "agent"]);
2524        let hdr = SlimHeader {
2525            source: Some(valid),
2526            destination: Some(ProtoName {
2527                name: None,
2528                str_name: None,
2529            }),
2530            ..Default::default()
2531        };
2532        let msg = ProtoMessage::new(
2533            HashMap::new(),
2534            ProtoSubscribeType(ProtoSubscribe {
2535                header: Some(hdr),
2536                ..Default::default()
2537            }),
2538        );
2539        assert!(matches!(
2540            msg.validate(),
2541            Err(MessageError::DestinationEncodedNameNotFound)
2542        ));
2543    }
2544
2545    #[test]
2546    fn test_participant_settings_convenience_methods() {
2547        let bidirectional = ParticipantSettings::bidirectional();
2548        assert!(bidirectional.sends_data);
2549        assert!(bidirectional.receives_data);
2550        assert!(bidirectional.is_sender());
2551        assert!(bidirectional.is_receiver());
2552
2553        let send_only = ParticipantSettings::send_only();
2554        assert!(send_only.sends_data);
2555        assert!(!send_only.receives_data);
2556        assert!(send_only.is_sender());
2557        assert!(!send_only.is_receiver());
2558
2559        let receive_only = ParticipantSettings::receive_only();
2560        assert!(!receive_only.sends_data);
2561        assert!(receive_only.receives_data);
2562        assert!(!receive_only.is_sender());
2563        assert!(receive_only.is_receiver());
2564    }
2565}
2566
2567// ConnType → LinkConnectionType conversion
2568impl From<slim_config::conn_type::ConnType> for crate::dataplane::proto::v1::LinkConnectionType {
2569    fn from(ct: slim_config::conn_type::ConnType) -> Self {
2570        match ct {
2571            slim_config::conn_type::ConnType::Peer => {
2572                crate::dataplane::proto::v1::LinkConnectionType::Peer
2573            }
2574            slim_config::conn_type::ConnType::Edge => {
2575                crate::dataplane::proto::v1::LinkConnectionType::Edge
2576            }
2577            _ => crate::dataplane::proto::v1::LinkConnectionType::Remote,
2578        }
2579    }
2580}