Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crates/auth/src/outbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
//! assertion in the SMART Backend Services style) is planned and is the
//! reason the trait carries an `audience` argument — that lets the future
//! signer scope tokens per receiver. The current impls ignore it.
//!
//! Consumers that need this today: the web UI's server-side self-call
//! (`crates/hfs/src/main.rs`, mounting `helios_ui`) currently uses the static
//! bearer, so an auth-enabled deployment must provision `HFS_OUTBOUND_BEARER_TOKEN`
//! with a valid token or the UI's conformance pages degrade. See the
//! `TODO(service-token)` there.

use std::sync::Arc;

Expand Down
178 changes: 178 additions & 0 deletions crates/fhir/src/compartment/loader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! CompartmentDefinition loader.
//!
//! Reads the FHIR spec `CompartmentDefinition` resources from the data
//! directory, as stored-shape JSON ready to seed into storage. This is the
//! compartment analogue of
//! [`SearchParameterLoader::load_spec_resources`](crate::search::loader::SearchParameterLoader::load_spec_resources):
//! the server seeds these into primary storage at startup so
//! `GET /CompartmentDefinition` and the web UI read the same definitions the
//! codegen'd membership table (`crate::get_compartment_params`) is built from.

use std::path::Path;

use serde_json::Value;

use crate::FhirVersion;
use crate::search::errors::LoaderError;

/// Loader for CompartmentDefinition spec resources.
pub struct CompartmentDefinitionLoader {
fhir_version: FhirVersion,
}

impl CompartmentDefinitionLoader {
/// Creates a new loader for the specified FHIR version.
pub fn new(fhir_version: FhirVersion) -> Self {
Self { fhir_version }
}

/// Returns the FHIR version.
pub fn version(&self) -> FhirVersion {
self.fhir_version
}

/// Returns the spec bundle filename for the configured FHIR version.
///
/// Mirrors the `compartment-definitions-{version}.json` bundles shipped
/// under the data directory, alongside `search-parameters-{version}.json`.
#[allow(unreachable_patterns)]
pub fn spec_filename(&self) -> &'static str {
match self.fhir_version {
#[cfg(feature = "R4")]
FhirVersion::R4 => "compartment-definitions-r4.json",
#[cfg(feature = "R4B")]
FhirVersion::R4B => "compartment-definitions-r4b.json",
#[cfg(feature = "R5")]
FhirVersion::R5 => "compartment-definitions-r5.json",
#[cfg(feature = "R6")]
FhirVersion::R6 => "compartment-definitions-r6.json",
_ => "compartment-definitions-r4.json",
}
}

/// Returns the raw CompartmentDefinition resources from the spec bundle,
/// as stored-shape JSON.
///
/// The seeding companion the server hands to storage: each entry carries
/// its bundle id (`patient`, `encounter`, …), so re-seeding is idempotent
/// via `create`'s `AlreadyExists`.
pub fn load_spec_resources(&self, data_dir: &Path) -> Result<Vec<Value>, LoaderError> {
let path = data_dir.join(self.spec_filename());
let content =
std::fs::read_to_string(&path).map_err(|e| LoaderError::ConfigLoadFailed {
path: path.display().to_string(),
message: e.to_string(),
})?;
let json: Value =
serde_json::from_str(&content).map_err(|e| LoaderError::ConfigLoadFailed {
path: path.display().to_string(),
message: format!("Invalid JSON: {}", e),
})?;

let mut resources = Vec::new();
if let Some(entries) = json.get("entry").and_then(|e| e.as_array()) {
for entry in entries {
if let Some(resource) = entry.get("resource")
&& resource.get("resourceType").and_then(|t| t.as_str())
== Some("CompartmentDefinition")
{
resources.push(resource.clone());
}
}
}
Ok(resources)
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;

/// Repo-root `data/` directory, from this crate's manifest dir.
fn workspace_data_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data")
}

#[cfg(feature = "R4")]
#[test]
fn loads_r4_compartment_definitions() {
let loader = CompartmentDefinitionLoader::new(FhirVersion::R4);
let resources = loader.load_spec_resources(&workspace_data_dir()).unwrap();
// Device, Encounter, Patient, Practitioner, RelatedPerson (questionnaire
// has no code and is excluded from the bundle).
assert_eq!(resources.len(), 5);
assert!(
resources
.iter()
.all(|r| r["resourceType"] == "CompartmentDefinition")
);
assert!(resources.iter().any(|r| r["code"] == "Patient"));
assert!(
resources
.iter()
.all(|r| r.get("id").and_then(|i| i.as_str()).is_some())
);
}

#[test]
fn missing_dir_errors() {
let loader = CompartmentDefinitionLoader::new(FhirVersion::default_enabled());
assert!(
loader
.load_spec_resources(Path::new("/nonexistent/data/dir"))
.is_err()
);
}

