use core::{
fmt::{self, Display, Formatter},
str::Utf8Error,
};
use thiserror::Error;
use crate::error::WordType;
use crate::tag::Tag;
#[derive(Debug, PartialEq)]
pub enum Word<'a> {
Category(WordCategory),
Tag(Tag),
Attribute(WordAttribute<'a>),
Message(&'a str),
}
impl Word<'_> {
pub fn category(&self) -> Option<&WordCategory> {
match self {
Word::Category(category) => Some(category),
_ => None,
}
}
pub fn tag(&self) -> Option<Tag> {
match self {
Word::Tag(tag) => Some(*tag),
_ => None,
}
}
pub fn generic(&self) -> Option<&str> {
match self {
Word::Message(generic) => Some(generic),
_ => None,
}
}
pub fn word_type(&self) -> WordType {
match self {
Word::Category(_) => WordType::Category,
Word::Tag(_) => WordType::Tag,
Word::Attribute(_) => WordType::Attribute,
Word::Message(_) => WordType::Message,
}
}
}
impl Display for Word<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Word::Category(category) => write!(f, "{category}"),
Word::Tag(tag) => write!(f, ".tag={tag}"),
Word::Attribute(WordAttribute {
key,
value,
value_raw: _,
}) => {
write!(f, "={}={}", key, value.unwrap_or(""))
}
Word::Message(generic) => write!(f, "{generic}"),
}
}
}
impl<'a> TryFrom<&'a [u8]> for Word<'a> {
type Error = WordError;
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
match value.first() {
Some(b'!') => match value {
b"!done" => Ok(Word::Category(WordCategory::Done)),
b"!re" => Ok(Word::Category(WordCategory::Reply)),
b"!trap" => Ok(Word::Category(WordCategory::Trap)),
b"!fatal" => Ok(Word::Category(WordCategory::Fatal)),
b"!empty" => Ok(Word::Category(WordCategory::Empty)),
_ => Ok(Word::Message(core::str::from_utf8(value)?)),
},
Some(b'.') => {
if value.starts_with(b".tag=") {
let tag = Tag::try_from_ascii_bytes(&value[5..])?;
Ok(Word::Tag(tag))
} else {
Ok(Word::Message(core::str::from_utf8(value)?))
}
}
Some(b'=') => Ok(Word::Attribute(WordAttribute::try_from(value)?)),
_ => Ok(Word::Message(core::str::from_utf8(value)?)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WordCategory {
Done,
Reply,
Trap,
Fatal,
Empty,
}
impl TryFrom<&str> for WordCategory {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"!done" => Ok(Self::Done),
"!re" => Ok(Self::Reply),
"!trap" => Ok(Self::Trap),
"!fatal" => Ok(Self::Fatal),
"!empty" => Ok(Self::Empty),
_ => Err(()),
}
}
}
impl Display for WordCategory {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
WordCategory::Done => write!(f, "!done"),
WordCategory::Reply => write!(f, "!re"),
WordCategory::Trap => write!(f, "!trap"),
WordCategory::Fatal => write!(f, "!fatal"),
WordCategory::Empty => write!(f, "!empty"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WordAttribute<'a> {
pub key: &'a str,
pub value: Option<&'a str>,
pub value_raw: Option<&'a [u8]>,
}
impl<'a> TryFrom<&'a [u8]> for WordAttribute<'a> {
type Error = WordError;
fn try_from(word: &'a [u8]) -> Result<Self, Self::Error> {
if word.is_empty() || word[0] != b'=' {
return Err(WordError::Attribute);
}
let mut parts = word[1..].splitn(2, |&b| b == b'=');
let key_bytes = parts.next().ok_or(WordError::Attribute)?;
let key = core::str::from_utf8(key_bytes).map_err(|_| WordError::AttributeKeyNotUtf8)?;
let value_raw = parts.next().filter(|value| !value.is_empty());
let value = value_raw.and_then(|v| core::str::from_utf8(v).ok());
Ok(Self {
key,
value,
value_raw,
})
}
}
#[derive(Error, Debug, PartialEq, Clone)]
pub enum WordError {
#[error("UTF-8 decoding error: {0}")]
Utf8(#[from] Utf8Error),
#[error("Tag parsing error: {0}")]
Tag(#[from] uuid::Error),
#[error("Invalid attribute format")]
Attribute,
#[error("Attribute key is not valid UTF-8")]
AttributeKeyNotUtf8,
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::format;
use uuid::Uuid;
use super::*;
impl<'a> From<(&'a str, Option<&'a str>)> for WordAttribute<'a> {
fn from(value: (&'a str, Option<&'a str>)) -> Self {
Self {
key: value.0,
value: value.1,
value_raw: value.1.map(|v| v.as_bytes()),
}
}
}
#[test]
fn test_word_parsing() {
assert_eq!(
Word::try_from(b"!done".as_ref()).unwrap(),
Word::Category(WordCategory::Done)
);
assert_eq!(
Word::try_from(b".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8".as_ref()).unwrap(),
Word::Tag(Tag::from(Uuid::from_bytes([
0xa1, 0xa2, 0xa3, 0xa4, 0xb1, 0xb2, 0xc1, 0xc2, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6,
0xd7, 0xd8
])))
);
assert_eq!(
Word::try_from(b"=name=ether1".as_ref()).unwrap(),
Word::Attribute(("name", Some("ether1")).into())
);
assert_eq!(
Word::try_from(b"=tag=".as_ref()).unwrap(),
Word::Attribute(("tag", None).into())
);
assert_eq!(
Word::try_from(b"!fatal".as_ref()).unwrap(),
Word::Category(WordCategory::Fatal)
);
assert_eq!(
Word::try_from(b"!empty".as_ref()).unwrap(),
Word::Category(WordCategory::Empty)
);
assert_eq!(
Word::try_from(b"unknownword".as_ref()).unwrap(),
Word::Message("unknownword")
);
assert!(Word::try_from(b".tag=not-a-valid-uuid".as_ref()).is_err());
assert!(Word::try_from(b"\xFF\xFF".as_ref()).is_err());
}
#[test]
fn test_display_for_word() {
let word = Word::Category(WordCategory::Done);
assert_eq!(format!("{}", word), "!done");
let word = Word::Tag(Tag::from(Uuid::from_bytes([
0xa1, 0xa2, 0xa3, 0xa4, 0xb1, 0xb2, 0xc1, 0xc2, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6,
0xd7, 0xd8,
])));
assert_eq!(
format!("{}", word),
".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"
);
let word = Word::Attribute(("name", Some("ether1")).into());
assert_eq!(format!("{}", word), "=name=ether1");
let word = Word::Attribute(("disabled", None).into());
assert_eq!(format!("{}", word), "=disabled=");
let word = Word::Message("unknownword");
assert_eq!(format!("{}", word), "unknownword");
let word = Word::Category(WordCategory::Empty);
assert_eq!(format!("{}", word), "!empty");
}
}