eilmeldung 1.4.3

a feature-rich TUI RSS Reader based on the news-flash library
// use crate::prelude::*;

use std::{cmp::Ordering, collections::HashMap, fmt::Display, str::FromStr};

use itertools::Itertools;
use logos::Logos;
use news_flash::models::{Article, Feed, FeedID};
use serde::Deserialize;

#[derive(Clone, Debug, logos::Logos)]
#[logos(skip r"[ \t\n\f]+")]
enum SortToken {
    #[token(">")]
    Descending,

    #[token("<")]
    Ascending,

    #[token("feed")]
    KeyFeed,

    #[token("date")]
    KeyDate,

    #[token("synced")]
    KeySynced,

    #[token("title")]
    KeyTitle,

    #[token("author")]
    KeyAuthor,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SortDirection {
    Ascending,
    Descending,
}

impl Display for SortDirection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SortDirection::Ascending => write!(f, "󰒼"),
            SortDirection::Descending => write!(f, "󰒽"),
        }
    }
}

impl SortDirection {
    pub fn reversed(self) -> Self {
        use SortDirection as S;
        match self {
            S::Ascending => S::Descending,
            S::Descending => S::Ascending,
        }
    }

    fn apply(&self, ordering: Ordering) -> Ordering {
        if matches!(self, SortDirection::Descending) {
            ordering.reverse()
        } else {
            ordering
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SortKey {
    Feed(SortDirection),
    Date(SortDirection),
    Synced(SortDirection),
    Title(SortDirection),
    Author(SortDirection),
}

impl Display for SortKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use SortKey as S;
        match self {
            S::Feed(direction) => write!(f, "{direction}feed"),
            S::Date(direction) => write!(f, "{direction}date"),
            S::Synced(direction) => write!(f, "{direction}synced"),
            S::Title(direction) => write!(f, "{direction}title"),
            S::Author(direction) => write!(f, "{direction}author"),
        }
    }
}

impl SortKey {
    pub fn reversed(self) -> Self {
        use SortKey as K;
        match self {
            K::Feed(direction) => K::Feed(direction.reversed()),
            K::Date(direction) => K::Date(direction.reversed()),
            K::Synced(direction) => K::Synced(direction.reversed()),
            K::Title(direction) => K::Title(direction.reversed()),
            K::Author(direction) => K::Author(direction.reversed()),
        }
    }

    pub fn compare(
        &self,
        article_1: &Article,
        article_2: &Article,
        feed_map: &HashMap<FeedID, Feed>,
    ) -> Ordering {
        use SortKey as K;

        match self {
            K::Date(direction) => direction
                .apply(article_1.date.cmp(&article_2.date))
                .reverse(),
            K::Title(direction) => direction.apply(
                article_1
                    .title
                    .as_ref()
                    .map(|title| title.to_uppercase())
                    .cmp(&article_2.title.as_ref().map(|title| title.to_uppercase())),
            ),
            K::Synced(direction) => direction
                .apply(article_1.synced.cmp(&article_2.synced))
                .reverse(),
            K::Author(direction) => direction.apply(
                article_1
                    .author
                    .as_ref()
                    .map(|author| author.to_uppercase())
                    .cmp(
                        &article_2
                            .author
                            .as_ref()
                            .map(|author| author.to_uppercase()),
                    ),
            ),
            K::Feed(direction) => {
                let label1 = feed_map
                    .get(&article_1.feed_id)
                    .map(|feed| feed.label.to_uppercase());
                let label2 = feed_map
                    .get(&article_2.feed_id)
                    .map(|feed| feed.label.to_uppercase());
                direction.apply(label1.cmp(&label2))
            }
        }
    }
}

#[derive(Default, Clone, Debug, getset::Getters, Eq, PartialEq)]
pub struct SortOrder {
    #[get = "pub"]
    order: Vec<SortKey>,
}

impl Display for SortOrder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            self.order
                .iter()
                .map(|sort_key| sort_key.to_string())
                .join(" ")
        )
    }
}

#[derive(Debug, thiserror::Error, Clone, PartialEq, Default)]
pub enum SortOrderParseError {
    #[default]
    #[error("unknown error")]
    UnknownError,

    #[error("order direction (< or >) or key expected")]
    OrderDirectionOrKeyExpected(usize, String),

    #[error("expecting order key (date, feed, etc.)")]
    OrderKeyExpected(usize, String),

    #[error("duplicate key found")]
    DuplicateKeyFound(usize, String),
}

impl SortOrder {
    pub fn new(order: Vec<SortKey>) -> Self {
        Self { order }
    }

    pub fn reversed(self) -> Self {
        SortOrder {
            order: self
                .order
                .into_iter()
                .rev()
                .map(|key| key.reversed())
                .collect(),
        }
    }

    pub fn reverse(self, reverse: bool) -> SortOrder {
        if reverse { self.reversed() } else { self }
    }

    pub fn sort(&self, articles: &mut [Article], feed_map: &HashMap<FeedID, Feed>) {
        if self.order.is_empty() {
            return;
        }
        articles.sort_by(|article_1, article_2| {
            for key in self.order.iter() {
                let ordering = key.compare(article_1, article_2, feed_map);

                if !matches!(ordering, Ordering::Equal) {
                    return ordering;
                }
            }

            Ordering::Equal
        });
    }
}

impl FromStr for SortOrder {
    type Err = SortOrderParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse_sort_order(s)
    }
}

impl<'de> Deserialize<'de> for SortOrder {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;

