Skip to main content

sim_lib_view_expr_tree/
model.rs

1//! Ordinary expression value consumed by the expression-tree surface.
2
3use sim_expr_tree_calc::{
4    CalcOutcome, CalcReceipt, CalcStatus, EncodedFace, FaceContent, FaceDimension, FaceIssue,
5};
6use sim_kernel::{Expr, Symbol};
7use sim_value::build;
8
9/// The snapshot's open data tag.
10pub const SNAPSHOT_TYPE: &str = "expression-tree-snapshot";
11
12/// A source or result face outcome safe to expose to a surface.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum FaceState {
15    /// The bounded face is complete.
16    Complete,
17    /// A bound stopped projection.
18    Truncated {
19        /// Stable bound name.
20        dimension: String,
21        /// Configured maximum.
22        limit: usize,
23        /// Observed size.
24        observed: usize,
25    },
26    /// The value has no safe presentation projection.
27    Unsupported {
28        /// Bounded explanation.
29        reason: String,
30    },
31    /// The selected codec failed closed.
32    CodecFailure {
33        /// Bounded codec diagnostic.
34        message: String,
35    },
36}
37
38/// One already bounded source or result face.
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct FaceSnapshot {
41    content: Option<Expr>,
42    codec: Option<String>,
43    state: FaceState,
44}
45
46impl FaceSnapshot {
47    /// Creates a complete text face.
48    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    /// Creates a complete opaque byte face. Rendering exposes only its size.
57    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    /// Creates an explicitly truncated face.
66    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    /// Creates a face for a value with no safe projection.
79    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    /// Creates a face whose selected codec failed closed.
90    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    /// Copies the already bounded result of the expression-tree codec policy.
101    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/// Every non-evaluating expression-tree freshness state.
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub enum Freshness {
178    /// No calculation has committed.
179    NeverCalculated,
180    /// The current result is verified.
181    Fresh,
182    /// An observation changed and verification is pending.
183    MaybeStale,
184    /// Automatic calculation is queued.
185    Pending,
186    /// The latest calculation failed.
187    Failed,
188    /// Effective policy freezes calculation.
189    Frozen,
190    /// Policy or authority blocked calculation.
191    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/// Optional human timestamps used only for explanation.
223#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
224pub struct TimestampSummary {
225    /// When source was observed changing.
226    pub source_changed_ms: Option<u64>,
227    /// When the current result was checked.
228    pub result_checked_ms: Option<u64>,
229}
230
231/// Bounded calculation-receipt facts shown in one outline row.
232#[derive(Clone, Debug, Eq, PartialEq)]
233pub struct ReceiptSummary {
234    /// Stable request id.
235    pub request_id: u64,
236    /// Outcome token.
237    pub outcome: String,
238    /// Retained dependency count.
239    pub dependencies: usize,
240    /// Omitted dependency count.
241    pub omitted_dependencies: usize,
242    /// Logical start tick.
243    pub started_tick: u64,
244    /// Logical finish tick.
245    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/// Fetched child state for an expanded directory.
272#[derive(Clone, Debug, Eq, PartialEq)]
273pub enum ChildPage {
274    /// A collapsed directory fetched no descendants.
275    NotFetched,
276    /// Every child in this page is present.
277    Complete(Vec<NodeSnapshot>),
278    /// The bounded page ends in a server-issued continuation.
279    Truncated {
280        /// Children admitted to this page.
281        nodes: Vec<NodeSnapshot>,
282        /// Opaque continuation token.
283        continuation: String,
284        /// Known omitted child count, if supplied by the source.
285        remaining: Option<usize>,
286    },
287}
288
289/// Expanded details for one cell row.
290#[derive(Clone, Debug, Eq, PartialEq)]
291pub struct NodeDetail {
292    /// Bounded source face.
293    pub source: FaceSnapshot,
294    /// Bounded result face.
295    pub result: FaceSnapshot,
296    /// Current freshness.
297    pub freshness: Freshness,
298    /// Source revision.
299    pub source_revision: u64,
300    /// Result revision, if any.
301    pub result_revision: Option<u64>,
302    /// Optional explanatory wall observations.
303    pub timestamps: TimestampSummary,
304    /// Effective policy badges in stable order.
305    pub policy_badges: Vec<String>,
306    /// Latest bounded receipt summary.
307    pub receipt: Option<ReceiptSummary>,
308}
309
310/// One finite directory or cell snapshot.
311#[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    /// Creates a collapsed directory. No descendant payload is accepted.
327    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    /// Creates an expanded directory with a complete or truncated child page.
337    ///
338    /// `ChildPage::NotFetched` remains valid and renders an empty expanded
339    /// directory, which is useful while a requested page is in flight.
340    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    /// Creates a collapsed cell. Source, result, and receipt faces are absent.
355    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    /// Creates an expanded cell with bounded details.
365    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/// One revisioned, finite surface snapshot.
493#[derive(Clone, Debug, Eq, PartialEq)]
494pub struct ExpressionTreeSnapshot {
495    tree: Expr,
496    revision: u64,
497    nodes: Vec<NodeSnapshot>,
498}
499
500impl ExpressionTreeSnapshot {
501    /// Creates one snapshot over an authoritative tree target.
502    pub fn new(tree: Expr, revision: u64, nodes: Vec<NodeSnapshot>) -> Self {
503        Self {
504            tree,
505            revision,
506            nodes,
507        }
508    }
509
510    /// Encodes the snapshot as an ordinary open SIM map.
511    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}