1use base64::Engine as _;
19use serde_json::{Value, json};
20
21use crate::sqlquery::engine::TableSchema;
22use crate::sqlquery::output::value_to_fhir_part;
23use crate::sqlquery::{ColumnFhirType, SqlQueryError};
24use crate::{ProcessedResult, SofError};
25
26pub const FHIR_JSON_MIME: &str = "application/fhir+json";
28
29pub const FHIR_XML_MIME: &str = "application/fhir+xml";
31
32pub fn accept_has_mime(accept: Option<&str>, mime: &str) -> bool {
35 let Some(accept) = accept else {
36 return false;
37 };
38 accept
39 .split(',')
40 .map(|s| s.split(';').next().unwrap_or("").trim())
41 .any(|m| m.eq_ignore_ascii_case(mime))
42}
43
44pub fn accept_requires_unsupported_fhir_xml(accept: Option<&str>) -> bool {
50 accept_has_mime(accept, FHIR_XML_MIME) && !accept_has_mime(accept, FHIR_JSON_MIME)
51}
52
53pub fn wrap_in_binary_envelope(content_type: &str, payload: &[u8]) -> Result<Vec<u8>, SofError> {
57 let media_type = content_type
60 .split(';')
61 .next()
62 .unwrap_or(content_type)
63 .trim();
64 let binary = json!({
65 "resourceType": "Binary",
66 "contentType": media_type,
67 "data": base64::engine::general_purpose::STANDARD.encode(payload),
68 });
69 serde_json::to_vec(&binary).map_err(SofError::SerializationError)
70}
71
72pub fn format_view_fhir_parameters(
83 result: &ProcessedResult,
84 view_json: &Value,
85) -> Result<Vec<u8>, SofError> {
86 let schema = TableSchema::from_view_definition(view_json);
87 let column_types: Vec<ColumnFhirType> = result
88 .columns
89 .iter()
90 .map(|name| {
91 schema
92 .columns
93 .iter()
94 .find(|c| &c.name == name)
95 .map(|c| c.fhir_type.clone())
96 .unwrap_or_else(|| ColumnFhirType::String("string".into()))
97 })
98 .collect();
99
100 let mut row_params: Vec<Value> = Vec::with_capacity(result.rows.len());
101 for row in &result.rows {
102 let mut parts: Vec<Value> = Vec::with_capacity(row.values.len());
103 for (i, cell) in row.values.iter().enumerate() {
104 let Some(value) = cell else {
105 continue; };
107 if value.is_null() {
108 continue;
109 }
110 let name = &result.columns[i];
111 let ty = &column_types[i];
112 match value {
113 Value::Array(items) => {
114 for item in items {
115 if item.is_null() {
116 continue;
117 }
118 parts.push(part_or_sof_error(name, item, ty)?);
119 }
120 }
121 other => parts.push(part_or_sof_error(name, other, ty)?),
122 }
123 }
124 row_params.push(json!({ "name": "row", "part": parts }));
125 }
126
127 let body = if row_params.is_empty() {
128 json!({ "resourceType": "Parameters" })
129 } else {
130 json!({ "resourceType": "Parameters", "parameter": row_params })
131 };
132 serde_json::to_vec(&body).map_err(SofError::SerializationError)
133}
134
135fn part_or_sof_error(name: &str, value: &Value, ty: &ColumnFhirType) -> Result<Value, SofError> {
136 value_to_fhir_part(name, value, ty).map_err(|e| match e {
137 SqlQueryError::UnsupportedFhirValue(col) => SofError::InvalidViewDefinition(format!(
138 "column '{col}' holds a complex value that cannot be represented as a \
139 FHIR value[x] part; the 'fhir' output format supports scalar columns only"
140 )),
141 other => SofError::InvalidViewDefinition(other.to_string()),
142 })
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::ProcessedRow;
149 use serde_json::json;
150
151 fn view(columns: Value) -> Value {
152 json!({
153 "resourceType": "ViewDefinition",
154 "resource": "Patient",
155 "select": [{ "column": columns }]
156 })
157 }
158
159 #[test]
160 fn typed_columns_map_to_value_x() {
161 let result = ProcessedResult {
162 columns: vec!["id".into(), "active".into(), "age".into()],
163 rows: vec![ProcessedRow {
164 values: vec![Some(json!("p1")), Some(json!(true)), Some(json!(42))],
165 }],
166 };
167 let v = view(json!([
168 {"name": "id", "path": "id", "type": "id"},
169 {"name": "active", "path": "active", "type": "boolean"},
170 {"name": "age", "path": "age", "type": "integer"}
171 ]));
172 let bytes = format_view_fhir_parameters(&result, &v).unwrap();
173 let out: Value = serde_json::from_slice(&bytes).unwrap();
174 let part = &out["parameter"][0]["part"];
175 assert_eq!(part[0]["valueId"], json!("p1"));
176 assert_eq!(part[1]["valueBoolean"], json!(true));
177 assert_eq!(part[2]["valueInteger"], json!(42));
178 }
179
180 #[test]
181 fn untyped_column_defaults_to_string_and_null_is_omitted() {
182 let result = ProcessedResult {
183 columns: vec!["name".into(), "missing".into()],
184 rows: vec![ProcessedRow {
185 values: vec![Some(json!("Smith")), None],
186 }],
187 };
188 let v = view(json!([{"name": "name", "path": "name.family"}]));
189 let bytes = format_view_fhir_parameters(&result, &v).unwrap();
190 let out: Value = serde_json::from_slice(&bytes).unwrap();
191 let parts = out["parameter"][0]["part"].as_array().unwrap();
192 assert_eq!(parts.len(), 1);
193 assert_eq!(parts[0]["valueString"], json!("Smith"));
194 }
195
196 #[test]
197 fn collection_column_repeats_part_per_element() {
198 let result = ProcessedResult {
199 columns: vec!["given".into()],
200 rows: vec![ProcessedRow {
201 values: vec![Some(json!(["John", "Quincy"]))],
202 }],
203 };
204 let v = view(json!([
205 {"name": "given", "path": "name.given", "type": "string", "collection": true}
206 ]));
207 let bytes = format_view_fhir_parameters(&result, &v).unwrap();
208 let out: Value = serde_json::from_slice(&bytes).unwrap();
209 let parts = out["parameter"][0]["part"].as_array().unwrap();
210 assert_eq!(parts.len(), 2);
211 assert_eq!(parts[0]["name"], json!("given"));
212 assert_eq!(parts[0]["valueString"], json!("John"));
213 assert_eq!(parts[1]["valueString"], json!("Quincy"));
214 }
215
216 #[test]
217 fn empty_result_omits_parameter_key() {
218 let result = ProcessedResult {
219 columns: vec!["id".into()],
220 rows: vec![],
221 };
222 let v = view(json!([{"name": "id", "path": "id", "type": "id"}]));
223 let bytes = format_view_fhir_parameters(&result, &v).unwrap();
224 let out: Value = serde_json::from_slice(&bytes).unwrap();
225 assert_eq!(out["resourceType"], json!("Parameters"));
226 assert!(out.get("parameter").is_none());
227 }
228
229 #[test]
230 fn complex_value_errors() {
231 let result = ProcessedResult {
232 columns: vec!["name".into()],
233 rows: vec![ProcessedRow {
234 values: vec![Some(json!({"family": "Smith"}))],
235 }],
236 };
237 let v = view(json!([{"name": "name", "path": "name"}]));
238 let err = format_view_fhir_parameters(&result, &v).unwrap_err();
239 assert!(matches!(err, SofError::InvalidViewDefinition(_)));
240 }
241
242 #[test]
243 fn accept_matching() {
244 assert!(accept_has_mime(
245 Some("text/csv, application/fhir+json;q=0.9"),
246 FHIR_JSON_MIME
247 ));
248 assert!(accept_has_mime(
249 Some("Application/FHIR+JSON"),
250 FHIR_JSON_MIME
251 ));
252 assert!(!accept_has_mime(Some("application/json"), FHIR_JSON_MIME));
253 assert!(!accept_has_mime(None, FHIR_JSON_MIME));
254
255 assert!(accept_requires_unsupported_fhir_xml(Some(
256 "application/fhir+xml"
257 )));
258 assert!(!accept_requires_unsupported_fhir_xml(Some(
259 "application/fhir+xml, application/fhir+json"
260 )));
261 assert!(!accept_requires_unsupported_fhir_xml(Some("text/csv")));
262 }
263
264 #[test]
265 fn binary_envelope_round_trips() {
266 let bytes = wrap_in_binary_envelope("text/csv", b"a,b\n1,2\n").unwrap();
267 let v: Value = serde_json::from_slice(&bytes).unwrap();
268 assert_eq!(v["resourceType"], json!("Binary"));
269 assert_eq!(v["contentType"], json!("text/csv"));
270 let decoded = base64::engine::general_purpose::STANDARD
271 .decode(v["data"].as_str().unwrap())
272 .unwrap();
273 assert_eq!(decoded, b"a,b\n1,2\n");
274 }
275}