1use std::collections::{BTreeMap, BTreeSet};
2
3use trellis_core::{
4 OutputFrame, OutputFrameKind, OutputFrameKindTrace, OutputFrameTrace, OutputKey, Revision,
5 ScopeId, TransactionId, TransactionResult,
6};
7
8#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct OutputSnapshot<O> {
11 pub scope: ScopeId,
13 pub transaction_id: TransactionId,
15 pub revision: Revision,
17 pub state: Option<O>,
19 pub cleared: bool,
21 pub frame: OutputFrameTrace,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
27pub enum OutputLedgerError {
28 RevisionRegression {
30 context: OutputFrameTrace,
32 previous: Revision,
34 },
35 NotCleared {
37 key: OutputKey,
39 context: Option<OutputFrameTrace>,
41 },
42 FrameAfterClosedScope {
44 context: OutputFrameTrace,
46 },
47 ClosedScopeNotCleared {
49 scope: ScopeId,
51 outputs: Vec<OutputKey>,
53 contexts: Vec<OutputFrameTrace>,
55 },
56 StateMismatch {
58 key: OutputKey,
60 context: Option<OutputFrameTrace>,
62 },
63}
64
65#[derive(Clone, Debug, Default, Eq, PartialEq)]
67pub struct OutputLedger<O> {
68 pub(crate) outputs: BTreeMap<OutputKey, OutputSnapshot<O>>,
69 pub(crate) closed_scopes: BTreeSet<ScopeId>,
70 pub(crate) frames: Vec<OutputFrameTrace>,
71 pub(crate) frame_records: Vec<OutputFrame<O>>,
72 pub(crate) errors: Vec<OutputLedgerError>,
73}
74
75impl<O: Clone + PartialEq> OutputLedger<O> {
76 pub fn new() -> Self {
78 Self {
79 outputs: BTreeMap::new(),
80 closed_scopes: BTreeSet::new(),
81 frames: Vec::new(),
82 frame_records: Vec::new(),
83 errors: Vec::new(),
84 }
85 }
86
87 pub fn close_scope(&mut self, scope: ScopeId) {
89 self.closed_scopes.insert(scope);
90 }
91
92 pub fn apply_result<C>(&mut self, result: &TransactionResult<C, O>) {
94 for frame in &result.output_frames {
95 self.apply_frame(frame);
96 }
97 }
98
99 pub fn apply_frame(&mut self, frame: &OutputFrame<O>) {
101 let trace = output_frame_trace(frame);
102 self.frames.push(trace.clone());
103 self.frame_records.push(frame.clone());
104 if self.closed_scopes.contains(&frame.scope)
105 && !matches!(frame.kind, OutputFrameKind::Clear(_))
106 {
107 self.errors
108 .push(OutputLedgerError::FrameAfterClosedScope { context: trace });
109 return;
110 }
111
112 if let Some(previous) = self.outputs.get(&frame.output_key)
113 && frame.revision < previous.revision
114 {
115 self.errors.push(OutputLedgerError::RevisionRegression {
116 context: trace.clone(),
117 previous: previous.revision,
118 });
119 }
120
121 let state = match &frame.kind {
122 OutputFrameKind::Baseline(value)
123 | OutputFrameKind::Delta(value)
124 | OutputFrameKind::Rebaseline(value, _) => Some(value.clone()),
125 OutputFrameKind::Clear(_) => None,
126 };
127 self.outputs.insert(
128 frame.output_key,
129 OutputSnapshot {
130 scope: frame.scope,
131 transaction_id: frame.transaction_id,
132 revision: frame.revision,
133 state,
134 cleared: matches!(frame.kind, OutputFrameKind::Clear(_)),
135 frame: trace,
136 },
137 );
138 }
139
140 pub fn snapshot(&self, key: OutputKey) -> Option<&OutputSnapshot<O>> {
142 self.outputs.get(&key)
143 }
144
145 pub fn errors(&self) -> &[OutputLedgerError] {
147 &self.errors
148 }
149
150 pub fn frame_trace(&self) -> &[OutputFrameTrace] {
152 &self.frames
153 }
154
155 pub fn frame_records(&self) -> &[OutputFrame<O>] {
157 &self.frame_records
158 }
159
160 pub fn assert_revision_monotonic(&self) -> Result<(), OutputLedgerError> {
162 self.errors
163 .iter()
164 .find(|error| matches!(error, OutputLedgerError::RevisionRegression { .. }))
165 .cloned()
166 .map_or(Ok(()), Err)
167 }
168
169 pub fn assert_no_frame_for_closed_scope_except_terminal(
171 &self,
172 ) -> Result<(), OutputLedgerError> {
173 self.errors
174 .iter()
175 .find(|error| matches!(error, OutputLedgerError::FrameAfterClosedScope { .. }))
176 .cloned()
177 .map_or(Ok(()), Err)
178 }
179
180 pub fn assert_closed_scope_cleared(&self, scope: ScopeId) -> Result<(), OutputLedgerError> {
182 let uncleared = self
183 .outputs
184 .iter()
185 .filter(|(_, snapshot)| snapshot.scope == scope && !snapshot.cleared)
186 .map(|(key, _)| *key)
187 .collect::<Vec<_>>();
188 if uncleared.is_empty() {
189 Ok(())
190 } else {
191 let contexts = uncleared
192 .iter()
193 .filter_map(|key| self.outputs.get(key).map(|snapshot| snapshot.frame.clone()))
194 .collect();
195 Err(OutputLedgerError::ClosedScopeNotCleared {
196 scope,
197 outputs: uncleared,
198 contexts,
199 })
200 }
201 }
202
203 pub fn assert_cleared(&self, key: OutputKey) -> Result<(), OutputLedgerError> {
205 if self
206 .outputs
207 .get(&key)
208 .is_some_and(|snapshot| snapshot.cleared)
209 {
210 Ok(())
211 } else {
212 Err(OutputLedgerError::NotCleared {
213 key,
214 context: self
215 .outputs
216 .get(&key)
217 .map(|snapshot| snapshot.frame.clone()),
218 })
219 }
220 }
221
222 pub fn assert_current_equals(
224 &self,
225 key: OutputKey,
226 expected: &O,
227 ) -> Result<(), OutputLedgerError> {
228 if self
229 .outputs
230 .get(&key)
231 .and_then(|snapshot| snapshot.state.as_ref())
232 == Some(expected)
233 {
234 Ok(())
235 } else {
236 Err(OutputLedgerError::StateMismatch {
237 key,
238 context: self
239 .outputs
240 .get(&key)
241 .map(|snapshot| snapshot.frame.clone()),
242 })
243 }
244 }
245
246 pub fn assert_delta_sequence_matches_rebaseline(
248 &self,
249 key: OutputKey,
250 rebaseline: &O,
251 ) -> Result<(), OutputLedgerError> {
252 self.assert_current_equals(key, rebaseline)
253 }
254
255 pub fn assert_consumer_needs_no_hidden_graph_state(&self) -> Result<(), OutputLedgerError> {
257 self.errors.first().cloned().map_or(Ok(()), Err)
258 }
259}
260
261fn output_frame_trace<O>(frame: &OutputFrame<O>) -> OutputFrameTrace {
262 OutputFrameTrace {
263 output_key: frame.output_key,
264 scope: frame.scope,
265 transaction_id: frame.transaction_id,
266 revision: frame.revision,
267 kind: output_frame_kind(&frame.kind),
268 }
269}
270
271fn output_frame_kind<O>(kind: &OutputFrameKind<O>) -> OutputFrameKindTrace {
272 match kind {
273 OutputFrameKind::Baseline(_) => OutputFrameKindTrace::Baseline,
274 OutputFrameKind::Delta(_) => OutputFrameKindTrace::Delta,
275 OutputFrameKind::Clear(reason) => OutputFrameKindTrace::Clear(*reason),
276 OutputFrameKind::Rebaseline(_, reason) => OutputFrameKindTrace::Rebaseline(*reason),
277 }
278}