turngate 0.1.0

Prevent or reduce abuse of your public service
Documentation
use std::{
    sync::{
        mpsc::{channel, Receiver, TryRecvError},
        Arc,
    },
    time::{Duration, Instant},
};

use super::{Interaction, Label};

pub(super) enum InteractionUpdate {
    Score(i16),
    Finished(Instant),
}

#[derive(Debug)]
pub struct Visit {
    started: Instant,
    receiver: Option<Receiver<InteractionUpdate>>,
    labels: Arc<Vec<u128>>,
    duration: Duration,
    score: i32,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct VisitCounters {
    pub duration: Duration,
    pub score: i32,
}
impl Visit {
    /// Constructs the [`Visit`] and main [`Interaction`] pair
    pub fn interaction(labels: Vec<u128>) -> (Visit, Interaction) {
        let labels = Arc::new(labels);
        let (sender, receiver) = channel();
        (
            Visit::new(receiver, labels.clone()),
            Interaction::new(sender, labels),
        )
    }
    /// Private constructor called by [`Visit::interaction()`]
    fn new(receiver: Receiver<InteractionUpdate>, labels: Arc<Vec<u128>>) -> Self {
        Self {
            started: Instant::now(),
            receiver: Some(receiver),
            labels,
            duration: Duration::ZERO,
            score: 0,
        }
    }

    /// Get all the news from all interactions
    pub fn check(&mut self) -> VisitCounters {
        while let Some(rcvr) = &self.receiver {
            match rcvr.try_recv() {
                Err(TryRecvError::Disconnected) => {
                    self.receiver = None;
                    break;
                }
                Err(TryRecvError::Empty) => {
                    self.duration = Instant::now() - self.started;
                    break;
                }
                Ok(InteractionUpdate::Score(score)) => {
                    self.duration = Instant::now() - self.started;
                    self.score = self.score.saturating_add(score as i32);
                }
                Ok(InteractionUpdate::Finished(at)) => {
                    // if it's the last interacction dropped, it will stay
                    self.duration = at - self.started;
                }
            };
        }
        VisitCounters {
            duration: self.duration,
            score: self.score,
        }
    }

    /// End a visit before it's over
    pub fn stop(&mut self) {
        self.check();
        self.receiver = None;
    }

    /// Check if the visit is over or stopped
    pub fn is_finished(&self) -> bool {
        self.receiver.is_none()
    }

    pub fn labels<'a>(&'a self) -> impl Iterator<Item = Label<'static>> + 'a {
        self.labels.iter().cloned().map(Label::from)
    }
}

#[test]
fn test_visit_duration() {
    let sleep = Duration::from_millis(108);
    let (mut v, i) = Visit::interaction(vec![]);
    std::thread::sleep(sleep);
    drop(i);
    std::thread::sleep(sleep);
    let dur = v.check().duration.as_millis();
    assert_eq!(dur, sleep.as_millis());
}

#[test]
fn test_visit_duration_multi() {
    // The main interacction has weight 0
    let (mut visit, interaction) = Visit::interaction(vec![]);
    interaction.score(5);
    interaction.accept();
    assert_eq!(visit.check().score, 5, "main one completed");

    // We can still start new interactions even when the main one is succeeded
    let i7 = interaction.start(7).expect("interaction 7");
    let i108 = interaction.start(108).expect("interaction 108");
    let i150 = interaction.start(150).expect("interaction 250");
    let idur = interaction.start(0).expect("interaction for duration");
    assert_eq!(visit.check().score, 5 - 7 - 108 - 150, "pending");

    // Interactions may have various outcomes.
    // Notice also, that interaction methods are on & ref with interior mutability.
    i7.cancel();
    i108.accept();
    i150.reject();
    assert_eq!(
        visit.check().score,
        5 - 7 - 108 - 150 + 7 + 108 + 108,
        "finished"
    );

    // Visit tracks duration while there are interactions going on
    std::thread::sleep(std::time::Duration::from_millis(108));
    assert_eq!(visit.check().duration.as_millis(), 108, "pending duration");

    // Once all interactions are dropped, duration remains the same
    drop(interaction);
    drop(idur);
    drop(i7);
    drop(i150);
    drop(i108);
    std::thread::sleep(std::time::Duration::from_millis(108));
    assert_eq!(visit.check().duration.as_millis(), 108, "final duration");
}

#[test]
fn test_visit_duration_multi_interaction() {
    let sleep = Duration::from_millis(108);
    let (mut v, i) = Visit::interaction(vec![]);
    let i2 = i.start(5);
    std::thread::sleep(sleep);
    drop(i);
    std::thread::sleep(sleep);
    drop(i2);
    let dur = v.check().duration.as_millis();
    assert_eq!(dur, 2 * sleep.as_millis());
}

#[test]
fn test_visit_duration_pending() {
    let sleep = Duration::from_millis(108);
    let (mut v, i) = Visit::interaction(vec![]);
    std::thread::sleep(sleep);
    let dur = v.check().duration.as_millis();
    assert_eq!(dur, sleep.as_millis());
    drop(i);
}

#[test]
fn test_visit_duration_quick() {
    let sleep = Duration::from_millis(108);
    let (mut v, i) = Visit::interaction(vec![]);
    drop(i);
    std::thread::sleep(sleep);
    let dur = v.check().duration.as_millis();
    assert_eq!(dur, 0);
}

#[test]
fn test_visit_score() {
    let (mut visit, interaction) = Visit::interaction(vec![]);
    interaction.score(5);
    interaction.accept();
    assert_eq!(visit.check().score, 5, "init");

    // we can start new interactions even when the main one is succeeded
    let i7 = interaction.start(7).expect("interaction 7");
    let i108 = interaction.start(108).expect("interaction 108");
    let i150 = interaction.start(150).expect("interaction 250");
    assert_eq!(visit.check().score, 5 - 7 - 108 - 150, "pending");

    i7.cancel();
    i108.accept();
    i150.reject();
    assert_eq!(
        visit.check().score,
        5 - 7 - 108 - 150 + 7 + 2 * 108,
        "finished"
    );
}