use crate::error::{BallError, Result};
use crate::negotiation::{Accepted, FailurePolicy, Negotiation, NegotiationResult, Protocol};
use crate::store::Store;
use crate::task::Task;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Event {
Claim,
Review,
Close,
Update,
Sync,
Create,
Drop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Field {
Title,
Type,
Priority,
Status,
Parent,
DependsOn,
Description,
Tags,
Notes,
Links,
ClaimedBy,
Branch,
ClosedAt,
UpdatedAt,
ClosedChildren,
External,
SyncedAt,
DeliveredIn,
}
impl Field {
pub fn all() -> BTreeSet<Field> {
[
Field::Title,
Field::Type,
Field::Priority,
Field::Status,
Field::Parent,
Field::DependsOn,
Field::Description,
Field::Tags,
Field::Notes,
Field::Links,
Field::ClaimedBy,
Field::Branch,
Field::ClosedAt,
Field::UpdatedAt,
Field::ClosedChildren,
Field::External,
Field::SyncedAt,
Field::DeliveredIn,
]
.into_iter()
.collect()
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Projection {
pub owns: BTreeSet<Field>,
pub reads: BTreeSet<Field>,
pub external_prefixes: BTreeSet<String>,
}
impl Projection {
pub fn full() -> Self {
Self {
owns: Field::all(),
reads: BTreeSet::new(),
external_prefixes: BTreeSet::new(),
}
}
pub fn external_only(prefix: impl Into<String>) -> Self {
let mut external_prefixes = BTreeSet::new();
external_prefixes.insert(prefix.into());
Self {
owns: BTreeSet::new(),
reads: Field::all(),
external_prefixes,
}
}
pub fn overlaps(&self, other: &Projection) -> bool {
self.owns.intersection(&other.owns).next().is_some()
|| self
.external_prefixes
.intersection(&other.external_prefixes)
.next()
.is_some()
}
}
pub struct EventCtx<'a> {
pub event: Event,
pub store: &'a Store,
pub task_id: &'a str,
pub identity: &'a str,
pub post: Option<&'a Task>,
pub task_before: Option<&'a Task>,
pub commit: Option<&'a str>,
pub overrides: &'a [String],
}
impl<'a> EventCtx<'a> {
pub fn new(
event: Event,
store: &'a Store,
task_id: &'a str,
identity: &'a str,
) -> Self {
Self {
event,
store,
task_id,
identity,
post: None,
task_before: None,
commit: None,
overrides: &[],
}
}
#[must_use]
pub fn with_context(
mut self,
post: Option<&'a Task>,
task_before: Option<&'a Task>,
commit: Option<&'a str>,
overrides: &'a [String],
) -> Self {
self.post = post;
self.task_before = task_before;
self.commit = commit;
self.overrides = overrides;
self
}
}
pub trait Participant {
type Outcome;
type Protocol<'a>: Protocol<Outcome = Self::Outcome>
where
Self: 'a;
fn name(&self) -> &str;
fn subscriptions(&self) -> &[Event];
fn projection(&self) -> &Projection;
fn failure_policy(&self, event: Event) -> FailurePolicy;
fn protocol<'a>(
&'a self,
event: Event,
ctx: EventCtx<'a>,
) -> Option<Self::Protocol<'a>>;
}
pub fn run<P: Participant>(
participant: &P,
event: Event,
ctx: EventCtx<'_>,
) -> Result<NegotiationResult<P::Outcome>> {
if !participant.subscriptions().contains(&event) {
return Ok(NegotiationResult::Skipped(format!(
"{} does not subscribe to {event:?}",
participant.name()
)));
}
let policy = participant.failure_policy(event);
let Some(protocol) = participant.protocol(event, ctx) else {
return Ok(NegotiationResult::Skipped(format!(
"{} returned no protocol for {event:?}",
participant.name()
)));
};
Negotiation::new(protocol, policy).run()
}
pub fn run_strict<P: Participant>(
participant: &P,
event: Event,
ctx: EventCtx<'_>,
) -> Result<P::Outcome> {
match run(participant, event, ctx)? {
NegotiationResult::Ok(Accepted { outcome, .. }) => Ok(outcome),
NegotiationResult::Skipped(s) | NegotiationResult::Staged(s) => {
Err(BallError::Other(s))
}
}
}
#[cfg(test)]
#[path = "participant_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "participant_projection_tests.rs"]
mod projection_tests;