use std::collections::{BTreeMap, BTreeSet};
use trellis_core::{
OutputFrame, OutputFrameKind, OutputFrameKindTrace, OutputFrameTrace, OutputKey, Revision,
ScopeId, TransactionId, TransactionResult,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OutputSnapshot<O> {
pub scope: ScopeId,
pub transaction_id: TransactionId,
pub revision: Revision,
pub state: Option<O>,
pub cleared: bool,
pub frame: OutputFrameTrace,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OutputLedgerError {
RevisionRegression {
context: OutputFrameTrace,
previous: Revision,
},
NotCleared {
key: OutputKey,
context: Option<OutputFrameTrace>,
},
FrameAfterClosedScope {
context: OutputFrameTrace,
},
ClosedScopeNotCleared {
scope: ScopeId,
outputs: Vec<OutputKey>,
contexts: Vec<OutputFrameTrace>,
},
StateMismatch {
key: OutputKey,
context: Option<OutputFrameTrace>,
},
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct OutputLedger<O> {
pub(crate) outputs: BTreeMap<OutputKey, OutputSnapshot<O>>,
pub(crate) closed_scopes: BTreeSet<ScopeId>,
pub(crate) frames: Vec<OutputFrameTrace>,
pub(crate) frame_records: Vec<OutputFrame<O>>,
pub(crate) errors: Vec<OutputLedgerError>,
}
impl<O: Clone + PartialEq> OutputLedger<O> {
pub fn new() -> Self {
Self {
outputs: BTreeMap::new(),
closed_scopes: BTreeSet::new(),
frames: Vec::new(),
frame_records: Vec::new(),
errors: Vec::new(),
}
}
pub fn close_scope(&mut self, scope: ScopeId) {
self.closed_scopes.insert(scope);
}
pub fn apply_result<C>(&mut self, result: &TransactionResult<C, O>) {
for frame in &result.output_frames {
self.apply_frame(frame);
}
}
pub fn apply_frame(&mut self, frame: &OutputFrame<O>) {
let trace = output_frame_trace(frame);
self.frames.push(trace.clone());
self.frame_records.push(frame.clone());
if self.closed_scopes.contains(&frame.scope)
&& !matches!(frame.kind, OutputFrameKind::Clear(_))
{
self.errors
.push(OutputLedgerError::FrameAfterClosedScope { context: trace });
return;
}
if let Some(previous) = self.outputs.get(&frame.output_key)
&& frame.revision < previous.revision
{
self.errors.push(OutputLedgerError::RevisionRegression {
context: trace.clone(),
previous: previous.revision,
});
}
let state = match &frame.kind {
OutputFrameKind::Baseline(value)
| OutputFrameKind::Delta(value)
| OutputFrameKind::Rebaseline(value, _) => Some(value.clone()),
OutputFrameKind::Clear(_) => None,
};
self.outputs.insert(
frame.output_key,
OutputSnapshot {
scope: frame.scope,
transaction_id: frame.transaction_id,
revision: frame.revision,
state,
cleared: matches!(frame.kind, OutputFrameKind::Clear(_)),
frame: trace,
},
);
}
pub fn snapshot(&self, key: OutputKey) -> Option<&OutputSnapshot<O>> {
self.outputs.get(&key)
}
pub fn errors(&self) -> &[OutputLedgerError] {
&self.errors
}
pub fn frame_trace(&self) -> &[OutputFrameTrace] {
&self.frames
}
pub fn frame_records(&self) -> &[OutputFrame<O>] {
&self.frame_records
}
pub fn assert_revision_monotonic(&self) -> Result<(), OutputLedgerError> {
self.errors
.iter()
.find(|error| matches!(error, OutputLedgerError::RevisionRegression { .. }))
.cloned()
.map_or(Ok(()), Err)
}
pub fn assert_no_frame_for_closed_scope_except_terminal(
&self,
) -> Result<(), OutputLedgerError> {
self.errors
.iter()
.find(|error| matches!(error, OutputLedgerError::FrameAfterClosedScope { .. }))
.cloned()
.map_or(Ok(()), Err)
}
pub fn assert_closed_scope_cleared(&self, scope: ScopeId) -> Result<(), OutputLedgerError> {
let uncleared = self
.outputs
.iter()
.filter(|(_, snapshot)| snapshot.scope == scope && !snapshot.cleared)
.map(|(key, _)| *key)
.collect::<Vec<_>>();
if uncleared.is_empty() {
Ok(())
} else {
let contexts = uncleared
.iter()
.filter_map(|key| self.outputs.get(key).map(|snapshot| snapshot.frame.clone()))
.collect();
Err(OutputLedgerError::ClosedScopeNotCleared {
scope,
outputs: uncleared,
contexts,
})
}
}
pub fn assert_cleared(&self, key: OutputKey) -> Result<(), OutputLedgerError> {
if self
.outputs
.get(&key)
.is_some_and(|snapshot| snapshot.cleared)
{
Ok(())
} else {
Err(OutputLedgerError::NotCleared {
key,
context: self
.outputs
.get(&key)
.map(|snapshot| snapshot.frame.clone()),
})
}
}
pub fn assert_current_equals(
&self,
key: OutputKey,
expected: &O,
) -> Result<(), OutputLedgerError> {
if self
.outputs
.get(&key)
.and_then(|snapshot| snapshot.state.as_ref())
== Some(expected)
{
Ok(())
} else {
Err(OutputLedgerError::StateMismatch {
key,
context: self
.outputs
.get(&key)
.map(|snapshot| snapshot.frame.clone()),
})
}
}
pub fn assert_delta_sequence_matches_rebaseline(
&self,
key: OutputKey,
rebaseline: &O,
) -> Result<(), OutputLedgerError> {
self.assert_current_equals(key, rebaseline)
}
pub fn assert_consumer_needs_no_hidden_graph_state(&self) -> Result<(), OutputLedgerError> {
self.errors.first().cloned().map_or(Ok(()), Err)
}
}
fn output_frame_trace<O>(frame: &OutputFrame<O>) -> OutputFrameTrace {
OutputFrameTrace {
output_key: frame.output_key,
scope: frame.scope,
transaction_id: frame.transaction_id,
revision: frame.revision,
kind: output_frame_kind(&frame.kind),
}
}
fn output_frame_kind<O>(kind: &OutputFrameKind<O>) -> OutputFrameKindTrace {
match kind {
OutputFrameKind::Baseline(_) => OutputFrameKindTrace::Baseline,
OutputFrameKind::Delta(_) => OutputFrameKindTrace::Delta,
OutputFrameKind::Clear(reason) => OutputFrameKindTrace::Clear(*reason),
OutputFrameKind::Rebaseline(_, reason) => OutputFrameKindTrace::Rebaseline(*reason),
}
}