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 {
pub code: String,
#[serde(default)]
pub target_profile: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Binding {
pub strength: String,
pub value_set: Option<String>,
}
#[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 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())))?;
Ok(bundle
.entry
.into_iter()
.filter(|entry| entry.resource.get("resourceType").and_then(|v| v.as_str()) == Some(resource_type))
.filter_map(|entry| ::serde_json::from_value::<T>(entry.resource).ok())
.collect())
}
#[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 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"]);
}
}