        SortOrder::from_str(&s).map_err(|err| serde::de::Error::custom(err.to_string()))
    }
}

pub fn parse_sort_order(sort_order_str: &str) -> Result<SortOrder, SortOrderParseError> {
    let mut order = Vec::new();

    let mut lexer = SortToken::lexer(sort_order_str);

    while let Some(token) = lexer.next() {
        use SortDirection as D;
        use SortKey as K;
        use SortToken as T;

        let (direction, next) = match token {
            Ok(T::Ascending) => (D::Ascending, lexer.next()),
            Ok(T::Descending) => (D::Descending, lexer.next()),
            next @ Ok(_) => (D::Ascending, Some(next)), // default is ascending

            _ => {
                return Err(SortOrderParseError::OrderDirectionOrKeyExpected(
                    lexer.span().start,
                    lexer.slice().to_owned(),
                ));
            }
        };

        let key = match next {
            Some(Ok(T::KeyFeed)) => K::Feed(direction),
            Some(Ok(T::KeyDate)) => K::Date(direction),
            Some(Ok(T::KeySynced)) => K::Synced(direction),
            Some(Ok(T::KeyTitle)) => K::Title(direction),
            Some(Ok(T::KeyAuthor)) => K::Author(direction),
            _ => {
                return Err(SortOrderParseError::OrderKeyExpected(
                    lexer.span().start,
                    lexer.slice().to_owned(),
                ));
            }
        };

        if order
            .iter()
            .any(|&order_key| order_key == key || order_key == key.reversed())
        {
            return Err(SortOrderParseError::DuplicateKeyFound(
                lexer.span().start,
                lexer.slice().to_owned(),
            ));
        }

        order.push(key);
    }

    Ok(SortOrder { order })
}

#[cfg(test)]
mod test {

    use super::*;
    use SortDirection as D;
    use SortKey as K;
    use rstest::rstest;

    #[rstest]
    #[case("", vec![])]
    #[case("date", vec![K::Date(D::Ascending)])]
    #[case(">date", vec![K::Date(D::Descending)])]
    #[case("<date", vec![K::Date(D::Ascending)])]
    #[case(">feed", vec![K::Feed(D::Descending)])]
    #[case(">author", vec![K::Author(D::Descending)])]
    #[case(">synced", vec![K::Synced(D::Descending)])]
    #[case(">title", vec![K::Title(D::Descending)])]
    #[case("   <title", vec![K::Title(D::Ascending)])]
    #[case("<     date", vec![K::Date(D::Ascending)])]
    #[case(">date      ", vec![K::Date(D::Descending)])]
    #[case("<feed >date", vec![K::Feed(D::Ascending), K::Date(D::Descending)])]
    #[case("<feed     >date author", vec![K::Feed(D::Ascending), K::Date(D::Descending), K::Author(D::Ascending)])]
    #[case("<feed     >date <author >title", vec![K::Feed(D::Ascending), K::Date(D::Descending), K::Author(D::Ascending), K::Title(D::Descending)])]
    fn test_parse_sort_order(#[case] sort_order: &str, #[case] parsed: Vec<SortKey>) {
        assert_eq!(parse_sort_order(sort_order).unwrap().order, parsed);
    }

    #[rstest]
    #[case("foo")]
    #[case("   foo title")]
    #[case(">title article")]
    #[case(" author feed <date synched")]
    fn test_parse_fail_lexer_error(#[case] sort_order: &str) {
        claims::assert_matches!(
            parse_sort_order(sort_order),
            Err(SortOrderParseError::OrderDirectionOrKeyExpected(..))
        )
    }

    #[rstest]
    #[case("foo")]
    #[case("   foo title")]
    #[case(">title article")]
    #[case(" author feed <date synched")]
    fn test_parse_fail_order_direction_or_key_expected(#[case] sort_order: &str) {
        claims::assert_matches!(
            parse_sort_order(sort_order),
            Err(SortOrderParseError::OrderDirectionOrKeyExpected(..))
        )
    }

    #[rstest]
    #[case("<")]
    #[case("<<")]
    #[case("<>")]
    #[case(" <>")]
    #[case("<foo")]
    #[case("<title author <foo")]
    fn test_parse_fail_order_key_expected(#[case] sort_order: &str) {
        claims::assert_matches!(
            parse_sort_order(sort_order),
            Err(SortOrderParseError::OrderKeyExpected(..))
        )
    }

    #[rstest]
    #[case("title title")]
    #[case("<title title")]
    #[case("<title >title")]
    #[case("title >author <title")]
    #[case("date feed author date")]
    fn test_parse_fail_duplicate_key(#[case] sort_order: &str) {
        claims::assert_matches!(
            parse_sort_order(sort_order),
            Err(SortOrderParseError::DuplicateKeyFound(..))
        )
    }

    #[rstest]
    #[case(parse_sort_order("").unwrap(), parse_sort_order("").unwrap())]
    #[case(parse_sort_order(">date >title").unwrap(), parse_sort_order("<title <date").unwrap())]
    #[case(parse_sort_order("<date >title").unwrap(), parse_sort_order("<title >date").unwrap())]
    #[case(parse_sort_order("<date >title <author").unwrap(), parse_sort_order(">author <title >date").unwrap())]
    fn test_sort_order_reversed(
        #[case] sort_order: SortOrder,
        #[case] reversed_sort_order: SortOrder,
    ) {
        assert_eq!(sort_order.reversed(), reversed_sort_order);
    }
}