1use std::collections::{BTreeMap, BTreeSet};
2
3use async_trait::async_trait;
4use gatekeep::{
5 BindingProvenance, Clock, Context, Fact, FactId, FactResolution, FactResolutionMetadata,
6 FactResolver, KnownFacts, PartialFacts, Presence, QueryFactResolver, ResolveError,
7};
8use keepsake::{
9 ActiveRelationSource, LifecycleState, ObservationTime, RelationId, RelationSpec,
10 effective_state,
11};
12
13use crate::{
14 FactBinding, FactBindingError, KeepsakeRelationTarget, KeepsakeResolveError,
15 KeepsakeTargetError, QueryPresence, SubjectMapper, TenantScopedSubjectMapper,
16};
17
18#[derive(Clone, Debug)]
20pub struct KeepsakeResolver<S, M = TenantScopedSubjectMapper> {
21 source: S,
22 subject_mapper: M,
23 bindings: BTreeMap<FactId, FactBinding>,
24}
25
26impl<S> KeepsakeResolver<S, TenantScopedSubjectMapper> {
27 #[must_use]
29 pub const fn new(source: S) -> Self {
30 Self::with_subject_mapper(source, TenantScopedSubjectMapper)
31 }
32}
33
34impl<S, M> KeepsakeResolver<S, M> {
35 #[must_use]
37 pub const fn with_subject_mapper(source: S, subject_mapper: M) -> Self {
38 Self {
39 source,
40 subject_mapper,
41 bindings: BTreeMap::new(),
42 }
43 }
44
45 #[must_use]
47 pub fn map_subjects<Next>(self, subject_mapper: Next) -> KeepsakeResolver<S, Next> {
48 KeepsakeResolver {
49 source: self.source,
50 subject_mapper,
51 bindings: self.bindings,
52 }
53 }
54
55 #[must_use]
57 pub fn with_binding(mut self, binding: FactBinding) -> Self {
58 self.insert_binding(binding);
59 self
60 }
61
62 pub fn insert_binding(&mut self, binding: FactBinding) {
64 self.bindings.insert(binding.fact.clone(), binding);
65 }
66
67 pub fn with_relation_spec<F, R>(self) -> Result<Self, FactBindingError>
74 where
75 F: Fact,
76 R: RelationSpec,
77 {
78 self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
79 }
80
81 pub fn with_resolved_relation<F, R>(self) -> Result<Self, FactBindingError>
89 where
90 F: Fact,
91 R: RelationSpec,
92 {
93 self.with_relation_spec_query_presence::<F, R>(QueryPresence::Resolve)
94 }
95
96 pub fn with_deferred_relation<F, R>(self) -> Result<Self, FactBindingError>
104 where
105 F: Fact,
106 R: RelationSpec,
107 {
108 self.with_relation_spec_query_presence::<F, R>(QueryPresence::Defer)
109 }
110
111 pub fn with_relation_spec_query_presence<F, R>(
118 self,
119 query_presence: QueryPresence,
120 ) -> Result<Self, FactBindingError>
121 where
122 F: Fact,
123 R: RelationSpec,
124 {
125 Ok(
126 self.with_binding(FactBinding::for_relation_spec_with_query_presence::<F, R>(
127 query_presence,
128 )?),
129 )
130 }
131
132 pub fn with_relation_spec_on_subject<F, R>(
140 self,
141 subject_slot: gatekeep::SubjectSlot,
142 ) -> Result<Self, FactBindingError>
143 where
144 F: Fact,
145 R: RelationSpec,
146 {
147 Ok(
148 self.with_binding(FactBinding::for_relation_spec_on_subject::<F, R>(
149 subject_slot,
150 )?),
151 )
152 }
153
154 #[must_use]
156 pub const fn bindings(&self) -> &BTreeMap<FactId, FactBinding> {
157 &self.bindings
158 }
159
160 #[must_use]
162 pub const fn source(&self) -> &S {
163 &self.source
164 }
165
166 #[must_use]
168 pub const fn subject_mapper(&self) -> &M {
169 &self.subject_mapper
170 }
171}
172
173impl<S, M> KeepsakeResolver<S, M>
174where
175 M: SubjectMapper,
176{
177 pub fn target_for_binding(
185 &self,
186 binding: &FactBinding,
187 cx: &Context,
188 ) -> Result<KeepsakeRelationTarget, KeepsakeTargetError> {
189 let tenant_id = keepsake::TenantId::new(cx.tenant().as_str()).map_err(|source| {
190 KeepsakeTargetError::Tenant {
191 fact: binding.fact.clone(),
192 source,
193 }
194 })?;
195 let subject = if let Some(slot) = &binding.subject_slot {
196 let Some(subject) = cx.subjects().get(slot) else {
197 return Err(KeepsakeTargetError::MissingSubjectSlot {
198 fact: binding.fact.clone(),
199 slot: slot.clone(),
200 });
201 };
202 keepsake::SubjectRef::new(subject.kind(), subject.id()).map_err(|source| {
203 KeepsakeTargetError::Subject {
204 fact: binding.fact.clone(),
205 source,
206 }
207 })?
208 } else {
209 self.subject_mapper
210 .subject(cx)
211 .map_err(|source| KeepsakeTargetError::Subject {
212 fact: binding.fact.clone(),
213 source,
214 })?
215 };
216
217 Ok(KeepsakeRelationTarget {
218 tenant_id,
219 fact: binding.fact.clone(),
220 subject,
221 relation_id: binding.relation_id,
222 subject_slot: binding.subject_slot.clone(),
223 })
224 }
225
226 pub fn target_for_fact(
234 &self,
235 fact: &FactId,
236 cx: &Context,
237 ) -> Result<KeepsakeRelationTarget, KeepsakeTargetError> {
238 let binding = self
239 .bindings
240 .get(fact)
241 .ok_or_else(|| KeepsakeTargetError::MissingBinding { fact: fact.clone() })?;
242 self.target_for_binding(binding, cx)
243 }
244
245 pub fn targets_for_facts(
252 &self,
253 facts: &[FactId],
254 cx: &Context,
255 ) -> Result<Vec<KeepsakeRelationTarget>, KeepsakeTargetError> {
256 facts
257 .iter()
258 .map(|fact| self.target_for_fact(fact, cx))
259 .collect()
260 }
261}
262
263#[async_trait]
264impl<S, M> FactResolver for KeepsakeResolver<S, M>
265where
266 S: ActiveRelationSource,
267 M: SubjectMapper,
268{
269 type Error = KeepsakeResolveError<S::Error>;
270
271 async fn resolve_for_decision(
272 &self,
273 required: &[FactId],
274 cx: &Context,
275 clock: &dyn Clock,
276 ) -> Result<FactResolution<KnownFacts>, ResolveError<Self::Error>> {
277 let observed_at = clock.now_utc();
278 let bindings = self.bindings_for(required)?;
279 let active_relations = self
280 .active_relation_ids_by_subject(cx, &bindings, observed_at)
281 .await?;
282 let entries = bindings.into_iter().map(|binding| {
283 let presence = relation_presence(
284 &active_relations,
285 binding.subject_slot.as_ref(),
286 binding.relation_id,
287 );
288 (binding.fact.clone(), presence)
289 });
290 FactResolution::new(
291 KnownFacts::from_entries(entries).map_err(KeepsakeResolveError::Gatekeep)?,
292 Some(
293 active_relations
294 .metadata()
295 .map_err(KeepsakeResolveError::Provenance)?,
296 ),
297 observed_at,
298 )
299 .map_err(ResolveError::Resolution)
300 }
301}
302
303#[async_trait]
304impl<S, M> QueryFactResolver for KeepsakeResolver<S, M>
305where
306 S: ActiveRelationSource,
307 M: SubjectMapper,
308{
309 async fn resolve_for_query(
310 &self,
311 required: &[FactId],
312 cx: &Context,
313 clock: &dyn Clock,
314 ) -> Result<FactResolution<PartialFacts>, ResolveError<Self::Error>> {
315 let observed_at = clock.now_utc();
316 let bindings = self.bindings_for(required)?;
317 let needs_active_lookup = bindings
318 .iter()
319 .any(|binding| binding.query_presence == QueryPresence::Resolve);
320 let active_relations = if needs_active_lookup {
321 let resolved_bindings = bindings
322 .iter()
323 .copied()
324 .filter(|binding| binding.query_presence == QueryPresence::Resolve)
325 .collect::<Vec<_>>();
326 self.active_relation_ids_by_subject(cx, &resolved_bindings, observed_at)
327 .await?
328 } else {
329 EffectiveRelations::default()
330 };
331
332 let entries = bindings.into_iter().map(|binding| {
333 let presence = match binding.query_presence {
334 QueryPresence::Resolve => relation_presence(
335 &active_relations,
336 binding.subject_slot.as_ref(),
337 binding.relation_id,
338 ),
339 QueryPresence::Defer => Presence::Unknown,
340 };
341 (binding.fact.clone(), presence)
342 });
343 FactResolution::new(
344 PartialFacts::from_entries(entries),
345 Some(
346 active_relations
347 .metadata()
348 .map_err(KeepsakeResolveError::Provenance)?,
349 ),
350 observed_at,
351 )
352 .map_err(ResolveError::Resolution)
353 }
354}
355
356impl<S, M> KeepsakeResolver<S, M>
357where
358 S: ActiveRelationSource,
359 M: SubjectMapper,
360{
361 fn bindings_for<'binding>(
362 &'binding self,
363 required: &[FactId],
364 ) -> Result<Vec<&'binding FactBinding>, ResolveError<KeepsakeResolveError<S::Error>>> {
365 required
366 .iter()
367 .map(|fact| {
368 self.bindings
369 .get(fact)
370 .ok_or_else(|| ResolveError::MissingFact(fact.clone()))
371 })
372 .collect()
373 }
374
375 async fn active_relation_ids_by_subject(
376 &self,
377 cx: &Context,
378 bindings: &[&FactBinding],
379 at: time::OffsetDateTime,
380 ) -> Result<EffectiveRelations, ResolveError<KeepsakeResolveError<S::Error>>> {
381 let mut grouped = BTreeMap::<Option<gatekeep::SubjectSlot>, SubjectLookup>::new();
382 for binding in bindings {
383 let target = self
384 .target_for_binding(binding, cx)
385 .map_err(|error| match error {
386 KeepsakeTargetError::MissingBinding { fact } => ResolveError::MissingFact(fact),
387 KeepsakeTargetError::MissingSubjectSlot { fact, slot } => {
388 ResolveError::MissingSubject { fact, slot }
389 }
390 KeepsakeTargetError::Subject { source, .. }
391 | KeepsakeTargetError::Tenant { source, .. } => {
392 ResolveError::Backend(KeepsakeResolveError::from(source))
393 }
394 })?;
395 grouped
396 .entry(target.subject_slot)
397 .or_insert_with(|| SubjectLookup {
398 subject: target.subject,
399 relation_ids: BTreeSet::new(),
400 })
401 .relation_ids
402 .insert(target.relation_id);
403 }
404
405 let tenant_id = keepsake::TenantId::new(cx.tenant().as_str())
406 .map_err(|source| ResolveError::Backend(KeepsakeResolveError::from(source)))?;
407 let mut active = EffectiveRelations::default();
408 for (slot, lookup) in grouped {
409 let relation_ids = lookup.relation_ids.iter().copied().collect::<Vec<_>>();
410 let active_relations = self
411 .source
412 .active_relations_for_subject_by_ids(&tenant_id, &lookup.subject, &relation_ids)
413 .await
414 .map_err(KeepsakeResolveError::Source)?;
415
416 for assignment in lookup.effective::<S::Error>(&tenant_id, active_relations, at)? {
417 active.insert(slot.clone(), assignment.keepsake());
418 }
419 }
420
421 Ok(active)
422 }
423}
424
425struct SubjectLookup {
426 subject: keepsake::SubjectRef,
427 relation_ids: BTreeSet<RelationId>,
428}
429
430impl SubjectLookup {
431 fn effective<E>(
432 &self,
433 tenant: &keepsake::TenantId,
434 assignments: Vec<keepsake::ActiveRelation>,
435 at: time::OffsetDateTime,
436 ) -> Result<Vec<keepsake::ActiveRelation>, KeepsakeResolveError<E>> {
437 let mut effective = Vec::new();
438 for assignment in assignments {
439 let stored = assignment.keepsake();
440 if stored.tenant_id() != tenant
441 || stored.subject() != &self.subject
442 || !self.relation_ids.contains(&stored.relation_id())
443 {
444 return Err(KeepsakeResolveError::ScopeMismatch);
445 }
446
447 if effective_state(ObservationTime::Authoritative(at), &assignment, None)?
448 == LifecycleState::Applied
449 {
450 effective.push(assignment);
451 }
452 }
453 Ok(effective)
454 }
455}
456
457fn relation_presence(
458 active_relations: &EffectiveRelations,
459 subject_slot: Option<&gatekeep::SubjectSlot>,
460 relation_id: RelationId,
461) -> Presence {
462 if active_relations
463 .ids
464 .contains(&(subject_slot.cloned(), relation_id))
465 {
466 Presence::Present
467 } else {
468 Presence::Absent
469 }
470}
471
472#[derive(Default)]
473struct EffectiveRelations {
474 ids: BTreeSet<(Option<gatekeep::SubjectSlot>, RelationId)>,
475 expires_at: Option<time::OffsetDateTime>,
476}
477
478impl EffectiveRelations {
479 fn insert(&mut self, slot: Option<gatekeep::SubjectSlot>, stored: &keepsake::Keepsake) {
480 self.ids.insert((slot, stored.relation_id()));
481 if let Some(deadline) = stored.expires_at() {
482 self.expires_at = Some(
483 self.expires_at
484 .map_or(deadline, |current| current.min(deadline)),
485 );
486 }
487 }
488
489 fn metadata(&self) -> Result<FactResolutionMetadata, gatekeep::TenantBindingError> {
490 Ok(FactResolutionMetadata::new(
491 BindingProvenance::new("keepsake.effective-snapshot")?,
492 None,
493 self.expires_at,
494 ))
495 }
496}