pub const fn check_mikrotik_command(cmd: &str) -> &str {
let bytes = cmd.as_bytes();
let len = bytes.len();
assert!(len != 0, "MikroTik command cannot be empty.");
assert!(bytes[0] == b'/', "MikroTik command must start with '/'.");
let mut prev_was_delimiter = true; let mut i = 1;
while i < len {
let c = bytes[i] as char;
if c == '/' || c == ' ' {
assert!(
!prev_was_delimiter,
"No empty segments or consecutive delimiters allowed."
);
prev_was_delimiter = true;
} else {
let is_valid_char = c.is_ascii_alphanumeric() || c == '-' || c == '_';
assert!(
is_valid_char,
"Invalid character in MikroTik command. Must be [a-zA-Z0-9_-]"
);
prev_was_delimiter = false;
}
i += 1;
}
assert!(!prev_was_delimiter, "Command cannot end with a delimiter.");
cmd
}
#[macro_export]
macro_rules! command {
($cmd:literal $(, $key:ident $(= $value:expr)? )* $(,)?) => {{
const VALIDATED: &str = $crate::macros::check_mikrotik_command($cmd);
#[allow(unused_mut)]
let mut builder = $crate::command::CommandBuilder::new()
.command(VALIDATED);
$(
builder = builder.attribute(
stringify!($key),
command!(@opt $($value)?)
);
)*
builder.build()
}};
(@opt $value:expr) => { Some($value) };
(@opt) => { None };
}
#[cfg(test)]
mod tests {
use alloc::string::String;
fn parse_words(data: &[u8]) -> alloc::vec::Vec<String> {
let mut words = alloc::vec::Vec::new();
let mut i = 0;
while i < data.len() {
if i >= data.len() {
break;
}
let len = data[i] as usize;
i += 1;
if len == 0 {
break;
}
if i + len > data.len() {
panic!("Malformed command data");
}
let word = &data[i..i + len];
i += len;
words.push(String::from_utf8_lossy(word).into_owned());
}
words
}
#[test]
fn test_command_no_attributes() {
let cmd = command!("/system/resource/print");
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/system/resource/print");
assert!(words[1].starts_with(".tag="));
assert_eq!(words.len(), 2);
}
#[test]
fn test_command_with_one_attribute() {
let cmd = command!("/interface/ethernet/print", user = "admin");
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/interface/ethernet/print");
assert!(words[1].starts_with(".tag="));
assert_eq!(words[2], "=user=admin");
assert_eq!(words.len(), 3);
}
#[test]
fn test_command_with_multiple_attributes() {
let cmd = command!("/some/random", attribute_no_value, another = "value");
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/some/random");
assert!(words[1].starts_with(".tag="));
assert_eq!(words[2], "=attribute_no_value=");
assert_eq!(words[3], "=another=value");
assert_eq!(words.len(), 4);
}
}