use crate::{
ContentType, RunOptions, SofBundle, SofError, SofViewDefinition,
create_bundle_from_resources_for_version as sof_create_bundle_from_resources_for_version,
data_source::{DataSource, UniversalDataSource},
fhir_format::{self, accept_requires_unsupported_fhir_xml},
filter_resources_by_patient_and_group as sof_filter_resources_by_patient_and_group,
filter_resources_by_since as sof_filter_resources_by_since, format_parquet_multi_file,
get_fhir_version_string, get_newest_enabled_fhir_version,
lint::{Diagnostic, Severity, lint_view_definition},
parse_view_definition_for_version as sof_parse_view_definition_for_version,
process_view_definition, run_view_definition_with_options,
run_view_definition_with_options_remote,
};
use axum::{
Json,
extract::Query,
http::{HeaderMap, StatusCode, header},
response::{IntoResponse, Response},
};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use tracing::{debug, info};
use super::{
error::{ServerError, ServerResult},
models::{
ExtractedParameters, RunParameters, RunQueryParams, extract_all_parameters,
parse_content_type, validate_query_params,
},
};
pub async fn capability_statement() -> ServerResult<impl IntoResponse> {
info!("Handling CapabilityStatement request");
let capability_statement = create_capability_statement();
Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, "application/fhir+json")],
Json(capability_statement),
))
}
pub async fn sql_run_handler(
Query(params): Query<RunQueryParams>,
headers: HeaderMap,
body: Option<Json<serde_json::Value>>,
) -> ServerResult<Response> {
info!("Handling $sql-run request");
debug!("Query params: {:?}", params);
let accept_header = headers.get(header::ACCEPT).and_then(|h| h.to_str().ok());
if accept_requires_unsupported_fhir_xml(accept_header) {
return Err(ServerError::NotAcceptable(
"the application/fhir+xml representation is not supported; \
use application/fhir+json"
.to_string(),
));
}
let is_fhir_format = |f: &str| {
f.eq_ignore_ascii_case("fhir") || f.eq_ignore_ascii_case(fhir_format::FHIR_JSON_MIME)
};
let query_format = params.format.clone();
let query_format_fhir = query_format.as_deref().map(is_fhir_format).unwrap_or(false);
let mut params = params;
if query_format_fhir {
params.format = None;
}
let Some(Json(body)) = body else {
return Err(ServerError::BadRequest(
"GET $sql-run requires the subject to be resolvable by canonical URL or reference; \
this stateless server resolves neither. Use POST with subjectResource in a \
Parameters body instead."
.to_string(),
));
};
let validated_params =
validate_query_params(¶ms, accept_header).map_err(ServerError::BadRequest)?;
let is_bare_view_definition =
body.get("resourceType").and_then(|v| v.as_str()) == Some("ViewDefinition");
let inline_view_definition = if is_bare_view_definition {
Some(&body)
} else {
find_inline_view_definition(&body)
};
if let Some(view_definition_json) = inline_view_definition {
lint_inline_view_definition(view_definition_json)?;
}
let extracted_params = if is_bare_view_definition {
ExtractedParameters {
subject_resource: Some(body),
..Default::default()
}
} else {
let parameters = parse_parameters(body)?;
extract_all_parameters(parameters).map_err(ServerError::BadRequest)?
};
if extracted_params.subject_reference.is_some() || extracted_params.subject_canonical.is_some()
{
return Err(ServerError::NotImplemented(
"This stateless server resolves neither subjectCanonical nor subjectReference. \
Supply the subject inline with subjectResource."
.to_string(),
));
}
let view_def_json = extracted_params.subject_resource;
let resources_json = if extracted_params.resources.is_empty() {
None
} else {
Some(extracted_params.resources)
};
let format_from_body = extracted_params.format;
let header_from_body = extracted_params.header;
let body_format_fhir = format_from_body
.as_deref()
.map(is_fhir_format)
.unwrap_or(false);
let fhir_output = match format_from_body.as_deref().or(query_format.as_deref()) {
Some(f) => is_fhir_format(f),
None => fhir_format::accept_has_mime(accept_header, fhir_format::FHIR_JSON_MIME),
};
let wants_envelope =
!fhir_output && fhir_format::accept_has_mime(accept_header, fhir_format::FHIR_JSON_MIME);
let format_from_body = if body_format_fhir {
None
} else {
format_from_body
};
let view_def_json = view_def_json
.ok_or_else(|| ServerError::BadRequest("No ViewDefinition provided".to_string()))?;
let mut validated_params = validated_params;
if let Some(format_str) = format_from_body {
let header_param = if let Some(h) = header_from_body {
Some(h)
} else {
match params.header.as_deref() {
Some("true") => Some(true),
Some("false") => Some(false),
_ => None,
}
};
let content_type = parse_content_type(
None, Some(&format_str),
header_param,
)?;
validated_params.format = content_type;
} else if let Some(header_bool) = header_from_body {
if matches!(
validated_params.format,
ContentType::Csv | ContentType::CsvWithHeader
) {
let content_type = parse_content_type(None, Some("text/csv"), Some(header_bool))?;
validated_params.format = content_type;
}
}
let mut filtered_resources = resources_json.unwrap_or_default();
let patient_filter: Vec<String> = if !extracted_params.patient.is_empty() {
extracted_params.patient
} else {
crate::split_csv_refs(validated_params.patient.as_deref())
};
let group_filter: Vec<String> = if !extracted_params.group.is_empty() {
extracted_params.group
} else {
crate::split_csv_refs(validated_params.group.as_deref())
};
let source_param = extracted_params.source.or(validated_params.source.clone());
if let Some(limit) = extracted_params.limit {
validated_params.limit = Some(limit as usize);
}
if let Some(since_str) = extracted_params.since {
match DateTime::parse_from_rfc3339(&since_str) {
Ok(dt) => validated_params.since = Some(dt.with_timezone(&Utc)),
Err(_) => {
return Err(ServerError::BadRequest(format!(
"_since parameter must be a valid RFC3339 timestamp: {}",
since_str
)));
}
}
}
if extracted_params.max_file_size.is_some()
|| extracted_params.row_group_size.is_some()
|| extracted_params.page_size.is_some()
|| extracted_params.compression.is_some()
{
let mut parquet_opts =
validated_params
.parquet_options
.clone()
.unwrap_or_else(|| crate::ParquetOptions {
row_group_size_mb: 256,
page_size_kb: 1024,
compression: "snappy".to_string(),
max_file_size_mb: None,
});
if let Some(max_size) = extracted_params.max_file_size {
parquet_opts.max_file_size_mb = Some(max_size);
}
if let Some(row_group) = extracted_params.row_group_size {
parquet_opts.row_group_size_mb = row_group;
}
if let Some(page_size) = extracted_params.page_size {
parquet_opts.page_size_kb = page_size;
}
if let Some(compression) = extracted_params.compression {
parquet_opts.compression = compression;
}
validated_params.parquet_options = Some(parquet_opts);
}
let mut source_bundle = None;
let mut source_fhir_version = None;
if let Some(source) = &source_param {
info!("Loading data from source: {}", source);
let data_source = UniversalDataSource::new();
let loaded_bundle = data_source.load(source).await?;
source_fhir_version = Some(loaded_bundle.version());
let loaded_bundle = if !patient_filter.is_empty()
|| !group_filter.is_empty()
|| validated_params.since.is_some()
{
let mut source_resources = extract_resources_from_bundle(&loaded_bundle)?;
if !patient_filter.is_empty() || !group_filter.is_empty() {
source_resources = filter_resources_by_patient_and_group(
source_resources,
&patient_filter,
&group_filter,
source_fhir_version.unwrap(),
)?;
}
if let Some(since) = validated_params.since {
source_resources = filter_resources_by_since(source_resources, since)?;
}
create_bundle_from_resources_for_version(
source_resources,
source_fhir_version.unwrap(),
)?
} else {
loaded_bundle
};
source_bundle = Some(loaded_bundle);
}
let view_json_for_fhir = fhir_output.then(|| view_def_json.clone());
let view_definition = if let Some(version) = source_fhir_version {
info!(
"Parsing ViewDefinition as {:?} (matching source bundle)",
version
);
parse_view_definition_for_version(view_def_json, version)?
} else {
parse_view_definition(view_def_json)?
};
if !patient_filter.is_empty() || !group_filter.is_empty() {
let effective_version = source_fhir_version.unwrap_or_else(get_newest_enabled_fhir_version);
filtered_resources = filter_resources_by_patient_and_group(
filtered_resources,
&patient_filter,
&group_filter,
effective_version,
)?;
}
if let Some(since) = validated_params.since {
filtered_resources = filter_resources_by_since(filtered_resources, since)?;
}
let bundle = if let Some(source_bundle) = source_bundle {
if filtered_resources.is_empty() {
source_bundle
} else {
merge_bundles(source_bundle, filtered_resources)?
}
} else {
create_bundle_from_resources(filtered_resources)?
};
let run_options = RunOptions {
since: validated_params.since,
limit: validated_params.limit,
page: None, parquet_options: validated_params.parquet_options.clone(),
};
info!(
"Executing ViewDefinition with output format: {:?}",
validated_params.format
);
if let Some(view_json) = view_json_for_fhir {
let mut processed = process_view_definition(view_definition, bundle)?;
if let Some(limit) = validated_params.limit {
processed.rows.truncate(limit);
}
let body = fhir_format::format_view_fhir_parameters(&processed, &view_json)?;
return Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, fhir_format::FHIR_JSON_MIME)],
body,
)
.into_response());
}
if validated_params.format == ContentType::Parquet
&& validated_params
.parquet_options
.as_ref()
.and_then(|opts| opts.max_file_size_mb)
.is_some()
{
let processed_result = process_view_definition(view_definition, bundle)?;
let max_file_size_bytes = validated_params
.parquet_options
.as_ref()
.and_then(|opts| opts.max_file_size_mb)
.map(|mb| mb as usize * 1024 * 1024)
.unwrap_or(usize::MAX);
let file_buffers = format_parquet_multi_file(
processed_result,
validated_params.parquet_options.as_ref(),
max_file_size_bytes,
)?;
if file_buffers.len() > 1 {
info!(
"Generating ZIP archive with {} Parquet files",
file_buffers.len()
);
let zip = crate::parquet_zip::create_zip_from_buffers(file_buffers, "data")?;
Ok((
StatusCode::OK,
[
(header::CONTENT_TYPE, "application/zip"),
(
header::CONTENT_DISPOSITION,
"attachment; filename=\"data.zip\"",
),
],
zip,
)
.into_response())
} else {
let payload = file_buffers.into_iter().next().unwrap_or_default();
if wants_envelope {
let body = fhir_format::wrap_in_binary_envelope(
ContentType::Parquet.mime_type(),
&payload,
)?;
return Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, fhir_format::FHIR_JSON_MIME)],
body,
)
.into_response());
}
Ok((
StatusCode::OK,
[
(header::CONTENT_TYPE, ContentType::Parquet.mime_type()),
(
header::CONTENT_DISPOSITION,
"attachment; filename=\"output.parquet\"",
),
],
payload,
)
.into_response())
}
} else {
let remote_config = crate::RemoteResolveConfig::from_env();
let filtered_output = if remote_config.is_active() {
run_view_definition_with_options_remote(
view_definition,
bundle,
validated_params.format,
run_options,
&remote_config,
)
.await?
} else {
run_view_definition_with_options(
view_definition,
bundle,
validated_params.format,
run_options,
)?
};
let mime_type = validated_params.format.mime_type();
if wants_envelope {
let body = fhir_format::wrap_in_binary_envelope(mime_type, &filtered_output)?;
return Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, fhir_format::FHIR_JSON_MIME)],
body,
)
.into_response());
}
let response = if matches!(
validated_params.format,
ContentType::Parquet | ContentType::ArrowIpc
) {
let filename = if validated_params.format == ContentType::Parquet {
"attachment; filename=\"output.parquet\""
} else {
"attachment; filename=\"output.arrow\""
};
(
StatusCode::OK,
[
(header::CONTENT_TYPE, mime_type),
(header::CONTENT_DISPOSITION, filename),
],
filtered_output,
)
.into_response()
} else {
(
StatusCode::OK,
[(header::CONTENT_TYPE, mime_type)],
filtered_output,
)
.into_response()
};
Ok(response)
}
}
fn create_capability_statement() -> serde_json::Value {
let fhir_version = get_fhir_version_string();
serde_json::json!({
"resourceType": "CapabilityStatement",
"id": "sof-server",
"name": "SQL-on-FHIR Server",
"title": "SQL-on-FHIR Server CapabilityStatement",
"status": "active",
"date": chrono::Utc::now().to_rfc3339(),
"publisher": "SQL-on-FHIR Implementation",
"kind": "instance",
"software": {
"name": "sof-server",
"version": env!("CARGO_PKG_VERSION")
},
"implementation": {
"description": "SQL-on-FHIR ViewDefinition Runner",
"url": "http://localhost:8080"
},
"fhirVersion": fhir_version,
"format": [
"application/json",
"application/x-ndjson",
"text/csv",
"application/vnd.apache.parquet",
"application/octet-stream",
"application/fhir+json"
],
"rest": [{
"mode": "server",
"operation": [{
"name": "sql-run",
"definition": "/OperationDefinition/sof-sql-run",
"documentation": "Execute a ViewDefinition supplied inline and return the rows. Supports CSV, JSON, NDJSON, Parquet, and FHIR Parameters (_format=fhir) output; flat formats may also be returned as a Binary resource envelope via 'Accept: application/fhir+json'. Invoked at the system level (POST /$sql-run). The subject must be supplied inline via 'subjectResource': with no resource store, neither 'subjectCanonical' nor 'subjectReference' can be resolved."
}]
}]
})
}
pub async fn sql_run_operation_definition() -> ServerResult<impl IntoResponse> {
Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, "application/fhir+json")],
Json(create_sql_run_operation_definition()),
))
}
fn create_sql_run_operation_definition() -> serde_json::Value {
serde_json::json!({
"resourceType": "OperationDefinition",
"id": "sof-sql-run",
"url": "/OperationDefinition/sof-sql-run",
"name": "SQLRunSupported",
"title": "SQL Run (subset supported by sof-server)",
"status": "active",
"kind": "operation",
"code": "sql-run",
"base": crate::canonical::SQL_RUN_OPERATION_DEFINITION,
"system": true,
"type": false,
"instance": false,
"parameter": [
{
"name": "subjectResource",
"use": "in",
"min": 1,
"max": "1",
"type": "CanonicalResource",
"documentation": "Inline ViewDefinition to execute. Required here: this server resolves no subject by URL."
},
{"name": "resource", "use": "in", "min": 0, "max": "*", "type": "Resource"},
{"name": "_format", "use": "in", "min": 0, "max": "1", "type": "code"},
{"name": "header", "use": "in", "min": 0, "max": "1", "type": "boolean"},
{"name": "patient", "use": "in", "min": 0, "max": "*", "type": "Reference"},
{"name": "group", "use": "in", "min": 0, "max": "*", "type": "Reference"},
{"name": "_since", "use": "in", "min": 0, "max": "1", "type": "instant"},
{"name": "_limit", "use": "in", "min": 0, "max": "1", "type": "integer"},
{"name": "return", "use": "out", "min": 1, "max": "1", "type": "Binary"}
]
})
}
#[allow(dead_code)]
fn resolve_view_reference(reference: &str) -> ServerResult<SofViewDefinition> {
info!("Resolving ViewDefinition reference: {}", reference);
if !reference.starts_with("http://") && !reference.starts_with("https://") {
return Err(ServerError::NotImplemented(format!(
"Relative ViewDefinition references are not supported in this stateless implementation: {}",
reference
)));
}
if reference.contains('|') {
return Err(ServerError::NotImplemented(format!(
"Canonical URL references with versions are not yet supported: {}",
reference
)));
}
Err(ServerError::NotImplemented(format!(
"Loading ViewDefinitions from external URLs is not yet implemented: {}",
reference
)))
}
fn parse_view_definition(json: serde_json::Value) -> ServerResult<SofViewDefinition> {
parse_view_definition_for_version(json, get_newest_enabled_fhir_version())
}
fn parse_view_definition_for_version(
json: serde_json::Value,
version: helios_fhir::FhirVersion,
) -> ServerResult<SofViewDefinition> {
sof_parse_view_definition_for_version(json, version).map_err(ServerError::from)
}
fn find_inline_view_definition(json: &serde_json::Value) -> Option<&serde_json::Value> {
json.get("parameter")?.as_array()?.iter().find_map(|param| {
let resource = param.get("resource")?;
(resource.get("resourceType").and_then(|t| t.as_str()) == Some("ViewDefinition"))
.then_some(resource)
})
}
fn lint_inline_view_definition(json: &serde_json::Value) -> ServerResult<()> {
let errors: Vec<Diagnostic> = lint_view_definition(json)
.into_iter()
.filter(|diagnostic| diagnostic.severity == Severity::Error)
.collect();
if errors.is_empty() {
Ok(())
} else {
Err(ServerError::InvalidViewDefinition(errors))
}
}
fn invalid_parameters_error(
json: &serde_json::Value,
version: helios_fhir::FhirVersion,
message: String,
) -> ServerError {
if let Some(resource) = find_inline_view_definition(json)
&& let Err(error) = parse_view_definition_for_version(resource.clone(), version)
{
return error;
}
ServerError::BadRequest(message)
}
fn parse_parameters(json: serde_json::Value) -> ServerResult<RunParameters> {
if let Some(resource_type) = json.get("resourceType") {
if resource_type != "Parameters" {
return Err(ServerError::BadRequest(
"Request body must be a Parameters resource".to_string(),
));
}
} else {
return Err(ServerError::BadRequest(
"Missing resourceType field".to_string(),
));
}
let newest_version = get_newest_enabled_fhir_version();
match newest_version {
#[cfg(feature = "R4")]
helios_fhir::FhirVersion::R4 => {
let params = helios_fhir::r4::Parameters::deserialize(&json).map_err(|e| {
invalid_parameters_error(
&json,
newest_version,
format!("Invalid R4 Parameters: {}", e),
)
})?;
Ok(RunParameters::R4(params))
}
#[cfg(feature = "R4B")]
helios_fhir::FhirVersion::R4B => {
let params = helios_fhir::r4b::Parameters::deserialize(&json).map_err(|e| {
invalid_parameters_error(
&json,
newest_version,
format!("Invalid R4B Parameters: {}", e),
)
})?;
Ok(RunParameters::R4B(params))
}
#[cfg(feature = "R5")]
helios_fhir::FhirVersion::R5 => {
let params = helios_fhir::r5::Parameters::deserialize(&json).map_err(|e| {
invalid_parameters_error(
&json,
newest_version,
format!("Invalid R5 Parameters: {}", e),
)
})?;
Ok(RunParameters::R5(params))
}
#[cfg(feature = "R6")]
helios_fhir::FhirVersion::R6 => {
let params = helios_fhir::r6::Parameters::deserialize(&json).map_err(|e| {
invalid_parameters_error(
&json,
newest_version,
format!("Invalid R6 Parameters: {}", e),
)
})?;
Ok(RunParameters::R6(params))
}
#[allow(unreachable_patterns)]
_ => unreachable!(
"get_newest_enabled_fhir_version only returns versions enabled for helios-sof"
),
}
}
fn create_bundle_from_resources(resources: Vec<serde_json::Value>) -> ServerResult<SofBundle> {
create_bundle_from_resources_for_version(resources, get_newest_enabled_fhir_version())
}
fn create_bundle_from_resources_for_version(
resources: Vec<serde_json::Value>,
version: helios_fhir::FhirVersion,
) -> ServerResult<SofBundle> {
sof_create_bundle_from_resources_for_version(resources, version).map_err(|e| match e {
SofError::InvalidViewDefinition(msg) => ServerError::InternalError(msg),
other => ServerError::from(other),
})
}
fn extract_resources_from_bundle(bundle: &SofBundle) -> ServerResult<Vec<serde_json::Value>> {
let mut resources = Vec::new();
match bundle {
#[cfg(feature = "R4")]
SofBundle::R4(bundle) => {
if let Some(entries) = &bundle.entry {
for entry in entries {
if let Some(resource) = &entry.resource {
resources.push(serde_json::to_value(resource)?);
}
}
}
}
#[cfg(feature = "R4B")]
SofBundle::R4B(bundle) => {
if let Some(entries) = &bundle.entry {
for entry in entries {
if let Some(resource) = &entry.resource {
resources.push(serde_json::to_value(resource)?);
}
}
}
}
#[cfg(feature = "R5")]
SofBundle::R5(bundle) => {
if let Some(entries) = &bundle.entry {
for entry in entries {
if let Some(resource) = &entry.resource {
resources.push(serde_json::to_value(resource)?);
}
}
}
}
#[cfg(feature = "R6")]
SofBundle::R6(bundle) => {
if let Some(entries) = &bundle.entry {
for entry in entries {
if let Some(resource) = &entry.resource {
resources.push(serde_json::to_value(resource)?);
}
}
}
}
}
Ok(resources)
}
fn merge_bundles(
source_bundle: SofBundle,
additional_resources: Vec<serde_json::Value>,
) -> ServerResult<SofBundle> {
let mut all_resources = Vec::new();
match source_bundle {
#[cfg(feature = "R4")]
SofBundle::R4(bundle) => {
if let Some(entries) = bundle.entry {
for entry in entries {
if let Some(resource) = entry.resource {
all_resources.push(serde_json::to_value(&resource)?);
}
}
}
}
#[cfg(feature = "R4B")]
SofBundle::R4B(bundle) => {
if let Some(entries) = bundle.entry {
for entry in entries {
if let Some(resource) = entry.resource {
all_resources.push(serde_json::to_value(&resource)?);
}
}
}
}
#[cfg(feature = "R5")]
SofBundle::R5(bundle) => {
if let Some(entries) = bundle.entry {
for entry in entries {
if let Some(resource) = entry.resource {
all_resources.push(serde_json::to_value(&resource)?);
}
}
}
}
#[cfg(feature = "R6")]
SofBundle::R6(bundle) => {
if let Some(entries) = bundle.entry {
for entry in entries {
if let Some(resource) = entry.resource {
all_resources.push(serde_json::to_value(&resource)?);
}
}
}
}
}
all_resources.extend(additional_resources);
create_bundle_from_resources(all_resources)
}
fn filter_resources_by_patient_and_group(
resources: Vec<serde_json::Value>,
patient_refs: &[String],
group_refs: &[String],
fhir_version: helios_fhir::FhirVersion,
) -> ServerResult<Vec<serde_json::Value>> {
sof_filter_resources_by_patient_and_group(resources, patient_refs, group_refs, fhir_version)
.map_err(ServerError::from)
}
fn filter_resources_by_since(
resources: Vec<serde_json::Value>,
since: DateTime<Utc>,
) -> ServerResult<Vec<serde_json::Value>> {
sof_filter_resources_by_since(resources, since).map_err(ServerError::from)
}
pub async fn health_check() -> impl IntoResponse {
info!("Handling Health Check request");
Json(serde_json::json!({
"status": "ok",
"service": "sof-server",
"version": env!("CARGO_PKG_VERSION"),
"uptime_seconds": helios_observability::uptime::uptime_seconds(),
"started_at": helios_observability::uptime::started_at_rfc3339(),
"timestamp": chrono::Utc::now().to_rfc3339()
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_capability_statement_structure() {
let cap_stmt = create_capability_statement();
assert_eq!(cap_stmt["resourceType"], "CapabilityStatement");
assert_eq!(cap_stmt["kind"], "instance");
assert_eq!(cap_stmt["fhirVersion"], get_fhir_version_string());
let operations = &cap_stmt["rest"][0]["operation"];
assert!(operations.as_array().is_some());
assert_eq!(operations[0]["name"], "sql-run");
let doc = operations[0]["documentation"]
.as_str()
.expect("documentation must be a string");
assert!(
doc.contains("system level"),
"doc must state the invocation scope: {doc}"
);
assert!(
doc.contains("subjectResource"),
"doc must mention subjectResource as the supply mechanism: {doc}"
);
let formats: Vec<String> = cap_stmt["format"]
.as_array()
.expect("format must be an array")
.iter()
.filter_map(|f| f.as_str().map(String::from))
.collect();
for required in [
"application/json",
"application/x-ndjson",
"text/csv",
"application/vnd.apache.parquet",
"application/octet-stream",
"application/fhir+json",
] {
assert!(
formats.iter().any(|f| f == required),
"format must include {required}: {formats:?}"
);
}
}
#[cfg(feature = "R4")]
#[test]
fn test_invalid_view_definition_maps_to_422() {
use axum::response::IntoResponse;
let bad_view = serde_json::json!({
"resourceType": "ViewDefinition",
"status": "active",
"resource": "Patient",
"select": "not-an-array"
});
let err = parse_view_definition_for_version(bad_view, helios_fhir::FhirVersion::R4)
.expect_err("malformed ViewDefinition must error");
assert!(
matches!(err, ServerError::ProcessingError(_)),
"invalid ViewDefinition must map to ProcessingError (→ 422), got {err:?}"
);
let response = err.into_response();
assert_eq!(
response.status(),
StatusCode::UNPROCESSABLE_ENTITY,
"invalid ViewDefinition response must be 422"
);
}
#[tokio::test]
async fn test_sql_run_operation_definition_declares_supported_subset() {
let body = create_sql_run_operation_definition();
assert_eq!(body["resourceType"], "OperationDefinition");
assert_eq!(body["code"], "sql-run");
assert_eq!(
body["base"],
crate::canonical::SQL_RUN_OPERATION_DEFINITION,
"base must name the guide's definition so a client knows what this subsets"
);
assert_eq!(body["system"], true);
assert_eq!(body["type"], false);
assert_eq!(body["instance"], false);
let names: Vec<&str> = body["parameter"]
.as_array()
.unwrap()
.iter()
.map(|p| p["name"].as_str().unwrap())
.collect();
assert!(names.contains(&"subjectResource"));
for unsupported in ["subjectCanonical", "subjectReference", "context", "source"] {
assert!(
!names.contains(&unsupported),
"{unsupported} is not supported and must not be declared: {names:?}"
);
}
}
#[test]
fn test_capability_statement_declares_sql_run_at_system_level() {
let cs = create_capability_statement();
let ops = cs["rest"][0]["operation"].as_array().unwrap();
let sql_run = ops
.iter()
.find(|o| o["name"] == "sql-run")
.expect("$sql-run must be declared in rest.operation");
assert_eq!(sql_run["definition"], "/OperationDefinition/sof-sql-run");
assert!(
!ops.iter().any(|o| o["name"] == "sql-export"),
"this server runs no async jobs, so it must not advertise $sql-export"
);
}
#[cfg(feature = "R4")]
#[test]
fn test_filter_resources_by_patient() {
let resources = vec![
serde_json::json!({
"resourceType": "Patient",
"id": "123"
}),
serde_json::json!({
"resourceType": "Patient",
"id": "456"
}),
serde_json::json!({
"resourceType": "Observation",
"id": "obs1",
"subject": {
"reference": "Patient/123"
}
}),
serde_json::json!({
"resourceType": "Observation",
"id": "obs2",
"subject": {
"reference": "Patient/456"
}
}),
];
let filtered = filter_resources_by_patient_and_group(
resources,
&["Patient/123".to_string()],
&[],
helios_fhir::FhirVersion::R4,
)
.unwrap();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0]["id"], "123");
assert_eq!(filtered[1]["id"], "obs1");
}
#[cfg(feature = "R4")]
#[test]
fn test_filter_with_unresolvable_group_returns_bad_request() {
let resources = vec![serde_json::json!({
"resourceType": "Patient",
"id": "123"
})];
let err = filter_resources_by_patient_and_group(
resources,
&[],
&["Group/test".to_string()],
helios_fhir::FhirVersion::R4,
)
.expect_err("absent Group target must error");
match &err {
ServerError::ReferencedResourceNotFound(msg) => {
assert!(
msg.contains("Group/test"),
"error must name the absent reference: {msg}"
);
}
other => panic!("expected ReferencedResourceNotFound, got {other:?}"),
}
let response = err.into_response();
assert_eq!(
response.status(),
StatusCode::BAD_REQUEST,
"absent reference must surface as 400"
);
}
#[test]
fn test_resolve_view_reference_relative() {
let result = resolve_view_reference("ViewDefinition/123");
assert!(result.is_err());
if let Err(ServerError::NotImplemented(msg)) = result {
assert!(msg.contains("Relative ViewDefinition references are not supported"));
} else {
panic!("Expected NotImplemented error");
}
}
#[test]
fn test_resolve_view_reference_canonical() {
let result = resolve_view_reference("http://example.org/ViewDefinition/test|1.0.0");
assert!(result.is_err());
if let Err(ServerError::NotImplemented(msg)) = result {
assert!(msg.contains("Canonical URL references with versions are not yet supported"));
} else {
panic!("Expected NotImplemented error");
}
}
#[test]
fn test_resolve_view_reference_absolute() {
let result = resolve_view_reference("http://example.org/ViewDefinition/123");
assert!(result.is_err());
if let Err(ServerError::NotImplemented(msg)) = result {
assert!(
msg.contains("Loading ViewDefinitions from external URLs is not yet implemented")
);
} else {
panic!("Expected NotImplemented error");
}
}
}