1use serde_json::Value;
16
17#[derive(Debug, Default, Clone)]
21pub struct ExtractedRunParams {
22 pub format: Option<String>,
24 pub header: Option<bool>,
26 pub limit: Option<u64>,
28 pub since: Option<String>,
30 pub patient: Vec<String>,
32 pub group: Vec<String>,
34 pub view_resource: Option<Value>,
36 pub view_reference: Option<String>,
38 pub inline_resources: Vec<Value>,
40 pub source: Option<String>,
42 pub max_file_size: Option<u64>,
44 pub row_group_size: Option<u64>,
46 pub page_size: Option<u64>,
48 pub compression: Option<String>,
50}
51
52pub fn split_csv_refs(value: Option<&str>) -> Vec<String> {
58 match value {
59 Some(s) => s
60 .split(',')
61 .map(|t| t.trim().to_string())
62 .filter(|t| !t.is_empty())
63 .collect(),
64 None => Vec::new(),
65 }
66}
67
68pub fn body_has_view_definition(body: &Value) -> bool {
72 match body.get("resourceType").and_then(|v| v.as_str()) {
73 Some("ViewDefinition") => true,
74 Some("Parameters") => body
75 .get("parameter")
76 .and_then(|p| p.as_array())
77 .map(|params| {
78 params.iter().any(|p| {
79 matches!(
80 parameter_name(p).as_deref(),
81 Some("viewResource") | Some("viewReference")
82 )
83 })
84 })
85 .unwrap_or(false),
86 _ => false,
87 }
88}
89
90pub fn extract_run_params_from_json(body: &Value) -> ExtractedRunParams {
99 let mut out = ExtractedRunParams::default();
100
101 if body.get("resourceType").and_then(|v| v.as_str()) != Some("Parameters") {
102 return out;
103 }
104 let Some(entries) = body.get("parameter").and_then(|p| p.as_array()) else {
105 return out;
106 };
107
108 for p in entries {
109 let Some(name) = parameter_name(p) else {
110 continue;
111 };
112 match name.as_str() {
113 "_format" | "format" => {
114 if out.format.is_none() {
115 out.format = read_str(p, &["valueCode", "valueString"]);
116 }
117 }
118 "header" => {
119 if out.header.is_none() {
120 if let Some(b) = p.get("valueBoolean").and_then(|v| v.as_bool()) {
121 out.header = Some(b);
122 } else if let Some(s) = p.get("valueString").and_then(|v| v.as_str()) {
123 out.header = Some(s == "true" || s == "1");
124 }
125 }
126 }
127 "_limit" => {
128 if out.limit.is_none() {
129 out.limit = p
130 .get("valueInteger")
131 .or_else(|| p.get("valuePositiveInt"))
132 .and_then(|v| v.as_u64());
133 }
134 }
135 "_since" => {
136 if out.since.is_none() {
137 out.since = read_str(p, &["valueInstant", "valueDateTime", "valueString"]);
138 }
139 }
140 "patient" => {
141 if let Some(s) = read_reference_or_string(p) {
142 out.patient.push(s);
143 }
144 }
145 "group" => {
146 if let Some(s) = read_reference_or_string(p) {
147 out.group.push(s);
148 }
149 }
150 "viewResource" => {
151 if out.view_resource.is_none() {
152 if let Some(r) = p.get("resource") {
153 out.view_resource = Some(r.clone());
154 }
155 }
156 }
157 "viewReference" => {
158 if out.view_reference.is_none() {
159 out.view_reference = read_reference_or_string(p);
160 }
161 }
162 "resource" => {
163 if let Some(r) = p.get("resource") {
164 if r.get("resourceType").and_then(|v| v.as_str()) == Some("Bundle") {
171 if let Some(entries) = r.get("entry").and_then(|e| e.as_array()) {
172 for entry in entries {
173 if let Some(res) = entry.get("resource") {
174 out.inline_resources.push(res.clone());
175 }
176 }
177 }
178 } else {
179 out.inline_resources.push(r.clone());
180 }
181 }
182 }
183 "source" => {
184 if out.source.is_none() {
185 out.source = read_str(p, &["valueString", "valueUri"]);
186 }
187 }
188 "maxFileSize" => {
189 if out.max_file_size.is_none() {
190 out.max_file_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
191 }
192 }
193 "rowGroupSize" => {
194 if out.row_group_size.is_none() {
195 out.row_group_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
196 }
197 }
198 "pageSize" => {
199 if out.page_size.is_none() {
200 out.page_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
201 }
202 }
203 "compression" => {
204 if out.compression.is_none() {
205 out.compression = read_str(p, &["valueCode", "valueString"]);
206 }
207 }
208 _ => {}
209 }
210 }
211 out
212}
213
214fn parameter_name(p: &Value) -> Option<String> {
217 let raw = p.get("name")?;
218 if let Some(s) = raw.as_str() {
219 return Some(s.to_string());
220 }
221 if let Some(v) = raw.get("value").and_then(|v| v.as_str()) {
222 return Some(v.to_string());
223 }
224 None
225}
226
227fn read_str(p: &Value, keys: &[&str]) -> Option<String> {
228 for key in keys {
229 if let Some(s) = p.get(*key).and_then(|v| v.as_str()) {
230 return Some(s.to_string());
231 }
232 }
233 None
234}
235
236fn read_u64(p: &Value, keys: &[&str]) -> Option<u64> {
237 for key in keys {
238 if let Some(n) = p.get(*key).and_then(|v| v.as_u64()) {
239 return Some(n);
240 }
241 }
242 None
243}
244
245fn read_reference_or_string(p: &Value) -> Option<String> {
247 if let Some(s) = p
248 .get("valueReference")
249 .and_then(|r| r.get("reference"))
250 .and_then(|v| v.as_str())
251 {
252 return Some(s.to_string());
253 }
254 p.get("valueString")
255 .and_then(|v| v.as_str())
256 .map(|s| s.to_string())
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use serde_json::json;
263
264 fn params(parameter: Vec<Value>) -> Value {
265 json!({"resourceType": "Parameters", "parameter": parameter})
266 }
267
268 #[test]
269 fn bare_viewdefinition_detected() {
270 assert!(body_has_view_definition(
271 &json!({"resourceType": "ViewDefinition"})
272 ));
273 }
274
275 #[test]
276 fn parameters_with_view_resource_detected() {
277 assert!(body_has_view_definition(¶ms(vec![json!({
278 "name": "viewResource",
279 "resource": {"resourceType": "ViewDefinition"}
280 })])));
281 }
282
283 #[test]
284 fn parameters_with_view_reference_detected() {
285 assert!(body_has_view_definition(¶ms(vec![json!({
286 "name": "viewReference",
287 "valueReference": {"reference": "ViewDefinition/x"}
288 })])));
289 }
290
291 #[test]
292 fn parameters_without_view_returns_false() {
293 assert!(!body_has_view_definition(¶ms(vec![json!({
294 "name": "patient",
295 "valueString": "Patient/123"
296 })])));
297 }
298
299 #[test]
300 fn empty_for_non_parameters_body() {
301 let p = extract_run_params_from_json(&json!({"resourceType": "ViewDefinition"}));
302 assert!(p.patient.is_empty());
303 assert!(p.format.is_none());
304 }
305
306 #[test]
307 fn format_accepts_code_and_string() {
308 let p = extract_run_params_from_json(¶ms(vec![
309 json!({"name": "_format", "valueCode": "csv"}),
310 ]));
311 assert_eq!(p.format.as_deref(), Some("csv"));
312 let p = extract_run_params_from_json(¶ms(vec![
313 json!({"name": "_format", "valueString": "csv"}),
314 ]));
315 assert_eq!(p.format.as_deref(), Some("csv"));
316 }
317
318 #[test]
319 fn header_boolean_or_string() {
320 let p = extract_run_params_from_json(¶ms(vec![
321 json!({"name": "header", "valueBoolean": true}),
322 ]));
323 assert_eq!(p.header, Some(true));
324 let p = extract_run_params_from_json(¶ms(vec![
325 json!({"name": "header", "valueString": "false"}),
326 ]));
327 assert_eq!(p.header, Some(false));
328 }
329
330 #[test]
331 fn limit_integer_or_positive_int() {
332 let p = extract_run_params_from_json(¶ms(vec![
333 json!({"name": "_limit", "valueInteger": 100}),
334 ]));
335 assert_eq!(p.limit, Some(100));
336 let p = extract_run_params_from_json(¶ms(vec![
337 json!({"name": "_limit", "valuePositiveInt": 50}),
338 ]));
339 assert_eq!(p.limit, Some(50));
340 }
341
342 #[test]
343 fn since_accepts_three_shapes() {
344 for key in ["valueInstant", "valueDateTime", "valueString"] {
345 let p = extract_run_params_from_json(¶ms(vec![
346 json!({"name": "_since", key: "2024-01-02T03:04:05Z"}),
347 ]));
348 assert_eq!(p.since.as_deref(), Some("2024-01-02T03:04:05Z"));
349 }
350 }
351
352 #[test]
353 fn patient_repeated_entries_accumulate() {
354 let p = extract_run_params_from_json(¶ms(vec![
355 json!({"name": "patient", "valueReference": {"reference": "Patient/1"}}),
356 json!({"name": "patient", "valueString": "Patient/2"}),
357 ]));
358 assert_eq!(
359 p.patient,
360 vec!["Patient/1".to_string(), "Patient/2".to_string()]
361 );
362 }
363
364 #[test]
365 fn group_repeated_entries_accumulate() {
366 let p = extract_run_params_from_json(¶ms(vec![
367 json!({"name": "group", "valueReference": {"reference": "Group/1"}}),
368 json!({"name": "group", "valueString": "Group/2"}),
369 ]));
370 assert_eq!(p.group, vec!["Group/1".to_string(), "Group/2".to_string()]);
371 }
372
373 #[test]
374 fn inline_resources_accumulate() {
375 let p = extract_run_params_from_json(¶ms(vec![
376 json!({"name": "resource", "resource": {"resourceType": "Patient", "id": "1"}}),
377 json!({"name": "resource", "resource": {"resourceType": "Patient", "id": "2"}}),
378 ]));
379 assert_eq!(p.inline_resources.len(), 2);
380 }
381
382 #[test]
383 fn view_resource_extracted() {
384 let p = extract_run_params_from_json(¶ms(vec![json!({
385 "name": "viewResource",
386 "resource": {"resourceType": "ViewDefinition"}
387 })]));
388 assert!(p.view_resource.is_some());
389 }
390
391 #[test]
392 fn view_reference_string_or_reference() {
393 let p = extract_run_params_from_json(¶ms(vec![json!({
394 "name": "viewReference",
395 "valueReference": {"reference": "ViewDefinition/x"}
396 })]));
397 assert_eq!(p.view_reference.as_deref(), Some("ViewDefinition/x"));
398 let p = extract_run_params_from_json(¶ms(vec![json!({
399 "name": "viewReference",
400 "valueString": "ViewDefinition/y"
401 })]));
402 assert_eq!(p.view_reference.as_deref(), Some("ViewDefinition/y"));
403 }
404
405 #[test]
406 fn typed_serde_name_shape_accepted() {
407 let p = extract_run_params_from_json(¶ms(vec![json!({
409 "name": {"value": "_format"},
410 "valueCode": "json"
411 })]));
412 assert_eq!(p.format.as_deref(), Some("json"));
413 }
414
415 #[test]
416 fn parquet_options_extracted() {
417 let p = extract_run_params_from_json(¶ms(vec![
418 json!({"name": "maxFileSize", "valueInteger": 500}),
419 json!({"name": "rowGroupSize", "valueInteger": 128}),
420 json!({"name": "pageSize", "valueInteger": 1024}),
421 json!({"name": "compression", "valueCode": "snappy"}),
422 ]));
423 assert_eq!(p.max_file_size, Some(500));
424 assert_eq!(p.row_group_size, Some(128));
425 assert_eq!(p.page_size, Some(1024));
426 assert_eq!(p.compression.as_deref(), Some("snappy"));
427 }
428
429 #[test]
430 fn split_csv_refs_trims_and_drops_empty() {
431 assert_eq!(split_csv_refs(None), Vec::<String>::new());
432 assert_eq!(split_csv_refs(Some("")), Vec::<String>::new());
433 assert_eq!(
434 split_csv_refs(Some("Group/a, Group/b ,,Group/c")),
435 vec![
436 "Group/a".to_string(),
437 "Group/b".to_string(),
438 "Group/c".to_string()
439 ]
440 );
441 }
442
443 #[test]
444 fn unknown_param_names_ignored() {
445 let p = extract_run_params_from_json(¶ms(vec![
446 json!({"name": "_format", "valueCode": "json"}),
447 json!({"name": "unknownParam", "valueString": "ignored"}),
448 ]));
449 assert_eq!(p.format.as_deref(), Some("json"));
450 }
451}