turngate 0.1.0

Prevent or reduce abuse of your public service
Documentation
use super::{InteractionUpdate, Label};
use std::{
    sync::{
        atomic::{AtomicU8, Ordering},
        mpsc::Sender,
        Arc,
    },
    time::Instant,
};

/**
Single visit interaction of which there may be many.

If you get None or false from the methods, it means the visit is over and you should stop serving.

```
// The main interacction has weight 0
let (mut visit, interaction) = turngate::Visit::interaction(vec![]);
interaction.score(5);
interaction.accept();
assert_eq!(visit.check().score, 5, "main one accepted");

// We can still start new interactions even when the main one is accepted
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(idur);
drop(i7);
drop(i150);
drop(i108);
drop(interaction);
std::thread::sleep(std::time::Duration::from_millis(108));
assert_eq!(visit.check().duration.as_millis(), 108, "final duration");

```

One can also reverse previous results

```
let (mut visit, interaction) = turngate::Visit::interaction(vec![]);
let shopping = interaction.start(5).expect("shopping");
shopping.cancel();
assert_eq!(visit.check().score, 0, "shopping canceled");
shopping.accept();
assert_eq!(visit.check().score, 5, "shopping accepted");
shopping.reject();
assert_eq!(visit.check().score, -5, "shopping rejected");
shopping.accept();
assert_eq!(visit.check().score, 5, "shopping rejected");
```

Same with a single method and a value

```
let (mut visit, interaction) = turngate::Visit::interaction(vec![]);
let shopping = interaction.start(5).expect("shopping");
shopping.finish(None);
assert_eq!(visit.check().score, 0, "shopping canceled");
shopping.finish(true);
assert_eq!(visit.check().score, 5, "shopping accepted");
shopping.finish(false);
assert_eq!(visit.check().score, -5, "shopping rejected");
shopping.finish(true);
assert_eq!(visit.check().score, 5, "shopping rejected");
```

chaining

```
let (mut visit, interaction) = turngate::Visit::interaction(vec![]);
let good = interaction.start(15).map(|i|i.score(5).accept().is_active());
assert_eq!(visit.check().score, 20, "shopping accepted");
assert_eq!(good, Some(true), "shopping ccontinues");
```

*/
#[derive(Debug)]
pub struct Interaction {
    result: AtomicU8,
    weight: i16,
    sender: Sender<InteractionUpdate>,
    labels: Arc<Vec<u128>>,
}

impl Interaction {
    const REJECT: u8 = 0;
    const CANCEL: u8 = 1;
    const ACCEPT: u8 = 2;

    /// Constructed by `Visit`
    pub(super) fn new(sender: Sender<InteractionUpdate>, labels: Arc<Vec<u128>>) -> Self {
        Self {
            result: AtomicU8::new(Interaction::ACCEPT),
            weight: 0,
            sender,
            labels,
        }
    }
    pub fn labels<'a>(&'a self) -> impl Iterator<Item = Label<'static>> + 'a {
        self.labels.iter().cloned().map(Label::from)
    }
    /// Claim a new visitor interaction.
    ///
    /// If you get None, it means the visit is over. You should stop serving.
    ///
    /// The weight deducted from the visitor's score so that if something doesn't go according to plan, the visitor is penalized.
    pub fn start(&self, weight: u8) -> Option<Interaction> {
        // set the expectation, if we cannot contact visit, it's been destroyed, stop serving
        if !self.score(-(weight as i16)).is_active() {
            return None;
        }

        Some(Interaction {
            result: AtomicU8::new(Interaction::REJECT),
            weight: weight as i16,
            sender: self.sender.clone(),
            labels: self.labels.clone(),
        })
    }
    /// End the interaction and rate the visitor or cancel the expectation.
    /// score:
    ///     - None - cancel: balance zero, not good nor bad
    ///     - true - success:  + weight
    ///     - false - failure: - weight
    ///
    /// If you do not call this and the expectation is dropped, the visit will remain with negative score according to weight.
    pub fn finish(&self, success: impl Into<Option<bool>>) -> &Self {
        match success.into() {
            Some(true) => self.accept(),
            Some(false) => self.reject(),
            None => self.cancel(),
        }
    }
    /// Interaction did not go as expected, visit will remain with weight deducted.
    pub fn reject(&self) -> &Self {
        self.set_result(Interaction::REJECT)
    }
    /// Interaction was canceled, weight is neutralized.
    pub fn cancel(&self) -> &Self {
        self.set_result(Interaction::CANCEL)
    }
    /// Interaction went fine, score will be +weight.
    pub fn accept(&self) -> &Self {
        self.set_result(Interaction::ACCEPT)
    }
    /// add arbitrary score to the interaction
    pub fn score(&self, score: i16) -> &Self {
        self.sender.send(InteractionUpdate::Score(score)).ok();
        self
    }
    /// check if the visit is still going on, or else, stop serving    
    pub fn is_active(&self) -> bool {
        self.sender.send(InteractionUpdate::Score(0)).is_ok()
    }
    /// destroy the interaction, checking if the visit was still going on
    pub fn bye(self) -> bool {
        self.is_active()
    }
    fn set_result(&self, new: u8) -> &Self {
        let was = self.result.swap(new, Ordering::Relaxed) as i16;
        self.score(self.weight * (new as i16 - was).clamp(-2, 2))
    }
    fn finished(&self) -> bool {
        self.sender
            .send(InteractionUpdate::Finished(Instant::now()))
            .is_ok()
    }
}

impl Drop for Interaction {
    fn drop(&mut self) {
        self.finished();
    }
}