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::{
8    ActiveRelation, ActiveRelationSource, RelationId, RelationSpec,
9    SubjectRef as KeepsakeSubjectRef,
10};
11
12use crate::{
13    FactBinding, FactBindingError, KeepsakeResolveError, QueryPresence, SubjectMapper,
14    TenantScopedSubjectMapper,
15};
16
17/// Resolves gatekeep facts from active keepsake relations for the principal.
18#[derive(Clone, Debug)]
19pub struct KeepsakeResolver<S, M = TenantScopedSubjectMapper> {
20    source: S,
21    subject_mapper: M,
22    bindings: BTreeMap<FactId, FactBinding>,
23}
24
25impl<S> KeepsakeResolver<S, TenantScopedSubjectMapper> {
26    /// Builds a resolver around an active-relation source.
27    #[must_use]
28    pub const fn new(source: S) -> Self {
29        Self::with_subject_mapper(source, TenantScopedSubjectMapper)
30    }
31}
32
33impl<S, M> KeepsakeResolver<S, M> {
34    /// Builds a resolver with an explicit subject mapper.
35    #[must_use]
36    pub const fn with_subject_mapper(source: S, subject_mapper: M) -> Self {
37        Self {
38            source,
39            subject_mapper,
40            bindings: BTreeMap::new(),
41        }
42    }
43
44    /// Replaces the subject mapper.
45    #[must_use]
46    pub fn map_subjects<Next>(self, subject_mapper: Next) -> KeepsakeResolver<S, Next> {
47        KeepsakeResolver {
48            source: self.source,
49            subject_mapper,
50            bindings: self.bindings,
51        }
52    }
53
54    /// Adds or replaces a fact binding.
55    #[must_use]
56    pub fn with_binding(mut self, binding: FactBinding) -> Self {
57        self.insert_binding(binding);
58        self
59    }
60
61    /// Adds or replaces a fact binding.
62    pub fn insert_binding(&mut self, binding: FactBinding) {
63        self.bindings.insert(binding.fact.clone(), binding);
64    }
65
66    /// Adds or replaces a typed fact-to-relation binding.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
71    /// invalid stable id.
72    pub fn with_relation_spec<F, R>(self) -> Result<Self, FactBindingError>
73    where
74        F: Fact,
75        R: RelationSpec,
76    {
77        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
78    }
79
80    /// Adds or replaces a typed binding that is resolved during query
81    /// preparation.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
86    /// invalid stable id.
87    pub fn with_resolved_relation<F, R>(self) -> Result<Self, FactBindingError>
88    where
89        F: Fact,
90        R: RelationSpec,
91    {
92        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
93    }
94
95    /// Adds or replaces a typed binding that is deferred during query
96    /// preparation for row-level lowering.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
101    /// invalid stable id.
102    pub fn with_deferred_relation<F, R>(self) -> Result<Self, FactBindingError>
103    where
104        F: Fact,
105        R: RelationSpec,
106    {
107        self.with_relation_spec_query_presence::<F, R>(QueryPresence::Defer)
108    }
109
110    /// Adds or replaces a typed binding with explicit query-mode behavior.
111    ///
112    /// # Errors
113    ///
114    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
115    /// invalid stable id.
116    pub fn with_relation_spec_query_presence<F, R>(
117        self,
118        query_presence: QueryPresence,
119    ) -> Result<Self, FactBindingError>
120    where
121        F: Fact,
122        R: RelationSpec,
123    {
124        Ok(
125            self.with_binding(FactBinding::for_relation_spec_with_query_presence::<F, R>(
126                query_presence,
127            )?),
128        )
129    }
130
131    /// Adds or replaces a typed binding resolved against a request-scoped
132    /// subject slot.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
137    /// invalid stable id.
138    pub fn with_relation_spec_on_subject<F, R>(
139        self,
140        subject_slot: gatekeep::SubjectSlot,
141    ) -> Result<Self, FactBindingError>
142    where
143        F: Fact,
144        R: RelationSpec,
145    {
146        Ok(
147            self.with_binding(FactBinding::for_relation_spec_on_subject::<F, R>(
148                subject_slot,
149            )?),
150        )
151    }
152
153    /// Returns the configured bindings keyed by fact id.
154    #[must_use]
155    pub const fn bindings(&self) -> &BTreeMap<FactId, FactBinding> {
156        &self.bindings
157    }
158
159    /// Returns the wrapped active-relation source.
160    #[must_use]
161    pub const fn source(&self) -> &S {
162        &self.source
163    }
164
165    /// Returns the subject mapper.
166    #[must_use]
167    pub const fn subject_mapper(&self) -> &M {
168        &self.subject_mapper
169    }
170}
171
172#[async_trait]
173impl<S, M> FactResolver for KeepsakeResolver<S, M>
174where
175    S: ActiveRelationSource,
176    M: SubjectMapper,
177{
178    type Error = KeepsakeResolveError<S::Error>;
179
180    async fn resolve_for_decision(
181        &self,
182        required: &[FactId],
183        cx: &Context,
184    ) -> Result<KnownFacts, ResolveError<Self::Error>> {
185        let bindings = self.bindings_for(required)?;
186        let active_relations = self.active_relation_ids_by_subject(cx, &bindings).await?;
187        let entries = bindings.into_iter().map(|binding| {
188            let presence = relation_presence(
189                &active_relations,
190                binding.subject_slot.as_ref(),
191                binding.relation_id,
192            );
193            (binding.fact.clone(), presence)
194        });
195        Ok(KnownFacts::from_entries(entries).map_err(KeepsakeResolveError::Gatekeep)?)
196    }
197
198    async fn resolve_for_query(
199        &self,
200        required: &[FactId],
201        cx: &Context,
202    ) -> Result<PartialFacts, ResolveError<Self::Error>> {
203        let bindings = self.bindings_for(required)?;
204        let needs_active_lookup = bindings
205            .iter()
206            .any(|binding| binding.query_presence == QueryPresence::Resolve);
207        let active_relations = if needs_active_lookup {
208            let resolved_bindings = bindings
209                .iter()
210                .copied()
211                .filter(|binding| binding.query_presence == QueryPresence::Resolve)
212                .collect::<Vec<_>>();
213            self.active_relation_ids_by_subject(cx, &resolved_bindings)
214                .await?
215        } else {
216            BTreeSet::new()
217        };
218        let entries = bindings.into_iter().map(|binding| {
219            let presence = match binding.query_presence {
220                QueryPresence::Resolve => relation_presence(
221                    &active_relations,
222                    binding.subject_slot.as_ref(),
223                    binding.relation_id,
224                ),
225                QueryPresence::Defer => Presence::Unknown,
226            };
227            (binding.fact.clone(), presence)
228        });
229        Ok(PartialFacts::from_entries(entries))
230    }
231}
232
233impl<S, M> KeepsakeResolver<S, M>
234where
235    S: ActiveRelationSource,
236    M: SubjectMapper,
237{
238    fn bindings_for<'binding>(
239        &'binding self,
240        required: &[FactId],
241    ) -> Result<Vec<&'binding FactBinding>, ResolveError<KeepsakeResolveError<S::Error>>> {
242        required
243            .iter()
244            .map(|fact| {
245                self.bindings
246                    .get(fact)
247                    .ok_or_else(|| ResolveError::MissingFact(fact.clone()))
248            })
249            .collect()
250    }
251
252    async fn active_relation_ids_by_subject(
253        &self,
254        cx: &Context,
255        bindings: &[&FactBinding],
256    ) -> Result<
257        BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
258        ResolveError<KeepsakeResolveError<S::Error>>,
259    > {
260        let mut grouped = BTreeMap::<Option<gatekeep::SubjectSlot>, SubjectLookup>::new();
261        for binding in bindings {
262            let subject = self
263                .subject_for_binding(cx, binding)
264                .map_err(|error| match error {
265                    SubjectLookupError::MissingSlot(slot) => ResolveError::MissingSubject {
266                        fact: binding.fact.clone(),
267                        slot,
268                    },
269                    SubjectLookupError::Keepsake(error) => {
270                        ResolveError::Backend(KeepsakeResolveError::from(error))
271                    }
272                })?;
273            grouped
274                .entry(binding.subject_slot.clone())
275                .or_insert_with(|| SubjectLookup {
276                    subject,
277                    relation_ids: BTreeSet::new(),
278                })
279                .relation_ids
280                .insert(binding.relation_id);
281        }
282
283        let mut active = BTreeSet::new();
284        for (slot, lookup) in grouped {
285            let relation_ids = lookup.relation_ids.into_iter().collect::<Vec<_>>();
286            let active_relations = self
287                .source
288                .active_relations_for_subject_by_ids(&lookup.subject, &relation_ids)
289                .await
290                .map_err(KeepsakeResolveError::Source)?;
291            for relation_id in active_relation_ids(active_relations) {
292                active.insert((slot.clone(), relation_id));
293            }
294        }
295        Ok(active)
296    }
297
298    fn subject_for_binding(
299        &self,
300        cx: &Context,
301        binding: &FactBinding,
302    ) -> Result<KeepsakeSubjectRef, SubjectLookupError> {
303        if let Some(slot) = &binding.subject_slot {
304            let Some(subject) = cx.subjects.get(slot) else {
305                return Err(SubjectLookupError::MissingSlot(slot.clone()));
306            };
307            return KeepsakeSubjectRef::new(subject.kind(), subject.id())
308                .map_err(SubjectLookupError::Keepsake);
309        }
310        self.subject_mapper
311            .subject(cx)
312            .map_err(SubjectLookupError::Keepsake)
313    }
314}
315
316struct SubjectLookup {
317    subject: KeepsakeSubjectRef,
318    relation_ids: BTreeSet<RelationId>,
319}
320
321enum SubjectLookupError {
322    MissingSlot(gatekeep::SubjectSlot),
323    Keepsake(keepsake::KeepsakeError),
324}
325
326fn active_relation_ids(active_relations: Vec<ActiveRelation>) -> BTreeSet<RelationId> {
327    active_relations
328        .into_iter()
329        .map(|active| active.keepsake().relation_id())
330        .collect()
331}
332
333fn relation_presence(
334    active_relations: &BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
335    subject_slot: Option<&gatekeep::SubjectSlot>,
336    relation_id: RelationId,
337) -> Presence {
338    if active_relations.contains(&(subject_slot.cloned(), relation_id)) {
339        Presence::Present
340    } else {
341        Presence::Absent
342    }
343}