mikrotik_rs/protocol/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
use std::{
    collections::HashMap,
    fmt::{self, Display, Formatter},
    num::ParseIntError,
};

use error::{MissingWord, ProtocolError, WordType};
use sentence::Sentence;
use word::{Word, WordAttribute, WordCategory};

/// Module containing the command parser and response types.
pub mod command;
/// Module containing the error types for the command parser.
pub mod error;
/// Module containing the sentence parser and response types.
pub mod sentence;
/// Module containing the word parser and response types.
pub mod word;

/// Type alias for a fatal response [`String`].
pub type FatalResponse = String;

/// Various types of responses a command can produce.
#[derive(Debug, Clone)]
pub enum CommandResponse {
    /// Represents a successful command completion response.
    Done(DoneResponse),
    /// Represents a reply to a command, including a tag and multiple attributes.
    Reply(ReplyResponse),
    /// Represents an error or warning while executing a command, including a tag and message.
    Trap(TrapResponse),
    /// Represents a fatal error response.
    Fatal(FatalResponse),
}

impl CommandResponse {
    /// Returns the tag associated with the response, if available.
    ///
    /// Returns [`None`] for [`CommandResponse::Fatal`] responses as they do not contain tags.
    pub fn tag(&self) -> Option<u16> {
        match &self {
            Self::Done(d) => Some(d.tag),
            Self::Reply(r) => Some(r.tag),
            Self::Trap(t) => Some(t.tag),
            Self::Fatal(_) => None,
        }
    }
}

impl TryFrom<Sentence<'_>> for CommandResponse {
    type Error = ProtocolError;

    fn try_from(mut sentence_iter: Sentence) -> Result<Self, Self::Error> {
        let word = sentence_iter
            .next()
            .ok_or::<ProtocolError>(MissingWord::Category.into())??;

        let category = word.category().ok_or(ProtocolError::WordSequence {
            word: word.word_type(),
            expected: vec![WordType::Category],
        })?;

        match category {
            WordCategory::Done => {
                let word = sentence_iter
                    .next()
                    .ok_or::<ProtocolError>(MissingWord::Tag.into())??;

                // !done is composed of a single tag
                let tag = word.tag().ok_or(ProtocolError::WordSequence {
                    word: word.into(),
                    expected: vec![WordType::Tag],
                })?;
                Ok(CommandResponse::Done(DoneResponse { tag }))
            }
            WordCategory::Reply => {
                // !re is composed of a tag and a list of attributes
                // The tag is mandatory but its position is not fixed
                let mut tag = None;
                let mut attributes = HashMap::<String, Option<String>>::new();

                for word in sentence_iter {
                    let word = word?;
                    match word {
                        Word::Tag(t) => tag = Some(t),
                        Word::Attribute(WordAttribute { key, value }) => {
                            attributes.insert(key.to_owned(), value.map(String::from));
                        }
                        word => {
                            return Err(ProtocolError::WordSequence {
                                word: word.into(),
                                expected: vec![WordType::Tag, WordType::Attribute],
                            });
                        }
                    }
                }

                let tag = tag.ok_or::<ProtocolError>(MissingWord::Category.into())?;

                Ok(CommandResponse::Reply(ReplyResponse { tag, attributes }))
            }
            WordCategory::Trap => {
                // !trap is composed of a tag, and two optional attributes: category and message
                // The tag is mandatory but its position is not fixed
                // The category and message are optional and can appear in any order
                let mut tag = None;
                let mut category = None;
                let mut message = None;

                for word in sentence_iter {
                    let word = word?;
                    match word {
                        Word::Tag(t) => tag = Some(t),
                        Word::Attribute(WordAttribute { key, value }) => match key {
                            "category" => {
                                category = value.map(TrapCategory::try_from).transpose()?;
                            }
                            "message" => {
                                message = value.map(String::from);
                            }
                            key => {
                                return Err(TrapCategoryError::InvalidAttribute {
                                    key: key.into(),
                                    value: value.map(|v| v.into()),
                                }
                                .into());
                            }
                        },
                        word => {
                            return Err(ProtocolError::WordSequence {
                                word: word.into(),
                                expected: vec![WordType::Tag, WordType::Attribute],
                            });
                        }
                    }
                }

                let tag = tag.ok_or::<ProtocolError>(MissingWord::Category.into())?;
                let message = message.ok_or(TrapCategoryError::MissingMessageAttribute)?;

                Ok(CommandResponse::Trap(TrapResponse {
                    tag,
                    category,
                    message,
                }))
            }
            WordCategory::Fatal => {
                // !fatal is composed of a single message
                let word = sentence_iter
                    .next()
                    .ok_or::<ProtocolError>(MissingWord::Message.into())??;

                let reason = word.generic().ok_or(ProtocolError::WordSequence {
                    word: word.word_type(),
                    expected: vec![WordType::Message],
                })?;

                Ok(CommandResponse::Fatal(reason.to_string()))
            }
        }
    }
}

