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}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct TraceEvent {
42 pub phase: Phase,
43 pub depth: usize,
44 pub source: Option<String>,
46 pub position: String,
48 pub variables: Vec<TraceVariable>,
49 pub messages: Vec<String>,
51}
52
53impl TraceEvent {
54 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 Complete,
89 Truncated,
91 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#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Trace {
108 pub status: TraceStatus,
109 pub note: Option<String>,
111 pub events: Vec<TraceEvent>,
112}
113
114#[derive(Debug, Clone, Copy)]
116pub struct DebugReport<'a> {
117 pub result: Option<&'a Value>,
119 pub trace: &'a Trace,
120 pub error: Option<&'a Error>,
122 pub warnings: &'a [String],
124}
125
126pub type SpanLookup<'a> = &'a dyn Fn(Option<&str>, &str) -> Option<Span>;
130
131pub fn no_spans(_source: Option<&str>, _position: &str) -> Option<Span> {
133 None
134}
135
136pub 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 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
223fn 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 } 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}