Skip to main content

gatekeep_keepsake/
resolver.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use async_trait::async_trait;
4use gatekeep::{
5    Context, Fact, FactId, FactResolver, KnownFacts, PartialFacts, Presence, ResolveError,
6};
7use keepsake::{ActiveRelation, ActiveRelationSource, RelationId, RelationSpec};
8
9use crate::{
10    FactBinding, FactBindingError, KeepsakeResolveError, QueryPresence, SubjectMapper,
11    TenantScopedSubjectMapper,
12};
13
14/// Resolves gatekeep facts from active keepsake relations for the principal.
15#[derive(Clone, Debug)]
16pub struct KeepsakeResolver<S, M = TenantScopedSubjectMapper> {
17    source: S,
18    subject_mapper: M,
19    bindings: BTreeMap<FactId, FactBinding>,
20}
21
22impl<S> KeepsakeResolver<S, TenantScopedSubjectMapper> {
23    /// Builds a resolver around an active-relation source.
24    #[must_use]
25    pub const fn new(source: S) -> Self {
26        Self::with_subject_mapper(source, TenantScopedSubjectMapper)
27    }
28}
29
30impl<S, M> KeepsakeResolver<S, M> {
31    /// Builds a resolver with an explicit subject mapper.
32    #[must_use]
33    pub const fn with_subject_mapper(source: S, subject_mapper: M) -> Self {
34        Self {
35            source,
36            subject_mapper,
37            bindings: BTreeMap::new(),
38        }
39    }
40
41    /// Replaces the subject mapper.
42    #[must_use]
43    pub fn map_subjects<Next>(self, subject_mapper: Next) -> KeepsakeResolver<S, Next> {
44        KeepsakeResolver {
45            source: self.source,
46            subject_mapper,
47            bindings: self.bindings,
48        }
49    }
50
51    /// Adds or replaces a fact binding.
52    #[must_use]
53    pub fn with_binding(mut self, binding: FactBinding) -> Self {
54        self.insert_binding(binding);
55        self
56    }
57
58    /// Adds or replaces a fact binding.
59    pub fn insert_binding(&mut self, binding: FactBinding) {
60        self.bindings.insert(binding.fact.clone(), binding);
61    }
62
63    /// Adds or replaces a typed fact-to-relation binding.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
68    /// invalid stable id.
69    pub fn with_relation_spec<F, R>(self) -> Result<Self, FactBindingError>
70    where
71        F: Fact,
72        R: RelationSpec,
73    {
74        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
75    }
76
77    /// Adds or replaces a typed binding that is resolved during query
78    /// preparation.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
83    /// invalid stable id.
84    pub fn with_resolved_relation<F, R>(self) -> Result<Self, FactBindingError>
85    where
86        F: Fact,
87        R: RelationSpec,
88    {
89        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
90    }
91
92    /// Adds or replaces a typed binding that is deferred during query
93    /// preparation for row-level lowering.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
98    /// invalid stable id.
99    pub fn with_deferred_relation<F, R>(self) -> Result<Self, FactBindingError>
100    where
101        F: Fact,
102        R: RelationSpec,
103    {
104        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Defer)
105    }
106
107    /// Adds or replaces a typed binding with explicit query-mode behavior.
108    ///
109    /// # Errors
110    ///
111    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
112    /// invalid stable id.
113    pub fn with_relation_spec_query_presence<F, R>(
114        self,
115        query_presence: QueryPresence,
116    ) -> Result<Self, FactBindingError>
117    where
118        F: Fact,
119        R: RelationSpec,
120    {
121        Ok(
122            self.with_binding(FactBinding::for_relation_spec_with_query_presence::<F, R>(
123                query_presence,
124            )?),
125        )
126    }
127
128    /// Returns the configured bindings keyed by fact id.
129    #[must_use]
130    pub const fn bindings(&self) -> &BTreeMap<FactId, FactBinding> {
131        &self.bindings
132    }
133
134    /// Returns the wrapped active-relation source.
135    #[must_use]
136    pub const fn source(&self) -> &S {
137        &self.source
138    }
139
140    /// Returns the subject mapper.
141    #[must_use]
142    pub const fn subject_mapper(&self) -> &M {
143        &self.subject_mapper
144    }
145}
146
147#[async_trait]
148impl<S, M> FactResolver for KeepsakeResolver<S, M>
149where
150    S: ActiveRelationSource,
151    M: SubjectMapper,
152{
153    type Error = KeepsakeResolveError<S::Error>;
154
155    async fn resolve_for_decision(
156        &self,
157        required: &[FactId],
158        cx: &Context,
159    ) -> Result<KnownFacts, ResolveError<Self::Error>> {
160        let bindings = self.bindings_for(required)?;
161        let relation_ids = relation_ids(bindings.iter().copied());
162        let active_relations = self.active_relation_ids(cx, &relation_ids).await?;
163        let entries = bindings.into_iter().map(|binding| {
164            let presence = relation_presence(&active_relations, binding.relation_id);
165            (binding.fact.clone(), presence)
166        });
167        Ok(KnownFacts::from_entries(entries).map_err(KeepsakeResolveError::Gatekeep)?)
168    }
169
170    async fn resolve_for_query(
171        &self,
172        required: &[FactId],
173        cx: &Context,
174    ) -> Result<PartialFacts, ResolveError<Self::Error>> {
175        let bindings = self.bindings_for(required)?;
176        let needs_active_lookup = bindings
177            .iter()
178            .any(|binding| binding.query_presence == QueryPresence::Resolve);
179        let active_relations = if needs_active_lookup {
180            let relation_ids = relation_ids(
181                bindings
182                    .iter()
183                    .copied()
184                    .filter(|binding| binding.query_presence == QueryPresence::Resolve),
185            );
186            self.active_relation_ids(cx, &relation_ids).await?
187        } else {
188            BTreeSet::new()
189        };
190        let entries = bindings.into_iter().map(|binding| {
191            let presence = match binding.query_presence {
192                QueryPresence::Resolve => relation_presence(&active_relations, binding.relation_id),
193                QueryPresence::Defer => Presence::Unknown,
194            };
195            (binding.fact.clone(), presence)
196        });
197        Ok(PartialFacts::from_entries(entries))
198    }
199}
200
201impl<S, M> KeepsakeResolver<S, M>
202where
203    S: ActiveRelationSource,
204    M: SubjectMapper,
205{
206    fn bindings_for<'binding>(
207        &'binding self,
208        required: &[FactId],
209    ) -> Result<Vec<&'binding FactBinding>, ResolveError<KeepsakeResolveError<S::Error>>> {
210        required
211            .iter()
212            .map(|fact| {
213                self.bindings
214                    .get(fact)
215                    .ok_or_else(|| ResolveError::MissingFact(fact.clone()))
216            })
217            .collect()
218    }
219
220    async fn active_relation_ids(
221        &self,
222        cx: &Context,
223        relation_ids: &[RelationId],
224    ) -> Result<BTreeSet<RelationId>, KeepsakeResolveError<S::Error>> {
225        let subject = self.subject_mapper.subject(cx)?;
226        let active_relations = self
227            .source
228            .active_relations_for_subject_by_ids(&subject, relation_ids)
229            .await
230            .map_err(KeepsakeResolveError::Source)?;
231        Ok(active_relation_ids(active_relations))
232    }
233}
234
235fn relation_ids<'binding>(
236    bindings: impl IntoIterator<Item = &'binding FactBinding>,
237) -> Vec<RelationId> {
238    bindings
239        .into_iter()
240        .map(FactBinding::relation_id)
241        .collect::<BTreeSet<_>>()
242        .into_iter()
243        .collect()
244}
245
246fn active_relation_ids(active_relations: Vec<ActiveRelation>) -> BTreeSet<RelationId> {
247    active_relations
248        .into_iter()
249        .map(|active| active.keepsake().relation_id())
250        .collect()
251}
252
253fn relation_presence(active_relations: &BTreeSet<RelationId>, relation_id: RelationId) -> Presence {
254    if active_relations.contains(&relation_id) {
255        Presence::Present
256    } else {
257        Presence::Absent
258    }
259}