use sim_expr_tree_calc::{
CalcOutcome, CalcReceipt, CalcStatus, EncodedFace, FaceContent, FaceDimension, FaceIssue,
};
use sim_kernel::{Expr, Symbol};
use sim_value::build;
pub const SNAPSHOT_TYPE: &str = "expression-tree-snapshot";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FaceState {
Complete,
Truncated {
dimension: String,
limit: usize,
observed: usize,
},
Unsupported {
reason: String,
},
CodecFailure {
message: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FaceSnapshot {
content: Option<Expr>,
codec: Option<String>,
state: FaceState,
}
impl FaceSnapshot {
pub fn text(content: impl Into<String>, codec: impl Into<String>) -> Self {
Self {
content: Some(Expr::String(content.into())),
codec: Some(codec.into()),
state: FaceState::Complete,
}
}
pub fn bytes(content: Vec<u8>, codec: impl Into<String>) -> Self {
Self {
content: Some(Expr::Bytes(content)),
codec: Some(codec.into()),
state: FaceState::Complete,
}
}
pub fn truncated(dimension: impl Into<String>, limit: usize, observed: usize) -> Self {
Self {
content: None,
codec: None,
state: FaceState::Truncated {
dimension: dimension.into(),
limit,
observed,
},
}
}
pub fn unsupported(reason: impl Into<String>) -> Self {
Self {
content: None,
codec: None,
state: FaceState::Unsupported {
reason: reason.into(),
},
}
}
pub fn codec_failure(message: impl Into<String>) -> Self {
Self {
content: None,
codec: None,
state: FaceState::CodecFailure {
message: message.into(),
},
}
}
pub fn from_encoded(face: &EncodedFace) -> Self {
let content = face.content().map(|content| match content {
FaceContent::Text(text) => Expr::String(text.clone()),
FaceContent::Bytes(bytes) => Expr::Bytes(bytes.clone()),
});
let metadata = face.metadata();
let state = match metadata.issue() {
FaceIssue::Complete => FaceState::Complete,
FaceIssue::Truncated {
dimension,
limit,
observed,
} => FaceState::Truncated {
dimension: face_dimension(*dimension).to_owned(),
limit: *limit,
observed: *observed,
},
FaceIssue::Unsupported { reason } => FaceState::Unsupported {
reason: reason.clone(),
},
FaceIssue::CodecFailure { message } => FaceState::CodecFailure {
message: message.clone(),
},
};
Self {
content,
codec: metadata.codec().map(str::to_owned),
state,
}
}
pub(crate) fn to_expr(&self) -> Expr {
let (state, detail) = match &self.state {
FaceState::Complete => ("complete", Vec::new()),
FaceState::Truncated {
dimension,
limit,
observed,
} => (
"truncated",
vec![
("dimension", build::sym(dimension)),
("limit", build::uint(*limit as u64)),
("observed", build::uint(*observed as u64)),
],
),
FaceState::Unsupported { reason } => {
("unsupported", vec![("reason", build::text(reason))])
}
FaceState::CodecFailure { message } => {
("codec-failure", vec![("message", build::text(message))])
}
};
let mut fields = vec![
("state", build::sym(state)),
("content", self.content.clone().unwrap_or(Expr::Nil)),
(
"codec",
self.codec.as_ref().map(build::text).unwrap_or(Expr::Nil),
),
];
fields.extend(detail);
build::map(fields)
}
}
fn face_dimension(dimension: FaceDimension) -> &'static str {
match dimension {
FaceDimension::Bytes => "bytes",
FaceDimension::Depth => "depth",
FaceDimension::Items => "items",
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Freshness {
NeverCalculated,
Fresh,
MaybeStale,
Pending,
Failed,
Frozen,
Blocked,
}
impl Freshness {
pub(crate) const fn token(self) -> &'static str {
match self {
Self::NeverCalculated => "never-calculated",
Self::Fresh => "fresh",
Self::MaybeStale => "maybe-stale",
Self::Pending => "pending",
Self::Failed => "failed",
Self::Frozen => "frozen",
Self::Blocked => "blocked",
}
}
}
impl From<CalcStatus> for Freshness {
fn from(status: CalcStatus) -> Self {
match status {
CalcStatus::NeverCalculated => Self::NeverCalculated,
CalcStatus::Fresh => Self::Fresh,
CalcStatus::MaybeStale => Self::MaybeStale,
CalcStatus::Pending => Self::Pending,
CalcStatus::Failed => Self::Failed,
CalcStatus::Frozen => Self::Frozen,
CalcStatus::Blocked => Self::Blocked,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct TimestampSummary {
pub source_changed_ms: Option<u64>,
pub result_checked_ms: Option<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReceiptSummary {
pub request_id: u64,
pub outcome: String,
pub dependencies: usize,
pub omitted_dependencies: usize,
pub started_tick: u64,
pub finished_tick: u64,
}
impl From<&CalcReceipt> for ReceiptSummary {
fn from(receipt: &CalcReceipt) -> Self {
Self {
request_id: receipt.request_id.get(),
outcome: outcome_token(&receipt.outcome).to_owned(),
dependencies: receipt.dependencies.len(),
omitted_dependencies: receipt.omitted_dependencies,
started_tick: receipt.started_tick,
finished_tick: receipt.finished_tick,
}
}
}
fn outcome_token(outcome: &CalcOutcome) -> &'static str {
match outcome {
CalcOutcome::Succeeded => "succeeded",
CalcOutcome::Failed { .. } => "failed",
CalcOutcome::Blocked { .. } => "blocked",
CalcOutcome::Cancelled => "cancelled",
CalcOutcome::BudgetExhausted { .. } => "budget-exhausted",
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ChildPage {
NotFetched,
Complete(Vec<NodeSnapshot>),
Truncated {
nodes: Vec<NodeSnapshot>,
continuation: String,
remaining: Option<usize>,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeDetail {
pub source: FaceSnapshot,
pub result: FaceSnapshot,
pub freshness: Freshness,
pub source_revision: u64,
pub result_revision: Option<u64>,
pub timestamps: TimestampSummary,
pub policy_badges: Vec<String>,
pub receipt: Option<ReceiptSummary>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeSnapshot {
path: String,
name: String,
revision: u64,
body: NodeBody,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum NodeBody {
Directory(ChildPage),
Cell(Option<Box<NodeDetail>>),
}
impl NodeSnapshot {
pub fn collapsed_dir(path: impl Into<String>, name: impl Into<String>, revision: u64) -> Self {
Self {
path: path.into(),
name: name.into(),
revision,
body: NodeBody::Directory(ChildPage::NotFetched),
}
}
pub fn expanded_dir(
path: impl Into<String>,
name: impl Into<String>,
revision: u64,
children: ChildPage,
) -> Self {
Self {
path: path.into(),
name: name.into(),
revision,
body: NodeBody::Directory(children),
}
}
pub fn collapsed_cell(path: impl Into<String>, name: impl Into<String>, revision: u64) -> Self {
Self {
path: path.into(),
name: name.into(),
revision,
body: NodeBody::Cell(None),
}
}
pub fn expanded_cell(
path: impl Into<String>,
name: impl Into<String>,
revision: u64,
detail: NodeDetail,
) -> Self {
Self {
path: path.into(),
name: name.into(),
revision,
body: NodeBody::Cell(Some(Box::new(detail))),
}
}
pub(crate) fn to_expr(&self) -> Expr {
let (node_type, open, body) = match &self.body {
NodeBody::Directory(children) => (
"directory",
!matches!(children, ChildPage::NotFetched),
children_expr(children),
),
NodeBody::Cell(detail) => (
"cell",
detail.is_some(),
detail
.as_ref()
.map(|detail| detail_expr(detail))
.unwrap_or(Expr::Nil),
),
};
build::map(vec![
("node-type", build::sym(node_type)),
("path", build::text(&self.path)),
("name", build::text(&self.name)),
("revision", build::uint(self.revision)),
("open", Expr::Bool(open)),
("body", body),
])
}
}
fn children_expr(children: &ChildPage) -> Expr {
match children {
ChildPage::NotFetched => Expr::Nil,
ChildPage::Complete(nodes) => build::map(vec![
("page-state", build::sym("complete")),
(
"nodes",
build::list(nodes.iter().map(NodeSnapshot::to_expr).collect()),
),
]),
ChildPage::Truncated {
nodes,
continuation,
remaining,
} => build::map(vec![
("page-state", build::sym("truncated")),
(
"nodes",
build::list(nodes.iter().map(NodeSnapshot::to_expr).collect()),
),
("continuation", build::text(continuation)),
(
"remaining",
remaining
.map(|value| build::uint(value as u64))
.unwrap_or(Expr::Nil),
),
]),
}
}
fn detail_expr(detail: &NodeDetail) -> Expr {
build::map(vec![
("source", detail.source.to_expr()),
("result", detail.result.to_expr()),
("freshness", build::sym(detail.freshness.token())),
("source-revision", build::uint(detail.source_revision)),
(
"result-revision",
detail.result_revision.map(build::uint).unwrap_or(Expr::Nil),
),
(
"source-changed-ms",
detail
.timestamps
.source_changed_ms
.map(build::uint)
.unwrap_or(Expr::Nil),
),
(
"result-checked-ms",
detail
.timestamps
.result_checked_ms
.map(build::uint)
.unwrap_or(Expr::Nil),
),
(
"policy-badges",
build::list(detail.policy_badges.iter().map(build::text).collect()),
),
(
"receipt",
detail
.receipt
.as_ref()
.map(receipt_expr)
.unwrap_or(Expr::Nil),
),
])
}
fn receipt_expr(receipt: &ReceiptSummary) -> Expr {
build::map(vec![
("request-id", build::uint(receipt.request_id)),
("outcome", build::sym(&receipt.outcome)),
("dependencies", build::uint(receipt.dependencies as u64)),
(
"omitted-dependencies",
build::uint(receipt.omitted_dependencies as u64),
),
("started-tick", build::uint(receipt.started_tick)),
("finished-tick", build::uint(receipt.finished_tick)),
])
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExpressionTreeSnapshot {
tree: Expr,
revision: u64,
nodes: Vec<NodeSnapshot>,
}
impl ExpressionTreeSnapshot {
pub fn new(tree: Expr, revision: u64, nodes: Vec<NodeSnapshot>) -> Self {
Self {
tree,
revision,
nodes,
}
}
pub fn to_expr(&self) -> Expr {
build::map(vec![
(
"type",
Expr::Symbol(Symbol::qualified("expr-tree-view", SNAPSHOT_TYPE)),
),
("tree", self.tree.clone()),
("revision", build::uint(self.revision)),
(
"nodes",
build::list(self.nodes.iter().map(NodeSnapshot::to_expr).collect()),
),
])
}
}