1use sim_expr_tree_calc::{
4 CalcOutcome, CalcReceipt, CalcStatus, EncodedFace, FaceContent, FaceDimension, FaceIssue,
5};
6use sim_kernel::{Expr, Symbol};
7use sim_value::build;
8
9pub const SNAPSHOT_TYPE: &str = "expression-tree-snapshot";
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum FaceState {
15 Complete,
17 Truncated {
19 dimension: String,
21 limit: usize,
23 observed: usize,
25 },
26 Unsupported {
28 reason: String,
30 },
31 CodecFailure {
33 message: String,
35 },
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct FaceSnapshot {
41 content: Option<Expr>,
42 codec: Option<String>,
43 state: FaceState,
44}
45
46impl FaceSnapshot {
47 pub fn text(content: impl Into<String>, codec: impl Into<String>) -> Self {
49 Self {
50 content: Some(Expr::String(content.into())),
51 codec: Some(codec.into()),
52 state: FaceState::Complete,
53 }
54 }
55
56 pub fn bytes(content: Vec<u8>, codec: impl Into<String>) -> Self {
58 Self {
59 content: Some(Expr::Bytes(content)),
60 codec: Some(codec.into()),
61 state: FaceState::Complete,
62 }
63 }
64
65 pub fn truncated(dimension: impl Into<String>, limit: usize, observed: usize) -> Self {
67 Self {
68 content: None,
69 codec: None,
70 state: FaceState::Truncated {
71 dimension: dimension.into(),
72 limit,
73 observed,
74 },
75 }
76 }
77
78 pub fn unsupported(reason: impl Into<String>) -> Self {
80 Self {
81 content: None,
82 codec: None,
83 state: FaceState::Unsupported {
84 reason: reason.into(),
85 },
86 }
87 }
88
89 pub fn codec_failure(message: impl Into<String>) -> Self {
91 Self {
92 content: None,
93 codec: None,
94 state: FaceState::CodecFailure {
95 message: message.into(),
96 },
97 }
98 }
99
100 pub fn from_encoded(face: &EncodedFace) -> Self {
102 let content = face.content().map(|content| match content {
103 FaceContent::Text(text) => Expr::String(text.clone()),
104 FaceContent::Bytes(bytes) => Expr::Bytes(bytes.clone()),
105 });
106 let metadata = face.metadata();
107 let state = match metadata.issue() {
108 FaceIssue::Complete => FaceState::Complete,
109 FaceIssue::Truncated {
110 dimension,
111 limit,
112 observed,
113 } => FaceState::Truncated {
114 dimension: face_dimension(*dimension).to_owned(),
115 limit: *limit,
116 observed: *observed,
117 },
118 FaceIssue::Unsupported { reason } => FaceState::Unsupported {
119 reason: reason.clone(),
120 },
121 FaceIssue::CodecFailure { message } => FaceState::CodecFailure {
122 message: message.clone(),
123 },
124 };
125 Self {
126 content,
127 codec: metadata.codec().map(str::to_owned),
128 state,
129 }
130 }
131
132 pub(crate) fn to_expr(&self) -> Expr {
133 let (state, detail) = match &self.state {
134 FaceState::Complete => ("complete", Vec::new()),
135 FaceState::Truncated {
136 dimension,
137 limit,
138 observed,
139 } => (
140 "truncated",
141 vec![
142 ("dimension", build::sym(dimension)),
143 ("limit", build::uint(*limit as u64)),
144 ("observed", build::uint(*observed as u64)),
145 ],
146 ),
147 FaceState::Unsupported { reason } => {
148 ("unsupported", vec![("reason", build::text(reason))])
149 }
150 FaceState::CodecFailure { message } => {
151 ("codec-failure", vec![("message", build::text(message))])
152 }
153 };
154 let mut fields = vec![
155 ("state", build::sym(state)),
156 ("content", self.content.clone().unwrap_or(Expr::Nil)),
157 (
158 "codec",
159 self.codec.as_ref().map(build::text).unwrap_or(Expr::Nil),
160 ),
161 ];
162 fields.extend(detail);
163 build::map(fields)
164 }
165}
166
167fn face_dimension(dimension: FaceDimension) -> &'static str {
168 match dimension {
169 FaceDimension::Bytes => "bytes",
170 FaceDimension::Depth => "depth",
171 FaceDimension::Items => "items",
172 }
173}
174
175#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub enum Freshness {
178 NeverCalculated,
180 Fresh,
182 MaybeStale,
184 Pending,
186 Failed,
188 Frozen,
190 Blocked,
192}
193
194impl Freshness {
195 pub(crate) const fn token(self) -> &'static str {
196 match self {
197 Self::NeverCalculated => "never-calculated",
198 Self::Fresh => "fresh",
199 Self::MaybeStale => "maybe-stale",
200 Self::Pending => "pending",
201 Self::Failed => "failed",
202 Self::Frozen => "frozen",
203 Self::Blocked => "blocked",
204 }
205 }
206}
207
208impl From<CalcStatus> for Freshness {
209 fn from(status: CalcStatus) -> Self {
210 match status {
211 CalcStatus::NeverCalculated => Self::NeverCalculated,
212 CalcStatus::Fresh => Self::Fresh,
213 CalcStatus::MaybeStale => Self::MaybeStale,
214 CalcStatus::Pending => Self::Pending,
215 CalcStatus::Failed => Self::Failed,
216 CalcStatus::Frozen => Self::Frozen,
217 CalcStatus::Blocked => Self::Blocked,
218 }
219 }
220}
221
222#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
224pub struct TimestampSummary {
225 pub source_changed_ms: Option<u64>,
227 pub result_checked_ms: Option<u64>,
229}
230
231#[derive(Clone, Debug, Eq, PartialEq)]
233pub struct ReceiptSummary {
234 pub request_id: u64,
236 pub outcome: String,
238 pub dependencies: usize,
240 pub omitted_dependencies: usize,
242 pub started_tick: u64,
244 pub finished_tick: u64,
246}
247
248impl From<&CalcReceipt> for ReceiptSummary {
249 fn from(receipt: &CalcReceipt) -> Self {
250 Self {
251 request_id: receipt.request_id.get(),
252 outcome: outcome_token(&receipt.outcome).to_owned(),
253 dependencies: receipt.dependencies.len(),
254 omitted_dependencies: receipt.omitted_dependencies,
255 started_tick: receipt.started_tick,
256 finished_tick: receipt.finished_tick,
257 }
258 }
259}
260
261fn outcome_token(outcome: &CalcOutcome) -> &'static str {
262 match outcome {
263 CalcOutcome::Succeeded => "succeeded",
264 CalcOutcome::Failed { .. } => "failed",
265 CalcOutcome::Blocked { .. } => "blocked",
266 CalcOutcome::Cancelled => "cancelled",
267 CalcOutcome::BudgetExhausted { .. } => "budget-exhausted",
268 }
269}
270
271#[derive(Clone, Debug, Eq, PartialEq)]
273pub enum ChildPage {
274 NotFetched,
276 Complete(Vec<NodeSnapshot>),
278 Truncated {
280 nodes: Vec<NodeSnapshot>,
282 continuation: String,
284 remaining: Option<usize>,
286 },
287}
288
289#[derive(Clone, Debug, Eq, PartialEq)]
291pub struct NodeDetail {
292 pub source: FaceSnapshot,
294 pub result: FaceSnapshot,
296 pub freshness: Freshness,
298 pub source_revision: u64,
300 pub result_revision: Option<u64>,
302 pub timestamps: TimestampSummary,
304 pub policy_badges: Vec<String>,
306 pub receipt: Option<ReceiptSummary>,
308}
309
310#[derive(Clone, Debug, Eq, PartialEq)]
312pub struct NodeSnapshot {
313 path: String,
314 name: String,
315 revision: u64,
316 body: NodeBody,
317}
318
319#[derive(Clone, Debug, Eq, PartialEq)]
320enum NodeBody {
321 Directory(ChildPage),
322 Cell(Option<Box<NodeDetail>>),
323}
324
325impl NodeSnapshot {
326 pub fn collapsed_dir(path: impl Into<String>, name: impl Into<String>, revision: u64) -> Self {
328 Self {
329 path: path.into(),
330 name: name.into(),
331 revision,
332 body: NodeBody::Directory(ChildPage::NotFetched),
333 }
334 }
335
336 pub fn expanded_dir(
341 path: impl Into<String>,
342 name: impl Into<String>,
343 revision: u64,
344 children: ChildPage,
345 ) -> Self {
346 Self {
347 path: path.into(),
348 name: name.into(),
349 revision,
350 body: NodeBody::Directory(children),
351 }
352 }
353
354 pub fn collapsed_cell(path: impl Into<String>, name: impl Into<String>, revision: u64) -> Self {
356 Self {
357 path: path.into(),
358 name: name.into(),
359 revision,
360 body: NodeBody::Cell(None),
361 }
362 }
363
364 pub fn expanded_cell(
366 path: impl Into<String>,
367 name: impl Into<String>,
368 revision: u64,
369 detail: NodeDetail,
370 ) -> Self {
371 Self {
372 path: path.into(),
373 name: name.into(),
374 revision,
375 body: NodeBody::Cell(Some(Box::new(detail))),
376 }
377 }
378
379 pub(crate) fn to_expr(&self) -> Expr {
380 let (node_type, open, body) = match &self.body {
381 NodeBody::Directory(children) => (
382 "directory",
383 !matches!(children, ChildPage::NotFetched),
384 children_expr(children),
385 ),
386 NodeBody::Cell(detail) => (
387 "cell",
388 detail.is_some(),
389 detail
390 .as_ref()
391 .map(|detail| detail_expr(detail))
392 .unwrap_or(Expr::Nil),
393 ),
394 };
395 build::map(vec![
396 ("node-type", build::sym(node_type)),
397 ("path", build::text(&self.path)),
398 ("name", build::text(&self.name)),
399 ("revision", build::uint(self.revision)),
400 ("open", Expr::Bool(open)),
401 ("body", body),
402 ])
403 }
404}
405
406fn children_expr(children: &ChildPage) -> Expr {
407 match children {
408 ChildPage::NotFetched => Expr::Nil,
409 ChildPage::Complete(nodes) => build::map(vec![
410 ("page-state", build::sym("complete")),
411 (
412 "nodes",
413 build::list(nodes.iter().map(NodeSnapshot::to_expr).collect()),
414 ),
415 ]),
416 ChildPage::Truncated {
417 nodes,
418 continuation,
419 remaining,
420 } => build::map(vec![
421 ("page-state", build::sym("truncated")),
422 (
423 "nodes",
424 build::list(nodes.iter().map(NodeSnapshot::to_expr).collect()),
425 ),
426 ("continuation", build::text(continuation)),
427 (
428 "remaining",
429 remaining
430 .map(|value| build::uint(value as u64))
431 .unwrap_or(Expr::Nil),
432 ),
433 ]),
434 }
435}
436
437fn detail_expr(detail: &NodeDetail) -> Expr {
438 build::map(vec![
439 ("source", detail.source.to_expr()),
440 ("result", detail.result.to_expr()),
441 ("freshness", build::sym(detail.freshness.token())),
442 ("source-revision", build::uint(detail.source_revision)),
443 (
444 "result-revision",
445 detail.result_revision.map(build::uint).unwrap_or(Expr::Nil),
446 ),
447 (
448 "source-changed-ms",
449 detail
450 .timestamps
451 .source_changed_ms
452 .map(build::uint)
453 .unwrap_or(Expr::Nil),
454 ),
455 (
456 "result-checked-ms",
457 detail
458 .timestamps
459 .result_checked_ms
460 .map(build::uint)
461 .unwrap_or(Expr::Nil),
462 ),
463 (
464 "policy-badges",
465 build::list(detail.policy_badges.iter().map(build::text).collect()),
466 ),
467 (
468 "receipt",
469 detail
470 .receipt
471 .as_ref()
472 .map(receipt_expr)
473 .unwrap_or(Expr::Nil),
474 ),
475 ])
476}
477
478fn receipt_expr(receipt: &ReceiptSummary) -> Expr {
479 build::map(vec![
480 ("request-id", build::uint(receipt.request_id)),
481 ("outcome", build::sym(&receipt.outcome)),
482 ("dependencies", build::uint(receipt.dependencies as u64)),
483 (
484 "omitted-dependencies",
485 build::uint(receipt.omitted_dependencies as u64),
486 ),
487 ("started-tick", build::uint(receipt.started_tick)),
488 ("finished-tick", build::uint(receipt.finished_tick)),
489 ])
490}
491
492#[derive(Clone, Debug, Eq, PartialEq)]
494pub struct ExpressionTreeSnapshot {
495 tree: Expr,
496 revision: u64,
497 nodes: Vec<NodeSnapshot>,
498}
499
500impl ExpressionTreeSnapshot {
501 pub fn new(tree: Expr, revision: u64, nodes: Vec<NodeSnapshot>) -> Self {
503 Self {
504 tree,
505 revision,
506 nodes,
507 }
508 }
509
510 pub fn to_expr(&self) -> Expr {
512 build::map(vec![
513 (
514 "type",
515 Expr::Symbol(Symbol::qualified("expr-tree-view", SNAPSHOT_TYPE)),
516 ),
517 ("tree", self.tree.clone()),
518 ("revision", build::uint(self.revision)),
519 (
520 "nodes",
521 build::list(self.nodes.iter().map(NodeSnapshot::to_expr).collect()),
522 ),
523 ])
524 }
525}