use fml_model::Span;
use serde_json::{Map, Value, json};
use crate::engine::{Error, ErrorKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Enter,
Step,
Exit,
}
impl Phase {
pub fn code(self) -> &'static str {
match self {
Phase::Enter => "enter",
Phase::Step => "step",
Phase::Exit => "exit",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceVariable {
pub name: String,
pub context_type: String,
pub value: Value,
pub data_type: Option<String>,
pub path: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceEvent {
pub phase: Phase,
pub depth: usize,
pub source: Option<String>,
pub position: String,
pub variables: Vec<TraceVariable>,
pub messages: Vec<String>,
}
impl TraceEvent {
pub fn approximate_size(&self) -> usize {
let variables: usize = self
.variables
.iter()
.map(|v| v.name.len() + v.context_type.len() + json_size(&v.value) + 96)
.sum();
let messages: usize = self.messages.iter().map(|m| m.len() + 48).sum();
self.position.len()
+ self.source.as_ref().map_or(0, String::len)
+ 128
+ variables
+ messages
}
}
fn json_size(value: &Value) -> usize {
match value {
Value::Null | Value::Bool(_) => 5,
Value::Number(_) => 12,
Value::String(text) => text.len() + 2,
Value::Array(items) => 2 + items.iter().map(|v| json_size(v) + 1).sum::<usize>(),
Value::Object(map) => {
2 + map
.iter()
.map(|(k, v)| k.len() + 4 + json_size(v))
.sum::<usize>()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraceStatus {
Complete,
Truncated,
Stopped,
}
impl TraceStatus {
pub fn code(self) -> &'static str {
match self {
TraceStatus::Complete => "complete",
TraceStatus::Truncated => "truncated",
TraceStatus::Stopped => "stopped",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Trace {
pub status: TraceStatus,
pub note: Option<String>,
pub events: Vec<TraceEvent>,
}
#[derive(Debug, Clone, Copy)]
pub struct DebugReport<'a> {
pub result: Option<&'a Value>,
pub trace: &'a Trace,
pub error: Option<&'a Error>,
pub warnings: &'a [String],
}
pub type SpanLookup<'a> = &'a dyn Fn(Option<&str>, &str) -> Option<Span>;
pub fn no_spans(_source: Option<&str>, _position: &str) -> Option<Span> {
None
}
pub fn debug_parameters(report: &DebugReport<'_>, spans: SpanLookup<'_>) -> Value {
let trace = report.trace;
let mut parameters = Vec::new();
if let Some(result) = report.result {
parameters.push(json!({ "name": "return", "resource": result }));
}
parameters.push(json!({ "name": "traceStatus", "valueCode": trace.status.code() }));
for event in &trace.events {
let mut parts = Vec::new();
if let Some(source) = &event.source {
parts.push(json!({ "name": "source", "valueString": source }));
}
if let Some(span) = spans(event.source.as_deref(), &event.position) {
parts.push(json!({ "name": "startRow", "valueInteger": span.start_row }));
parts.push(json!({ "name": "startColumn", "valueInteger": span.start_column }));
parts.push(json!({ "name": "endRow", "valueInteger": span.end_row }));
parts.push(json!({ "name": "endColumn", "valueInteger": span.end_column }));
}
parts.push(json!({ "name": "expressionPosition", "valueString": event.position }));
parts.push(json!({ "name": "phase", "valueCode": event.phase.code() }));
parts.push(json!({ "name": "depth", "valueInteger": event.depth }));
for variable in &event.variables {
parts.push(variable_parameter(variable));
}
for message in &event.messages {
parts.push(json!({
"name": "message",
"part": [{ "name": "text", "valueString": message }]
}));
}
parameters.push(json!({ "name": "trace", "part": parts }));
}
let mut issues = Vec::new();
if let Some(error) = report.error {
let code = match error.kind {
ErrorKind::Failed => "processing",
ErrorKind::Limit => "too-costly",
ErrorKind::Cancelled => "incomplete",
};
let mut issue = json!({ "severity": "error", "code": code, "diagnostics": error.message });
if let Some(position) = &error.position {
issue["expression"] = json!([position]);
let mut extensions = Vec::new();
if let Some(map) = &error.map {
extensions.push(json!({
"url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-file",
"valueString": map
}));
}
if let Some(span) = spans(error.map.as_deref(), position) {
extensions.push(json!({
"url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line",
"valueInteger": span.start_row + 1
}));
extensions.push(json!({
"url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col",
"valueInteger": span.start_column + 1
}));
}
if !extensions.is_empty() {
issue["extension"] = Value::Array(extensions);
}
}
issues.push(issue);
}
if let Some(note) = &trace.note {
issues.push(json!({ "severity": "warning", "code": "too-costly", "diagnostics": note }));
}
for warning in report.warnings {
issues.push(
json!({ "severity": "warning", "code": "informational", "diagnostics": warning }),
);
}
if !issues.is_empty() {
parameters.push(json!({
"name": "outcome",
"resource": { "resourceType": "OperationOutcome", "issue": issues }
}));
}
json!({ "resourceType": "Parameters", "parameter": parameters })
}
fn variable_parameter(variable: &TraceVariable) -> Value {
let mut parts = vec![
json!({ "name": "name", "valueString": variable.name }),
json!({ "name": "contextType", "valueString": variable.context_type }),
];
let data_type = variable.data_type.as_deref();
let value = &variable.value;
let is_resource = value.get("resourceType").is_some();
let is_data_type = data_type.is_some_and(|t| !t.contains('.') && t != "BackboneElement");
if is_resource {
parts.push(json!({ "name": "resource", "resource": value }));
} else if value.is_null() {
} else if let (Some(type_name), true) = (data_type, is_data_type) {
let mut part = Map::new();
part.insert("name".into(), "value".into());
part.insert(format!("value{}", capitalize(type_name)), value.clone());
parts.push(Value::Object(part));
} else if !value.is_object() && !value.is_array() {
parts.push(json!({ "name": "value", "valueString": scalar_text(value) }));
} else {
parts.push(json!({ "name": "jsonValue", "valueString": value.to_string() }));
}
if let Some(data_type) = data_type {
parts.push(json!({ "name": "dataType", "valueString": data_type }));
}
if let Some(path) = &variable.path {
parts.push(json!({ "name": "path", "valueString": path }));
}
json!({ "name": "variable", "part": parts })
}
fn scalar_text(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
other => other.to_string(),
}
}
fn capitalize(text: &str) -> String {
let mut chars = text.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}