1use serde_json::{Map, Value, json};
10
11use super::{ColumnFhirType, QueryResult, SqlQueryError};
12
13pub fn format_fhir_parameters(result: &QueryResult) -> Result<Vec<u8>, SqlQueryError> {
22 let mut row_params: Vec<Value> = Vec::with_capacity(result.rows.len());
23 for row in &result.rows {
24 let mut parts: Vec<Value> = Vec::with_capacity(row.len());
25 for (i, cell) in row.iter().enumerate() {
26 let Some(value) = cell else {
27 continue; };
29 let col_name = &result.columns[i];
30 let col_type = result
31 .column_types
32 .get(i)
33 .cloned()
34 .unwrap_or(ColumnFhirType::String("string".into()));
35 let part = value_to_fhir_part(col_name, value, &col_type)?;
36 parts.push(part);
37 }
38 row_params.push(json!({ "name": "row", "part": parts }));
39 }
40 let body = if row_params.is_empty() {
41 json!({ "resourceType": "Parameters" })
42 } else {
43 json!({ "resourceType": "Parameters", "parameter": row_params })
44 };
45 serde_json::to_vec(&body).map_err(|e| SqlQueryError::MalformedLibrary(e.to_string()))
46}
47
48pub(crate) fn value_to_fhir_part(
49 name: &str,
50 value: &Value,
51 ty: &ColumnFhirType,
52) -> Result<Value, SqlQueryError> {
53 if matches!(value, Value::Object(_) | Value::Array(_)) {
54 return Err(SqlQueryError::UnsupportedFhirValue(name.to_string()));
55 }
56
57 let (key, json_value): (&'static str, Value) = match ty {
58 ColumnFhirType::Boolean => ("valueBoolean", coerce_bool(value)),
59 ColumnFhirType::Integer => {
60 match integer_or_promote(value) {
64 IntKind::Integer(v) => ("valueInteger", Value::Number(v.into())),
65 IntKind::Integer64(s) => ("valueInteger64", Value::String(s)),
66 IntKind::Other(v) => ("valueInteger", v),
67 }
68 }
69 ColumnFhirType::Integer64 => ("valueInteger64", coerce_integer64_string(value)),
71 ColumnFhirType::Decimal => ("valueDecimal", coerce_decimal(value)),
72 ColumnFhirType::Date => ("valueDate", coerce_string(value)),
73 ColumnFhirType::DateTime => ("valueDateTime", coerce_string(value)),
74 ColumnFhirType::Instant => ("valueInstant", coerce_instant(value)),
76 ColumnFhirType::Time => ("valueTime", coerce_string(value)),
77 ColumnFhirType::Base64Binary => ("valueBase64Binary", coerce_string(value)),
78 ColumnFhirType::String(code) => (value_x_key_for(code), coerce_string(value)),
79 };
80
81 let mut obj = Map::new();
82 obj.insert("name".to_string(), Value::String(name.to_string()));
83 obj.insert(key.to_string(), json_value);
84 Ok(Value::Object(obj))
85}
86
87enum IntKind {
88 Integer(i32),
89 Integer64(String),
90 Other(Value),
91}
92
93fn integer_or_promote(v: &Value) -> IntKind {
97 if let Some(i) = v.as_i64() {
98 if (i32::MIN as i64..=i32::MAX as i64).contains(&i) {
99 IntKind::Integer(i as i32)
100 } else {
101 IntKind::Integer64(i.to_string())
102 }
103 } else if let Some(s) = v.as_str() {
104 if let Ok(i) = s.parse::<i64>() {
105 if (i32::MIN as i64..=i32::MAX as i64).contains(&i) {
106 IntKind::Integer(i as i32)
107 } else {
108 IntKind::Integer64(i.to_string())
109 }
110 } else {
111 IntKind::Other(Value::String(s.to_string()))
112 }
113 } else {
114 IntKind::Other(coerce_integer(v))
115 }
116}
117
118fn coerce_instant(v: &Value) -> Value {
123 let Value::String(s) = v else {
124 return coerce_string(v);
125 };
126 match chrono::DateTime::parse_from_rfc3339(s) {
127 Ok(dt) => {
128 let rounded = dt
129 .with_timezone(&chrono::Utc)
130 .format("%Y-%m-%dT%H:%M:%S%.3fZ")
131 .to_string();
132 Value::String(rounded)
133 }
134 Err(_) => Value::String(s.clone()),
135 }
136}
137
138fn value_x_key_for(code: &str) -> &'static str {
139 match code {
140 "code" => "valueCode",
141 "id" => "valueId",
142 "uri" => "valueUri",
143 "url" => "valueUrl",
144 "canonical" => "valueCanonical",
145 "markdown" => "valueMarkdown",
146 "oid" => "valueOid",
147 "uuid" => "valueUuid",
148 _ => "valueString",
149 }
150}
151
152fn coerce_bool(v: &Value) -> Value {
153 match v {
154 Value::Bool(b) => Value::Bool(*b),
155 Value::Number(n) => Value::Bool(n.as_i64().unwrap_or(0) != 0),
156 Value::String(s) => Value::Bool(s == "true" || s == "1"),
157 _ => Value::Null,
158 }
159}
160
161fn coerce_integer(v: &Value) -> Value {
162 match v {
163 Value::Number(n) if n.is_i64() => Value::Number(n.clone()),
164 Value::Number(n) => n
165 .as_f64()
166 .and_then(|f| {
167 if f.fract() == 0.0 {
168 serde_json::Number::from_f64(f).map(Value::Number)
169 } else {
170 None
171 }
172 })
173 .unwrap_or(Value::Null),
174 Value::String(s) => s
175 .parse::<i64>()
176 .map(|i| Value::Number(i.into()))
177 .unwrap_or_else(|_| Value::String(s.clone())),
178 _ => Value::Null,
179 }
180}
181
182fn coerce_integer64_string(v: &Value) -> Value {
183 match v {
184 Value::Number(n) if n.is_i64() => Value::String(n.to_string()),
185 Value::String(s) => Value::String(s.clone()),
186 other => Value::String(other.to_string()),
187 }
188}
189
190fn coerce_decimal(v: &Value) -> Value {
191 match v {
192 Value::Number(n) => Value::Number(n.clone()),
193 Value::String(s) => match s.parse::<f64>() {
194 Ok(f) => serde_json::Number::from_f64(f)
195 .map(Value::Number)
196 .unwrap_or_else(|| Value::String(s.clone())),
197 Err(_) => Value::String(s.clone()),
198 },
199 _ => Value::Null,
200 }
201}
202
203fn coerce_string(v: &Value) -> Value {
204 match v {
205 Value::String(s) => Value::String(s.clone()),
206 Value::Number(n) => Value::String(n.to_string()),
207 Value::Bool(b) => Value::String(b.to_string()),
208 _ => Value::Null,
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use serde_json::json;
216
217 fn qr(columns: &[(&str, ColumnFhirType)], rows: Vec<Vec<Option<Value>>>) -> QueryResult {
218 QueryResult {
219 columns: columns.iter().map(|(n, _)| (*n).to_string()).collect(),
220 column_types: columns.iter().map(|(_, t)| t.clone()).collect(),
221 rows,
222 }
223 }
224
225 #[test]
226 fn renders_basic_types() {
227 let result = qr(
228 &[
229 ("b", ColumnFhirType::Boolean),
230 ("i", ColumnFhirType::Integer),
231 ("i64", ColumnFhirType::Integer64),
232 ("d", ColumnFhirType::Decimal),
233 ("s", ColumnFhirType::String("string".into())),
234 ("c", ColumnFhirType::String("code".into())),
235 ("ts", ColumnFhirType::Instant),
236 ],
237 vec![vec![
238 Some(json!(true)),
239 Some(json!(42)),
240 Some(json!(9999999999_i64)),
241 Some(json!(2.5)),
242 Some(json!("hello")),
243 Some(json!("a-code")),
244 Some(json!("2025-01-02T03:04:05Z")),
245 ]],
246 );
247 let bytes = format_fhir_parameters(&result).unwrap();
248 let v: Value = serde_json::from_slice(&bytes).unwrap();
249 let row = &v["parameter"][0]["part"];
250 assert_eq!(row[0]["valueBoolean"], json!(true));
251 assert_eq!(row[1]["valueInteger"], json!(42));
252 assert_eq!(row[2]["valueInteger64"], json!("9999999999"));
253 assert_eq!(row[3]["valueDecimal"], json!(2.5));
254 assert_eq!(row[4]["valueString"], json!("hello"));
255 assert_eq!(row[5]["valueCode"], json!("a-code"));
256 assert_eq!(row[6]["valueInstant"], json!("2025-01-02T03:04:05.000Z"));
258 }
259
260 #[test]
261 fn instant_normalises_subsecond_precision_to_millis() {
262 let result = qr(
263 &[("ts", ColumnFhirType::Instant)],
264 vec![vec![Some(json!("2025-01-02T03:04:05.123456789Z"))]],
265 );
266 let bytes = format_fhir_parameters(&result).unwrap();
267 let v: Value = serde_json::from_slice(&bytes).unwrap();
268 assert_eq!(
270 v["parameter"][0]["part"][0]["valueInstant"],
271 json!("2025-01-02T03:04:05.123Z")
272 );
273 }
274
275 #[test]
276 fn integer_inference_promotes_out_of_i32_range_to_integer64() {
277 let result = qr(
280 &[("n", ColumnFhirType::Integer)],
281 vec![vec![Some(json!(9_999_999_999_i64))]],
282 );
283 let bytes = format_fhir_parameters(&result).unwrap();
284 let v: Value = serde_json::from_slice(&bytes).unwrap();
285 let part = &v["parameter"][0]["part"][0];
286 assert!(part.get("valueInteger").is_none());
287 assert_eq!(part["valueInteger64"], json!("9999999999"));
288 }
289
290 #[test]
291 fn null_columns_omitted() {
292 let result = qr(
293 &[
294 ("a", ColumnFhirType::Integer),
295 ("b", ColumnFhirType::Integer),
296 ],
297 vec![vec![Some(json!(1)), None]],
298 );
299 let bytes = format_fhir_parameters(&result).unwrap();
300 let v: Value = serde_json::from_slice(&bytes).unwrap();
301 let part = &v["parameter"][0]["part"];
302 assert_eq!(part.as_array().unwrap().len(), 1);
303 assert_eq!(part[0]["name"], json!("a"));
304 }
305
306 #[test]
307 fn empty_result_omits_parameter_key() {
308 let result = qr(&[("id", ColumnFhirType::String("id".into()))], Vec::new());
312 let bytes = format_fhir_parameters(&result).unwrap();
313 let v: Value = serde_json::from_slice(&bytes).unwrap();
314 assert_eq!(v["resourceType"], json!("Parameters"));
315 assert!(
316 v.get("parameter").is_none(),
317 "empty result must omit the 'parameter' key, got {v}"
318 );
319 }
320
321 #[test]
322 fn composite_value_errors() {
323 let result = qr(
324 &[("a", ColumnFhirType::String("string".into()))],
325 vec![vec![Some(json!({"nested": 1}))]],
326 );
327 let err = format_fhir_parameters(&result).unwrap_err();
328 assert!(matches!(err, SqlQueryError::UnsupportedFhirValue(_)));
329 }
330}