seymour_protocol/
lib.rs

1use std::fmt;
2use std::str::FromStr;
3
4use thiserror::Error;
5
6// ############
7// # Protocol #
8// ############
9//
10// [connect]
11// > USER <username>
12// < 20 <user_id>
13// > LISTFEEDS
14// < 21
15// < 22 <feed_id> <feed_url> :<feed_name>
16// < 25
17// > LISTUNREAD
18// < 23
19// < 24 <entry_id> <feed_id> <feed_url> <entry_title> :<entry_link>
20// < 25
21// > MARKREAD <entry_id>
22// < 28
23
24/// Commands sent to seymour server
25#[derive(Debug)]
26pub enum Command {
27    /// Select the user user
28    User { username: String },
29
30    /// List the current user's subscriptions
31    ///
32    /// Requires a client to issue a User
33    /// command prior.
34    ListSubscriptions,
35
36    /// Subscribe the current user to a new feed
37    ///
38    /// Requires a client to issue a User
39    /// command prior.
40    Subscribe { url: String },
41
42    /// Unsubscribe the current user from a feed
43    ///
44    /// Requires a client to issue a User
45    /// command prior.
46    Unsubscribe { id: i64 },
47
48    /// List the current user's unread feed entries
49    ///
50    /// Requires a client to issue a User
51    /// command prior.
52    ListUnread,
53
54    /// Mark a feed entry as read by the current user
55    ///
56    /// Requires a client to issue a User
57    /// command prior.
58    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/// Responses sent from seymour server
169#[derive(Debug)]
170pub enum Response {
171    /// Acknowledgement for selecting current user
172    AckUser { id: i64 },
173
174    /// Beginning of a list of subscriptions
175    ///
176    /// Must be followed by zero or more Subscription lines and
177    /// one EndList.
178    StartSubscriptionList,
179
180    /// A single subscription entry
181    ///
182    /// Must be preceeded by one StartSubscriptionList and
183    /// followed by one EndList.
184    Subscription { id: i64, url: String },
185
186    /// Beginning of a list of feed entries
187    ///
188    /// Must be followed by zero or more Entry lines and
189    /// one EndList.
190    StartEntryList,
191
192    /// A single feed entry
193    ///
194    /// Must be preceeded by one StartEntryList and
195    /// followed by one EndList.
196    Entry {
197        id: i64,
198        feed_id: i64,
199        feed_url: String,
200        title: String,
201        url: String,
202    },
203
204    /// Ends a list sent by the server
205    ///
206    /// Must be preceeded by at least either a StartSubscriptionList
207    /// or a StartEntryList.
208    EndList,
209
210    /// Acknowledgement for subscribing the current user
211    /// to a new feed
212    AckSubscribe,
213
214    /// Acknowledgement for unsubscribing the current user
215    /// from a feed
216    AckUnsubscribe,
217
218    /// Acknowledgement for marking a feed entry as read
219    /// by the current user
220    AckMarkRead,
221
222    /// Error stating that the specified resource was
223    /// not found
224    ResourceNotFound(String),
225
226    /// Error stating that the command sent was not valid
227    BadCommand(String),
228
229    /// Error stating that the command sent requires a
230    /// selected user, but no user has been selected
231    NeedUser(String),
232
233    /// Error stating that the seymour server hit an
234    /// internal problem while attempting to serve
235    /// the request
236    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}