/// Represents a (tagged) successful command completion response.
#[derive(Debug, Clone)]
pub struct DoneResponse {
    /// The tag associated with the command.
    pub tag: u16,
}

impl Display for DoneResponse {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "DoneResponse {{ tag: {} }}", self.tag)
    }
}

/// Represents a reply to a command, including a tag and multiple attributes.
#[derive(Debug, Clone)]
pub struct ReplyResponse {
    /// The tag associated with the command.
    pub tag: u16,
    /// The attributes of the reply.
    pub attributes: HashMap<String, Option<String>>,
}

impl Display for ReplyResponse {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "ReplyResponse {{ tag: {}, attributes: {:?} }}",
            self.tag, self.attributes
        )
    }
}

/// Represents an error or warning while executing a command, including a tag and message.
#[derive(Debug, Clone)]
pub struct TrapResponse {
    /// The tag associated with the command.
    pub tag: u16,
    /// The category of the trap.
    pub category: Option<TrapCategory>,
    /// The message associated with the trap.
    pub message: String,
}

impl Display for TrapResponse {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "TrapResponse {{ tag: {}, category: {:?}, message: \"{}\" }}",
            self.tag, self.category, self.message
        )
    }
}

/// Categories for `TrapResponse`, defining the nature of the trap.
#[derive(Debug, Clone)]
#[repr(u8)]
pub enum TrapCategory {
    /// 0 - missing item or command
    MissingItemOrCommand = 0,
    /// 1 - argument value failure
    ArgumentValueFailure = 1,
    /// 2 - execution of command interrupted
    CommandExecutionInterrupted = 2,
    /// 3 - scripting related failure
    ScriptingFailure = 3,
    /// 4 - general failure
    GeneralFailure = 4,
    /// 5 - API related failure
    APIFailure = 5,
    /// 6 - TTY related failure
    TTYFailure = 6,
    /// 7 - value generated with :return command
    ReturnValue = 7,
}

impl TryFrom<u8> for TrapCategory {
    type Error = TrapCategoryError;

    fn try_from(n: u8) -> Result<Self, Self::Error> {
        match n {
            0 => Ok(TrapCategory::MissingItemOrCommand),
            1 => Ok(TrapCategory::ArgumentValueFailure),
            2 => Ok(TrapCategory::CommandExecutionInterrupted),
            3 => Ok(TrapCategory::ScriptingFailure),
            4 => Ok(TrapCategory::GeneralFailure),
            5 => Ok(TrapCategory::APIFailure),
            6 => Ok(TrapCategory::TTYFailure),
            7 => Ok(TrapCategory::ReturnValue),
            n => Err(TrapCategoryError::OutOfRange(n)),
        }
    }
}

impl TryFrom<&str> for TrapCategory {
    type Error = ProtocolError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        let n = s
            .parse::<u8>()
            .map_err(|e| ProtocolError::TrapCategory(TrapCategoryError::Invalid(e)))?;
        TrapCategory::try_from(n).map_err(ProtocolError::from)
    }
}

/// Errors that can occur while parsing trap categories in response sentences.
///
/// This enum provides more detailed information about issues that can arise while parsing trap
/// categories, such as missing categories, errors while converting category strings to integers,
/// or categories that are out of range.
#[derive(Debug)]
pub enum TrapCategoryError {
    /// Invalid value encountered while parsing a trap category.
    Invalid(ParseIntError),
    /// Error indicating that a trap category is out of range. Valid categories are 0-7.
    OutOfRange(u8),
    /// Trap expects a category or message, but got something else.
    InvalidAttribute {
        /// The key of the invalid attribute.
        key: String,
        /// The value of the invalid attribute, if present.
        value: Option<String>
    },
    /// Missing category attribute in a trap response.
    MissingMessageAttribute,
}