1use std::{
2 fmt::{self, Display, Formatter},
3 num::ParseIntError,
4 str::Utf8Error,
5};
6
7use super::error::WordType;
8
9#[derive(Debug, PartialEq)]
27pub enum Word<'a> {
28 Category(WordCategory),
30 Tag(u16),
32 Attribute(WordAttribute<'a>),
34 Message(&'a str),
36}
37
38impl Word<'_> {
39 pub fn category(&self) -> Option<&WordCategory> {
41 match self {
42 Word::Category(category) => Some(category),
43 _ => None,
44 }
45 }
46
47 pub fn tag(&self) -> Option<u16> {
49 match self {
50 Word::Tag(tag) => Some(*tag),
51 _ => None,
52 }
53 }
54
55 pub fn generic(&self) -> Option<&str> {
58 match self {
59 Word::Message(generic) => Some(generic),
60 _ => None,
61 }
62 }
63
64 pub fn word_type(&self) -> WordType {
66 match self {
67 Word::Category(_) => WordType::Category,
68 Word::Tag(_) => WordType::Tag,
69 Word::Attribute(_) => WordType::Attribute,
70 Word::Message(_) => WordType::Message,
71 }
72 }
73}
74
75impl Display for Word<'_> {
76 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
77 match self {
78 Word::Category(category) => write!(f, "{}", category),
79 Word::Tag(tag) => write!(f, ".tag={}", tag),
80 Word::Attribute(WordAttribute {
81 key,
82 value,
83 value_raw: _,
84 }) => {
85 write!(f, "={}={}", key, value.unwrap_or(""))
86 }
87 Word::Message(generic) => write!(f, "{}", generic),
88 }
89 }
90}
91
92impl<'a> TryFrom<&'a [u8]> for Word<'a> {
93 type Error = WordError;
94
95 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
96 if let Ok(s) = std::str::from_utf8(value) {
99 if let Ok(category) = WordCategory::try_from(s) {
101 return Ok(Word::Category(category));
102 }
103
104 if let Some(stripped) = s.strip_prefix(".tag=") {
106 let tag = stripped.parse::<u16>()?;
107 return Ok(Word::Tag(tag));
108 }
109 }
110
111 if !value.is_empty() && value[0] == b'=' {
113 return Ok(Word::Attribute(WordAttribute::try_from(value)?));
115 }
116
117 Ok(Word::Message(std::str::from_utf8(value)?))
119 }
120}
121
122#[derive(Debug, Clone, PartialEq)]
126pub enum WordCategory {
127 Done,
129 Reply,
131 Trap,
133 Fatal,
135}
136
137impl TryFrom<&str> for WordCategory {
138 type Error = ();
139
140 fn try_from(value: &str) -> Result<Self, Self::Error> {
141 match value {
142 "!done" => Ok(Self::Done),
143 "!re" => Ok(Self::Reply),
144 "!trap" => Ok(Self::Trap),
145 "!fatal" => Ok(Self::Fatal),
146 _ => Err(()),
147 }
148 }
149}
150
151impl Display for WordCategory {
152 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
153 match self {
154 WordCategory::Done => write!(f, "!done"),
155 WordCategory::Reply => write!(f, "!re"),
156 WordCategory::Trap => write!(f, "!trap"),
157 WordCategory::Fatal => write!(f, "!fatal"),
158 }
159 }
160}
161
162#[derive(Debug, PartialEq)]
164pub struct WordAttribute<'a> {
165 pub key: &'a str,
167 pub value: Option<&'a str>,
169 pub value_raw: Option<&'a [u8]>,
171}
172
173impl<'a> TryFrom<&'a [u8]> for WordAttribute<'a> {
174 type Error = WordError;
175
176 fn try_from(word: &'a [u8]) -> Result<Self, Self::Error> {
177 if word.is_empty() || word[0] != b'=' {
179 return Err(WordError::Attribute);
180 }
181
182 let mut parts = word[1..].splitn(2, |&b| b == b'=');
184
185 let key_bytes = parts.next().ok_or(WordError::Attribute)?;
187 let key = std::str::from_utf8(key_bytes).map_err(|_| WordError::AttributeKeyNotUtf8)?;
188
189 let value_raw = parts.next();
191
192 let value = value_raw.and_then(|v| std::str::from_utf8(v).ok());
194
195 Ok(Self {
196 key,
197 value_raw,
198 value,
199 })
200 }
201}
202
203#[derive(Debug, PartialEq)]
205pub enum WordError {
206 Utf8(Utf8Error),
208 Tag(ParseIntError),
210 Attribute,
212 AttributeKeyNotUtf8,
214}
215
216impl From<Utf8Error> for WordError {
217 fn from(e: Utf8Error) -> Self {
218 Self::Utf8(e)
219 }
220}
221
222impl From<ParseIntError> for WordError {
223 fn from(e: ParseIntError) -> Self {
224 Self::Tag(e)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 impl<'a> From<(&'a str, Option<&'a str>)> for WordAttribute<'a> {
233 fn from(value: (&'a str, Option<&'a str>)) -> Self {
234 Self {
235 key: value.0,
236 value: value.1,
237 value_raw: value.1.map(|v| v.as_bytes()),
238 }
239 }
240 }
241
242 #[test]
243 fn test_word_parsing() {
244 assert_eq!(
246 Word::try_from(b"!done".as_ref()).unwrap(),
247 Word::Category(WordCategory::Done)
248 );
249
250 assert_eq!(
251 Word::try_from(b".tag=123".as_ref()).unwrap(),
252 Word::Tag(123)
253 );
254
255 assert_eq!(
256 Word::try_from(b"=name=ether1".as_ref()).unwrap(),
257 Word::Attribute(("name", Some("ether1")).into())
258 );
259
260 assert_eq!(
261 Word::try_from(b"!fatal".as_ref()).unwrap(),
262 Word::Category(WordCategory::Fatal)
263 );
264
265 assert_eq!(
266 Word::try_from(b"unknownword".as_ref()).unwrap(),
267 Word::Message("unknownword")
268 );
269
270 assert!(Word::try_from(b".tag=notanumber".as_ref()).is_err());
272
273 assert!(Word::try_from(b"\xFF\xFF".as_ref()).is_err());
275 }
276
277 #[test]
278 fn test_display_for_word() {
279 let word = Word::Category(WordCategory::Done);
281 assert_eq!(format!("{}", word), "!done");
282
283 let word = Word::Tag(123);
284 assert_eq!(format!("{}", word), ".tag=123");
285
286 let word = Word::Attribute(("name", Some("ether1")).into());
287 assert_eq!(format!("{}", word), "=name=ether1");
288
289 let word = Word::Message("unknownword");
290 assert_eq!(format!("{}", word), "unknownword");
291 }
292}