axioval_engine/
session.rs1use std::{
2 any::Any,
3 collections::{BTreeMap, BTreeSet},
4 sync::Arc,
5};
6
7use axioval_ir::{Project, SourceId};
8use thiserror::Error;
9
10use crate::{ServiceRegistry, ServiceRegistryError};
11
12pub trait SnapshotBoundService: Any + Send + Sync {
18 fn source_snapshots(&self) -> &[SourceSnapshot];
20}
21
22#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct SourceSnapshot {
25 source: SourceId,
26 revision: Arc<str>,
27 fingerprint: Arc<str>,
28 schema: Option<Arc<str>>,
29}
30
31impl SourceSnapshot {
32 pub fn try_new(
34 source: SourceId,
35 revision: impl Into<Arc<str>>,
36 fingerprint: impl Into<Arc<str>>,
37 ) -> Result<Self, EvidenceSessionError> {
38 let revision = revision.into();
39 let fingerprint = fingerprint.into();
40 if revision.trim().is_empty() || fingerprint.trim().is_empty() {
41 return Err(EvidenceSessionError::InvalidSnapshotIdentity);
42 }
43 Ok(Self {
44 source,
45 revision,
46 fingerprint,
47 schema: None,
48 })
49 }
50 pub fn with_schema(
52 mut self,
53 schema: impl Into<Arc<str>>,
54 ) -> Result<Self, EvidenceSessionError> {
55 let schema = schema.into();
56 if schema.trim().is_empty() {
57 return Err(EvidenceSessionError::InvalidSnapshotIdentity);
58 }
59 self.schema = Some(schema);
60 Ok(self)
61 }
62 pub fn source(&self) -> &SourceId {
64 &self.source
65 }
66 pub fn revision(&self) -> &str {
68 &self.revision
69 }
70 pub fn fingerprint(&self) -> &str {
72 &self.fingerprint
73 }
74 pub fn schema(&self) -> Option<&str> {
76 self.schema.as_deref()
77 }
78}
79
80#[derive(Clone, Debug, Error, Eq, PartialEq)]
82pub enum EvidenceSessionError {
83 #[error("source snapshot revision and fingerprint must be non-empty")]
85 InvalidSnapshotIdentity,
86 #[error("duplicate source snapshot: {0}")]
88 DuplicateSource(SourceId),
89 #[error("project source has no snapshot declaration: {0}")]
91 MissingSource(SourceId),
92 #[error("snapshot source is not present in the project: {0}")]
94 UnexpectedSource(SourceId),
95 #[error("evidence service has no source snapshot binding")]
97 UnboundService,
98 #[error("evidence service has duplicate source snapshot binding: {0}")]
100 DuplicateServiceSource(SourceId),
101 #[error("evidence service snapshot does not match the session: {0}")]
103 ServiceSnapshotMismatch(SourceId),
104 #[error(transparent)]
106 ServiceRegistry(#[from] ServiceRegistryError),
107}
108
109pub struct EvidenceSession {
116 project: Arc<Project>,
117 snapshots: BTreeMap<SourceId, SourceSnapshot>,
118 services: ServiceRegistry,
119}
120
121impl EvidenceSession {
122 pub fn try_new(
124 project: Project,
125 snapshots: impl IntoIterator<Item = SourceSnapshot>,
126 ) -> Result<Self, EvidenceSessionError> {
127 let project_sources = project
128 .objects()
129 .map(|object| object.id.source.clone())
130 .collect::<BTreeSet<_>>();
131 let mut indexed = BTreeMap::new();
132 for snapshot in snapshots {
133 let source = snapshot.source.clone();
134 if indexed.insert(source.clone(), snapshot).is_some() {
135 return Err(EvidenceSessionError::DuplicateSource(source));
136 }
137 }
138 if let Some(source) = project_sources
139 .difference(&indexed.keys().cloned().collect())
140 .next()
141 {
142 return Err(EvidenceSessionError::MissingSource(source.clone()));
143 }
144 if let Some(source) = indexed
145 .keys()
146 .find(|source| !project_sources.contains(*source))
147 {
148 return Err(EvidenceSessionError::UnexpectedSource((*source).clone()));
149 }
150 Ok(Self {
151 project: Arc::new(project),
152 snapshots: indexed,
153 services: ServiceRegistry::new(),
154 })
155 }
156
157 pub fn with_service<T: SnapshotBoundService>(
159 mut self,
160 service: T,
161 ) -> Result<Self, EvidenceSessionError> {
162 let bindings = service.source_snapshots();
163 if bindings.is_empty() {
164 return Err(EvidenceSessionError::UnboundService);
165 }
166 let mut sources = BTreeSet::new();
167 for binding in bindings {
168 if !sources.insert(binding.source.clone()) {
169 return Err(EvidenceSessionError::DuplicateServiceSource(
170 binding.source.clone(),
171 ));
172 }
173 if self.snapshots.get(&binding.source) != Some(binding) {
174 return Err(EvidenceSessionError::ServiceSnapshotMismatch(
175 binding.source.clone(),
176 ));
177 }
178 }
179 self.services.register(service)?;
180 Ok(self)
181 }
182
183 #[must_use]
185 pub fn project(&self) -> &Project {
186 &self.project
187 }
188
189 pub fn snapshots(&self) -> impl ExactSizeIterator<Item = &SourceSnapshot> {
191 self.snapshots.values()
192 }
193
194 #[must_use]
196 pub fn snapshot(&self, source: &SourceId) -> Option<&SourceSnapshot> {
197 self.snapshots.get(source)
198 }
199
200 #[must_use]
202 pub fn services(&self) -> &ServiceRegistry {
203 &self.services
204 }
205
206 #[must_use]
208 pub fn service<T: Any + Send + Sync>(&self) -> Option<&T> {
209 self.services.get::<T>()
210 }
211}