use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use octofhir_fhirschema::{StructureDefinition, translate};
use serde_json::Value;
use crate::core::convert;
use crate::definitions::{Definitions, Structure};
pub struct PackageDefinitions<D> {
by_url: HashMap<String, Arc<Structure>>,
base: D,
}
impl<D: Definitions> PackageDefinitions<D> {
pub fn new(base: D) -> Self {
Self {
by_url: HashMap::new(),
base,
}
}
pub fn add(&mut self, resource: &Value) -> Result<bool, String> {
if resource["resourceType"] != "StructureDefinition" {
return Ok(false);
}
let url = resource["url"].as_str().unwrap_or_default().to_owned();
let definition: StructureDefinition = serde_json::from_value(resource.clone())
.map_err(|e| format!("StructureDefinition `{url}`: {e}"))?;
let schema =
translate(definition, None).map_err(|e| format!("StructureDefinition `{url}`: {e}"))?;
let structure = Arc::new(convert(&schema, &url));
if let Some(version) = resource["version"].as_str() {
self.by_url
.insert(format!("{url}|{version}"), structure.clone());
}
self.by_url.insert(url, structure);
Ok(true)
}
pub fn add_release(&mut self, resource: &Value, release: &str) -> Result<bool, String> {
if resource["resourceType"] != "StructureDefinition" {
return Ok(false);
}
let Some(name) = resource["url"]
.as_str()
.and_then(|url| url.strip_prefix("http://hl7.org/fhir/StructureDefinition/"))
else {
return Ok(false);
};
let url = format!("http://hl7.org/fhir/{release}/StructureDefinition/{name}");
let mut resource = resource.clone();
resource["url"] = Value::String(url.clone());
if release.starts_with('3') || release.starts_with('1') {
stu3_types_to_r4(&mut resource);
}
let definition: StructureDefinition = serde_json::from_value(resource)
.map_err(|e| format!("StructureDefinition `{url}`: {e}"))?;
let schema =
translate(definition, None).map_err(|e| format!("StructureDefinition `{url}`: {e}"))?;
self.by_url
.insert(url.clone(), Arc::new(convert(&schema, &url)));
Ok(true)
}
pub fn add_release_folder(
&mut self,
folder: &Path,
release: &str,
) -> std::io::Result<(usize, Vec<String>)> {
let mut added = 0;
let mut problems = Vec::new();
for entry in std::fs::read_dir(folder)? {
let path = entry?.path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(resource) = serde_json::from_str::<Value>(&text) else {
continue;
};
match self.add_release(&resource, release) {
Ok(true) => added += 1,
Ok(false) => {}
Err(problem) => problems.push(format!("{}: {problem}", path.display())),
}
}
Ok((added, problems))
}
pub fn add_folder(&mut self, folder: &Path) -> std::io::Result<(usize, Vec<String>)> {
let mut added = 0;
let mut problems = Vec::new();
for entry in std::fs::read_dir(folder)? {
let path = entry?.path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(resource) = serde_json::from_str::<Value>(&text) else {
continue;
};
match self.add(&resource) {
Ok(true) => added += 1,
Ok(false) => {}
Err(problem) => problems.push(format!("{}: {problem}", path.display())),
}
}
Ok((added, problems))
}
pub fn len(&self) -> usize {
self.by_url.len()
}
pub fn is_empty(&self) -> bool {
self.by_url.is_empty()
}
}
fn stu3_types_to_r4(resource: &mut Value) {
let system = match resource["type"].as_str().unwrap_or_default() {
"boolean" => "Boolean",
"integer" | "positiveInt" | "unsignedInt" => "Integer",
"decimal" => "Decimal",
"date" => "Date",
"dateTime" | "instant" => "DateTime",
"time" => "Time",
_ => "String",
};
let system = format!("http://hl7.org/fhirpath/System.{system}");
for view in ["snapshot", "differential"] {
let Some(elements) = resource[view]["element"].as_array_mut() else {
continue;
};
for element in elements {
let Some(types) = element["type"].as_array() else {
continue;
};
let mut merged: Vec<Value> = Vec::new();
for entry in types {
let mut entry = entry.clone();
if entry.get("code").is_none() {
entry["code"] = Value::String(system.clone());
}
for key in ["profile", "targetProfile"] {
if let Some(single) = entry[key].as_str() {
entry[key] = Value::Array(vec![Value::String(single.to_owned())]);
}
}
match merged.iter_mut().find(|m| m["code"] == entry["code"]) {
Some(existing) => {
for key in ["profile", "targetProfile"] {
if let Some(more) = entry[key].as_array() {
let list = existing[key].as_array_mut();
match list {
Some(list) => list.extend(more.iter().cloned()),
None => existing[key] = Value::Array(more.clone()),
}
}
}
}
None => merged.push(entry),
}
}
element["type"] = Value::Array(merged);
}
}
}
impl<D: Definitions> Definitions for PackageDefinitions<D> {
fn structure(&self, url: &str) -> Option<Arc<Structure>> {
self.by_url
.get(url)
.or_else(|| self.by_url.get(url.split('|').next().unwrap_or(url)))
.cloned()
.or_else(|| self.base.structure(url))
}
fn covers(&self, url: &str) -> bool {
self.by_url
.contains_key(url.split('|').next().unwrap_or(url))
|| self.base.covers(url)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::{CoreDefinitions, Lookup, Release, Types};
#[test]
fn profiles_and_logical_models_resolve_over_the_core() {
let mut definitions = PackageDefinitions::new(CoreDefinitions::new(Release::R4));
let logical = json!({
"resourceType": "StructureDefinition",
"url": "http://example.org/StructureDefinition/Lab",
"name": "Lab",
"status": "draft",
"kind": "logical",
"abstract": false,
"type": "http://example.org/StructureDefinition/Lab",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Base",
"derivation": "specialization",
"differential": { "element": [
{ "id": "Lab", "path": "Lab" },
{ "id": "Lab.code", "path": "Lab.code", "min": 1, "max": "1", "type": [{ "code": "string" }] },
{ "id": "Lab.value", "path": "Lab.value", "min": 0, "max": "1", "type": [{ "code": "decimal" }] }
]}
});
assert_eq!(definitions.add(&logical), Ok(true));
assert_eq!(
definitions.add(&json!({ "resourceType": "ValueSet" })),
Ok(false)
);
let types = Types::new(&definitions);
let lab = types.structure("http://example.org/StructureDefinition/Lab");
assert!(matches!(types.lookup(&lab, "code"), Lookup::Found(_)));
assert!(matches!(
types.lookup(&lab, "nope"),
Lookup::Missing | Lookup::Unknown
));
let profile = json!({
"resourceType": "StructureDefinition",
"url": "http://example.org/StructureDefinition/my-patient",
"name": "MyPatient",
"status": "draft",
"kind": "resource",
"abstract": false,
"type": "Patient",
"baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient",
"derivation": "constraint",
"differential": { "element": [
{ "id": "Patient", "path": "Patient" },
{ "id": "Patient.identifier", "path": "Patient.identifier", "min": 1 }
]}
});
assert_eq!(definitions.add(&profile), Ok(true));
let types = Types::new(&definitions);
let profile = types.structure("http://example.org/StructureDefinition/my-patient");
assert!(matches!(
types.lookup(&profile, "identifier"),
Lookup::Found(_)
));
assert!(matches!(
types.lookup(&profile, "birthDate"),
Lookup::Found(_)
));
assert!(matches!(
types.lookup(&profile, "identifer"),
Lookup::Missing
));
let patient = types.structure("http://hl7.org/fhir/StructureDefinition/Patient");
assert!(matches!(
types.lookup(&patient, "birthDate"),
Lookup::Found(_)
));
}
}