backbone_core/write_guard.rs
1//! The write guard: the rules a write must satisfy, asked on every path the
2//! generic service writes through.
3//!
4//! [`GenericCrudService`](crate::service::GenericCrudService) consults its
5//! guard on create, update (full and partial, single and bulk), soft delete,
6//! restore and hard delete, after the write's resulting row is known and
7//! before anything is persisted. A guard answers with violations to refuse and
8//! violations to only report: a rule that runs in shadow is logged and the
9//! write proceeds, so a newly compiled rule shows what it would refuse before
10//! it refuses anything. The schema generator compiles a module's declared rules
11//! into guards; without one, every write is allowed ([`AllowAll`]).
12
13use async_trait::async_trait;
14
15use crate::violation::Violation;
16
17/// What kind of write is being checked.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum WriteKind {
20 Create,
21 /// A full or partial update.
22 Update,
23 /// A soft delete.
24 Delete,
25 Restore,
26 /// Removal of a soft-deleted row for good.
27 HardDelete,
28}
29
30/// One write, as the guard sees it.
31#[derive(Debug)]
32pub struct WriteCtx<'a, E> {
33 pub kind: WriteKind,
34 /// The row as stored before the write; `None` on create.
35 pub before: Option<&'a E>,
36 /// The row the write would store; `None` on delete and hard delete.
37 pub after: Option<&'a E>,
38}
39
40/// A guard's answer.
41#[derive(Debug, Default, Clone, PartialEq)]
42pub struct GuardOutcome {
43 /// Broken rules that refuse the write.
44 pub refuse: Vec<Violation>,
45 /// Broken rules running in shadow: reported, not refused.
46 pub shadow: Vec<Violation>,
47}
48
49impl GuardOutcome {
50 pub fn allow() -> Self {
51 Self::default()
52 }
53
54 /// Fold another guard's answer into this one.
55 pub fn merge(mut self, other: GuardOutcome) -> Self {
56 self.refuse.extend(other.refuse);
57 self.shadow.extend(other.shadow);
58 self
59 }
60}
61
62/// The rules one entity's writes must satisfy.
63#[async_trait]
64pub trait WriteGuard<E>: Send + Sync {
65 async fn check(&self, ctx: &WriteCtx<'_, E>) -> GuardOutcome;
66}
67
68/// A guard that refuses nothing: the default for an entity whose schema
69/// declares no rules.
70#[derive(Debug, Default, Clone, Copy)]
71pub struct AllowAll;
72
73#[async_trait]
74impl<E: Sync> WriteGuard<E> for AllowAll {
75 async fn check(&self, _ctx: &WriteCtx<'_, E>) -> GuardOutcome {
76 GuardOutcome::allow()
77 }
78}