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, KeepsakeRelationTarget, KeepsakeResolveError,
11    KeepsakeTargetError, QueryPresence, SubjectMapper, 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    /// Adds or replaces a typed binding resolved against a request-scoped
129    /// subject slot.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`FactBindingError::Gatekeep`] if the fact marker exposes an
134    /// invalid stable id.
135    pub fn with_relation_spec_on_subject<F, R>(
136        self,
137        subject_slot: gatekeep::SubjectSlot,
138    ) -> Result<Self, FactBindingError>
139    where
140        F: Fact,
141        R: RelationSpec,
142    {
143        Ok(
144            self.with_binding(FactBinding::for_relation_spec_on_subject::<F, R>(
145                subject_slot,
146            )?),
147        )
148    }
149
150    /// Returns the configured bindings keyed by fact id.
151    #[must_use]
152    pub const fn bindings(&self) -> &BTreeMap<FactId, FactBinding> {
153        &self.bindings
154    }
155
156    /// Returns the wrapped active-relation source.
157    #[must_use]
158    pub const fn source(&self) -> &S {
159        &self.source
160    }
161
162    /// Returns the subject mapper.
163    #[must_use]
164    pub const fn subject_mapper(&self) -> &M {
165        &self.subject_mapper
166    }
167}
168
169impl<S, M> KeepsakeResolver<S, M>
170where
171    M: SubjectMapper,
172{
173    /// Resolves one binding into the keepsake subject/relation target used for lookups.
174    ///
175    /// # Errors
176    ///
177    /// Returns [`KeepsakeTargetError::MissingSubjectSlot`] when the binding
178    /// targets a request-scoped subject absent from the context, or
179    /// [`KeepsakeTargetError::Subject`] when keepsake rejects the mapped subject.
180    pub fn target_for_binding(
181        &self,
182        binding: &FactBinding,
183        cx: &Context,
184    ) -> Result<KeepsakeRelationTarget, KeepsakeTargetError> {
185        let subject = if let Some(slot) = &binding.subject_slot {
186            let Some(subject) = cx.subjects.get(slot) else {
187                return Err(KeepsakeTargetError::MissingSubjectSlot {
188                    fact: binding.fact.clone(),
189                    slot: slot.clone(),
190                });
191            };
192            keepsake::SubjectRef::new(subject.kind(), subject.id()).map_err(|source| {
193                KeepsakeTargetError::Subject {
194                    fact: binding.fact.clone(),
195                    source,
196                }
197            })?
198        } else {
199            self.subject_mapper
200                .subject(cx)
201                .map_err(|source| KeepsakeTargetError::Subject {
202                    fact: binding.fact.clone(),
203                    source,
204                })?
205        };
206
207        Ok(KeepsakeRelationTarget {
208            fact: binding.fact.clone(),
209            subject,
210            relation_id: binding.relation_id,
211            subject_slot: binding.subject_slot.clone(),
212        })
213    }
214
215    /// Resolves a configured fact id into its keepsake subject/relation target.
216    ///
217    /// # Errors
218    ///
219    /// Returns [`KeepsakeTargetError::MissingBinding`] when the fact has no
220    /// configured binding. Also returns the subject-resolution errors documented
221    /// by [`Self::target_for_binding`].
222    pub fn target_for_fact(
223        &self,
224        fact: &FactId,
225        cx: &Context,
226    ) -> Result<KeepsakeRelationTarget, KeepsakeTargetError> {
227        let binding = self
228            .bindings
229            .get(fact)
230            .ok_or_else(|| KeepsakeTargetError::MissingBinding { fact: fact.clone() })?;
231        self.target_for_binding(binding, cx)
232    }
233
234    /// Resolves configured fact ids into keepsake subject/relation targets.
235    ///
236    /// # Errors
237    ///
238    /// Returns the first [`KeepsakeTargetError`] encountered while resolving the
239    /// requested facts.
240    pub fn targets_for_facts(
241        &self,
242        facts: &[FactId],
243        cx: &Context,
244    ) -> Result<Vec<KeepsakeRelationTarget>, KeepsakeTargetError> {
245        facts
246            .iter()
247            .map(|fact| self.target_for_fact(fact, cx))
248            .collect()
249    }
250}
251
252#[async_trait]
253impl<S, M> FactResolver for KeepsakeResolver<S, M>
254where
255    S: ActiveRelationSource,
256    M: SubjectMapper,
257{
258    type Error = KeepsakeResolveError<S::Error>;
259
260    async fn resolve_for_decision(
261        &self,
262        required: &[FactId],
263        cx: &Context,
264    ) -> Result<KnownFacts, ResolveError<Self::Error>> {
265        let bindings = self.bindings_for(required)?;
266        let active_relations = self.active_relation_ids_by_subject(cx, &bindings).await?;
267        let entries = bindings.into_iter().map(|binding| {
268            let presence = relation_presence(
269                &active_relations,
270                binding.subject_slot.as_ref(),
271                binding.relation_id,
272            );
273            (binding.fact.clone(), presence)
274        });
275        Ok(KnownFacts::from_entries(entries).map_err(KeepsakeResolveError::Gatekeep)?)
276    }
277
278    async fn resolve_for_query(
279        &self,
280        required: &[FactId],
281        cx: &Context,
282    ) -> Result<PartialFacts, ResolveError<Self::Error>> {
283        let bindings = self.bindings_for(required)?;
284        let needs_active_lookup = bindings
285            .iter()
286            .any(|binding| binding.query_presence == QueryPresence::Resolve);
287        let active_relations = if needs_active_lookup {
288            let resolved_bindings = bindings
289                .iter()
290                .copied()
291                .filter(|binding| binding.query_presence == QueryPresence::Resolve)
292                .collect::<Vec<_>>();
293            self.active_relation_ids_by_subject(cx, &resolved_bindings)
294                .await?
295        } else {
296            BTreeSet::new()
297        };
298        let entries = bindings.into_iter().map(|binding| {
299            let presence = match binding.query_presence {
300                QueryPresence::Resolve => relation_presence(
301                    &active_relations,
302                    binding.subject_slot.as_ref(),
303                    binding.relation_id,
304                ),
305                QueryPresence::Defer => Presence::Unknown,
306            };
307            (binding.fact.clone(), presence)
308        });
309        Ok(PartialFacts::from_entries(entries))
310    }
311}
312
313impl<S, M> KeepsakeResolver<S, M>
314where
315    S: ActiveRelationSource,
316    M: SubjectMapper,
317{
318    fn bindings_for<'binding>(
319        &'binding self,
320        required: &[FactId],
321    ) -> Result<Vec<&'binding FactBinding>, ResolveError<KeepsakeResolveError<S::Error>>> {
322        required
323            .iter()
324            .map(|fact| {
325                self.bindings
326                    .get(fact)
327                    .ok_or_else(|| ResolveError::MissingFact(fact.clone()))
328            })
329            .collect()
330    }
331
332    async fn active_relation_ids_by_subject(
333        &self,
334        cx: &Context,
335        bindings: &[&FactBinding],
336    ) -> Result<
337        BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
338        ResolveError<KeepsakeResolveError<S::Error>>,
339    > {
340        let mut grouped = BTreeMap::<Option<gatekeep::SubjectSlot>, SubjectLookup>::new();
341        for binding in bindings {
342            let target = self
343                .target_for_binding(binding, cx)
344                .map_err(|error| match error {
345                    KeepsakeTargetError::MissingBinding { fact } => ResolveError::MissingFact(fact),
346                    KeepsakeTargetError::MissingSubjectSlot { fact, slot } => {
347                        ResolveError::MissingSubject { fact, slot }
348                    }
349                    KeepsakeTargetError::Subject { source, .. } => {
350                        ResolveError::Backend(KeepsakeResolveError::from(source))
351                    }
352                })?;
353            grouped
354                .entry(target.subject_slot)
355                .or_insert_with(|| SubjectLookup {
356                    subject: target.subject,
357                    relation_ids: BTreeSet::new(),
358                })
359                .relation_ids
360                .insert(target.relation_id);
361        }
362
363        let mut active = BTreeSet::new();
364        for (slot, lookup) in grouped {
365            let relation_ids = lookup.relation_ids.into_iter().collect::<Vec<_>>();
366            let active_relations = self
367                .source
368                .active_relations_for_subject_by_ids(&lookup.subject, &relation_ids)
369                .await
370                .map_err(KeepsakeResolveError::Source)?;
371            for relation_id in active_relation_ids(active_relations) {
372                active.insert((slot.clone(), relation_id));
373            }
374        }
375        Ok(active)
376    }
377}
378
379struct SubjectLookup {
380    subject: keepsake::SubjectRef,
381    relation_ids: BTreeSet<RelationId>,
382}
383
384fn active_relation_ids(active_relations: Vec<ActiveRelation>) -> BTreeSet<RelationId> {
385    active_relations
386        .into_iter()
387        .map(|active| active.keepsake().relation_id())
388        .collect()
389}
390
391fn relation_presence(
392    active_relations: &BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
393    subject_slot: Option<&gatekeep::SubjectSlot>,
394    relation_id: RelationId,
395) -> Presence {
396    if active_relations.contains(&(subject_slot.cloned(), relation_id)) {
397        Presence::Present
398    } else {
399        Presence::Absent
400    }
401}