use std::collections::BTreeMap;
use std::path::Path;
use ::serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Bundle {
#[serde(default)]
pub entry: Vec<Entry>,
}
#[derive(Debug, Deserialize)]
pub struct Entry {
pub resource: ::serde_json::Value,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StructureDefinition {
pub name: String,
#[serde(rename = "type")]
pub type_name: String,
pub kind: String,
#[serde(default, rename = "abstract")]
pub is_abstract: bool,
pub url: String,
pub version: Option<String>,
pub description: Option<String>,
pub snapshot: Option<Snapshot>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Snapshot {
#[serde(default)]
pub element: Vec<ElementDefinition>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElementDefinition {
pub path: String,
#[serde(default)]
pub min: u32,
pub max: Option<String>,
pub short: Option<String>,
pub definition: Option<String>,
pub content_reference: Option<String>,
#[serde(default, rename = "type")]
pub types: Vec<ElementType>,
pub binding: Option<Binding>,
pub is_summary: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ElementType {
#[serde(default)]
pub code: String,
#[serde(default, deserialize_with = "string_or_seq")]
pub target_profile: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Binding {
pub strength: String,
#[serde(rename = "valueSet")]
value_set_canonical: Option<String>,
value_set_reference: Option<BindingReference>,
value_set_uri: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BindingReference {
pub reference: Option<String>,
}
impl Binding {
#[must_use]
pub fn value_set(&self) -> Option<&str> {
self.value_set_canonical
.as_deref()
.or_else(|| self.value_set_reference.as_ref()?.reference.as_deref())
.or(self.value_set_uri.as_deref())
}
}
fn string_or_seq<'de, D: ::serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<String>, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
Ok(match OneOrMany::deserialize(deserializer)? {
OneOrMany::One(s) => vec![s],
OneOrMany::Many(v) => v,
})
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeSystem {
pub name: Option<String>,
pub url: Option<String>,
pub description: Option<String>,
pub content: Option<String>,
#[serde(default)]
pub concept: Vec<Concept>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Concept {
pub code: String,
pub display: Option<String>,
pub definition: Option<String>,
#[serde(default)]
pub concept: Vec<Concept>,
}
impl CodeSystem {
#[must_use]
pub fn codes(&self) -> Vec<&Concept> {
fn walk<'a>(concepts: &'a [Concept], out: &mut Vec<&'a Concept>) {
for concept in concepts {
out.push(concept);
walk(&concept.concept, out);
}
}
let mut out = Vec::new();
walk(&self.concept, &mut out);
let mut seen = std::collections::HashSet::new();
out.retain(|c| seen.insert(c.code.clone()));
out
}
}
impl ElementDefinition {
#[must_use]
pub fn is_multiple(&self) -> bool {
match self.max.as_deref() {
Some("*") => true,
Some(other) => other.parse::<u32>().is_ok_and(|n| n > 1),
None => false,
}
}
#[must_use]
pub fn is_choice(&self) -> bool {
self.path.ends_with("[x]")
}
#[must_use]
pub fn base_path(&self) -> &str {
self.path.strip_suffix("[x]").unwrap_or(&self.path)
}
#[must_use]
pub fn leaf(&self) -> &str {
self.base_path().rsplit('.').next().unwrap_or_default()
}
#[must_use]
pub fn owner_path(&self) -> Option<&str> {
self.base_path().rsplit_once('.').map(|(owner, _)| owner)
}
#[must_use]
pub fn is_system_element(&self) -> bool {
if self.types.iter().any(|t| t.code.starts_with("http://hl7.org/fhirpath/System.")) {
return true;
}
self.leaf() == "id" || self.path == "Extension.url"
}
#[must_use]
pub fn content_reference_path(&self) -> Option<&str> {
self.content_reference.as_deref()?.rsplit('#').next()
}
}
pub fn read_structure_definitions(path: &Path) -> std::io::Result<Vec<StructureDefinition>> {
Ok(read_resources::<StructureDefinition>(path, "StructureDefinition")?
.into_iter()
.filter(|sd| sd.snapshot.is_some())
.collect())
}
pub fn read_code_systems(path: &Path) -> std::io::Result<Vec<CodeSystem>> {
read_resources::<CodeSystem>(path, "CodeSystem")
}
fn read_resources<T: for<'de> Deserialize<'de>>(
path: &Path,
resource_type: &str,
) -> std::io::Result<Vec<T>> {
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
let bundle: Bundle = ::serde_json::from_reader(reader)
.map_err(|e| std::io::Error::other(format!("{}: {e}", path.display())))?;
let mut out = Vec::new();
for entry in bundle.entry {
if entry.resource.get("resourceType").and_then(|v| v.as_str()) != Some(resource_type) {
continue;
}
let name = entry
.resource
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("<unnamed>")
.to_string();
let parsed = ::serde_json::from_value::<T>(entry.resource).map_err(|e| {
std::io::Error::other(format!(
"{}: could not read {resource_type} {name:?}: {e}",
path.display()
))
})?;
out.push(parsed);
}
Ok(out)
}
#[must_use]
pub fn by_type_name(definitions: &[StructureDefinition]) -> BTreeMap<String, StructureDefinition> {
definitions
.iter()
.map(|sd| (sd.type_name.clone(), sd.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn element(path: &str, max: &str) -> ElementDefinition {
ElementDefinition {
path: path.to_string(),
min: 0,
max: Some(max.to_string()),
short: None,
definition: None,
content_reference: None,
types: Vec::new(),
binding: None,
is_summary: None,
}
}
#[test]
fn path_parts() {
let el = element("Observation.component.value[x]", "1");
assert!(el.is_choice());
assert_eq!(el.base_path(), "Observation.component.value");
assert_eq!(el.leaf(), "value");
assert_eq!(el.owner_path(), Some("Observation.component"));
assert!(!el.is_multiple());
}
#[test]
fn root_has_no_owner() {
assert_eq!(element("Patient", "1").owner_path(), None);
}
#[test]
fn multiplicity() {
assert!(element("Patient.name", "*").is_multiple());
assert!(!element("Patient.gender", "1").is_multiple());
assert!(element("X.y", "5").is_multiple());
}
#[test]
fn content_reference_forms_agree() {
let mut el = element("Observation.component.referenceRange", "*");
el.content_reference = Some("#Observation.referenceRange".to_string());
assert_eq!(el.content_reference_path(), Some("Observation.referenceRange"));
el.content_reference = Some(
"http://hl7.org/fhir/StructureDefinition/Observation#Observation.referenceRange"
.to_string(),
);
assert_eq!(el.content_reference_path(), Some("Observation.referenceRange"));
}
#[test]
fn system_elements_are_recognized_in_every_release() {
let mut el = element("Element.id", "1");
el.types = vec![ElementType {
code: "http://hl7.org/fhirpath/System.String".to_string(),
target_profile: Vec::new(),
}];
assert!(el.is_system_element());
let mut el = element("Element.id", "1");
el.types = vec![ElementType { code: "string".to_string(), target_profile: Vec::new() }];
assert!(el.is_system_element());
let mut el = element("Extension.url", "1");
el.types = vec![ElementType { code: "uri".to_string(), target_profile: Vec::new() }];
assert!(el.is_system_element());
let mut el = element("Patient.birthDate", "1");
el.types = vec![ElementType { code: "date".to_string(), target_profile: Vec::new() }];
assert!(!el.is_system_element());
}
#[test]
fn target_profile_reads_both_shapes() {
let one: ElementType =
::serde_json::from_value(::serde_json::json!({ "code": "Reference",
"targetProfile": "http://hl7.org/fhir/StructureDefinition/Patient" }))
.unwrap();
assert_eq!(one.target_profile, ["http://hl7.org/fhir/StructureDefinition/Patient"]);
let many: ElementType =
::serde_json::from_value(::serde_json::json!({ "code": "Reference",
"targetProfile": ["a", "b"] }))
.unwrap();
assert_eq!(many.target_profile, ["a", "b"]);
let none: ElementType =
::serde_json::from_value(::serde_json::json!({ "code": "string" })).unwrap();
assert!(none.target_profile.is_empty());
}
#[test]
fn binding_value_set_reads_every_spelling() {
let b: Binding = ::serde_json::from_value(::serde_json::json!({
"strength": "required", "valueSet": "http://x/vs|4.0.1" }))
.unwrap();
assert_eq!(b.value_set(), Some("http://x/vs|4.0.1"));
let b: Binding = ::serde_json::from_value(::serde_json::json!({
"strength": "required", "valueSetReference": { "reference": "http://x/vs" } }))
.unwrap();
assert_eq!(b.value_set(), Some("http://x/vs"));
let b: Binding = ::serde_json::from_value(::serde_json::json!({
"strength": "required", "valueSetUri": "http://x/vs" }))
.unwrap();
assert_eq!(b.value_set(), Some("http://x/vs"));
let b: Binding =
::serde_json::from_value(::serde_json::json!({ "strength": "example" })).unwrap();
assert_eq!(b.value_set(), None);
}
#[test]
fn code_system_flattens_nested_concepts() {
let system: CodeSystem = ::serde_json::from_value(::serde_json::json!({
"resourceType": "CodeSystem",
"name": "Example",
"content": "complete",
"concept": [
{ "code": "a", "concept": [ { "code": "b" } ] },
{ "code": "c" }
]
}))
.unwrap();
let codes: Vec<&str> = system.codes().iter().map(|c| c.code.as_str()).collect();
assert_eq!(codes, ["a", "b", "c"]);
}
}