Skip to main content

fml_engine/
trace.rs

1//! Debug trace of a transform, in the form the Mapping Language IG proposes for `$transform`
2//! with `debug=true` (debug.md): `enter` / `step` / `exit` events with their depth, the position
3//! in the StructureMap, and a snapshot of the variables in scope.
4
5use fml_model::Span;
6use serde_json::{Map, Value, json};
7
8use crate::engine::{Error, ErrorKind};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Phase {
12    Enter,
13    Step,
14    Exit,
15}
16
17impl Phase {
18    pub fn code(self) -> &'static str {
19        match self {
20            Phase::Enter => "enter",
21            Phase::Step => "step",
22            Phase::Exit => "exit",
23        }
24    }
25}
26
27/// One variable binding at an event.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TraceVariable {
30    pub name: String,
31    /// `input` or `output`.
32    pub context_type: String,
33    /// The value as FHIR JSON.
34    pub value: Value,
35    /// The FHIR type, where known: `Patient`, `string`, `QuestionnaireResponse.item`.
36    pub data_type: Option<String>,
37}
38
39/// One evaluation unit.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct TraceEvent {
42    pub phase: Phase,
43    pub depth: usize,
44    /// The map the event is in: its canonical URL.
45    pub source: Option<String>,
46    /// Where in the map, as a FHIRPath location: `StructureMap.group[0].rule[2].target[1]`.
47    pub position: String,
48    pub variables: Vec<TraceVariable>,
49    /// `log (…)` messages produced by this event.
50    pub messages: Vec<String>,
51}
52
53impl TraceEvent {
54    /// Roughly the size of the event as JSON, in bytes: enough to keep a trace within a budget.
55    pub fn approximate_size(&self) -> usize {
56        let variables: usize = self
57            .variables
58            .iter()
59            .map(|v| v.name.len() + v.context_type.len() + json_size(&v.value) + 96)
60            .sum();
61        let messages: usize = self.messages.iter().map(|m| m.len() + 48).sum();
62        self.position.len()
63            + self.source.as_ref().map_or(0, String::len)
64            + 128
65            + variables
66            + messages
67    }
68}
69
70fn json_size(value: &Value) -> usize {
71    match value {
72        Value::Null | Value::Bool(_) => 5,
73        Value::Number(_) => 12,
74        Value::String(text) => text.len() + 2,
75        Value::Array(items) => 2 + items.iter().map(|v| json_size(v) + 1).sum::<usize>(),
76        Value::Object(map) => {
77            2 + map
78                .iter()
79                .map(|(k, v)| k.len() + 4 + json_size(v))
80                .sum::<usize>()
81        }
82    }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum TraceStatus {
87    /// Evaluation ran to the end and every event was kept.
88    Complete,
89    /// Evaluation went on past the kept events: the trace reached its limits.
90    Truncated,
91    /// Evaluation stopped on an error; open scopes have no `exit`.
92    Stopped,
93}
94
95impl TraceStatus {
96    pub fn code(self) -> &'static str {
97        match self {
98            TraceStatus::Complete => "complete",
99            TraceStatus::Truncated => "truncated",
100            TraceStatus::Stopped => "stopped",
101        }
102    }
103}
104
105/// The recorded events of a transform.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Trace {
108    pub status: TraceStatus,
109    /// Why events were left out, for a `truncated` trace.
110    pub note: Option<String>,
111    pub events: Vec<TraceEvent>,
112}
113
114/// What a `$transform` with `debug=true` reports.
115#[derive(Debug, Clone, Copy)]
116pub struct DebugReport<'a> {
117    /// The result, when the transform produced one.
118    pub result: Option<&'a Value>,
119    pub trace: &'a Trace,
120    /// The failure, when it failed.
121    pub error: Option<&'a Error>,
122    /// Values left out of the result ([`Transformed::warnings`](crate::Transformed::warnings)).
123    pub warnings: &'a [String],
124}
125
126/// Where a part of a map is in its FML text: the map's canonical URL (the trace's `source`) and
127/// the part's location (`StructureMap.group[0].rule[1]`). For maps compiled from FML, this comes
128/// from [`fml_model::Locations`].
129pub type SpanLookup<'a> = &'a dyn Fn(Option<&str>, &str) -> Option<Span>;
130
131/// No text positions: maps that were not compiled from FML here.
132pub fn no_spans(_source: Option<&str>, _position: &str) -> Option<Span> {
133    None
134}
135
136/// The `Parameters` response of `$transform` with `debug=true` (debug.md): `return` when there
137/// is a result, `traceStatus`, the `trace` events with their text positions where `spans` knows
138/// them, and an `outcome` with the failure, the reason events were left out, and values left out
139/// of the result.
140pub fn debug_parameters(report: &DebugReport<'_>, spans: SpanLookup<'_>) -> Value {
141    let trace = report.trace;
142    let mut parameters = Vec::new();
143    if let Some(result) = report.result {
144        parameters.push(json!({ "name": "return", "resource": result }));
145    }
146    parameters.push(json!({ "name": "traceStatus", "valueCode": trace.status.code() }));
147    for event in &trace.events {
148        let mut parts = Vec::new();
149        if let Some(source) = &event.source {
150            parts.push(json!({ "name": "source", "valueString": source }));
151        }
152        if let Some(span) = spans(event.source.as_deref(), &event.position) {
153            parts.push(json!({ "name": "startRow", "valueInteger": span.start_row }));
154            parts.push(json!({ "name": "startColumn", "valueInteger": span.start_column }));
155            parts.push(json!({ "name": "endRow", "valueInteger": span.end_row }));
156            parts.push(json!({ "name": "endColumn", "valueInteger": span.end_column }));
157        }
158        parts.push(json!({ "name": "expressionPosition", "valueString": event.position }));
159        parts.push(json!({ "name": "phase", "valueCode": event.phase.code() }));
160        parts.push(json!({ "name": "depth", "valueInteger": event.depth }));
161        for variable in &event.variables {
162            parts.push(variable_parameter(variable));
163        }
164        for message in &event.messages {
165            parts.push(json!({
166                "name": "message",
167                "part": [{ "name": "text", "valueString": message }]
168            }));
169        }
170        parameters.push(json!({ "name": "trace", "part": parts }));
171    }
172    let mut issues = Vec::new();
173    if let Some(error) = report.error {
174        let code = match error.kind {
175            ErrorKind::Failed => "processing",
176            ErrorKind::Limit => "too-costly",
177            ErrorKind::Cancelled => "incomplete",
178        };
179        let mut issue = json!({ "severity": "error", "code": code, "diagnostics": error.message });
180        if let Some(position) = &error.position {
181            issue["expression"] = json!([position]);
182            let mut extensions = Vec::new();
183            if let Some(map) = &error.map {
184                extensions.push(json!({
185                    "url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-file",
186                    "valueString": map
187                }));
188            }
189            if let Some(span) = spans(error.map.as_deref(), position) {
190                // The location extensions count from 1.
191                extensions.push(json!({
192                    "url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line",
193                    "valueInteger": span.start_row + 1
194                }));
195                extensions.push(json!({
196                    "url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col",
197                    "valueInteger": span.start_column + 1
198                }));
199            }
200            if !extensions.is_empty() {
201                issue["extension"] = Value::Array(extensions);
202            }
203        }
204        issues.push(issue);
205    }
206    if let Some(note) = &trace.note {
207        issues.push(json!({ "severity": "warning", "code": "too-costly", "diagnostics": note }));
208    }
209    for warning in report.warnings {
210        issues.push(
211            json!({ "severity": "warning", "code": "informational", "diagnostics": warning }),
212        );
213    }
214    if !issues.is_empty() {
215        parameters.push(json!({
216            "name": "outcome",
217            "resource": { "resourceType": "OperationOutcome", "issue": issues }
218        }));
219    }
220    json!({ "resourceType": "Parameters", "parameter": parameters })
221}
222
223/// A variable as a `trace.variable` part: a resource in `resource`, a primitive or data type in
224/// `value[x]`, anything else (a backbone element) as `jsonValue`.
225fn variable_parameter(variable: &TraceVariable) -> Value {
226    let mut parts = vec![
227        json!({ "name": "name", "valueString": variable.name }),
228        json!({ "name": "contextType", "valueString": variable.context_type }),
229    ];
230    let data_type = variable.data_type.as_deref();
231    let value = &variable.value;
232    let is_resource = value.get("resourceType").is_some();
233    let is_data_type = data_type.is_some_and(|t| !t.contains('.') && t != "BackboneElement");
234    if is_resource {
235        parts.push(json!({ "name": "resource", "resource": value }));
236    } else if value.is_null() {
237        // An unset value: an empty collection.
238    } else if let (Some(type_name), true) = (data_type, is_data_type) {
239        let mut part = Map::new();
240        part.insert("name".into(), "value".into());
241        part.insert(format!("value{}", capitalize(type_name)), value.clone());
242        parts.push(Value::Object(part));
243    } else if !value.is_object() && !value.is_array() {
244        parts.push(json!({ "name": "value", "valueString": scalar_text(value) }));
245    } else {
246        parts.push(json!({ "name": "jsonValue", "valueString": value.to_string() }));
247    }
248    if let Some(data_type) = data_type {
249        parts.push(json!({ "name": "dataType", "valueString": data_type }));
250    }
251    json!({ "name": "variable", "part": parts })
252}
253
254fn scalar_text(value: &Value) -> String {
255    match value {
256        Value::String(text) => text.clone(),
257        other => other.to_string(),
258    }
259}
260
261fn capitalize(text: &str) -> String {
262    let mut chars = text.chars();
263    match chars.next() {
264        Some(first) => first.to_uppercase().chain(chars).collect(),
265        None => String::new(),
266    }
267}