1use std::fmt;
2use std::str::FromStr;
3
4use thiserror::Error;
5
6#[derive(Debug)]
26pub enum Command {
27 User { username: String },
29
30 ListSubscriptions,
35
36 Subscribe { url: String },
41
42 Unsubscribe { id: i64 },
47
48 ListUnread,
53
54 MarkRead { id: i64 },
59}
60
61impl fmt::Display for Command {
62 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63 match self {
64 Command::User { username } => write!(f, "USER {}", username),
65 Command::ListSubscriptions => write!(f, "LISTSUBSCRIPTIONS"),
66 Command::Subscribe { url } => write!(f, "SUBSCRIBE {}", url),
67 Command::Unsubscribe { id } => write!(f, "UNSUBSCRIBE {}", id),
68 Command::ListUnread => write!(f, "LISTUNREAD"),
69 Command::MarkRead { id } => write!(f, "MARKREAD {}", id),
70 }
71 }
72}
73
74fn check_arguments(parts: &Vec<&str>, expected: usize) -> Result<(), ParseMessageError> {
75 if parts.len() > expected + 1 {
76 return Err(ParseMessageError::TooManyArguments {
77 expected,
78 actual: parts.len() - 1,
79 });
80 }
81
82 Ok(())
83}
84
85fn at_position<T: FromStr>(
86 parts: &[&str],
87 argument_name: &str,
88 position: usize,
89) -> Result<T, ParseMessageError> {
90 let possible = parts
91 .get(position)
92 .ok_or_else(|| ParseMessageError::MissingArgument(argument_name.to_string()))?;
93
94 possible
95 .parse()
96 .map_err(|_| ParseMessageError::InvalidIntegerArgument {
97 argument: argument_name.to_string(),
98 value: possible.to_string(),
99 })
100}
101
102#[derive(Debug, Error)]
103pub enum ParseMessageError {
104 #[error("empty message")]
105 EmptyMessage,
106 #[error("unknown message type \"{0}\"")]
107 UnknownType(String),
108 #[error("missing argument \"{0}\"")]
109 MissingArgument(String),
110 #[error("too many arguments (expected {expected}, got {actual})")]
111 TooManyArguments { expected: usize, actual: usize },
112 #[error("invalid integer value \"{value}\" for argument \"{argument}\"")]
113 InvalidIntegerArgument { argument: String, value: String },
114}
115
116impl FromStr for Command {
117 type Err = ParseMessageError;
118
119 fn from_str(value: &str) -> Result<Self, Self::Err> {
120 let parts: Vec<&str> = value.split(' ').collect();
121
122 let command = parts.get(0).ok_or(ParseMessageError::EmptyMessage)?;
123
124 match *command {
125 "USER" => {
126 check_arguments(&parts, 1)?;
127
128 let username: String = at_position(&parts, "username", 1)?;
129
130 Ok(Command::User { username })
131 }
132 "LISTSUBSCRIPTIONS" => {
133 check_arguments(&parts, 0)?;
134
135 Ok(Command::ListSubscriptions)
136 }
137 "SUBSCRIBE" => {
138 check_arguments(&parts, 1)?;
139
140 let url: String = at_position(&parts, "url", 1)?;
141
142 Ok(Command::Subscribe { url })
143 }
144 "UNSUBSCRIBE" => {
145 check_arguments(&parts, 1)?;
146
147 let id: i64 = at_position(&parts, "id", 1)?;
148
149 Ok(Command::Unsubscribe { id })
150 }
151 "LISTUNREAD" => {
152 check_arguments(&parts, 0)?;
153
154 Ok(Command::ListUnread)
155 }
156 "MARKREAD" => {
157 check_arguments(&parts, 1)?;
158
159 let id: i64 = at_position(&parts, "id", 1)?;
160
161 Ok(Command::MarkRead { id })
162 }
163 _ => Err(ParseMessageError::UnknownType(command.to_string())),
164 }
165 }
166}
167
168#[derive(Debug)]
170pub enum Response {
171 AckUser { id: i64 },
173
174 StartSubscriptionList,
179
180 Subscription { id: i64, url: String },
185
186 StartEntryList,
191
192 Entry {
197 id: i64,
198 feed_id: i64,
199 feed_url: String,
200 title: String,
201 url: String,
202 },
203
204 EndList,
209
210 AckSubscribe,
213
214 AckUnsubscribe,
217
218 AckMarkRead,
221
222 ResourceNotFound(String),
225
226 BadCommand(String),
228
229 NeedUser(String),
232
233 InternalError(String),
237}
238
239impl From<ParseMessageError> for Response {
240 fn from(e: ParseMessageError) -> Response {
241 Response::BadCommand(e.to_string())
242 }
243}
244
245impl fmt::Display for Response {
246 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
247 match self {
248 Response::AckUser { id } => write!(f, "20 {}", id),
249 Response::StartSubscriptionList => write!(f, "21"),
250 Response::Subscription { id, url } => write!(f, "22 {} {}", id, url),
251 Response::StartEntryList => write!(f, "23"),
252 Response::Entry {
253 id,
254 feed_id,
255 feed_url,
256 title,
257 url,
258 } => write!(f, "24 {} {} {} {} {}", id, feed_id, feed_url, url, title),
259 Response::EndList => write!(f, "25"),
260 Response::AckSubscribe => write!(f, "26"),
261 Response::AckUnsubscribe => write!(f, "27"),
262 Response::AckMarkRead => write!(f, "28"),
263
264 Response::ResourceNotFound(message) => write!(f, "40 {}", message),
265 Response::BadCommand(message) => write!(f, "41 {}", message),
266 Response::NeedUser(message) => write!(f, "42 {}", message),
267
268 Response::InternalError(message) => write!(f, "51 {}", message),
269 }
270 }
271}
272
273impl FromStr for Response {
274 type Err = ParseMessageError;
275
276 fn from_str(value: &str) -> Result<Self, Self::Err> {
277 let parts: Vec<&str> = value.split(' ').collect();
278
279 let response = parts.get(0).ok_or(ParseMessageError::EmptyMessage)?;
280
281 match *response {
282 "20" => {
283 check_arguments(&parts, 1)?;
284
285 let id: i64 = at_position(&parts, "id", 1)?;
286
287 Ok(Response::AckUser { id })
288 }
289 "21" => {
290 check_arguments(&parts, 0)?;
291
292 Ok(Response::StartSubscriptionList)
293 }
294 "22" => {
295 check_arguments(&parts, 2)?;
296
297 let id: i64 = at_position(&parts, "id", 1)?;
298 let url: String = at_position(&parts, "url", 2)?;
299
300 Ok(Response::Subscription { id, url })
301 }
302 "23" => {
303 check_arguments(&parts, 0)?;
304
305 Ok(Response::StartEntryList)
306 }
307 "24" => {
308 let index = value
309 .find(' ')
310 .ok_or_else(|| ParseMessageError::MissingArgument("code".to_string()))?;
311
312 let line = &value[index + 1..];
313
314 let index = line
315 .find(' ')
316 .ok_or_else(|| ParseMessageError::MissingArgument("id".to_string()))?;
317
318 let id: i64 = line[..index].parse().map_err(|_| {
319 ParseMessageError::InvalidIntegerArgument {
320 argument: "id".to_string(),
321 value: line[..index].to_string(),
322 }
323 })?;
324
325 let line = &line[index + 1..];
326 let index = line
327 .find(' ')
328 .ok_or_else(|| ParseMessageError::MissingArgument("feed_id".to_string()))?;
329
330 let feed_id: i64 = line[..index].parse().map_err(|_| {
331 ParseMessageError::InvalidIntegerArgument {
332 argument: "feed_id".to_string(),
333 value: line[..index].to_string(),
334 }
335 })?;
336
337 let line = &line[index + 1..];
338 let index = line
339 .find(' ')
340 .ok_or_else(|| ParseMessageError::MissingArgument("feed_url".to_string()))?;
341 let feed_url = line[..index].to_string();
342
343 let line = &line[index + 1..];
344 let index = line
345 .find(' ')
346 .ok_or_else(|| ParseMessageError::MissingArgument("url".to_string()))?;
347 let url = line[..index].to_string();
348
349 let title = line[index + 1..].to_string();
350
351 Ok(Response::Entry {
352 id,
353 feed_id,
354 feed_url,
355 title,
356 url,
357 })
358 }
359 "25" => {
360 check_arguments(&parts, 0)?;
361
362 Ok(Response::EndList)
363 }
364 "26" => {
365 check_arguments(&parts, 0)?;
366
367 Ok(Response::AckSubscribe)
368 }
369 "27" => {
370 check_arguments(&parts, 0)?;
371
372 Ok(Response::AckUnsubscribe)
373 }
374 "28" => {
375 check_arguments(&parts, 0)?;
376
377 Ok(Response::AckMarkRead)
378 }
379
380 "40" => {
381 check_arguments(&parts, 1)?;
382
383 let message: String = at_position(&parts, "message", 1)?;
384
385 Ok(Response::ResourceNotFound(message))
386 }
387 "41" => {
388 check_arguments(&parts, 1)?;
389
390 let message: String = at_position(&parts, "message", 1)?;
391
392 Ok(Response::BadCommand(message))
393 }
394 "42" => {
395 check_arguments(&parts, 1)?;
396
397 let message: String = at_position(&parts, "message", 1)?;
398
399 Ok(Response::NeedUser(message))
400 }
401
402 "50" => {
403 check_arguments(&parts, 1)?;
404
405 let message: String = at_position(&parts, "message", 1)?;
406
407 Ok(Response::InternalError(message))
408 }
409 _ => Err(ParseMessageError::UnknownType(response.to_string())),
410 }
411 }
412}