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
299        let entries = bindings.into_iter().map(|binding| {
300            let presence = match binding.query_presence {
301                QueryPresence::Resolve => relation_presence(
302                    &active_relations,
303                    binding.subject_slot.as_ref(),
304                    binding.relation_id,
305                ),
306                QueryPresence::Defer => Presence::Unknown,
307            };
308            (binding.fact.clone(), presence)
309        });
310        Ok(PartialFacts::from_entries(entries))
311    }
312}
313
314impl<S, M> KeepsakeResolver<S, M>
315where
316    S: ActiveRelationSource,
317    M: SubjectMapper,
318{
319    fn bindings_for<'binding>(
320        &'binding self,
321        required: &[FactId],
322    ) -> Result<Vec<&'binding FactBinding>, ResolveError<KeepsakeResolveError<S::Error>>> {
323        required
324            .iter()
325            .map(|fact| {
326                self.bindings
327                    .get(fact)
328                    .ok_or_else(|| ResolveError::MissingFact(fact.clone()))
329            })
330            .collect()
331    }
332
333    async fn active_relation_ids_by_subject(
334        &self,
335        cx: &Context,
336        bindings: &[&FactBinding],
337    ) -> Result<
338        BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
339        ResolveError<KeepsakeResolveError<S::Error>>,
340    > {
341        let mut grouped = BTreeMap::<Option<gatekeep::SubjectSlot>, SubjectLookup>::new();
342        for binding in bindings {
343            let target = self
344                .target_for_binding(binding, cx)
345                .map_err(|error| match error {
346                    KeepsakeTargetError::MissingBinding { fact } => ResolveError::MissingFact(fact),
347                    KeepsakeTargetError::MissingSubjectSlot { fact, slot } => {
348                        ResolveError::MissingSubject { fact, slot }
349                    }
350                    KeepsakeTargetError::Subject { source, .. } => {
351                        ResolveError::Backend(KeepsakeResolveError::from(source))
352                    }
353                })?;
354            grouped
355                .entry(target.subject_slot)
356                .or_insert_with(|| SubjectLookup {
357                    subject: target.subject,
358                    relation_ids: BTreeSet::new(),
359                })
360                .relation_ids
361                .insert(target.relation_id);
362        }
363
364        let mut active = BTreeSet::new();
365        for (slot, lookup) in grouped {
366            let relation_ids = lookup.relation_ids.into_iter().collect::<Vec<_>>();
367            let active_relations = self
368                .source
369                .active_relations_for_subject_by_ids(&lookup.subject, &relation_ids)
370                .await
371                .map_err(KeepsakeResolveError::Source)?;
372            for relation_id in active_relation_ids(active_relations) {
373                active.insert((slot.clone(), relation_id));
374            }
375        }
376        Ok(active)
377    }
378}
379
380struct SubjectLookup {
381    subject: keepsake::SubjectRef,
382    relation_ids: BTreeSet<RelationId>,
383}
384
385fn active_relation_ids(active_relations: Vec<ActiveRelation>) -> BTreeSet<RelationId> {
386    active_relations
387        .into_iter()
388        .map(|active| active.keepsake().relation_id())
389        .collect()
390}
391
392fn relation_presence(
393    active_relations: &BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
394    subject_slot: Option<&gatekeep::SubjectSlot>,
395    relation_id: RelationId,
396) -> Presence {
397    if active_relations.contains(&(subject_slot.cloned(), relation_id)) {
398        Presence::Present
399    } else {
400        Presence::Absent
401    }
402}