Skip to main content

gatekeep_keepsake/
effective.rs

1use gatekeep::Presence;
2use keepsake::{
3    EffectiveRelationError, FulfillmentEvidence, LifecycleState, ObservationTime, RelationSnapshot,
4    effective_state,
5};
6
7use crate::KeepsakeRelationTarget;
8
9/// Failure to resolve effective facts from an explicitly scoped observation.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
11pub enum EffectiveFactError {
12    /// An assignment belongs to another tenant, subject or relation.
13    #[error("effective relation observation scope mismatch")]
14    ScopeMismatch,
15    /// Fulfillment evidence targets another tenant or assignment incarnation.
16    #[error("fulfillment evidence scope mismatch")]
17    FulfillmentScopeMismatch,
18    /// Time or lifecycle evidence cannot establish effective presence.
19    #[error(transparent)]
20    Evidence(#[from] EffectiveRelationError),
21}
22
23impl KeepsakeRelationTarget {
24    /// Resolves effective presence from a current scoped assignment or absence.
25    ///
26    /// The caller owns the observation's completeness and concurrency provenance:
27    /// protected writes must retain the database scope lock through commit. A
28    /// scoped absence is not proof that another transaction cannot apply a relation.
29    /// Fulfillment evidence must describe this assignment at the supplied time.
30    ///
31    /// # Errors
32    /// Returns a scope mismatch or unavailable time/lifecycle evidence.
33    pub fn effective_presence(
34        &self,
35        time: ObservationTime,
36        snapshot: &RelationSnapshot,
37        fulfillment: Option<&FulfillmentEvidence>,
38    ) -> Result<Presence, EffectiveFactError> {
39        time.instant()?;
40        if snapshot.tenant_id() != &self.tenant_id
41            || snapshot.subject() != &self.subject
42            || snapshot.relation_id() != self.relation_id
43        {
44            return Err(EffectiveFactError::ScopeMismatch);
45        }
46
47        let Some(active) = snapshot.active() else {
48            return if fulfillment.is_some() {
49                Err(EffectiveFactError::FulfillmentScopeMismatch)
50            } else {
51                Ok(Presence::Absent)
52            };
53        };
54
55        if let Some(evidence) = fulfillment
56            && (evidence.tenant_id() != active.keepsake().tenant_id()
57                || evidence.keepsake_id() != active.keepsake().id())
58        {
59            return Err(EffectiveFactError::FulfillmentScopeMismatch);
60        }
61
62        let fulfillment = fulfillment.map(FulfillmentEvidence::snapshot);
63        Ok(
64            if effective_state(time, active, fulfillment)? == LifecycleState::Applied {
65                Presence::Present
66            } else {
67                Presence::Absent
68            },
69        )
70    }
71}