1use 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#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct TraceVariable {
30 pub name: String,
31 pub context_type: String,
33 pub value: Value,
35 pub data_type: Option<String>,
37 pub path: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct TraceEvent {
45 pub phase: Phase,
46 pub depth: usize,
47 pub source: Option<String>,
49 pub position: String,
51 pub variables: Vec<TraceVariable>,
52 pub messages: Vec<String>,
54}
55
56impl TraceEvent {
57 pub fn approximate_size(&self) -> usize {
59 let variables: usize = self
60 .variables
61 .iter()
62 .map(|v| v.name.len() + v.context_type.len() + json_size(&v.value) + 96)
63 .sum();
64 let messages: usize = self.messages.iter().map(|m| m.len() + 48).sum();
65 self.position.len()
66 + self.source.as_ref().map_or(0, String::len)
67 + 128
68 + variables
69 + messages
70 }
71}
72
73fn json_size(value: &Value) -> usize {
74 match value {
75 Value::Null | Value::Bool(_) => 5,
76 Value::Number(_) => 12,
77 Value::String(text) => text.len() + 2,
78 Value::Array(items) => 2 + items.iter().map(|v| json_size(v) + 1).sum::<usize>(),
79 Value::Object(map) => {
80 2 + map
81 .iter()
82 .map(|(k, v)| k.len() + 4 + json_size(v))
83 .sum::<usize>()
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum TraceStatus {
90 Complete,
92 Truncated,
94 Stopped,
96}
97
98impl TraceStatus {
99 pub fn code(self) -> &'static str {
100 match self {
101 TraceStatus::Complete => "complete",
102 TraceStatus::Truncated => "truncated",
103 TraceStatus::Stopped => "stopped",
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct Trace {
111 pub status: TraceStatus,
112 pub note: Option<String>,
114 pub events: Vec<TraceEvent>,
115}
116
117#[derive(Debug, Clone, Copy)]
119pub struct DebugReport<'a> {
120 pub result: Option<&'a Value>,
122 pub trace: &'a Trace,
123 pub error: Option<&'a Error>,
125 pub warnings: &'a [String],
127}
128
129pub type SpanLookup<'a> = &'a dyn Fn(Option<&str>, &str) -> Option<Span>;
133
134pub fn no_spans(_source: Option<&str>, _position: &str) -> Option<Span> {
136 None
137}
138
139pub fn debug_parameters(report: &DebugReport<'_>, spans: SpanLookup<'_>) -> Value {
144 let trace = report.trace;
145 let mut parameters = Vec::new();
146 if let Some(result) = report.result {
147 parameters.push(json!({ "name": "return", "resource": result }));
148 }
149 parameters.push(json!({ "name": "traceStatus", "valueCode": trace.status.code() }));
150 for event in &trace.events {
151 let mut parts = Vec::new();
152 if let Some(source) = &event.source {
153 parts.push(json!({ "name": "source", "valueString": source }));
154 }
155 if let Some(span) = spans(event.source.as_deref(), &event.position) {
156 parts.push(json!({ "name": "startRow", "valueInteger": span.start_row }));
157 parts.push(json!({ "name": "startColumn", "valueInteger": span.start_column }));
158 parts.push(json!({ "name": "endRow", "valueInteger": span.end_row }));
159 parts.push(json!({ "name": "endColumn", "valueInteger": span.end_column }));
160 }
161 parts.push(json!({ "name": "expressionPosition", "valueString": event.position }));
162 parts.push(json!({ "name": "phase", "valueCode": event.phase.code() }));
163 parts.push(json!({ "name": "depth", "valueInteger": event.depth }));
164 for variable in &event.variables {
165 parts.push(variable_parameter(variable));
166 }
167 for message in &event.messages {
168 parts.push(json!({
169 "name": "message",
170 "part": [{ "name": "text", "valueString": message }]
171 }));
172 }
173 parameters.push(json!({ "name": "trace", "part": parts }));
174 }
175 let mut issues = Vec::new();
176 if let Some(error) = report.error {
177 let code = match error.kind {
178 ErrorKind::Failed => "processing",
179 ErrorKind::Limit => "too-costly",
180 ErrorKind::Cancelled => "incomplete",
181 };
182 let mut issue = json!({ "severity": "error", "code": code, "diagnostics": error.message });
183 if let Some(position) = &error.position {
184 issue["expression"] = json!([position]);
185 let mut extensions = Vec::new();
186 if let Some(map) = &error.map {
187 extensions.push(json!({
188 "url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-file",
189 "valueString": map
190 }));
191 }
192 if let Some(span) = spans(error.map.as_deref(), position) {
193 extensions.push(json!({
195 "url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-line",
196 "valueInteger": span.start_row + 1
197 }));
198 extensions.push(json!({
199 "url": "http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-col",
200 "valueInteger": span.start_column + 1
201 }));
202 }
203 if !extensions.is_empty() {
204 issue["extension"] = Value::Array(extensions);
205 }
206 }
207 issues.push(issue);
208 }
209 if let Some(note) = &trace.note {
210 issues.push(json!({ "severity": "warning", "code": "too-costly", "diagnostics": note }));
211 }
212 for warning in report.warnings {
213 issues.push(
214 json!({ "severity": "warning", "code": "informational", "diagnostics": warning }),
215 );
216 }
217 if !issues.is_empty() {
218 parameters.push(json!({
219 "name": "outcome",
220 "resource": { "resourceType": "OperationOutcome", "issue": issues }
221 }));
222 }
223 json!({ "resourceType": "Parameters", "parameter": parameters })
224}
225
226fn variable_parameter(variable: &TraceVariable) -> Value {
229 let mut parts = vec![
230 json!({ "name": "name", "valueString": variable.name }),
231 json!({ "name": "contextType", "valueString": variable.context_type }),
232 ];
233 let data_type = variable.data_type.as_deref();
234 let value = &variable.value;
235 let is_resource = value.get("resourceType").is_some();
236 let is_data_type = data_type.is_some_and(|t| !t.contains('.') && t != "BackboneElement");
237 if is_resource {
238 parts.push(json!({ "name": "resource", "resource": value }));
239 } else if value.is_null() {
240 } else if let (Some(type_name), true) = (data_type, is_data_type) {
242 let mut part = Map::new();
243 part.insert("name".into(), "value".into());
244 part.insert(format!("value{}", capitalize(type_name)), value.clone());
245 parts.push(Value::Object(part));
246 } else if !value.is_object() && !value.is_array() {
247 parts.push(json!({ "name": "value", "valueString": scalar_text(value) }));
248 } else {
249 parts.push(json!({ "name": "jsonValue", "valueString": value.to_string() }));
250 }
251 if let Some(data_type) = data_type {
252 parts.push(json!({ "name": "dataType", "valueString": data_type }));
253 }
254 if let Some(path) = &variable.path {
255 parts.push(json!({ "name": "path", "valueString": path }));
256 }
257 json!({ "name": "variable", "part": parts })
258}
259
260fn scalar_text(value: &Value) -> String {
261 match value {
262 Value::String(text) => text.clone(),
263 other => other.to_string(),
264 }
265}
266
267fn capitalize(text: &str) -> String {
268 let mut chars = text.chars();
269 match chars.next() {
270 Some(first) => first.to_uppercase().chain(chars).collect(),
271 None => String::new(),
272 }
273}