use std::str::FromStr;
use news_flash::models::Url;
use ratatui::style::{Color, ParseColorError};
use serde::Deserialize;
use crate::prelude::*;
#[derive(thiserror::Error, Debug)]
pub enum CommandParseError {
#[error("expecting command")]
CommandExpected,
#[error("expecting command name")]
CommandNameExpected(#[from] strum::ParseError),
#[error("expecting tag")]
TagExpected,
#[error("article scope")]
ArticleScopeExpected,
#[error("expecting color")]
ColorExpected(#[from] ParseColorError),
#[error("expecting URL")]
URLExpected(#[from] url::ParseError),
#[error("expecting panel")]
PanelExpected,
#[error("expecting position")]
PositionExpected,
#[error("action scope expected")]
ActionScopeExpected,
#[error("expecting article search query")]
ArticleQueryExpected(#[from] QueryParseError),
#[error("expecting share target")]
ShareTargetExpected,
#[error("expecting file path")]
FilePathExpected,
#[error("sort order expected")]
SortOrderExpected(#[from] SortOrderParseError),
#[error("enclosure type expected")]
EnclosureTypeExpected,
#[error("expecting `NOW` as parameter for confirmation")]
ConfirmationExpected,
#[error("search term expected (quoted string, regex or single word)")]
SearchTermExpected,
#[error("URL hint expected")]
UrlHintExpected,
#[error("expecting a word")]
WordExpected(String),
#[error("expecting something")]
SomethingExpected(String),
#[error("unexpected")]
NothingExcepted(String),
}
fn split_off_first(s: &str) -> (String, Option<String>) {
let trimmed = s.trim();
let end_pos = trimmed.find(" ");
let first = match end_pos {
Some(pos) => &trimmed[..pos],
None => trimmed,
};
let args = end_pos
.map(|pos| (trimmed[pos + 1..]).to_owned())
.to_owned();
(first.trim().to_owned(), args)
}
fn expect_word(s: &mut Option<String>, to_expect: &str) -> Result<String, CommandParseError> {
let Some(args) = s.as_mut() else {
return Err(CommandParseError::WordExpected(to_expect.to_owned()));
};
let (word, tail) = split_off_first(args.as_str());
*s = tail;
Ok(word)
}
fn expect_something(s: Option<String>, to_expect: &str) -> Result<String, CommandParseError> {
s.ok_or(CommandParseError::SomethingExpected(to_expect.to_owned()))
}
fn expect_from_str<T: FromStr>(
s: &mut Option<String>,
to_expect: &str,
) -> Result<T, CommandParseError>
where
T::Err: Into<CommandParseError>,
{
let word = expect_word(s, to_expect);
T::from_str(word.as_deref().unwrap_or_default()).map_err(|e| e.into())
}
fn expect_nothing(s: Option<String>) -> Result<(), CommandParseError> {
match s {
Some(s) => Err(CommandParseError::NothingExcepted(s)),
None => Ok(()),
}
}
impl Command {
pub fn parse(s: &str, eager: bool) -> Result<Self, CommandParseError> {
use CommandParseError as E;
let mut args = if s.is_empty() {
None
} else {
Some(s.to_owned())
};
let command: Command = expect_from_str(&mut args, "expecting command")?;
use Command as C;
Ok(match command {
C::CommandConfirm(_) => {
let Some(args) = args else {
return Err(E::CommandExpected);
};
C::CommandConfirm(Box::new(Command::parse(&args, eager)?))
}
C::In(..) => {
let panel: Panel =
expect_from_str(&mut args, "expecting panel: feeds, articles, content")?;
let Some(args) = args else {
return Err(E::CommandExpected);
};
C::In(panel, Box::new(Command::parse(&args, eager)?))
}
C::PanelFocus(_) => {
let panel: Panel =
expect_from_str(&mut args, "expecting panel: feeds, articles, content")?;
C::PanelFocus(panel)
}
C::ActionSetRead(..) => {
let action_scope: ActionScope = match args {
None => {
if eager {
return Err(E::ActionScopeExpected);
} else {
ActionScope::Current
}
}
_ => expect_from_str(&mut args, "expecting action scope")?,
};
C::ActionSetRead(action_scope)
}
C::ActionSetUnread(..) => {
C::ActionSetUnread(ActionScope::from_option_string(args.as_deref())?)
}
C::ActionOpenInBrowser(..) => {
C::ActionOpenInBrowser(ActionScope::from_option_string(args.as_deref())?)
}
C::ActionSetMarked(..) => {
C::ActionSetMarked(ActionScope::from_option_string(args.as_deref())?)
}
C::ActionSetUnmarked(..) => {
C::ActionSetUnmarked(ActionScope::from_option_string(args.as_deref())?)
}
C::ActionSetFlagged(..) => {
C::ActionSetFlagged(ActionScope::from_option_string(args.as_deref())?)
}
C::ActionSetUnflagged(..) => {
C::ActionSetUnflagged(ActionScope::from_option_string(args.as_deref())?)
}
C::ActionFlagInvert(..) => {
C::ActionFlagInvert(ActionScope::from_option_string(args.as_deref())?)
}
tag_command @ (C::ActionTagArticles(..) | C::ActionUntagArticles(..)) => {
let tag =
expect_word(&mut args, "expecting tag name").map_err(|_| E::TagExpected)?;
match tag_command {
C::ActionTagArticles(..) => {
C::ActionTagArticles(ActionScope::from_option_string(args.as_deref())?, tag)
}
C::ActionUntagArticles(..) => C::ActionUntagArticles(
ActionScope::from_option_string(args.as_deref())?,
tag,
),
_ => unreachable!(),
}
}
C::FeedListFeedAdd(..) => {
let url = Url::new(expect_from_str::<reqwest::Url>(
&mut args,
"expecting feed URL",
)?);
let name = args;
C::FeedListFeedAdd(Some(url), name)
}
C::FeedListFeedChangeUrl(..) => {
let url = Url::new(expect_from_str::<reqwest::Url>(
&mut args,
"expecting feed URL",
)?);
expect_nothing(args)?;
C::FeedListFeedChangeUrl(Some(url))
}
C::FeedListPasteFeedOrCategory(..) => {
let position =
expect_from_str::<PastePosition>(&mut args, "expecting paste position")
.map_err(|_| E::PositionExpected)?;
expect_nothing(args)?;
C::FeedListPasteFeedOrCategory(position)
}
C::FeedListCategoryAdd(..) => {
C::FeedListCategoryAdd(expect_something(args, "expecting category name")?)
}
C::FeedListRenameEntity(..) => {
C::FeedListRenameEntity(expect_something(args, "expecting new name")?)
}
C::FeedListTagChangeColor(..) => {
let color: Color = expect_from_str(&mut args, "expecting tag color")
.map_err(|_| E::ColorExpected(ParseColorError))?;
expect_nothing(args)?;
C::FeedListTagChangeColor(color)
}
C::TagAdd(..) => {
let tag_title = expect_word(&mut args, "expecting tag name")?;
let color: Option<Color> = match args {
None => {
if eager {
return Err(E::ColorExpected(ParseColorError));
} else {
None
}
}
_ => Some(expect_from_str(&mut args, "expecting tag color")?),
};
expect_nothing(args)?;
C::TagAdd(tag_title, color)
}
C::FeedListExpandCategories(_) => C::FeedListExpandCategories(
expect_from_str(&mut args, "article scope expected")
.map_err(|_| E::ArticleScopeExpected)?,
),
C::Show(..) => C::Show(
expect_from_str::<ArticleScope>(&mut args, "expecting article scope")
.map_err(|_| E::ArticleScopeExpected)?,
),
C::ArticleOpenEnclosure(..) => match args {
None if !eager => C::ArticleOpenEnclosure(None),
None => return Err(E::EnclosureTypeExpected),
Some(..) => C::ArticleOpenEnclosure(Some(
expect_from_str(&mut args, "enclosure type expected")
.map_err(|_| E::EnclosureTypeExpected)?,
)),
},
C::ArticleListSearch(..) => C::ArticleListSearch(ArticleQuery::from_str(
expect_something(args, "expecting article query")
.map_err(|_| {
E::ArticleQueryExpected(QueryParseError::KeyOrWordExpected(
0,
"".to_owned(),
))
})?
.as_str(),
)?),
C::Search(..) => C::Search(Some(
expect_from_str::<SearchTerm>(&mut args, "expecting search term")
.map_err(|_| E::SearchTermExpected)?,
)),
filter @ (C::ArticleListFilterSet(..) | C::ArticleListFilterSetSticky(..)) => {
let query = ArticleQuery::from_str(
expect_something(args, "expecting article query")
.map_err(|_| {
E::ArticleQueryExpected(QueryParseError::KeyOrWordExpected(
0,
"".to_owned(),
))
})?
.as_str(),
)?;
match filter {
C::ArticleListFilterSet(..) => C::ArticleListFilterSet(query),
C::ArticleListFilterSetSticky(..) => C::ArticleListFilterSetSticky(query),
_ => unreachable!(),
}
}
C::ArticleListQuery(..) => C::ArticleListQuery(ArticleQuery::from_str(
expect_something(args, "expecting article query")
.map_err(|_| {
E::ArticleQueryExpected(QueryParseError::KeyOrWordExpected(
0,
"".to_owned(),
))
})?
.as_str(),
)?),
C::ArticleListSort(..) => C::ArticleListSort(SortOrder::from_str(
expect_something(args, "expecting sort order")
.map_err(|_| {
E::SortOrderExpected(SortOrderParseError::OrderDirectionOrKeyExpected(
0,
"".to_owned(),
))
})?
.as_str(),
)?),
C::ContentFollowHint(..) => C::ContentFollowHint(expect_word(&mut args, "URL hint")?),
C::ContentShareHint(..) => {
let share_target = expect_word(&mut args, "expecting share target")
.map_err(|_| E::ShareTargetExpected)?;
let hint = expect_word(&mut args, "URL hint").map_err(|_| E::UrlHintExpected)?;
C::ContentShareHint(share_target, hint)
}
C::ArticleShare(..) => C::ArticleShare(
expect_word(&mut args, "expecting share target")
.map_err(|_| E::ShareTargetExpected)?,
),
cmd @ (C::ExportOpml(..) | C::ImportOpml(..)) => {
let path = expect_something(args, "expecting file path")
.map_err(|_| E::FilePathExpected)?;
match cmd {
C::ExportOpml(..) => C::ExportOpml(path),
C::ImportOpml(..) => C::ImportOpml(path),
_ => unreachable!(),
}
}
C::Logout(..) => {
let word = expect_word(
&mut args,
"expecting confirmation `NOW` if you are really sure",
)
.map_err(|_| E::ConfirmationExpected)?;
expect_nothing(args)?;
C::Logout(word)
}
C::CommandLineOpen(..) => C::CommandLineOpen(args),
command_without_args => command_without_args,
})
}
}
impl<'de> Deserialize<'de> for Command {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Command::parse(&s, false).map_err(|err| serde::de::Error::custom(err.to_string()))
}
}