use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Debug)]
pub enum Command {
User { username: String },
ListSubscriptions,
Subscribe { url: String },
Unsubscribe { id: i64 },
ListUnread,
MarkRead { id: i64 },
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Command::User { username } => write!(f, "USER {}", username),
Command::ListSubscriptions => write!(f, "LISTSUBSCRIPTIONS"),
Command::Subscribe { url } => write!(f, "SUBSCRIBE {}", url),
Command::Unsubscribe { id } => write!(f, "UNSUBSCRIBE {}", id),
Command::ListUnread => write!(f, "LISTUNREAD"),
Command::MarkRead { id } => write!(f, "MARKREAD {}", id),
}
}
}
fn check_arguments(parts: &Vec<&str>, expected: usize) -> Result<(), ParseMessageError> {
if parts.len() > expected + 1 {
return Err(ParseMessageError::TooManyArguments {
expected,
actual: parts.len() - 1,
});
}
Ok(())
}
fn at_position<T: FromStr>(
parts: &[&str],
argument_name: &str,
position: usize,
) -> Result<T, ParseMessageError> {
let possible = parts
.get(position)
.ok_or_else(|| ParseMessageError::MissingArgument(argument_name.to_string()))?;
possible
.parse()
.map_err(|_| ParseMessageError::InvalidIntegerArgument {
argument: argument_name.to_string(),
value: possible.to_string(),
})
}
#[derive(Debug, Error)]
pub enum ParseMessageError {
#[error("empty message")]
EmptyMessage,
#[error("unknown message type \"{0}\"")]
UnknownType(String),
#[error("missing argument \"{0}\"")]
MissingArgument(String),
#[error("too many arguments (expected {expected}, got {actual})")]
TooManyArguments { expected: usize, actual: usize },
#[error("invalid integer value \"{value}\" for argument \"{argument}\"")]
InvalidIntegerArgument { argument: String, value: String },
}
impl FromStr for Command {
type Err = ParseMessageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = value.split(' ').collect();
let command = parts.get(0).ok_or(ParseMessageError::EmptyMessage)?;
match *command {
"USER" => {
check_arguments(&parts, 1)?;
let username: String = at_position(&parts, "username", 1)?;
Ok(Command::User { username })
}
"LISTSUBSCRIPTIONS" => {
check_arguments(&parts, 0)?;
Ok(Command::ListSubscriptions)
}
"SUBSCRIBE" => {
check_arguments(&parts, 1)?;
let url: String = at_position(&parts, "url", 1)?;
Ok(Command::Subscribe { url })
}
"UNSUBSCRIBE" => {
check_arguments(&parts, 1)?;
let id: i64 = at_position(&parts, "id", 1)?;
Ok(Command::Unsubscribe { id })
}
"LISTUNREAD" => {
check_arguments(&parts, 0)?;
Ok(Command::ListUnread)
}
"MARKREAD" => {
check_arguments(&parts, 1)?;
let id: i64 = at_position(&parts, "id", 1)?;
Ok(Command::MarkRead { id })
}
_ => Err(ParseMessageError::UnknownType(command.to_string())),
}
}
}
#[derive(Debug)]
pub enum Response {
AckUser { id: i64 },
StartSubscriptionList,
Subscription { id: i64, url: String },
StartEntryList,
Entry {
id: i64,
feed_id: i64,
feed_url: String,
title: String,
url: String,
},
EndList,
AckSubscribe,
AckUnsubscribe,
AckMarkRead,
ResourceNotFound(String),
BadCommand(String),
NeedUser(String),
InternalError(String),
}
impl From<ParseMessageError> for Response {
fn from(e: ParseMessageError) -> Response {
Response::BadCommand(e.to_string())
}
}
impl fmt::Display for Response {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Response::AckUser { id } => write!(f, "20 {}", id),
Response::StartSubscriptionList => write!(f, "21"),
Response::Subscription { id, url } => write!(f, "22 {} {}", id, url),
Response::StartEntryList => write!(f, "23"),
Response::Entry {
id,
feed_id,
feed_url,
title,
url,
} => write!(f, "24 {} {} {} {} {}", id, feed_id, feed_url, url, title),
Response::EndList => write!(f, "25"),
Response::AckSubscribe => write!(f, "26"),
Response::AckUnsubscribe => write!(f, "27"),
Response::AckMarkRead => write!(f, "28"),
Response::ResourceNotFound(message) => write!(f, "40 {}", message),
Response::BadCommand(message) => write!(f, "41 {}", message),
Response::NeedUser(message) => write!(f, "42 {}", message),
Response::InternalError(message) => write!(f, "51 {}", message),
}
}
}
impl FromStr for Response {
type Err = ParseMessageError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = value.split(' ').collect();
let response = parts.get(0).ok_or(ParseMessageError::EmptyMessage)?;
match *response {
"20" => {
check_arguments(&parts, 1)?;
let id: i64 = at_position(&parts, "id", 1)?;
Ok(Response::AckUser { id })
}
"21" => {
check_arguments(&parts, 0)?;
Ok(Response::StartSubscriptionList)
}
"22" => {
check_arguments(&parts, 2)?;
let id: i64 = at_position(&parts, "id", 1)?;
let url: String = at_position(&parts, "url", 2)?;
Ok(Response::Subscription { id, url })
}
"23" => {
check_arguments(&parts, 0)?;
Ok(Response::StartEntryList)
}
"24" => {
let index = value
.find(' ')
.ok_or_else(|| ParseMessageError::MissingArgument("code".to_string()))?;
let line = &value[index + 1..];
let index = line
.find(' ')
.ok_or_else(|| ParseMessageError::MissingArgument("id".to_string()))?;
let id: i64 = line[..index].parse().map_err(|_| {
ParseMessageError::InvalidIntegerArgument {
argument: "id".to_string(),
value: line[..index].to_string(),
}
})?;
let line = &line[index + 1..];
let index = line
.find(' ')
.ok_or_else(|| ParseMessageError::MissingArgument("feed_id".to_string()))?;
let feed_id: i64 = line[..index].parse().map_err(|_| {
ParseMessageError::InvalidIntegerArgument {
argument: "feed_id".to_string(),
value: line[..index].to_string(),
}
})?;
let line = &line[index + 1..];
let index = line
.find(' ')
.ok_or_else(|| ParseMessageError::MissingArgument("feed_url".to_string()))?;
let feed_url = line[..index].to_string();
let line = &line[index + 1..];
let index = line
.find(' ')
.ok_or_else(|| ParseMessageError::MissingArgument("url".to_string()))?;
let url = line[..index].to_string();
let title = line[index + 1..].to_string();
Ok(Response::Entry {
id,
feed_id,
feed_url,
title,
url,
})
}
"25" => {
check_arguments(&parts, 0)?;
Ok(Response::EndList)
}
"26" => {
check_arguments(&parts, 0)?;
Ok(Response::AckSubscribe)
}
"27" => {
check_arguments(&parts, 0)?;
Ok(Response::AckUnsubscribe)
}
"28" => {
check_arguments(&parts, 0)?;
Ok(Response::AckMarkRead)
}
"40" => {
check_arguments(&parts, 1)?;
let message: String = at_position(&parts, "message", 1)?;
Ok(Response::ResourceNotFound(message))
}
"41" => {
check_arguments(&parts, 1)?;
let message: String = at_position(&parts, "message", 1)?;
Ok(Response::BadCommand(message))
}
"42" => {
check_arguments(&parts, 1)?;
let message: String = at_position(&parts, "message", 1)?;
Ok(Response::NeedUser(message))
}
"50" => {
check_arguments(&parts, 1)?;
let message: String = at_position(&parts, "message", 1)?;
Ok(Response::InternalError(message))
}
_ => Err(ParseMessageError::UnknownType(response.to_string())),
}
}
}