mikrotik_rs/protocol/
mod.rs

1use std::{
2    collections::HashMap,
3    fmt::{self, Display, Formatter},
4    num::ParseIntError,
5};
6
7use error::{MissingWord, ProtocolError, WordType};
8use sentence::Sentence;
9use word::{Word, WordAttribute, WordCategory};
10
11/// Module containing the command parser and response types.
12pub mod command;
13/// Module containing the error types for the command parser.
14pub mod error;
15/// Module containing the sentence parser and response types.
16pub mod sentence;
17/// Module containing the word parser and response types.
18pub mod word;
19
20/// Type alias for a fatal response [`String`].
21pub type FatalResponse = String;
22
23/// Various types of responses a command can produce.
24#[derive(Debug, Clone)]
25pub enum CommandResponse {
26    /// Represents a successful command completion response.
27    Done(DoneResponse),
28    /// Represents a reply to a command, including a tag and multiple attributes.
29    Reply(ReplyResponse),
30    /// Represents an error or warning while executing a command, including a tag and message.
31    Trap(TrapResponse),
32    /// Represents a fatal error response.
33    Fatal(FatalResponse),
34}
35
36impl CommandResponse {
37    /// Returns the tag associated with the response, if available.
38    ///
39    /// Returns [`None`] for [`CommandResponse::Fatal`] responses as they do not contain tags.
40    pub fn tag(&self) -> Option<u16> {
41        match &self {
42            Self::Done(d) => Some(d.tag),
43            Self::Reply(r) => Some(r.tag),
44            Self::Trap(t) => Some(t.tag),
45            Self::Fatal(_) => None,
46        }
47    }
48}
49
50impl TryFrom<Sentence<'_>> for CommandResponse {
51    type Error = ProtocolError;
52
53    fn try_from(mut sentence_iter: Sentence) -> Result<Self, Self::Error> {
54        let word = sentence_iter
55            .next()
56            .ok_or::<ProtocolError>(MissingWord::Category.into())??;
57
58        let category = word.category().ok_or(ProtocolError::WordSequence {
59            word: word.word_type(),
60            expected: vec![WordType::Category],
61        })?;
62
63        match category {
64            WordCategory::Done => {
65                let word = sentence_iter
66                    .next()
67                    .ok_or::<ProtocolError>(MissingWord::Tag.into())??;
68
69                // !done is composed of a single tag
70                let tag = word.tag().ok_or(ProtocolError::WordSequence {
71                    word: word.into(),
72                    expected: vec![WordType::Tag],
73                })?;
74                Ok(CommandResponse::Done(DoneResponse { tag }))
75            }
76            WordCategory::Reply => {
77                // !re is composed of a tag and a list of attributes
78                // The tag is mandatory but its position is not fixed
79                let mut tag = None;
80                let mut attributes = HashMap::<String, Option<String>>::new();
81                let mut attributes_raw = HashMap::<String, Option<Vec<u8>>>::new();
82
83                for word in sentence_iter {
84                    let word = word?;
85                    match word {
86                        Word::Tag(t) => tag = Some(t),
87                        Word::Attribute(WordAttribute {
88                            key,
89                            value,
90                            value_raw,
91                        }) => {
92                            attributes.insert(key.to_owned(), value.map(String::from));
93                            attributes_raw.insert(key.to_owned(), value_raw.map(Vec::from));
94                        }
95                        word => {
96                            return Err(ProtocolError::WordSequence {
97                                word: word.into(),
98                                expected: vec![WordType::Tag, WordType::Attribute],
99                            });
100                        }
101                    }
102                }
103
104                let tag = tag.ok_or::<ProtocolError>(MissingWord::Category.into())?;
105
106                Ok(CommandResponse::Reply(ReplyResponse {
107                    tag,
108                    attributes,
109                    attributes_raw,
110                }))
111            }
112            WordCategory::Trap => {
113                // !trap is composed of a tag, and two optional attributes: category and message
114                // The tag is mandatory but its position is not fixed
115                // The category and message are optional and can appear in any order
116                let mut tag = None;
117                let mut category = None;
118                let mut message = None;
119
120                for word in sentence_iter {
121                    let word = word?;
122                    match word {
123                        Word::Tag(t) => tag = Some(t),
124                        Word::Attribute(WordAttribute {
125                            key,
126                            value,
127                            value_raw: _,
128                        }) => match key {
129                            "category" => {
130                                category = value.map(TrapCategory::try_from).transpose()?;
131                            }
132                            "message" => {
133                                message = value.map(String::from);
134                            }
135                            key => {
136                                return Err(TrapCategoryError::InvalidAttribute {
137                                    key: key.into(),
138                                    value: value.map(|v| v.into()),
139                                }
140                                .into());
141                            }
142                        },
143                        word => {
144                            return Err(ProtocolError::WordSequence {
145                                word: word.into(),
146                                expected: vec![WordType::Tag, WordType::Attribute],
147                            });
148                        }
149                    }
150                }
151
152                let tag = tag.ok_or::<ProtocolError>(MissingWord::Category.into())?;
153                let message = message.ok_or(TrapCategoryError::MissingMessageAttribute)?;
154
155                Ok(CommandResponse::Trap(TrapResponse {
156                    tag,
157                    category,
158                    message,
159                }))
160            }
161            WordCategory::Fatal => {
162                // !fatal is composed of a single message
163                let word = sentence_iter
164                    .next()
165                    .ok_or::<ProtocolError>(MissingWord::Message.into())??;
166
167                let reason = word.generic().ok_or(ProtocolError::WordSequence {
168                    word: word.word_type(),
169                    expected: vec![WordType::Message],
170                })?;
171
172                Ok(CommandResponse::Fatal(reason.to_string()))
173            }
174        }
175    }
176}
177
178/// Represents a (tagged) successful command completion response.
179#[derive(Debug, Clone)]
180pub struct DoneResponse {
181    /// The tag associated with the command.
182    pub tag: u16,
183}
184
185impl Display for DoneResponse {
186    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
187        write!(f, "DoneResponse {{ tag: {} }}", self.tag)
188    }
189}
190
191/// Represents a reply to a command, including a tag and multiple attributes.
192#[derive(Debug, Clone)]
193pub struct ReplyResponse {
194    /// The tag associated with the command.
195    pub tag: u16,
196    /// The attributes of the reply.
197    pub attributes: HashMap<String, Option<String>>,
198    /// The raw attributes of the reply.
199    pub attributes_raw: HashMap<String, Option<Vec<u8>>>,
200}
201
202impl Display for ReplyResponse {
203    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
204        write!(
205            f,
206            "ReplyResponse {{ tag: {}, attributes: {:?} }}",
207            self.tag, self.attributes
208        )
209    }
210}
211
212/// Represents an error or warning while executing a command, including a tag and message.
213#[derive(Debug, Clone)]
214pub struct TrapResponse {
215    /// The tag associated with the command.
216    pub tag: u16,
217    /// The category of the trap.
218    pub category: Option<TrapCategory>,
219    /// The message associated with the trap.
220    pub message: String,
221}
222
223impl Display for TrapResponse {
224    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
225        write!(
226            f,
227            "TrapResponse {{ tag: {}, category: {:?}, message: \"{}\" }}",
228            self.tag, self.category, self.message
229        )
230    }
231}
232
233/// Categories for `TrapResponse`, defining the nature of the trap.
234#[derive(Debug, Clone)]
235#[repr(u8)]
236pub enum TrapCategory {
237    /// 0 - missing item or command
238    MissingItemOrCommand = 0,
239    /// 1 - argument value failure
240    ArgumentValueFailure = 1,
241    /// 2 - execution of command interrupted
242    CommandExecutionInterrupted = 2,
243    /// 3 - scripting related failure
244    ScriptingFailure = 3,
245    /// 4 - general failure
246    GeneralFailure = 4,
247    /// 5 - API related failure
248    APIFailure = 5,
249    /// 6 - TTY related failure
250    TTYFailure = 6,
251    /// 7 - value generated with :return command
252    ReturnValue = 7,
253}
254
255impl TryFrom<u8> for TrapCategory {
256    type Error = TrapCategoryError;
257
258    fn try_from(n: u8) -> Result<Self, Self::Error> {
259        match n {
260            0 => Ok(TrapCategory::MissingItemOrCommand),
261            1 => Ok(TrapCategory::ArgumentValueFailure),
262            2 => Ok(TrapCategory::CommandExecutionInterrupted),
263            3 => Ok(TrapCategory::ScriptingFailure),
264            4 => Ok(TrapCategory::GeneralFailure),
265            5 => Ok(TrapCategory::APIFailure),
266            6 => Ok(TrapCategory::TTYFailure),
267            7 => Ok(TrapCategory::ReturnValue),
268            n => Err(TrapCategoryError::OutOfRange(n)),
269        }
270    }
271}
272
273impl TryFrom<&str> for TrapCategory {
274    type Error = ProtocolError;
275
276    fn try_from(s: &str) -> Result<Self, Self::Error> {
277        let n = s
278            .parse::<u8>()
279            .map_err(|e| ProtocolError::TrapCategory(TrapCategoryError::Invalid(e)))?;
280        TrapCategory::try_from(n).map_err(ProtocolError::from)
281    }
282}
283
284/// Errors that can occur while parsing trap categories in response sentences.
285///
286/// This enum provides more detailed information about issues that can arise while parsing trap
287/// categories, such as missing categories, errors while converting category strings to integers,
288/// or categories that are out of range.
289#[derive(Debug)]
290pub enum TrapCategoryError {
291    /// Invalid value encountered while parsing a trap category.
292    Invalid(ParseIntError),
293    /// Error indicating that a trap category is out of range. Valid categories are 0-7.
294    OutOfRange(u8),
295    /// Trap expects a category or message, but got something else.
296    InvalidAttribute {
297        /// The key of the invalid attribute.
298        key: String,
299        /// The value of the invalid attribute, if present.
300        value: Option<String>,
301    },
302    /// Missing category attribute in a trap response.
303    MissingMessageAttribute,
304}