use std::{
any::Any,
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
use axioval_ir::{Project, SourceId};
use thiserror::Error;
use crate::{ServiceRegistry, ServiceRegistryError};
pub trait SnapshotBoundService: Any + Send + Sync {
fn source_snapshots(&self) -> &[SourceSnapshot];
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceSnapshot {
source: SourceId,
revision: Arc<str>,
fingerprint: Arc<str>,
schema: Option<Arc<str>>,
}
impl SourceSnapshot {
pub fn try_new(
source: SourceId,
revision: impl Into<Arc<str>>,
fingerprint: impl Into<Arc<str>>,
) -> Result<Self, EvidenceSessionError> {
let revision = revision.into();
let fingerprint = fingerprint.into();
if revision.trim().is_empty() || fingerprint.trim().is_empty() {
return Err(EvidenceSessionError::InvalidSnapshotIdentity);
}
Ok(Self {
source,
revision,
fingerprint,
schema: None,
})
}
pub fn with_schema(
mut self,
schema: impl Into<Arc<str>>,
) -> Result<Self, EvidenceSessionError> {
let schema = schema.into();
if schema.trim().is_empty() {
return Err(EvidenceSessionError::InvalidSnapshotIdentity);
}
self.schema = Some(schema);
Ok(self)
}
pub fn source(&self) -> &SourceId {
&self.source
}
pub fn revision(&self) -> &str {
&self.revision
}
pub fn fingerprint(&self) -> &str {
&self.fingerprint
}
pub fn schema(&self) -> Option<&str> {
self.schema.as_deref()
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum EvidenceSessionError {
#[error("source snapshot revision and fingerprint must be non-empty")]
InvalidSnapshotIdentity,
#[error("duplicate source snapshot: {0}")]
DuplicateSource(SourceId),
#[error("project source has no snapshot declaration: {0}")]
MissingSource(SourceId),
#[error("snapshot source is not present in the project: {0}")]
UnexpectedSource(SourceId),
#[error("evidence service has no source snapshot binding")]
UnboundService,
#[error("evidence service has duplicate source snapshot binding: {0}")]
DuplicateServiceSource(SourceId),
#[error("evidence service snapshot does not match the session: {0}")]
ServiceSnapshotMismatch(SourceId),
#[error(transparent)]
ServiceRegistry(#[from] ServiceRegistryError),
}
pub struct EvidenceSession {
project: Arc<Project>,
snapshots: BTreeMap<SourceId, SourceSnapshot>,
services: ServiceRegistry,
}
impl EvidenceSession {
pub fn try_new(
project: Project,
snapshots: impl IntoIterator<Item = SourceSnapshot>,
) -> Result<Self, EvidenceSessionError> {
let project_sources = project
.objects()
.map(|object| object.id.source.clone())
.collect::<BTreeSet<_>>();
let mut indexed = BTreeMap::new();
for snapshot in snapshots {
let source = snapshot.source.clone();
if indexed.insert(source.clone(), snapshot).is_some() {
return Err(EvidenceSessionError::DuplicateSource(source));
}
}
if let Some(source) = project_sources
.difference(&indexed.keys().cloned().collect())
.next()
{
return Err(EvidenceSessionError::MissingSource(source.clone()));
}
if let Some(source) = indexed
.keys()
.find(|source| !project_sources.contains(*source))
{
return Err(EvidenceSessionError::UnexpectedSource((*source).clone()));
}
Ok(Self {
project: Arc::new(project),
snapshots: indexed,
services: ServiceRegistry::new(),
})
}
pub fn with_service<T: SnapshotBoundService>(
mut self,
service: T,
) -> Result<Self, EvidenceSessionError> {
let bindings = service.source_snapshots();
if bindings.is_empty() {
return Err(EvidenceSessionError::UnboundService);
}
let mut sources = BTreeSet::new();
for binding in bindings {
if !sources.insert(binding.source.clone()) {
return Err(EvidenceSessionError::DuplicateServiceSource(
binding.source.clone(),
));
}
if self.snapshots.get(&binding.source) != Some(binding) {
return Err(EvidenceSessionError::ServiceSnapshotMismatch(
binding.source.clone(),
));
}
}
self.services.register(service)?;
Ok(self)
}
#[must_use]
pub fn project(&self) -> &Project {
&self.project
}
pub fn snapshots(&self) -> impl ExactSizeIterator<Item = &SourceSnapshot> {
self.snapshots.values()
}
#[must_use]
pub fn snapshot(&self, source: &SourceId) -> Option<&SourceSnapshot> {
self.snapshots.get(source)
}
#[must_use]
pub fn services(&self) -> &ServiceRegistry {
&self.services
}
#[must_use]
pub fn service<T: Any + Send + Sync>(&self) -> Option<&T> {
self.services.get::<T>()
}
}