use serde_json::Value;
#[derive(Debug, Default, Clone)]
pub struct ExtractedRunParams {
pub format: Option<String>,
pub header: Option<bool>,
pub limit: Option<u64>,
pub since: Option<String>,
pub patient: Vec<String>,
pub group: Vec<String>,
pub view_resource: Option<Value>,
pub view_reference: Option<String>,
pub inline_resources: Vec<Value>,
pub source: Option<String>,
pub max_file_size: Option<u64>,
pub row_group_size: Option<u64>,
pub page_size: Option<u64>,
pub compression: Option<String>,
}
pub fn split_csv_refs(value: Option<&str>) -> Vec<String> {
match value {
Some(s) => s
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect(),
None => Vec::new(),
}
}
pub fn body_has_view_definition(body: &Value) -> bool {
match body.get("resourceType").and_then(|v| v.as_str()) {
Some("ViewDefinition") => true,
Some("Parameters") => body
.get("parameter")
.and_then(|p| p.as_array())
.map(|params| {
params.iter().any(|p| {
matches!(
parameter_name(p).as_deref(),
Some("viewResource") | Some("viewReference")
)
})
})
.unwrap_or(false),
_ => false,
}
}
pub fn extract_run_params_from_json(body: &Value) -> ExtractedRunParams {
let mut out = ExtractedRunParams::default();
if body.get("resourceType").and_then(|v| v.as_str()) != Some("Parameters") {
return out;
}
let Some(entries) = body.get("parameter").and_then(|p| p.as_array()) else {
return out;
};
for p in entries {
let Some(name) = parameter_name(p) else {
continue;
};
match name.as_str() {
"_format" | "format" => {
if out.format.is_none() {
out.format = read_str(p, &["valueCode", "valueString"]);
}
}
"header" => {
if out.header.is_none() {
if let Some(b) = p.get("valueBoolean").and_then(|v| v.as_bool()) {
out.header = Some(b);
} else if let Some(s) = p.get("valueString").and_then(|v| v.as_str()) {
out.header = Some(s == "true" || s == "1");
}
}
}
"_limit" => {
if out.limit.is_none() {
out.limit = p
.get("valueInteger")
.or_else(|| p.get("valuePositiveInt"))
.and_then(|v| v.as_u64());
}
}
"_since" => {
if out.since.is_none() {
out.since = read_str(p, &["valueInstant", "valueDateTime", "valueString"]);
}
}
"patient" => {
if let Some(s) = read_reference_or_string(p) {
out.patient.push(s);
}
}
"group" => {
if let Some(s) = read_reference_or_string(p) {
out.group.push(s);
}
}
"viewResource" => {
if out.view_resource.is_none() {
if let Some(r) = p.get("resource") {
out.view_resource = Some(r.clone());
}
}
}
"viewReference" => {
if out.view_reference.is_none() {
out.view_reference = read_reference_or_string(p);
}
}
"resource" => {
if let Some(r) = p.get("resource") {
if r.get("resourceType").and_then(|v| v.as_str()) == Some("Bundle") {
if let Some(entries) = r.get("entry").and_then(|e| e.as_array()) {
for entry in entries {
if let Some(res) = entry.get("resource") {
out.inline_resources.push(res.clone());
}
}
}
} else {
out.inline_resources.push(r.clone());
}
}
}
"source" => {
if out.source.is_none() {
out.source = read_str(p, &["valueString", "valueUri"]);
}
}
"maxFileSize" => {
if out.max_file_size.is_none() {
out.max_file_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
}
}
"rowGroupSize" => {
if out.row_group_size.is_none() {
out.row_group_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
}
}
"pageSize" => {
if out.page_size.is_none() {
out.page_size = read_u64(p, &["valueInteger", "valuePositiveInt"]);
}
}
"compression" => {
if out.compression.is_none() {
out.compression = read_str(p, &["valueCode", "valueString"]);
}
}
_ => {}
}
}
out
}
fn parameter_name(p: &Value) -> Option<String> {
let raw = p.get("name")?;
if let Some(s) = raw.as_str() {
return Some(s.to_string());
}
if let Some(v) = raw.get("value").and_then(|v| v.as_str()) {
return Some(v.to_string());
}
None
}
fn read_str(p: &Value, keys: &[&str]) -> Option<String> {
for key in keys {
if let Some(s) = p.get(*key).and_then(|v| v.as_str()) {
return Some(s.to_string());
}
}
None
}
fn read_u64(p: &Value, keys: &[&str]) -> Option<u64> {
for key in keys {
if let Some(n) = p.get(*key).and_then(|v| v.as_u64()) {
return Some(n);
}
}
None
}
fn read_reference_or_string(p: &Value) -> Option<String> {
if let Some(s) = p
.get("valueReference")
.and_then(|r| r.get("reference"))
.and_then(|v| v.as_str())
{
return Some(s.to_string());
}
p.get("valueString")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn params(parameter: Vec<Value>) -> Value {
json!({"resourceType": "Parameters", "parameter": parameter})
}
#[test]
fn bare_viewdefinition_detected() {
assert!(body_has_view_definition(
&json!({"resourceType": "ViewDefinition"})
));
}
#[test]
fn parameters_with_view_resource_detected() {
assert!(body_has_view_definition(¶ms(vec![json!({
"name": "viewResource",
"resource": {"resourceType": "ViewDefinition"}
})])));
}
#[test]
fn parameters_with_view_reference_detected() {
assert!(body_has_view_definition(¶ms(vec![json!({
"name": "viewReference",
"valueReference": {"reference": "ViewDefinition/x"}
})])));
}
#[test]
fn parameters_without_view_returns_false() {
assert!(!body_has_view_definition(¶ms(vec![json!({
"name": "patient",
"valueString": "Patient/123"
})])));
}
#[test]
fn empty_for_non_parameters_body() {
let p = extract_run_params_from_json(&json!({"resourceType": "ViewDefinition"}));
assert!(p.patient.is_empty());
assert!(p.format.is_none());
}
#[test]
fn format_accepts_code_and_string() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "_format", "valueCode": "csv"}),
]));
assert_eq!(p.format.as_deref(), Some("csv"));
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "_format", "valueString": "csv"}),
]));
assert_eq!(p.format.as_deref(), Some("csv"));
}
#[test]
fn header_boolean_or_string() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "header", "valueBoolean": true}),
]));
assert_eq!(p.header, Some(true));
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "header", "valueString": "false"}),
]));
assert_eq!(p.header, Some(false));
}
#[test]
fn limit_integer_or_positive_int() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "_limit", "valueInteger": 100}),
]));
assert_eq!(p.limit, Some(100));
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "_limit", "valuePositiveInt": 50}),
]));
assert_eq!(p.limit, Some(50));
}
#[test]
fn since_accepts_three_shapes() {
for key in ["valueInstant", "valueDateTime", "valueString"] {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "_since", key: "2024-01-02T03:04:05Z"}),
]));
assert_eq!(p.since.as_deref(), Some("2024-01-02T03:04:05Z"));
}
}
#[test]
fn patient_repeated_entries_accumulate() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "patient", "valueReference": {"reference": "Patient/1"}}),
json!({"name": "patient", "valueString": "Patient/2"}),
]));
assert_eq!(
p.patient,
vec!["Patient/1".to_string(), "Patient/2".to_string()]
);
}
#[test]
fn group_repeated_entries_accumulate() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "group", "valueReference": {"reference": "Group/1"}}),
json!({"name": "group", "valueString": "Group/2"}),
]));
assert_eq!(p.group, vec!["Group/1".to_string(), "Group/2".to_string()]);
}
#[test]
fn inline_resources_accumulate() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "resource", "resource": {"resourceType": "Patient", "id": "1"}}),
json!({"name": "resource", "resource": {"resourceType": "Patient", "id": "2"}}),
]));
assert_eq!(p.inline_resources.len(), 2);
}
#[test]
fn view_resource_extracted() {
let p = extract_run_params_from_json(¶ms(vec![json!({
"name": "viewResource",
"resource": {"resourceType": "ViewDefinition"}
})]));
assert!(p.view_resource.is_some());
}
#[test]
fn view_reference_string_or_reference() {
let p = extract_run_params_from_json(¶ms(vec![json!({
"name": "viewReference",
"valueReference": {"reference": "ViewDefinition/x"}
})]));
assert_eq!(p.view_reference.as_deref(), Some("ViewDefinition/x"));
let p = extract_run_params_from_json(¶ms(vec![json!({
"name": "viewReference",
"valueString": "ViewDefinition/y"
})]));
assert_eq!(p.view_reference.as_deref(), Some("ViewDefinition/y"));
}
#[test]
fn typed_serde_name_shape_accepted() {
let p = extract_run_params_from_json(¶ms(vec![json!({
"name": {"value": "_format"},
"valueCode": "json"
})]));
assert_eq!(p.format.as_deref(), Some("json"));
}
#[test]
fn parquet_options_extracted() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "maxFileSize", "valueInteger": 500}),
json!({"name": "rowGroupSize", "valueInteger": 128}),
json!({"name": "pageSize", "valueInteger": 1024}),
json!({"name": "compression", "valueCode": "snappy"}),
]));
assert_eq!(p.max_file_size, Some(500));
assert_eq!(p.row_group_size, Some(128));
assert_eq!(p.page_size, Some(1024));
assert_eq!(p.compression.as_deref(), Some("snappy"));
}
#[test]
fn split_csv_refs_trims_and_drops_empty() {
assert_eq!(split_csv_refs(None), Vec::<String>::new());
assert_eq!(split_csv_refs(Some("")), Vec::<String>::new());
assert_eq!(
split_csv_refs(Some("Group/a, Group/b ,,Group/c")),
vec![
"Group/a".to_string(),
"Group/b".to_string(),
"Group/c".to_string()
]
);
}
#[test]
fn unknown_param_names_ignored() {
let p = extract_run_params_from_json(¶ms(vec![
json!({"name": "_format", "valueCode": "json"}),
json!({"name": "unknownParam", "valueString": "ignored"}),
]));
assert_eq!(p.format.as_deref(), Some("json"));
}
}