#![cfg(feature = "fhirpath")]
use std::path::Path;
use fml_check::{CoreDefinitions, PackageDefinitions, Release};
use fml_engine::{Engine, FhirPathExpressions};
use serde_json::{Value, json};
fn bundle(name: &str) -> Vec<Value> {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(name);
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let bundle: Value = serde_json::from_str(&text).unwrap();
bundle["resources"].as_array().unwrap().clone()
}
fn resources() -> Vec<Value> {
let mut all = bundle("../fml-xml/tests/data/cda-core.json");
all.extend(bundle("tests/data/ccda-templates.json"));
all
}
fn definitions() -> PackageDefinitions<CoreDefinitions> {
let mut definitions = PackageDefinitions::new(CoreDefinitions::new(Release::R4));
for resource in resources() {
definitions.add(&resource).unwrap();
}
definitions
}
fn model() -> fml_xml::Model {
let mut model = fml_xml::Model::new();
for resource in resources() {
model.add(&resource);
}
model
}
async fn run(map: &str, source: &Value) -> Value {
run_with(map, source, true).await
}
async fn run_with(map: &str, source: &Value, template_versions: bool) -> Value {
let compiled = fml_model::compile(map);
assert!(!compiled.has_errors(), "{:?}", compiled.diagnostics);
let definitions = definitions();
let expressions = FhirPathExpressions::new(Release::R4).await.unwrap();
Engine::new(&definitions, expressions)
.with_template_versions(template_versions)
.transform(&compiled.map, source)
.await
.unwrap_or_else(|e| panic!("{e}"))
.target
}
const CONCERN: &str = r#"<act xmlns="urn:hl7-org:v3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" classCode="ACT" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.30" extension="2015-08-01"/>
<id root="36e3e930-7b14-11db-9fe1-0800200c9a66"/>
<code code="CONC" codeSystem="2.16.840.1.113883.5.6"/>
<statusCode code="active"/>
<effectiveTime><low value="20080501"/></effectiveTime>
<entryRelationship typeCode="SUBJ">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.7"/>
<templateId root="2.16.840.1.113883.10.20.22.4.7" extension="2014-06-09"/>
<id root="4adc1020-7b14-11db-9fe1-0800200c9a66"/>
<code code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<statusCode code="completed"/>
<effectiveTime><low value="200805011030-0500"/></effectiveTime>
<value xsi:type="CD" code="419511003" displayName="Propensity to adverse reaction to drug" codeSystem="2.16.840.1.113883.6.96"/>
<entryRelationship typeCode="MFST" inversionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.9" extension="2014-06-09"/>
<code code="ASSERTION" codeSystem="2.16.840.1.113883.5.4"/>
<value xsi:type="CD" code="247472004" displayName="Hives" codeSystem="2.16.840.1.113883.6.96"/>
</observation>
</entryRelationship>
<entryRelationship typeCode="SUBJ" inversionInd="true">
<observation classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.8" extension="2014-06-09"/>
<code code="SEV" codeSystem="2.16.840.1.113883.5.4"/>
<value xsi:type="CD" code="6736007" displayName="Moderate" codeSystem="2.16.840.1.113883.6.96"/>
</observation>
</entryRelationship>
</observation>
</entryRelationship>
</act>"#;
const TO_FHIR: &str = r#"map "http://example.org/StructureMap/allergy-to-fhir" = "AllergyToFhir"
uses "http://hl7.org/cda/stds/core/StructureDefinition/Act" alias Act as source
uses "http://hl7.org/cda/stds/core/StructureDefinition/CD" alias CD as source
uses "http://hl7.org/cda/us/ccda/StructureDefinition/AllergyIntoleranceObservation" alias AllergyObservation as source
uses "http://hl7.org/cda/us/ccda/StructureDefinition/ReactionObservation" alias ReactionObservation as source
uses "http://hl7.org/cda/us/ccda/StructureDefinition/SeverityObservation" alias SeverityObservation as source
uses "http://hl7.org/fhir/StructureDefinition/AllergyIntolerance" alias AllergyIntolerance as target
uses "http://hl7.org/fhir/StructureDefinition/Annotation" alias Annotation as target
group Concern(source act : Act, target tgt : AllergyIntolerance) {
act.entryRelationship as er then {
er.observation : AllergyObservation as obs then Allergy(obs, tgt);
er.observation : ReactionObservation as wrong -> tgt.note as n, n.text = 'a concern holds no reaction';
};
}
group Allergy(source obs : AllergyObservation, target tgt : AllergyIntolerance) {
obs.id as id -> tgt.identifier as i then {
id.root as r -> i.system = 'urn:ietf:rfc:3986', i.value = append('urn:uuid:', r);
};
obs.effectiveTime as et then {
et.low as low then { low.value as v -> tgt.onset = v; };
};
obs.value : CD as v -> tgt.code as cc then CdToConcept(v, cc);
obs.entryRelationship as er then {
er.observation : ReactionObservation as r -> tgt.reaction as reaction then {
r.value : CD as v -> reaction.manifestation as m then CdToConcept(v, m);
};
er.observation -> tgt.note;
};
}
group CdToConcept(source v : CD, target cc : CodeableConcept) {
v -> cc.coding as coding then {
v.code as code -> coding.code = code;
v.displayName as d -> coding.display = d;
v.codeSystem as s where (s = '2.16.840.1.113883.6.96') -> coding.system = 'http://snomed.info/sct';
};
}
group SeverityNote(source o : SeverityObservation, target n : Annotation) <<types>> {
o.value as v then { v.displayName as d -> n.text = append('severity: ', d); };
}
group ReactionNote(source o : ReactionObservation, target n : Annotation) <<types>> {
o -> n.text = 'reaction';
}
"#;
#[tokio::test]
async fn cda_xml_to_fhir_by_templates() {
let model = model();
let document = fml_xml::read(CONCERN, &model, None).unwrap();
assert!(document.issues.is_empty(), "{:#?}", document.issues);
let result = run(TO_FHIR, &document.json).await;
assert_eq!(
result,
json!({
"resourceType": "AllergyIntolerance",
"identifier": [{ "system": "urn:ietf:rfc:3986", "value": "urn:uuid:4adc1020-7b14-11db-9fe1-0800200c9a66" }],
"code": { "coding": [{ "system": "http://snomed.info/sct", "code": "419511003", "display": "Propensity to adverse reaction to drug" }] },
"onsetDateTime": "2008-05-01T10:30-05:00",
"note": [{ "text": "reaction" }, { "text": "severity: Moderate" }],
"reaction": [{ "manifestation": [{ "coding": [{ "system": "http://snomed.info/sct", "code": "247472004", "display": "Hives" }] }] }]
})
);
}
const TO_CDA: &str = r#"map "http://example.org/StructureMap/allergy-to-cda" = "AllergyToCda"
uses "http://hl7.org/fhir/StructureDefinition/AllergyIntolerance" alias AllergyIntolerance as source
uses "http://hl7.org/cda/stds/core/StructureDefinition/Observation" alias Observation as target
group Allergy(source ai : AllergyIntolerance, target obs : Observation) {
ai -> obs.classCode = 'OBS', obs.moodCode = 'EVN';
ai -> obs.templateId as t, t.root = '2.16.840.1.113883.10.20.22.4.7', t.extension = '2014-06-09';
ai.onset : dateTime as d -> obs.effectiveTime as et, et.low as low, low.value = d;
ai.code as c -> obs.value = create('CD') as v then {
c.coding as coding then {
coding.code as code -> v.code = code;
coding.display as d -> v.displayName = d;
coding.system as s where (s = 'http://snomed.info/sct') -> v.codeSystem = '2.16.840.1.113883.6.96';
};
};
ai.reaction as r -> obs.entryRelationship as er, er.typeCode = 'MFST', er.inversionInd = true, er.observation as ro then {
r -> ro.classCode = 'OBS', ro.moodCode = 'EVN';
r.manifestation as m -> ro.value = create('CD') as v then {
m.coding as coding then { coding.code as code -> v.code = code; };
};
};
}
"#;
#[tokio::test]
async fn fhir_to_cda_xml() {
let allergy = json!({
"resourceType": "AllergyIntolerance",
"onsetDateTime": "2008-05-01T10:30:00-05:00",
"code": { "coding": [{ "system": "http://snomed.info/sct", "code": "419511003", "display": "Propensity" }] },
"reaction": [{ "manifestation": [{ "coding": [{ "system": "http://snomed.info/sct", "code": "247472004" }] }] }]
});
let result = run(TO_CDA, &allergy).await;
assert_eq!(
result["value"],
json!([{ "@type": "CD", "code": "419511003", "displayName": "Propensity", "codeSystem": "2.16.840.1.113883.6.96" }])
);
let model = model();
let written = fml_xml::write(
&result,
&model,
"http://hl7.org/cda/stds/core/StructureDefinition/Observation",
)
.unwrap();
assert!(written.issues.is_empty(), "{:#?}", written.issues);
let expected = r#"<observation xmlns="urn:hl7-org:v3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" classCode="OBS" moodCode="EVN">
<templateId root="2.16.840.1.113883.10.20.22.4.7" extension="2014-06-09"/>
<effectiveTime><low value="20080501103000-0500"/></effectiveTime>
<value xsi:type="CD" code="419511003" codeSystem="2.16.840.1.113883.6.96" displayName="Propensity"/>
<entryRelationship typeCode="MFST" inversionInd="true">
<observation classCode="OBS" moodCode="EVN"><value xsi:type="CD" code="247472004"/></observation>
</entryRelationship>
</observation>"#;
let differences = fml_xml::compare(expected, &written.xml).unwrap();
assert!(differences.is_empty(), "{differences:#?}\n{}", written.xml);
}
#[tokio::test]
async fn template_versions_can_be_ignored() {
let model = model();
let older = CONCERN.replace(
r#"<templateId root="2.16.840.1.113883.10.20.22.4.7" extension="2014-06-09"/>"#,
"",
);
let document = fml_xml::read(&older, &model, None).unwrap();
let strict = run_with(TO_FHIR, &document.json, true).await;
assert!(strict.get("code").is_none(), "{strict}");
let any_version = run_with(TO_FHIR, &document.json, false).await;
assert_eq!(any_version["code"]["coding"][0]["code"], "419511003");
}
#[tokio::test]
async fn type_filters_name_logical_types() {
let map = r#"map "http://example.org/StructureMap/values" = "Values"
uses "http://hl7.org/cda/stds/core/StructureDefinition/Observation" alias Observation as source
uses "http://hl7.org/fhir/StructureDefinition/Observation" alias FhirObservation as target
group Values(source obs : Observation, target tgt : FhirObservation) {
obs.value : PQ as v -> tgt.value = create('Quantity') as q then {
v.value as n -> q.value = n;
v.unit as u -> q.unit = u;
};
obs.value : ST as v -> tgt.note as note then {
v.xmlText as t -> note.text = t;
};
}
"#;
let model = model();
let xml = r#"<observation xmlns="urn:hl7-org:v3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" classCode="OBS" moodCode="EVN">
<value xsi:type="PQ" value="120" unit="mm[Hg]"/>
<value xsi:type="ST">high</value>
</observation>"#;
let document = fml_xml::read(xml, &model, None).unwrap();
let result = run(map, &document.json).await;
assert_eq!(
result["valueQuantity"],
json!({ "value": 120, "unit": "mm[Hg]" })
);
assert_eq!(result["note"][0]["text"], "high");
}