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(default, rename = "type")]
type_name_raw: Option<String>,
#[serde(default)]
#[allow(dead_code, reason = "read via constrains(); kept as the parsed shape")]
constrained_type: Option<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>,
#[serde(default)]
pub base: Option<String>,
}
impl StructureDefinition {
#[must_use]
pub fn kind_name(&self) -> &str {
if self.kind != "datatype" {
return &self.kind;
}
if self.type_name().starts_with(char::is_lowercase) {
"primitive-type"
} else {
"complex-type"
}
}
#[must_use]
pub fn type_name(&self) -> &str {
self.type_name_raw.as_deref().unwrap_or(&self.name)
}
}
#[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, rename = "targetProfile", deserialize_with = "string_or_seq")]
target_profile_raw: Vec<String>,
#[serde(default, deserialize_with = "string_or_seq")]
profile: Vec<String>,
}
impl ElementType {
#[must_use]
pub fn target_profiles(&self) -> &[String] {
if self.target_profile_raw.is_empty() {
&self.profile
} else {
&self.target_profile_raw
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Binding {
#[serde(default = "weakest_binding", alias = "conformance")]
pub strength: String,
#[serde(rename = "valueSet")]
value_set_canonical: Option<String>,
#[serde(alias = "referenceResource")]
value_set_reference: Option<BindingReference>,
value_set_uri: Option<String>,
}
fn weakest_binding() -> String {
"example".to_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 ValueSet {
pub url: Option<String>,
pub compose: Option<Compose>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Compose {
#[serde(default)]
pub include: Vec<Include>,
#[serde(default)]
pub exclude: Vec<Include>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Include {
pub system: Option<String>,
#[serde(default)]
pub concept: Vec<IncludeConcept>,
#[serde(default, rename = "valueSet")]
pub value_set: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct IncludeConcept {
pub code: String,
pub display: Option<String>,
}
#[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>> {
let mut defs: Vec<StructureDefinition> =
read_resources::<StructureDefinition>(path, "StructureDefinition")?
.into_iter()
.filter(|sd| sd.snapshot.is_some())
.collect();
if !defs.iter().any(|d| d.kind_name() == "primitive-type") {
let mut referenced: std::collections::BTreeSet<String> = [
"base64Binary",
"boolean",
"code",
"date",
"dateTime",
"decimal",
"id",
"idref",
"instant",
"integer",
"oid",
"string",
"time",
"uri",
"uuid",
]
.iter()
.map(|s| (*s).to_string())
.collect();
for d in &defs {
for el in d.snapshot.iter().flat_map(|s| &s.element) {
for t in &el.types {
if is_primitive_code(&t.code) {
referenced.insert(t.code.clone());
}
}
}
}
for code in referenced {
if let Ok(sd) = ::serde_json::from_value::<StructureDefinition>(::serde_json::json!({
"name": code, "type": code, "kind": "primitive-type",
"url": format!("http://hl7.org/fhir/StructureDefinition/{code}"),
"description": format!("The FHIR `{code}` primitive, built in to this release rather than declared."),
"snapshot": { "element": [{ "path": code }] }
})) {
defs.push(sd);
}
}
}
let declared: Vec<String> = defs
.iter()
.filter(|d| matches!(d.kind_name(), "complex-type" | "primitive-type"))
.map(|d| d.type_name().to_string())
.filter(|t| t != "Extension")
.collect();
if !declared.is_empty() {
for d in &mut defs {
for el in d.snapshot.iter_mut().flat_map(|s| &mut s.element) {
if el.types.iter().any(|t| t.code == "*") {
el.types = declared
.iter()
.map(|code| ElementType {
code: code.clone(),
target_profile_raw: Vec::new(),
profile: Vec::new(),
})
.collect();
}
}
}
}
if !defs.iter().any(|d| d.type_name() == "Element")
&& defs.iter().any(|d| d.type_name() == "Extension")
&& let Ok(sd) = ::serde_json::from_value::<StructureDefinition>(::serde_json::json!({
"name": "Element", "type": "Element", "kind": "complex-type",
"url": "http://hl7.org/fhir/StructureDefinition/Element",
"description": "The base of every element: an id and extensions. Absent from DSTU1, which had no primitive extensions.",
"snapshot": { "element": [
{ "path": "Element" },
{ "path": "Element.id", "min": 0, "max": "1", "type": [{ "code": "id" }] },
{ "path": "Element.extension", "min": 0, "max": "*", "type": [{ "code": "Extension" }] }
] }
}))
{
defs.push(sd);
}
let declares_any_primitive = defs.iter().any(|d| d.kind_name() == "primitive-type");
if declares_any_primitive
&& !defs.iter().any(|d| d.type_name() == "xhtml")
&& let Ok(sd) = ::serde_json::from_value::<StructureDefinition>(::serde_json::json!({
"name": "xhtml",
"type": "xhtml",
"kind": "primitive-type",
"url": "http://hl7.org/fhir/StructureDefinition/xhtml",
"description": "XHTML, as used by Narrative.div. Built in to this release rather than declared.",
"snapshot": { "element": [{ "path": "xhtml" }] }
}))
{
defs.push(sd);
}
Ok(defs)
}
pub fn read_code_systems(path: &Path) -> std::io::Result<Vec<CodeSystem>> {
let mut out = read_resources::<CodeSystem>(path, "CodeSystem")?;
out.extend(inline_code_systems(path)?);
Ok(out)
}
fn inline_code_systems(path: &Path) -> std::io::Result<Vec<CodeSystem>> {
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
let raw: ::serde_json::Value = ::serde_json::from_reader(reader)
.map_err(|e| std::io::Error::other(format!("{}: {e}", path.display())))?;
let bundle: Bundle = ::serde_json::from_value(raw)
.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("ValueSet") {
continue;
}
let Some(cs) = entry
.resource
.get("codeSystem")
.or_else(|| entry.resource.get("define"))
else {
continue;
};
let mut synthesized = cs.clone();
if let Some(obj) = synthesized.as_object_mut() {
obj.insert("resourceType".into(), "CodeSystem".into());
for field in ["name", "id", "title", "description"] {
if let Some(v) = entry.resource.get(field) {
obj.insert(field.into(), v.clone());
}
}
obj.entry("content".to_string())
.or_insert_with(|| "complete".into());
if !obj.contains_key("url")
&& let Some(sys) = cs.get("system")
{
obj.insert("url".into(), sys.clone());
}
}
if let Ok(c) = ::serde_json::from_value::<CodeSystem>(synthesized) {
out.push(c);
}
}
Ok(out)
}
pub fn read_value_sets(path: &Path) -> std::io::Result<Vec<ValueSet>> {
read_resources::<ValueSet>(path, "ValueSet")
}
fn normalize_name_references(bundle: &mut ::serde_json::Value) {
let Some(entries) = bundle.get_mut("entry").and_then(|e| e.as_array_mut()) else {
return;
};
for entry in entries.iter_mut() {
let Some(resource) = entry.get_mut("resource") else {
continue;
};
for section in ["snapshot", "differential"] {
let Some(elements) = resource
.get_mut(section)
.and_then(|s| s.get_mut("element"))
.and_then(|e| e.as_array_mut())
else {
continue;
};
let by_name: BTreeMap<String, String> = elements
.iter()
.filter_map(|el| {
let name = el.get("name")?.as_str()?.to_string();
let path = el.get("path")?.as_str()?.to_string();
Some((name, path))
})
.collect();
for el in elements.iter_mut() {
let Some(target) = el.get("nameReference").and_then(|v| v.as_str()) else {
continue;
};
if let Some(path) = by_name.get(target).cloned()
&& let Some(obj) = el.as_object_mut()
{
obj.insert("contentReference".into(), format!("#{path}").into());
}
}
}
}
}
fn is_primitive_code(code: &str) -> bool {
!code.is_empty()
&& code.starts_with(char::is_lowercase)
&& code.chars().all(|c| c.is_ascii_alphanumeric())
}
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 mut raw: ::serde_json::Value = ::serde_json::from_reader(reader)
.map_err(|e| std::io::Error::other(format!("{}: {e}", path.display())))?;
normalize_name_references(&mut raw);
let bundle: Bundle = ::serde_json::from_value(raw)
.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().to_string(), 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 dstu2_name_reference_resolves_through_the_element_name() {
let mut bundle = ::serde_json::json!({
"resourceType": "Bundle",
"entry": [{ "resource": {
"resourceType": "StructureDefinition",
"name": "Bundle",
"snapshot": { "element": [
{ "path": "Bundle.link", "name": "link" },
{ "path": "Bundle.entry.link", "nameReference": "link" },
]},
}}]
});
normalize_name_references(&mut bundle);
let els = &bundle["entry"][0]["resource"]["snapshot"]["element"];
assert_eq!(els[1]["contentReference"], "#Bundle.link");
assert!(els[0].get("contentReference").is_none());
}
#[test]
fn an_unresolvable_name_reference_is_left_alone() {
let mut bundle = ::serde_json::json!({
"resourceType": "Bundle",
"entry": [{ "resource": {
"resourceType": "StructureDefinition",
"snapshot": { "element": [
{ "path": "X.y", "nameReference": "nothing-has-this-name" },
]},
}}]
});
normalize_name_references(&mut bundle);
let el = &bundle["entry"][0]["resource"]["snapshot"]["element"][0];
assert!(el.get("contentReference").is_none());
}
#[test]
fn name_references_resolve_in_the_differential_too() {
let mut bundle = ::serde_json::json!({
"resourceType": "Bundle",
"entry": [{ "resource": {
"resourceType": "StructureDefinition",
"differential": { "element": [
{ "path": "P.part", "name": "part" },
{ "path": "P.part.part", "nameReference": "part" },
]},
}}]
});
normalize_name_references(&mut bundle);
assert_eq!(
bundle["entry"][0]["resource"]["differential"]["element"][1]["contentReference"],
"#P.part"
);
}
#[test]
fn the_reader_applies_the_name_reference_normalization() {
let dir = std::env::temp_dir().join("fhir-spec-wiring-test");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("dstu2.json");
std::fs::write(
&path,
::serde_json::json!({
"resourceType": "Bundle",
"entry": [{ "resource": {
"resourceType": "StructureDefinition",
"name": "Bundle", "type": "Bundle", "kind": "resource",
"url": "http://hl7.org/fhir/StructureDefinition/Bundle",
"snapshot": { "element": [
{ "path": "Bundle" },
{ "path": "Bundle.link", "name": "link",
"type": [{ "code": "BackboneElement" }] },
{ "path": "Bundle.entry.link", "nameReference": "link" },
]},
}}]
})
.to_string(),
)
.unwrap();
let defs = read_structure_definitions(&path).unwrap();
let bundle = defs.iter().find(|d| d.type_name() == "Bundle").unwrap();
let nested = bundle
.snapshot
.as_ref()
.unwrap()
.element
.iter()
.find(|e| e.path == "Bundle.entry.link")
.expect("Bundle.entry.link survived the read");
assert_eq!(nested.content_reference_path(), Some("Bundle.link"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_url_type_code_is_not_a_primitive() {
assert!(!is_primitive_code("http://hl7.org/fhirpath/System.String"));
assert!(!is_primitive_code(""));
assert!(!is_primitive_code("System.String"));
assert!(is_primitive_code("dateTime"));
assert!(is_primitive_code("base64Binary"));
assert!(!is_primitive_code("CodeableConcept"));
}
#[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_raw: Vec::new(),
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_raw: Vec::new(),
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_raw: Vec::new(),
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_raw: Vec::new(),
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_profiles(),
["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_profiles(), ["a", "b"]);
let none: ElementType =
::serde_json::from_value(::serde_json::json!({ "code": "string" })).unwrap();
assert!(none.target_profiles().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"]);
}
}