1use serde_json::Value;
34
35#[derive(Debug, Default, Clone)]
39pub struct ExtractedRunParams {
40 pub format: Option<String>,
42 pub header: Option<bool>,
44 pub limit: Option<u64>,
46 pub since: Option<String>,
48 pub patient: Vec<String>,
50 pub group: Vec<String>,
52 pub subject_resource: Option<Value>,
55 pub subject_reference: Option<String>,
58 pub subject_canonical: Option<String>,
61 pub inline_resources: Vec<Value>,
63 pub source: Option<String>,
65 pub max_file_size: Option<u64>,
67 pub row_group_size: Option<u64>,
69 pub page_size: Option<u64>,
71 pub compression: Option<String>,
73}
74
75pub fn split_csv_refs(value: Option<&str>) -> Vec<String> {
81 match value {
82 Some(s) => s
83 .split(',')
84 .map(|t| t.trim().to_string())
85 .filter(|t| !t.is_empty())
86 .collect(),
87 None => Vec::new(),
88 }
89}
90
91pub fn body_has_subject(body: &Value) -> bool {
95 match body.get("resourceType").and_then(|v| v.as_str()) {
96 Some("ViewDefinition") => true,
97 Some("Parameters") => body
98 .get("parameter")
99 .and_then(|p| p.as_array())
100 .map(|params| {
101 params.iter().any(|p| {
102 matches!(
103 parameter_name(p).as_deref(),
104 Some("subjectResource")
105 | Some("subjectReference")
106 | Some("subjectCanonical")
107 )
108 })
109 })
110 .unwrap_or(false),
111 _ => false,
112 }
113}
114
115pub fn extract_run_params_from_json(body: &Value) -> ExtractedRunParams {
124 let mut out = ExtractedRunParams::default();
125
126 if body.get("resourceType").and_then(|v| v.as_str()) != Some("Parameters") {
127 return out;
128 }
129 let Some(entries) = body.get("parameter").and_then(|p| p.as_array()) else {
130 return out;
131 };
132
133 for p in entries {
134 let Some(name) = parameter_name(p) else {
135 continue;
136 };
137 match name.as_str() {
138 "_format" | "format" => {
139 if out.format.is_none() {
140 out.format = read_str(p, &["valueCode", "valueString"]);
141 }
142 }
143 "header" => {
144 if out.header.is_none() {
145 if let Some(b) = p.get("valueBoolean").and_then(|v| v.as_bool()) {
146 out.header = Some(b);
147 } else if let Some(s) = p.get("valueString").and_then(|v| v.as_str()) {
148 out.header = Some(s == "true" || s == "1");
149 }
150 }
151 }
152 "_limit" => {
153 if out.limit.is_none() {
154 out.limit = p
155 .get("valueInteger")
156 .or_else(|| p.get("valuePositiveInt"))
157 .and_then(|v| v.as_u64());
158 }
159 }
160 "_since" => {
161 if out.since.is_none() {
162 out.since = read_str(p, &["valueInstant", "valueDateTime", "valueString"]);
163 }
164 }
165 "patient" => {
166 if let Some(s) = read_reference_or_string(p) {
167 out.patient.push(s);
168 }
169 }
170 "group" => {
171 if let Some(s) = read_reference_or_string(p) {
172 out.group.push(s);
173 }
174 }
175 "subjectResource" => {
176 if out.subject_resource.is_none() {
177 if let Some(r) = p.get("resource") {
178 out.subject_resource = Some(r.clone());
179 }
180 }
181 }
182 "subjectReference" => {
183 if out.subject_reference.is_none() {
184 out.subject_reference = read_reference_or_string(p);
185 }
186 }
187 "subjectCanonical" => {
188 if out.subject_canonical.is_none() {
189 out.subject_canonical = read_str(
190 p,
191 &["valueCanonical", "valueUri", "valueUrl", "valueString"],
192 );
193 }
194 }
195 "resource" => {
196 if let Some(r) = p.get("resource") {
197 if r.get("resourceType").and_then(|v| v.as_str()) == Some("Bundle") {
204 if let Some(entries) = r.get("entry").and_then(|e| e.as_array()) {
205 for entry in entries {
206 if let Some(res) = entry.get("resource") {
207 out.inline_resources.push(res.clone());
208 }
209 }
210 }
211 } else {
212 out.inline_resources.push(r.clone());
213 }
214 }
215 }
216 "source" => {
217 if out.source.is_none() {
218 out.source = read_str(p, &["valueString", "valueUri"]);
219 }
220 }
221 "maxFileSize" => {
222 if out.max_file_size.is_none() {
223 out.max_file_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
224 }
225 }
226 "rowGroupSize" => {
227 if out.row_group_size.is_none() {
228 out.row_group_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
229 }
230 }
231 "pageSize" => {
232 if out.page_size.is_none() {
233 out.page_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
234 }
235 }
236 "compression" => {
237 if out.compression.is_none() {
238 out.compression = read_str(p, &["valueCode", "valueString"]);
239 }
240 }
241 _ => {}
242 }
243 }
244 out
245}
246
247fn parameter_name(p: &Value) -> Option<String> {
250 let raw = p.get("name")?;
251 if let Some(s) = raw.as_str() {
252 return Some(s.to_string());
253 }
254 if let Some(v) = raw.get("value").and_then(|v| v.as_str()) {
255 return Some(v.to_string());
256 }
257 None
258}
259
260fn read_str(p: &Value, keys: &[&str]) -> Option<String> {
261 for key in keys {
262 if let Some(s) = p.get(*key).and_then(|v| v.as_str()) {
263 return Some(s.to_string());
264 }
265 }
266 None
267}
268
269fn read_u64(p: &Value, keys: &[&str]) -> Option<u64> {
270 for key in keys {
271 if let Some(n) = p.get(*key).and_then(|v| v.as_u64()) {
272 return Some(n);
273 }
274 }
275 None
276}
277
278fn read_reference_or_string(p: &Value) -> Option<String> {
280 if let Some(s) = p
281 .get("valueReference")
282 .and_then(|r| r.get("reference"))
283 .and_then(|v| v.as_str())
284 {
285 return Some(s.to_string());
286 }
287 p.get("valueString")
288 .and_then(|v| v.as_str())
289 .map(|s| s.to_string())
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use serde_json::json;
296
297 fn params(parameter: Vec<Value>) -> Value {
298 json!({"resourceType": "Parameters", "parameter": parameter})
299 }
300
301 #[test]
302 fn bare_viewdefinition_detected() {
303 assert!(body_has_subject(&json!({"resourceType": "ViewDefinition"})));
304 }
305
306 #[test]
307 fn parameters_with_subject_resource_detected() {
308 assert!(body_has_subject(¶ms(vec![json!({
309 "name": "subjectResource",
310 "resource": {"resourceType": "ViewDefinition"}
311 })])));
312 }
313
314 #[test]
315 fn parameters_with_subject_reference_detected() {
316 assert!(body_has_subject(¶ms(vec![json!({
317 "name": "subjectReference",
318 "valueReference": {"reference": "ViewDefinition/x"}
319 })])));
320 }
321
322 #[test]
323 fn parameters_with_subject_canonical_detected() {
324 assert!(body_has_subject(¶ms(vec![json!({
325 "name": "subjectCanonical",
326 "valueCanonical": "http://example.org/ViewDefinition/x"
327 })])));
328 }
329
330 #[test]
331 fn parameters_without_subject_returns_false() {
332 assert!(!body_has_subject(¶ms(vec![json!({
333 "name": "patient",
334 "valueString": "Patient/123"
335 })])));
336 }
337
338 #[test]
339 fn pre_ballot_view_parameter_names_are_not_subjects() {
340 assert!(!body_has_subject(¶ms(vec![json!({
343 "name": "viewResource",
344 "resource": {"resourceType": "ViewDefinition"}
345 })])));
346 }
347
348 #[test]
349 fn empty_for_non_parameters_body() {
350 let p = extract_run_params_from_json(&json!({"resourceType": "ViewDefinition"}));
351 assert!(p.patient.is_empty());
352 assert!(p.format.is_none());
353 }
354
355 #[test]
356 fn format_accepts_code_and_string() {
357 let p = extract_run_params_from_json(¶ms(vec![
358 json!({"name": "_format", "valueCode": "csv"}),
359 ]));
360 assert_eq!(p.format.as_deref(), Some("csv"));
361 let p = extract_run_params_from_json(¶ms(vec![
362 json!({"name": "_format", "valueString": "csv"}),
363 ]));
364 assert_eq!(p.format.as_deref(), Some("csv"));
365 }
366
367 #[test]
368 fn header_boolean_or_string() {
369 let p = extract_run_params_from_json(¶ms(vec![
370 json!({"name": "header", "valueBoolean": true}),
371 ]));
372 assert_eq!(p.header, Some(true));
373 let p = extract_run_params_from_json(¶ms(vec![
374 json!({"name": "header", "valueString": "false"}),
375 ]));
376 assert_eq!(p.header, Some(false));
377 }
378
379 #[test]
380 fn limit_integer_or_positive_int() {
381 let p = extract_run_params_from_json(¶ms(vec![
382 json!({"name": "_limit", "valueInteger": 100}),
383 ]));
384 assert_eq!(p.limit, Some(100));
385 let p = extract_run_params_from_json(¶ms(vec![
386 json!({"name": "_limit", "valuePositiveInt": 50}),
387 ]));
388 assert_eq!(p.limit, Some(50));
389 }
390
391 #[test]
392 fn since_accepts_three_shapes() {
393 for key in ["valueInstant", "valueDateTime", "valueString"] {
394 let p = extract_run_params_from_json(¶ms(vec![
395 json!({"name": "_since", key: "2024-01-02T03:04:05Z"}),
396 ]));
397 assert_eq!(p.since.as_deref(), Some("2024-01-02T03:04:05Z"));
398 }
399 }
400
401 #[test]
402 fn patient_repeated_entries_accumulate() {
403 let p = extract_run_params_from_json(¶ms(vec![
404 json!({"name": "patient", "valueReference": {"reference": "Patient/1"}}),
405 json!({"name": "patient", "valueString": "Patient/2"}),
406 ]));
407 assert_eq!(
408 p.patient,
409 vec!["Patient/1".to_string(), "Patient/2".to_string()]
410 );
411 }
412
413 #[test]
414 fn group_repeated_entries_accumulate() {
415 let p = extract_run_params_from_json(¶ms(vec![
416 json!({"name": "group", "valueReference": {"reference": "Group/1"}}),
417 json!({"name": "group", "valueString": "Group/2"}),
418 ]));
419 assert_eq!(p.group, vec!["Group/1".to_string(), "Group/2".to_string()]);
420 }
421
422 #[test]
423 fn inline_resources_accumulate() {
424 let p = extract_run_params_from_json(¶ms(vec![
425 json!({"name": "resource", "resource": {"resourceType": "Patient", "id": "1"}}),
426 json!({"name": "resource", "resource": {"resourceType": "Patient", "id": "2"}}),
427 ]));
428 assert_eq!(p.inline_resources.len(), 2);
429 }
430
431 #[test]
432 fn subject_resource_extracted() {
433 let p = extract_run_params_from_json(¶ms(vec![json!({
434 "name": "subjectResource",
435 "resource": {"resourceType": "ViewDefinition"}
436 })]));
437 assert!(p.subject_resource.is_some());
438 }
439
440 #[test]
441 fn subject_reference_string_or_reference() {
442 let p = extract_run_params_from_json(¶ms(vec![json!({
443 "name": "subjectReference",
444 "valueReference": {"reference": "ViewDefinition/x"}
445 })]));
446 assert_eq!(p.subject_reference.as_deref(), Some("ViewDefinition/x"));
447 let p = extract_run_params_from_json(¶ms(vec![json!({
448 "name": "subjectReference",
449 "valueString": "ViewDefinition/y"
450 })]));
451 assert_eq!(p.subject_reference.as_deref(), Some("ViewDefinition/y"));
452 }
453
454 #[test]
455 fn subject_canonical_accepts_canonical_uri_and_string() {
456 for key in ["valueCanonical", "valueUri", "valueUrl", "valueString"] {
457 let p = extract_run_params_from_json(¶ms(vec![json!({
458 "name": "subjectCanonical",
459 key: "http://example.org/ViewDefinition/x|1.0.0"
460 })]));
461 assert_eq!(
462 p.subject_canonical.as_deref(),
463 Some("http://example.org/ViewDefinition/x|1.0.0"),
464 "key {key}"
465 );
466 }
467 }
468
469 #[test]
470 fn subject_canonical_and_reference_are_independent() {
471 let p = extract_run_params_from_json(¶ms(vec![
475 json!({"name": "subjectCanonical", "valueCanonical": "http://example.org/vd"}),
476 json!({"name": "subjectReference", "valueString": "ViewDefinition/x"}),
477 ]));
478 assert_eq!(
479 p.subject_canonical.as_deref(),
480 Some("http://example.org/vd")
481 );
482 assert_eq!(p.subject_reference.as_deref(), Some("ViewDefinition/x"));
483 }
484
485 #[test]
486 fn typed_serde_name_shape_accepted() {
487 let p = extract_run_params_from_json(¶ms(vec![json!({
489 "name": {"value": "_format"},
490 "valueCode": "json"
491 })]));
492 assert_eq!(p.format.as_deref(), Some("json"));
493 }
494
495 #[test]
496 fn parquet_options_extracted() {
497 let p = extract_run_params_from_json(¶ms(vec![
498 json!({"name": "maxFileSize", "valueInteger": 500}),
499 json!({"name": "rowGroupSize", "valueInteger": 128}),
500 json!({"name": "pageSize", "valueInteger": 1024}),
501 json!({"name": "compression", "valueCode": "snappy"}),
502 ]));
503 assert_eq!(p.max_file_size, Some(500));
504 assert_eq!(p.row_group_size, Some(128));
505 assert_eq!(p.page_size, Some(1024));
506 assert_eq!(p.compression.as_deref(), Some("snappy"));
507 }
508
509 #[test]
510 fn split_csv_refs_trims_and_drops_empty() {
511 assert_eq!(split_csv_refs(None), Vec::<String>::new());
512 assert_eq!(split_csv_refs(Some("")), Vec::<String>::new());
513 assert_eq!(
514 split_csv_refs(Some("Group/a, Group/b ,,Group/c")),
515 vec![
516 "Group/a".to_string(),
517 "Group/b".to_string(),
518 "Group/c".to_string()
519 ]
520 );
521 }
522
523 #[test]
524 fn unknown_param_names_ignored() {
525 let p = extract_run_params_from_json(¶ms(vec![
526 json!({"name": "_format", "valueCode": "json"}),
527 json!({"name": "unknownParam", "valueString": "ignored"}),
528 ]));
529 assert_eq!(p.format.as_deref(), Some("json"));
530 }
531}