rusmpp_core/session/
session_state.rs

1use crate::CommandId;
2
3/// The [`SessionState`] represents the state of an ESME session in the `SMPP` 5.0 protocol.
4///
5/// The session state determines what operations are allowed at any given point in the
6/// communication between an ESME (External Short Message Entity) and an MC (Message Center).
7///
8/// The session state transitions are triggered by bind, unbind, and outbind operations.
9#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
10pub enum SessionState {
11    /// CLOSED state.
12    ///
13    /// This is the initial state before any connection is established.
14    /// In this state, no communication is possible between the ESME and MC.
15    #[default]
16    Closed,
17
18    /// OPEN state.
19    ///
20    /// This state is entered after a connection is established between
21    /// the ESME and MC, but before any `SMPP` bind operation is performed.
22    /// In this state, only bind operations are allowed.
23    Open,
24
25    /// BOUND_TX state (Transmitter mode).
26    ///
27    /// This state is entered after a successful bind_transmitter operation.
28    /// In this state, the ESME can send messages to the MC but cannot receive messages.
29    BoundTx,
30
31    /// BOUND_RX state (Receiver mode).
32    ///
33    /// This state is entered after a successful bind_receiver operation.
34    /// In this state, the ESME can receive messages from the MC but cannot send messages.
35    BoundRx,
36
37    /// BOUND_TRX state (Transceiver mode).
38    ///
39    /// This state is entered after a successful bind_transceiver operation.
40    /// In this state, the ESME can both send messages to and receive messages from the MC.
41    BoundTrx,
42
43    /// OUTBOUND state.
44    ///
45    /// This state is entered after an MC initiates an outbind operation to an ESME.
46    /// The ESME must respond with a bind_receiver or bind_transceiver operation.
47    /// In this state, no messaging operations are allowed until the ESME completes the binding process.
48    Outbound,
49
50    /// UNBOUND state.
51    ///
52    /// This state is entered after an unbind operation is initiated by either the ESME or MC.
53    /// The session is in the process of being terminated, but the unbind_resp has not yet been sent.
54    /// No messaging operations are allowed in this state.
55    Unbound,
56}
57
58impl SessionState {
59    /// Returns true if the session is a bound state.
60    ///
61    /// One of the following states:
62    /// [`SessionState::BoundTx`], [`SessionState::BoundRx`] or [`SessionState::BoundTrx`].
63    pub const fn is_bound(self) -> bool {
64        matches!(self, Self::BoundTx | Self::BoundRx | Self::BoundTrx)
65    }
66
67    /// Determines whether the current session state allows sending a given SMPP command as an ESME.
68    ///
69    /// # Arguments
70    ///
71    /// * `command` - The SMPP command to check.
72    ///
73    /// # Returns true if an ESME in that state can send this command.
74    ///
75    /// This follows the 2.4 Operation Matrix of the SMPP 5.0 specification
76    pub const fn can_send_as_esme(self, command: CommandId) -> bool {
77        match self {
78            SessionState::Closed => false,
79            SessionState::Open | SessionState::Outbound => {
80                matches!(
81                    command,
82                    CommandId::BindReceiver
83                        | CommandId::BindTransmitter
84                        | CommandId::BindTransceiver
85                        | CommandId::EnquireLink
86                        | CommandId::EnquireLinkResp
87                        | CommandId::GenericNack
88                )
89            }
90            SessionState::BoundTx => {
91                matches!(
92                    command,
93                    CommandId::BroadcastSm
94                        | CommandId::CancelBroadcastSm
95                        | CommandId::CancelSm
96                        | CommandId::DataSm
97                        | CommandId::EnquireLink
98                        | CommandId::EnquireLinkResp
99                        | CommandId::GenericNack
100                        | CommandId::QueryBroadcastSm
101                        | CommandId::QuerySm
102                        | CommandId::ReplaceSm
103                        | CommandId::SubmitMulti
104                        | CommandId::SubmitSm
105                        | CommandId::Unbind
106                        | CommandId::UnbindResp
107                )
108            }
109            SessionState::BoundRx => {
110                matches!(
111                    command,
112                    CommandId::DataSmResp
113                        | CommandId::DeliverSmResp
114                        | CommandId::EnquireLink
115                        | CommandId::EnquireLinkResp
116                        | CommandId::GenericNack
117                        | CommandId::Unbind
118                        | CommandId::UnbindResp
119                )
120            }
121            SessionState::BoundTrx => {
122                SessionState::BoundTx.can_send_as_esme(command)
123                    || SessionState::BoundRx.can_send_as_esme(command)
124            }
125            SessionState::Unbound => {
126                matches!(
127                    command,
128                    CommandId::EnquireLink | CommandId::EnquireLinkResp | CommandId::GenericNack
129                )
130            }
131        }
132    }
133
134    /// Determines whether the current session state allows an ESME to receive a given SMPP command.
135    ///
136    /// # Arguments
137    ///
138    /// * `command` - The SMPP command to check.
139    ///
140    /// # Returns true if an ESME in that state can receive this command.
141    ///
142    /// This follows the 2.4 Operation Matrix of the SMPP 5.0 specification
143    pub const fn can_receive_as_esme(self, command: CommandId) -> bool {
144        self.can_send_as_mc(command)
145    }
146
147    /// Determines whether the current session state allows a MC to send a given SMPP command.
148    ///
149    /// # Arguments
150    ///
151    /// * `command` - The SMPP command to check.
152    ///
153    /// # Returns true if a MC in that state can send this command.
154    ///
155    /// This follows the 2.4 Operation Matrix of the SMPP 5.0 specification
156    pub const fn can_send_as_mc(self, command: CommandId) -> bool {
157        match self {
158            SessionState::Closed => false,
159            SessionState::Open => {
160                matches!(
161                    command,
162                    CommandId::BindReceiverResp
163                        | CommandId::BindTransmitterResp
164                        | CommandId::BindTransceiverResp
165                        | CommandId::EnquireLink
166                        | CommandId::EnquireLinkResp
167                        | CommandId::GenericNack
168                        | CommandId::Outbind
169                )
170            }
171            SessionState::Outbound => {
172                matches!(
173                    command,
174                    CommandId::BindReceiverResp
175                        | CommandId::BindTransmitterResp
176                        | CommandId::BindTransceiverResp
177                        | CommandId::EnquireLink
178                        | CommandId::EnquireLinkResp
179                        | CommandId::GenericNack
180                )
181            }
182            SessionState::BoundTx => {
183                matches!(
184                    command,
185                    CommandId::BroadcastSmResp
186                        | CommandId::CancelBroadcastSmResp
187                        | CommandId::CancelSmResp
188                        | CommandId::DataSmResp
189                        | CommandId::EnquireLink
190                        | CommandId::EnquireLinkResp
191                        | CommandId::GenericNack
192                        | CommandId::QueryBroadcastSmResp
193                        | CommandId::QuerySmResp
194                        | CommandId::ReplaceSmResp
195                        | CommandId::SubmitMultiResp
196                        | CommandId::SubmitSmResp
197                        | CommandId::Unbind
198                        | CommandId::UnbindResp
199                )
200            }
201            SessionState::BoundRx => {
202                matches!(
203                    command,
204                    CommandId::AlertNotification
205                        | CommandId::DataSm
206                        | CommandId::DeliverSm
207                        | CommandId::EnquireLink
208                        | CommandId::EnquireLinkResp
209                        | CommandId::GenericNack
210                        | CommandId::Unbind
211                        | CommandId::UnbindResp
212                )
213            }
214            SessionState::BoundTrx => {
215                SessionState::BoundTx.can_send_as_mc(command)
216                    || SessionState::BoundRx.can_send_as_mc(command)
217            }
218            SessionState::Unbound => {
219                matches!(
220                    command,
221                    CommandId::EnquireLink | CommandId::EnquireLinkResp | CommandId::GenericNack
222                )
223            }
224        }
225    }
226
227    /// Determines whether the current session state allows a MC to receive a given SMPP command.
228    ///
229    /// # Arguments
230    ///
231    /// * `command` - The SMPP command to check.
232    ///
233    /// # Returns true if a MC in that state can receive this command.
234    ///
235    /// This follows the 2.4 Operation Matrix of the SMPP 5.0 specification
236    pub const fn can_receive_as_mc(self, command: CommandId) -> bool {
237        self.can_send_as_esme(command)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use strum::IntoEnumIterator;
245
246    const ESME_BOUND_TX_COMMANDS: [CommandId; 9] = [
247        CommandId::BroadcastSm,
248        CommandId::CancelBroadcastSm,
249        CommandId::CancelSm,
250        CommandId::DataSm,
251        CommandId::QueryBroadcastSm,
252        CommandId::QuerySm,
253        CommandId::ReplaceSm,
254        CommandId::SubmitMulti,
255        CommandId::SubmitSm,
256    ];
257
258    const ESME_BOUND_RX_COMMANDS: [CommandId; 2] =
259        [CommandId::DataSmResp, CommandId::DeliverSmResp];
260
261    #[test]
262    fn test_is_bound() {
263        assert!(!SessionState::Closed.is_bound());
264        assert!(SessionState::BoundTx.is_bound());
265        assert!(SessionState::BoundRx.is_bound());
266        assert!(SessionState::BoundTrx.is_bound());
267        assert!(!SessionState::Open.is_bound());
268        assert!(!SessionState::Outbound.is_bound());
269        assert!(!SessionState::Unbound.is_bound());
270    }
271
272    #[test]
273    fn test_status_close() {
274        for command in CommandId::iter() {
275            assert!(!SessionState::Closed.can_send_as_esme(command));
276            assert!(!SessionState::Closed.can_send_as_mc(command));
277            assert!(!SessionState::Closed.can_receive_as_esme(command));
278            assert!(!SessionState::Closed.can_receive_as_mc(command));
279        }
280    }
281
282    #[test]
283    fn test_link_nack() {
284        for command in [
285            CommandId::GenericNack,
286            CommandId::EnquireLink,
287            CommandId::EnquireLinkResp,
288        ] {
289            for state in [
290                SessionState::Open,
291                SessionState::Outbound,
292                SessionState::BoundTx,
293                SessionState::BoundRx,
294                SessionState::BoundTrx,
295                SessionState::Unbound,
296            ] {
297                assert!(state.can_send_as_esme(command));
298                assert!(state.can_send_as_mc(command));
299                assert!(state.can_receive_as_esme(command));
300                assert!(state.can_receive_as_mc(command));
301            }
302        }
303    }
304
305    #[test]
306    fn test_open_outbound() {
307        for state in [SessionState::Open, SessionState::Outbound] {
308            assert!(state.can_send_as_esme(CommandId::BindTransmitter));
309            assert!(state.can_send_as_esme(CommandId::BindTransceiver));
310            assert!(state.can_send_as_esme(CommandId::BindReceiver));
311            assert!(state.can_send_as_mc(CommandId::BindTransmitterResp));
312            assert!(state.can_send_as_mc(CommandId::BindTransceiverResp));
313            assert!(state.can_send_as_mc(CommandId::BindReceiverResp));
314        }
315    }
316
317    #[test]
318    fn test_tx() {
319        for command in ESME_BOUND_TX_COMMANDS {
320            assert!(SessionState::BoundTx.can_send_as_esme(command));
321            assert!(SessionState::BoundTx.can_receive_as_esme(command.matching_response()));
322            assert!(SessionState::BoundTx.can_receive_as_mc(command));
323            assert!(SessionState::BoundTx.can_send_as_mc(command.matching_response()));
324        }
325    }
326
327    #[test]
328    fn test_rx() {
329        for command in ESME_BOUND_RX_COMMANDS {
330            assert!(SessionState::BoundRx.can_send_as_esme(command));
331            assert!(SessionState::BoundRx.can_receive_as_esme(command.matching_request()));
332            assert!(SessionState::BoundRx.can_receive_as_mc(command));
333            assert!(SessionState::BoundRx.can_send_as_mc(command.matching_request()));
334        }
335    }
336
337    #[test]
338    fn test_trx() {
339        for command in ESME_BOUND_TX_COMMANDS {
340            assert!(SessionState::BoundTrx.can_send_as_esme(command));
341            assert!(SessionState::BoundTrx.can_receive_as_esme(command.matching_response()));
342            assert!(SessionState::BoundTrx.can_receive_as_mc(command));
343            assert!(SessionState::BoundTrx.can_send_as_mc(command.matching_response()));
344        }
345        for command in ESME_BOUND_RX_COMMANDS {
346            assert!(SessionState::BoundTrx.can_send_as_esme(command));
347            assert!(SessionState::BoundTrx.can_receive_as_esme(command.matching_request()));
348            assert!(SessionState::BoundTrx.can_receive_as_mc(command));
349            assert!(SessionState::BoundTrx.can_send_as_mc(command.matching_request()));
350        }
351    }
352}