///|
fn trace_status_text(status : TraceStatus) -> String {
match status {
Value => "VALUE"
Pass => "PASS"
Fail => "FAIL"
Error => "ERROR"
Skipped => "SKIPPED"
}
}
///|
fn trace_status_json(status : TraceStatus) -> String {
match status {
Value => "value"
Pass => "pass"
Fail => "fail"
Error => "error"
Skipped => "skipped"
}
}
///|
fn root_message_parts(message : String) -> (String, String) {
match message.split_once("\n") {
Some((rule_id, detail)) => (rule_id.to_owned(), detail.to_owned())
None => (message, "")
}
}
///|
fn take_chars(text : String, limit : Int) -> String {
if limit <= 0 {
return ""
}
let out = StringBuilder()
let mut count = 0
for character in text {
if count >= limit {
break
}
out.write_char(character)
count += 1
}
out.to_string()
}
///|
fn preview_json(value : Json, max_chars : Int) -> String {
let text = value.stringify()
if text.char_length() <= max_chars {
text
} else {
"\{take_chars(text, max_chars)}…(truncated)"
}
}
///|
fn node_has_detail(node : TraceNode) -> Bool {
node.result is Some(_) || node.status == Error || node.status == Skipped
}
///|
fn render_detail(
out : StringBuilder,
node : TraceNode,
prefix : String,
budget : Budget,
is_root : Bool,
) -> Unit {
out.write_string(prefix)
out.write_string("└─ ")
match node.status {
Skipped => out.write_string("reason: \{node.message}")
Error => {
let detail = if is_root {
root_message_parts(node.message).1
} else {
node.message
}
out.write_string("error: \{detail}")
}
_ =>
match node.result {
Some(value) =>
out.write_string(
"result = \{preview_json(value, budget.max_preview_chars)}",
)
None => out.write_string("result = null")
}
}
out.write_char('\n')
}
///|
fn render_node(
out : StringBuilder,
node : TraceNode,
prefix : String,
is_last : Bool,
budget : Budget,
is_root : Bool,
) -> Unit {
if !is_root {
out.write_string(prefix)
out.write_string(if is_last { "└─ " } else { "├─ " })
}
out.write_string(trace_status_text(node.status))
out.write_char(' ')
if is_root {
let (rule_id, _) = root_message_parts(node.message)
out.write_string("\{rule_id} [\{node.operator}]")
} else {
out.write_string(node.operator)
}
out.write_char('\n')
let child_prefix = if is_root {
""
} else {
"\{prefix}\{if is_last { " " } else { "│ " }}"
}
let has_detail = node_has_detail(node)
for index, child in node.children {
let child_is_last = index == node.children.length() - 1 && !has_detail
render_node(out, child, child_prefix, child_is_last, budget, false)
}
if has_detail {
render_detail(out, node, child_prefix, budget, is_root)
}
}
///|
pub fn render_report(report : EvaluationReport, budget : Budget) -> String {
let out = StringBuilder()
render_node(out, report.trace, "", true, budget, true)
let text = out.to_string()
if text.has_suffix("\n") {
text[:text.length() - 1].to_owned()
} else {
text
}
}
///|
fn eval_error_to_json(error : EvalError) -> Json {
match error {
MissingVariable(path) =>
Json::object({
"type": Json::string("missing_variable"),
"path": Json::string(path),
})
TypeMismatch(message) =>
Json::object({
"type": Json::string("type_mismatch"),
"message": Json::string(message),
})
BudgetExceeded(kind, current, limit) =>
Json::object({
"type": Json::string("budget_exceeded"),
"budget": Json::string(kind),
"current": Json::number(current.to_double()),
"limit": Json::number(limit.to_double()),
})
UnsupportedTraceMode(mode) =>
Json::object({
"type": Json::string("unsupported_trace_mode"),
"mode": Json::string(mode),
})
}
}
///|
fn decision_to_json(decision : Decision) -> Json {
match decision {
Decision::Pass => Json::object({ "status": Json::string("pass") })
Decision::Fail => Json::object({ "status": Json::string("fail") })
Decision::Indeterminate(error) =>
Json::object({
"status": Json::string("indeterminate"),
"error": eval_error_to_json(error),
})
}
}
///|
fn resolved_input_to_json(input : ResolvedInput) -> Json {
Json::object({ "source": Json::string(input.source), "value": input.value })
}
///|
fn trace_to_json(node : TraceNode) -> Json {
let result = match node.result {
Some(value) => value
None => Json::null()
}
Json::object({
"rule_path": Json::string(node.rule_path),
"operator": Json::string(node.operator),
"status": Json::string(trace_status_json(node.status)),
"resolved_inputs": Json::array(
node.resolved_inputs.map(resolved_input_to_json),
),
"result": result,
"message": Json::string(node.message),
"children": Json::array(node.children.map(trace_to_json)),
})
}
///|
pub fn report_to_json(report : EvaluationReport) -> Json {
Json::object({
"decision": decision_to_json(report.decision),
"trace": trace_to_json(report.trace),
"stats": Json::object({
"steps_executed": Json::number(report.stats.steps_executed.to_double()),
"nodes_evaluated": Json::number(report.stats.nodes_evaluated.to_double()),
"max_depth_reached": Json::number(
report.stats.max_depth_reached.to_double(),
),
"trace_nodes_emitted": Json::number(
report.stats.trace_nodes_emitted.to_double(),
),
}),
})
}
///|
fn diagnostic_severity_json(severity : DiagnosticSeverity) -> String {
match severity {
DiagnosticSeverity::Error => "error"
DiagnosticSeverity::Warning => "warning"
}
}
///|
fn diagnostic_to_json(diagnostic : Diagnostic) -> Json {
Json::object({
"code": Json::string(diagnostic.code),
"severity": Json::string(diagnostic_severity_json(diagnostic.severity)),
"rule_path": Json::string(diagnostic.rule_path),
"message": Json::string(diagnostic.message),
"suggestion": Json::string(diagnostic.suggestion),
})
}
///|
pub fn diagnostics_to_json(diagnostics : Array[Diagnostic]) -> Json {
Json::array(diagnostics.map(diagnostic_to_json))
}