/// The shipped `data/compartment-definitions-*.json` bundles must say
/// exactly what the codegen'd `get_compartment_params` table says — every
/// `(compartment, resource)` slot, every version. This is the guard against
/// the seeded copies drifting from `crates/fhir-gen/resources/`.
fn assert_parity(version: FhirVersion) {
let loader = CompartmentDefinitionLoader::new(version);
let defs = loader.load_spec_resources(&workspace_data_dir()).unwrap();
assert!(!defs.is_empty(), "{version:?} bundle is empty");
for def in &defs {
let code = def["code"].as_str().unwrap();
for resource in def["resource"].as_array().unwrap() {
let rtype = resource["code"].as_str().unwrap();
let params: Vec<&str> = resource
.get("param")
.and_then(|p| p.as_array())
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
let runtime = crate::get_compartment_params(version, code, rtype);
assert_eq!(
runtime,
params.as_slice(),
"{version:?} compartment {code} / {rtype}"
);
}
}
}

#[cfg(feature = "R4")]
#[test]
fn r4_bundle_matches_codegen_table() {
assert_parity(FhirVersion::R4);
}

#[cfg(feature = "R4B")]
#[test]
fn r4b_bundle_matches_codegen_table() {
assert_parity(FhirVersion::R4B);
}

#[cfg(feature = "R5")]
#[test]
fn r5_bundle_matches_codegen_table() {
assert_parity(FhirVersion::R5);
}

#[cfg(feature = "R6")]
#[test]
fn r6_bundle_matches_codegen_table() {
assert_parity(FhirVersion::R6);
}
}
15 changes: 15 additions & 0 deletions crates/fhir/src/compartment/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! FHIR CompartmentDefinition spec data.
//!
//! Companion to [`crate::search`]: where SearchParameters drive search
//! resolution, CompartmentDefinitions describe the compartment membership the
//! REST compartment-search handler enforces (via the codegen'd
//! [`crate::get_compartment_params`] table). This module only *loads* the spec
//! resources so the server can seed them into storage — the source of truth the
//! `GET /CompartmentDefinition` route and the web UI read from. Membership
//! itself stays codegen-driven.
//!
//! - [`loader`] – [`CompartmentDefinitionLoader`] for reading the spec bundle.
pub mod loader;

pub use loader::CompartmentDefinitionLoader;
1 change: 1 addition & 0 deletions crates/fhir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,7 @@ pub mod r5;
#[cfg(feature = "R6")]
pub mod r6;

pub mod compartment;
pub mod compartment_expressions;
pub mod parameters;
pub mod search;
Expand Down
6 changes: 6 additions & 0 deletions crates/fhir/src/search/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ impl SearchParameterDefinition {
/// unregistering, or re-statusing a *shadowed* parameter therefore never
/// changes which definition answers a query, and removing the winner
/// re-points the slot at the next candidate instead of leaving a hole (#239).
///
/// `Clone` is cheap: both indexes hold `Arc<SearchParameterDefinition>`, so a
/// clone copies the HashMap structure and bumps Arc refcounts — the definitions
/// themselves are shared. The per-tenant registry container relies on this to
/// materialize a tenant's registry as `base.clone()` plus that tenant's overlay.
#[derive(Clone)]
pub struct SearchParameterRegistry {
/// Parameters indexed by (resource_type, param_code). Each slot holds the
/// candidates in descending precedence order; the first entry wins.
Expand Down
14 changes: 8 additions & 6 deletions crates/hfs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ path = "src/main.rs"
[features]
default = ["R4", "sqlite", "ui"]

# FHIR version features
R4 = ["helios-fhir/R4", "helios-rest/R4", "helios-audit/R4"]
R4B = ["helios-fhir/R4B", "helios-rest/R4B", "helios-audit/R4B"]
R5 = ["helios-fhir/R5", "helios-rest/R5", "helios-audit/R5"]
R6 = ["helios-fhir/R6", "helios-rest/R6", "helios-audit/R6"]
# FHIR version features. `helios-ui?/…` keeps the UI's viewers (search
# parameters, compartments) scoped to the same versions as the server when
# the optional `ui` feature is on.
R4 = ["helios-fhir/R4", "helios-rest/R4", "helios-audit/R4", "helios-ui?/R4"]
R4B = ["helios-fhir/R4B", "helios-rest/R4B", "helios-audit/R4B", "helios-ui?/R4B"]
R5 = ["helios-fhir/R5", "helios-rest/R5", "helios-audit/R5", "helios-ui?/R5"]
R6 = ["helios-fhir/R6", "helios-rest/R6", "helios-audit/R6", "helios-ui?/R6"]

# Build options
skip-r6-download = []
Expand Down Expand Up @@ -58,7 +60,7 @@ helios-sof = { path = "../sof", version = "0.2.1", default-features = false }
helios-auth = { path = "../auth", version = "0.2.1" }
helios-audit = { path = "../audit", version = "0.2.1", default-features = false }
helios-subscriptions = { path = "../subscriptions", version = "0.2.1", optional = true, default-features = false }
helios-ui = { path = "../ui", version = "0.2.1", optional = true }
helios-ui = { path = "../ui", version = "0.2.1", optional = true, default-features = false }
helios-observability = { path = "../observability", version = "0.2.1" }

# Export job controller
Expand Down
Loading
Loading