use async_trait::async_trait;
use crate::violation::Violation;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteKind {
Create,
Update,
Delete,
Restore,
HardDelete,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct WriteCtx<'a, E> {
pub kind: WriteKind,
pub before: Option<&'a E>,
pub after: Option<&'a E>,
pub actor: Option<String>,
pub at: chrono::DateTime<chrono::Utc>,
pub transition: Option<String>,
}
impl<'a, E> WriteCtx<'a, E> {
pub fn new(kind: WriteKind, before: Option<&'a E>, after: Option<&'a E>) -> Self {
Self {
kind,
before,
after,
actor: backbone_orm::audit_context::current_request_audit()
.map(|a| a.actor)
.filter(|a| !a.is_empty()),
at: chrono::Utc::now(),
transition: current_transition(),
}
}
}
tokio::task_local! {
static TRANSITION: String;
}
pub async fn as_transition<F: std::future::Future>(verb: &str, f: F) -> F::Output {
TRANSITION.scope(verb.to_string(), f).await
}
pub fn current_transition() -> Option<String> {
TRANSITION.try_with(|v| v.clone()).ok()
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct GuardOutcome {
pub refuse: Vec<Violation>,
pub shadow: Vec<Violation>,
}
impl GuardOutcome {
pub fn allow() -> Self {
Self::default()
}
pub fn merge(mut self, other: GuardOutcome) -> Self {
self.refuse.extend(other.refuse);
self.shadow.extend(other.shadow);
self
}
}
#[async_trait]
pub trait WriteGuard<E>: Send + Sync {
async fn check(&self, ctx: &WriteCtx<'_, E>) -> GuardOutcome;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AllowAll;
#[async_trait]
impl<E: Sync> WriteGuard<E> for AllowAll {
async fn check(&self, _ctx: &WriteCtx<'_, E>) -> GuardOutcome {
GuardOutcome::allow()
}
}
#[cfg(test)]
mod transition_tests {
use super::*;
#[tokio::test]
async fn a_write_knows_whether_a_verb_performs_it() {
let row = 1u8;
assert_eq!(WriteCtx::new(WriteKind::Update, Some(&row), Some(&row)).transition, None);
let inside = as_transition("post", async { WriteCtx::new(WriteKind::Update, Some(&row), Some(&row)).transition }).await;
assert_eq!(inside.as_deref(), Some("post"));
}
}