pipitor 0.2.2

A Twitter bot that gathers, filters and Retweets Tweets automatically.
Documentation
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::ops::DerefMut;

use regex::Regex;
use serde::{de, Deserialize};
use smallvec::{smallvec, SmallVec};

use crate::twitter::Tweet;

mod private {
    #[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq, Hash)]
    pub enum Never {}
}

#[derive(Clone, Default)]
pub struct RuleMap {
    map: HashMap<TopicId, SmallVec<[Rule; 1]>>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(untagged)]
pub enum TopicId {
    Twitter(i64),
    #[doc(hidden)]
    _NonExhaustive(private::Never),
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Outbox {
    Twitter(i64),
    None,
    #[doc(hidden)]
    _NonExhaustive(private::Never),
}

#[derive(Clone, Debug, Deserialize)]
pub struct Rule {
    #[serde(default)]
    filter: Option<Filter>,
    #[serde(default)]
    exclude: Option<Filter>,
    #[serde(deserialize_with = "de_outbox")]
    outbox: SmallVec<[Outbox; 1]>,
}

#[derive(Clone, Debug)]
pub struct Filter {
    title: Regex,
    text: Option<Regex>,
}

impl RuleMap {
    pub fn new() -> Self {
        Default::default()
    }

    pub fn with_capacity(cap: usize) -> Self {
        RuleMap {
            map: HashMap::with_capacity(cap),
        }
    }

    pub fn insert(&mut self, topic: TopicId, rule: Rule) {
        self.map
            .entry(topic)
            .or_insert_with(SmallVec::new)
            .push(rule);
    }

    pub fn get(&self, topic: &TopicId) -> Option<&[Rule]> {
        self.map.get(topic).map(|vec| &**vec)
    }

    pub fn get_mut(
        &mut self,
        topic: &TopicId,
    ) -> Option<&mut (impl Extend<Rule> + DerefMut<Target = [Rule]>)> {
        self.map.get_mut(topic)
    }

    pub fn contains_topic(&self, topic: &TopicId) -> bool {
        self.map.contains_key(topic)
    }

    pub fn remove(
        &mut self,
        topic: &TopicId,
    ) -> Option<impl IntoIterator<Item = Rule> + DerefMut<Target = [Rule]>> {
        self.map.remove(topic)
    }

    pub fn topics(&self) -> impl Iterator<Item = &TopicId> {
        self.map.keys()
    }

    pub fn twitter_topics<'a>(&'a self) -> impl Iterator<Item = i64> + 'a {
        self.topics().filter_map(|topic| match *topic {
            TopicId::Twitter(user) => Some(user),
            _ => None,
        })
    }

    pub fn rules(&self) -> impl Iterator<Item = &Rule> {
        self.map.values().flatten()
    }

    pub fn outboxes(&self) -> impl Iterator<Item = &Outbox> {
        self.rules().flat_map(|rule| &rule.outbox)
    }

    pub fn twitter_outboxes<'a>(&'a self) -> impl Iterator<Item = i64> + 'a {
        self.outboxes().filter_map(|outbox| match *outbox {
            Outbox::Twitter(user) => Some(user),
            _ => None,
        })
    }

    pub fn route_tweet<'a>(&'a self, tweet: &'a Tweet) -> impl Iterator<Item = &'a Outbox> {
        self.get(&TopicId::Twitter(tweet.user.id))
            .into_iter()
            .flatten()
            .filter(move |r| {
                r.filter.as_ref().map_or(true, |f| f.matches_tweet(tweet))
                    && r.exclude.as_ref().map_or(true, |e| !e.matches_tweet(tweet))
            })
            .flat_map(|r| &r.outbox)
    }
}

impl Debug for RuleMap {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        Debug::fmt(&self.map, f)
    }
}

impl<'de> Deserialize<'de> for RuleMap {
    fn deserialize<D: de::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        struct Visitor;

        impl<'de> de::Visitor<'de> for Visitor {
            type Value = RuleMap;

            fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
                f.write_str("a sequence")
            }

            fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
                let map = HashMap::with_capacity(seq.size_hint().unwrap_or(0));
                let mut ret = RuleMap { map };

                #[derive(Deserialize)]
                struct RulePrototype {
                    topics: Vec<TopicId>,
                    #[serde(flatten)]
                    rule: Rule,
                }

                while let Some(RulePrototype { mut topics, rule }) = seq.next_element()? {
                    if let Some(last) = topics.pop() {
                        for topic in topics {
                            ret.insert(topic, rule.clone());
                        }
                        ret.insert(last, rule);
                    }
                }

                Ok(ret)
            }
        }

        d.deserialize_seq(Visitor)
    }
}

impl<'de> Deserialize<'de> for Outbox {
    fn deserialize<D: de::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        i64::deserialize(d).map(|id| {
            if id == 0 {
                Outbox::None
            } else {
                Outbox::Twitter(id)
            }
        })
    }
}

impl Rule {
    pub fn new(filter: Option<Filter>, exclude: Option<Filter>) -> Self {
        Rule {
            filter,
            exclude,
            outbox: SmallVec::new(),
        }
    }

    pub fn filter(&self) -> Option<&Filter> {
        self.filter.as_ref()
    }

    pub fn filter_mut(&mut self) -> &mut Option<Filter> {
        &mut self.filter
    }

    pub fn exclude(&self) -> Option<&Filter> {
        self.exclude.as_ref()
    }

    pub fn exclude_mut(&mut self) -> &mut Option<Filter> {
        &mut self.exclude
    }

    pub fn outbox(&self) -> &[Outbox] {
        &self.outbox
    }

    pub fn outbox_mut(
        &mut self,
    ) -> &mut (impl Default + Extend<Outbox> + DerefMut<Target = [Outbox]>) {
        &mut self.outbox
    }
}

impl Filter {
    pub fn from_title(title: Regex) -> Self {
        Filter { title, text: None }
    }

    pub fn matches_tweet(&self, tweet: &Tweet) -> bool {
        self.title.is_match(&tweet.text)
            || self
                .text
                .as_ref()
                .map_or(false, |t| t.is_match(&tweet.text))
    }
}

impl<'de> Deserialize<'de> for Filter {
    fn deserialize<D: de::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Prototype {
            Title(#[serde(with = "serde_regex")] Regex),
            Composite {
                #[serde(with = "serde_regex")]
                title: Regex,
                #[serde(default)]
                #[serde(with = "serde_regex")]
                text: Option<Regex>,
            },
        }

        Prototype::deserialize(d).map(|p| match p {
            Prototype::Title(title) => Filter { title, text: None },
            Prototype::Composite { title, text } => Filter { title, text },
        })
    }
}

fn de_outbox<'de, D: de::Deserializer<'de>>(d: D) -> Result<SmallVec<[Outbox; 1]>, D::Error> {
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum Prototype {
        One(Outbox),
        Seq(SmallVec<[Outbox; 1]>),
    }

    Prototype::deserialize(d).map(|p| match p {
        Prototype::One(o) => smallvec![o],
        Prototype::Seq(v) => v,
    })
}