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 serde_json::{Map, Value, json};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Phase {
9    Enter,
10    Step,
11    Exit,
12}
13
14impl Phase {
15    pub fn code(self) -> &'static str {
16        match self {
17            Phase::Enter => "enter",
18            Phase::Step => "step",
19            Phase::Exit => "exit",
20        }
21    }
22}
23
24/// One variable binding at an event.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct TraceVariable {
27    pub name: String,
28    /// `input` or `output`.
29    pub context_type: String,
30    /// The value as FHIR JSON.
31    pub value: Value,
32    /// The FHIR type, where known: `Patient`, `string`, `QuestionnaireResponse.item`.
33    pub data_type: Option<String>,
34}
35
36/// One evaluation unit.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct TraceEvent {
39    pub phase: Phase,
40    pub depth: usize,
41    /// The map the event is in: its canonical URL.
42    pub source: Option<String>,
43    /// Where in the map, as a FHIRPath location: `StructureMap.group[0].rule[2].target[1]`.
44    pub position: String,
45    pub variables: Vec<TraceVariable>,
46    /// `log (…)` messages produced by this event.
47    pub messages: Vec<String>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum TraceStatus {
52    /// Evaluation ran to the end.
53    Complete,
54    /// Evaluation stopped on an error; open scopes have no `exit`.
55    Stopped,
56}
57
58/// The recorded events of a transform.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Trace {
61    pub status: TraceStatus,
62    pub events: Vec<TraceEvent>,
63}
64
65/// The `Parameters` response of `$transform` with `debug=true`: `return` (when there is a
66/// result), `traceStatus`, the `trace` events and, on failure, an `outcome`.
67pub fn debug_parameters(result: Option<&Value>, trace: &Trace, error: Option<&str>) -> Value {
68    let mut parameters = Vec::new();
69    if let Some(result) = result {
70        parameters.push(json!({ "name": "return", "resource": result }));
71    }
72    parameters.push(json!({
73        "name": "traceStatus",
74        "valueCode": match trace.status {
75            TraceStatus::Complete => "complete",
76            TraceStatus::Stopped => "stopped",
77        }
78    }));
79    for event in &trace.events {
80        let mut parts = Vec::new();
81        if let Some(source) = &event.source {
82            parts.push(json!({ "name": "source", "valueString": source }));
83        }
84        parts.push(json!({ "name": "expressionPosition", "valueString": event.position }));
85        parts.push(json!({ "name": "phase", "valueCode": event.phase.code() }));
86        parts.push(json!({ "name": "depth", "valueInteger": event.depth }));
87        for variable in &event.variables {
88            parts.push(variable_parameter(variable));
89        }
90        for message in &event.messages {
91            parts.push(json!({
92                "name": "message",
93                "part": [{ "name": "text", "valueString": message }]
94            }));
95        }
96        parameters.push(json!({ "name": "trace", "part": parts }));
97    }
98    if let Some(error) = error {
99        parameters.push(json!({
100            "name": "outcome",
101            "resource": {
102                "resourceType": "OperationOutcome",
103                "issue": [{ "severity": "error", "code": "processing", "diagnostics": error }]
104            }
105        }));
106    }
107    json!({ "resourceType": "Parameters", "parameter": parameters })
108}
109
110/// A variable as a `trace.variable` part: a resource in `resource`, a primitive or data type in
111/// `value[x]`, anything else (a backbone element) as `jsonValue`.
112fn variable_parameter(variable: &TraceVariable) -> Value {
113    let mut parts = vec![
114        json!({ "name": "name", "valueString": variable.name }),
115        json!({ "name": "contextType", "valueString": variable.context_type }),
116    ];
117    let data_type = variable.data_type.as_deref();
118    let value = &variable.value;
119    let is_resource = value.get("resourceType").is_some();
120    let is_data_type = data_type.is_some_and(|t| !t.contains('.') && t != "BackboneElement");
121    if is_resource {
122        parts.push(json!({ "name": "resource", "resource": value }));
123    } else if value.is_null() {
124        // An unset value: an empty collection.
125    } else if let (Some(type_name), true) = (data_type, is_data_type) {
126        let mut part = Map::new();
127        part.insert("name".into(), "value".into());
128        part.insert(format!("value{}", capitalize(type_name)), value.clone());
129        parts.push(Value::Object(part));
130    } else if !value.is_object() && !value.is_array() {
131        parts.push(json!({ "name": "value", "valueString": scalar_text(value) }));
132    } else {
133        parts.push(json!({ "name": "jsonValue", "valueString": value.to_string() }));
134    }
135    if let Some(data_type) = data_type {
136        parts.push(json!({ "name": "dataType", "valueString": data_type }));
137    }
138    json!({ "name": "variable", "part": parts })
139}
140
141fn scalar_text(value: &Value) -> String {
142    match value {
143        Value::String(text) => text.clone(),
144        other => other.to_string(),
145    }
146}
147
148fn capitalize(text: &str) -> String {
149    let mut chars = text.chars();
150    match chars.next() {
151        Some(first) => first.to_uppercase().chain(chars).collect(),
152        None => String::new(),
153    }
154}