From b239112dc35b6cf1033941285c8c1ee825c22196 Mon Sep 17 00:00:00 2001 From: Bastian Schaffer Date: Tue, 3 Sep 2024 10:45:33 +0200 Subject: [PATCH] Update Mapping Tree Fixes: #151 --- pom.xml | 4 +- .../numcodex/sq2cql/model/MappingContext.java | 6 +- .../sq2cql/model/MappingTreeBase.java | 17 ++ .../sq2cql/model/MappingTreeModuleEntry.java | 16 + .../sq2cql/model/MappingTreeModuleRoot.java | 40 +++ .../numcodex/sq2cql/model/TermCodeNode.java | 61 ---- .../java/de/numcodex/sq2cql/EvaluationIT.java | 14 +- .../de/numcodex/sq2cql/TranslatorTest.java | 41 +-- src/test/java/de/numcodex/sq2cql/Util.java | 35 ++- .../sq2cql/model/MappingContextTest.java | 3 +- .../sq2cql/model/MappingTreeBaseTest.java | 288 ++++++++++++++++++ .../sq2cql/model/TermCodeNodeTest.java | 163 ---------- .../ConceptCriterionTest.java | 17 +- 13 files changed, 432 insertions(+), 273 deletions(-) create mode 100644 src/main/java/de/numcodex/sq2cql/model/MappingTreeBase.java create mode 100644 src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleEntry.java create mode 100644 src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleRoot.java delete mode 100644 src/main/java/de/numcodex/sq2cql/model/TermCodeNode.java create mode 100644 src/test/java/de/numcodex/sq2cql/model/MappingTreeBaseTest.java delete mode 100644 src/test/java/de/numcodex/sq2cql/model/TermCodeNodeTest.java diff --git a/pom.xml b/pom.xml index c8b12a0..676259d 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 7.2.1 1.20.0 2.0.13 - 2.2.0 + 3.0.0-test.1 @@ -177,7 +177,7 @@ download-single - https://github.com/medizininformatik-initiative/fhir-ontology-generator/raw/v${ontology.version}/example/mii_core_data_set/ontology/mapping.zip + https://github.com/medizininformatik-initiative/fhir-ontology-generator/raw/v${ontology.version}/example/fdpg-ontology/mapping.zip ${project.build.directory} diff --git a/src/main/java/de/numcodex/sq2cql/model/MappingContext.java b/src/main/java/de/numcodex/sq2cql/model/MappingContext.java index 8c328c2..f42576e 100644 --- a/src/main/java/de/numcodex/sq2cql/model/MappingContext.java +++ b/src/main/java/de/numcodex/sq2cql/model/MappingContext.java @@ -21,10 +21,10 @@ public class MappingContext { private final Map mappings; - private final TermCodeNode conceptTree; + private final MappingTreeBase conceptTree; private final Map codeSystemDefinitions; - private MappingContext(Map mappings, TermCodeNode conceptTree, + private MappingContext(Map mappings, MappingTreeBase conceptTree, Map codeSystemDefinitions) { this.mappings = mappings; this.conceptTree = conceptTree; @@ -48,7 +48,7 @@ public static MappingContext of() { * @param codeSystemAliases a map of code system URLs to their aliases * @return the mapping context */ - public static MappingContext of(Map mappings, TermCodeNode conceptTree, + public static MappingContext of(Map mappings, MappingTreeBase conceptTree, Map codeSystemAliases) { return new MappingContext(Map.copyOf(mappings), conceptTree, codeSystemAliases.entrySet().stream() .collect(Collectors.toConcurrentMap(Map.Entry::getKey, diff --git a/src/main/java/de/numcodex/sq2cql/model/MappingTreeBase.java b/src/main/java/de/numcodex/sq2cql/model/MappingTreeBase.java new file mode 100644 index 0000000..c3072b6 --- /dev/null +++ b/src/main/java/de/numcodex/sq2cql/model/MappingTreeBase.java @@ -0,0 +1,17 @@ +package de.numcodex.sq2cql.model; + + +import de.numcodex.sq2cql.model.structured_query.ContextualTermCode; + +import java.util.List; +import java.util.stream.Stream; + +public record MappingTreeBase(List moduleRoots) { + + public Stream expand(ContextualTermCode termCode) { + var key = termCode.termCode().code(); + + return moduleRoots.stream().flatMap(moduleRoot -> + moduleRoot.isModuleMatching(termCode) ? moduleRoot.expand(key) : Stream.empty()); + } +} diff --git a/src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleEntry.java b/src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleEntry.java new file mode 100644 index 0000000..e220ccd --- /dev/null +++ b/src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleEntry.java @@ -0,0 +1,16 @@ +package de.numcodex.sq2cql.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record MappingTreeModuleEntry(String key, List children) { + @JsonCreator + static MappingTreeModuleEntry fromJson(@JsonProperty("key") String key, + @JsonProperty("children") List children) { + return new MappingTreeModuleEntry(key, children); + } +} diff --git a/src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleRoot.java b/src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleRoot.java new file mode 100644 index 0000000..1d0cae2 --- /dev/null +++ b/src/main/java/de/numcodex/sq2cql/model/MappingTreeModuleRoot.java @@ -0,0 +1,40 @@ +package de.numcodex.sq2cql.model; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.numcodex.sq2cql.model.common.TermCode; +import de.numcodex.sq2cql.model.structured_query.ContextualTermCode; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.function.Function.identity; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record MappingTreeModuleRoot(TermCode context, String system, Map entries) { + @JsonCreator + static MappingTreeModuleRoot fromJson(@JsonProperty("context") TermCode context, + @JsonProperty("system") String system, + @JsonProperty("entries") List entries) { + return new MappingTreeModuleRoot( + context, + system, + entries.stream().collect(Collectors.toMap(MappingTreeModuleEntry::key, identity()))); + } + + public Stream expand(String key) { + var newTermCode = new ContextualTermCode(context, new TermCode(system, key, "")); + + return Stream.concat(Stream.of(newTermCode), entries.get(key).children().stream().flatMap(this::expand)); + } + + boolean isModuleMatching(ContextualTermCode contextualTermCode) { + return context.equals(contextualTermCode.context()) && + system.equals(contextualTermCode.termCode().system()) && + entries.containsKey(contextualTermCode.termCode().code()); + } +} diff --git a/src/main/java/de/numcodex/sq2cql/model/TermCodeNode.java b/src/main/java/de/numcodex/sq2cql/model/TermCodeNode.java deleted file mode 100644 index 99cad59..0000000 --- a/src/main/java/de/numcodex/sq2cql/model/TermCodeNode.java +++ /dev/null @@ -1,61 +0,0 @@ -package de.numcodex.sq2cql.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import de.numcodex.sq2cql.model.common.TermCode; -import de.numcodex.sq2cql.model.structured_query.ContextualTermCode; - -import java.util.List; -import java.util.stream.Stream; - -import static java.util.Objects.requireNonNull; - -/** - * @author Alexander Kiel - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record TermCodeNode(ContextualTermCode contextualTermCode, List children) { - - public TermCodeNode { - requireNonNull(contextualTermCode); - children = List.copyOf(children); - } - - public static TermCodeNode of(ContextualTermCode termCode) { - return new TermCodeNode(termCode, List.of()); - } - - public static TermCodeNode of(ContextualTermCode termCode, TermCodeNode... children) { - return new TermCodeNode(termCode, List.of(children)); - } - - @JsonCreator - public static TermCodeNode of(@JsonProperty("context") TermCode context, - @JsonProperty("termCode") TermCode termCode, - @JsonProperty("children") TermCodeNode... children) { - var contextualTermCode = ContextualTermCode.of(context, - requireNonNull(termCode, "missing JSON property: termCode")); - return new TermCodeNode(contextualTermCode, - children == null ? List.of() : List.of(children)); - } - - public Stream expand(ContextualTermCode termCode) { - if (requireNonNull(termCode).equals(this.contextualTermCode)) { - return leafConcepts(); - } else if (children.isEmpty()) { - return Stream.of(); - } else { - return children.stream().flatMap(n -> n.expand(termCode)); - } - } - - private Stream leafConcepts() { - if (children.isEmpty()) { - return Stream.of(contextualTermCode); - } else { - return Stream.concat(Stream.of(contextualTermCode), - children.stream().flatMap(TermCodeNode::leafConcepts)); - } - } -} diff --git a/src/test/java/de/numcodex/sq2cql/EvaluationIT.java b/src/test/java/de/numcodex/sq2cql/EvaluationIT.java index a607782..4db9a2c 100644 --- a/src/test/java/de/numcodex/sq2cql/EvaluationIT.java +++ b/src/test/java/de/numcodex/sq2cql/EvaluationIT.java @@ -5,9 +5,7 @@ import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.StringParam; import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.sq2cql.model.Mapping; -import de.numcodex.sq2cql.model.MappingContext; -import de.numcodex.sq2cql.model.TermCodeNode; +import de.numcodex.sq2cql.model.*; import de.numcodex.sq2cql.model.common.TermCode; import de.numcodex.sq2cql.model.structured_query.ContextualConcept; import de.numcodex.sq2cql.model.structured_query.ContextualTermCode; @@ -31,6 +29,7 @@ import java.util.Map; import java.util.UUID; +import static de.numcodex.sq2cql.Util.createTreeWithoutChildren; import static de.numcodex.sq2cql.model.common.Comparator.LESS_THAN; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; @@ -42,8 +41,9 @@ public class EvaluationIT { static final TermCode CONTEXT = TermCode.of("context", "context", "context"); - static final ContextualTermCode ROOT = ContextualTermCode.of(CONTEXT, TermCode.of("", "", "")); - static final ContextualTermCode BLOOD_PRESSURE = ContextualTermCode.of(CONTEXT, TermCode.of("http://loinc.org", "85354-9", + static final String BLOOD_PRESSURE_CODE = "85354-9"; + static final String BLOOD_PRESSURE_SYSTEM = "http://loinc.org"; + static final ContextualTermCode BLOOD_PRESSURE = ContextualTermCode.of(CONTEXT, TermCode.of(BLOOD_PRESSURE_SYSTEM, BLOOD_PRESSURE_CODE, "Blood pressure panel with all children optional")); static final TermCode DIASTOLIC_BLOOD_PRESSURE = TermCode.of("http://loinc.org", "8462-4", "Diastolic blood pressure"); @@ -99,7 +99,7 @@ public void evaluateBloodPressure() throws Exception { var valueFhirPath = format("component.where(code.coding.exists(system = '%s' and code = '%s')).value.first()", DIASTOLIC_BLOOD_PRESSURE.system(), DIASTOLIC_BLOOD_PRESSURE.code()); var mappings = Map.of(BLOOD_PRESSURE, Mapping.of(BLOOD_PRESSURE, "Observation", valueFhirPath)); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(BLOOD_PRESSURE)); + var conceptTree = createTreeWithoutChildren(BLOOD_PRESSURE); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); var translator = Translator.of(mappingContext); var criterion = NumericCriterion.of(ContextualConcept.of(BLOOD_PRESSURE), LESS_THAN, BigDecimal.valueOf(80), "mm[Hg]"); @@ -157,7 +157,7 @@ public void evaluateBloodPressureAttribute() throws Exception { } """); var mappings = Map.of(BLOOD_PRESSURE, mapping); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(BLOOD_PRESSURE)); + var conceptTree = createTreeWithoutChildren(BLOOD_PRESSURE); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); var translator = Translator.of(mappingContext); var structuredQuery = readStructuredQuery(""" diff --git a/src/test/java/de/numcodex/sq2cql/TranslatorTest.java b/src/test/java/de/numcodex/sq2cql/TranslatorTest.java index 7708375..a079034 100644 --- a/src/test/java/de/numcodex/sq2cql/TranslatorTest.java +++ b/src/test/java/de/numcodex/sq2cql/TranslatorTest.java @@ -1,10 +1,7 @@ package de.numcodex.sq2cql; import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.sq2cql.model.AttributeMapping; -import de.numcodex.sq2cql.model.Mapping; -import de.numcodex.sq2cql.model.MappingContext; -import de.numcodex.sq2cql.model.TermCodeNode; +import de.numcodex.sq2cql.model.*; import de.numcodex.sq2cql.model.common.TermCode; import de.numcodex.sq2cql.model.structured_query.*; import org.junit.jupiter.api.Nested; @@ -15,6 +12,7 @@ import java.util.Map; import static de.numcodex.sq2cql.Assertions.assertThat; +import static de.numcodex.sq2cql.Util.*; import static de.numcodex.sq2cql.model.common.Comparator.LESS_THAN; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -118,7 +116,7 @@ void nonExpandableConcept() { @Test void nonMappableConcept() { - var conceptTree = TermCodeNode.of(C71, TermCodeNode.of(C71_0), TermCodeNode.of(C71_1)); + var conceptTree = createTreeWithChildren(C71, C71_0, C71_1); var mappingContext = MappingContext.of(Map.of(), conceptTree, CODE_SYSTEM_ALIASES); var message = assertThrows(TranslationException.class, () -> Translator.of(mappingContext) @@ -136,7 +134,7 @@ void usage_Documentation() { TermCode.of("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "C71.1", "Malignant neoplasm of brain")); var mappings = Map.of(c71_1, Mapping.of(c71_1, "Condition")); - var conceptTree = TermCodeNode.of(c71_1); + var conceptTree = createTreeWithoutChildren(c71_1); var codeSystemAliases = Map.of("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "icd10"); var mappingContext = MappingContext.of(mappings, conceptTree, codeSystemAliases); @@ -167,7 +165,7 @@ void timeRestriction() { "Malignant neoplasm of brain")); var mappings = Map.of(c71_1, Mapping.of(c71_1, "Condition", null, null, List.of(), List.of(), "onset")); - var conceptTree = TermCodeNode.of(c71_1); + var conceptTree = createTreeWithoutChildren(c71_1); var codeSystemAliases = Map.of("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "icd10"); var mappingContext = MappingContext.of(mappings, conceptTree, codeSystemAliases); @@ -201,7 +199,7 @@ void timeRestriction_missingPathInMapping() { "Malignant neoplasm of brain")); var mappings = Map.of(c71_1, Mapping.of(c71_1, "Condition", null, null, List.of(), List.of(), null)); - var conceptTree = TermCodeNode.of(c71_1); + var conceptTree = createTreeWithoutChildren(c71_1); var codeSystemAliases = Map.of("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "icd10"); var mappingContext = MappingContext.of(mappings, conceptTree, codeSystemAliases); var query = StructuredQuery.of(List.of(List.of(ConceptCriterion.of(ContextualConcept.of(c71_1), @@ -220,8 +218,9 @@ void test_Task1() { Mapping.of(C71_1, "Condition", null, null, List.of(), List.of(VERIFICATION_STATUS_ATTR_MAPPING)), TMZ, Mapping.of(TMZ, "MedicationStatement")); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(TMZ), - TermCodeNode.of(C71, TermCodeNode.of(C71_0), TermCodeNode.of(C71_1))); + var conceptTree = new MappingTreeBase(List.of( + createTreeRootWithoutChildren(TMZ), + createTreeRootWithChildren(C71, C71_0, C71_1))); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); var structuredQuery = StructuredQuery.of(List.of(List.of( ConceptCriterion.of(ContextualConcept.of(C71)) @@ -270,8 +269,10 @@ void test_Task2() { Mapping.of(HYPERTENSION, "Condition", null, null, List.of(), List.of(VERIFICATION_STATUS_ATTR_MAPPING)), SERUM, Mapping.of(SERUM, "Specimen"), LIPID, Mapping.of(LIPID, "MedicationStatement")); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(HYPERTENSION), TermCodeNode.of(SERUM), - TermCodeNode.of(LIPID)); + var conceptTree = new MappingTreeBase(List.of( + createTreeRootWithoutChildren(HYPERTENSION), + createTreeRootWithoutChildren(SERUM), + createTreeRootWithoutChildren(LIPID))); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); var structuredQuery = StructuredQuery.of(List.of(List.of( ConceptCriterion.of(ContextualConcept.of(HYPERTENSION)) @@ -324,7 +325,7 @@ void geccoTask2() { Mapping.of(G47_31, "Condition", null, null, List.of(CodingModifier.of("verificationStatus.coding", CONFIRMED)), List.of()), TOBACCO_SMOKING_STATUS, Mapping.of(TOBACCO_SMOKING_STATUS, "Observation", "value")); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(COPD), TermCodeNode.of(G47_31)); + var conceptTree = new MappingTreeBase(List.of(createTreeRootWithoutChildren(COPD), createTreeRootWithoutChildren(G47_31))); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); var structuredQuery = StructuredQuery.of( List.of(List.of(ValueSetCriterion.of(ContextualConcept.of(FRAILTY_SCORE), VERY_FIT, WELL))), @@ -467,7 +468,7 @@ void onlyFixedCriteria() throws Exception { } """); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(COMBINED_CONSENT)); + var conceptTree = createTreeWithoutChildren(COMBINED_CONSENT); var mappings = Map.of(COMBINED_CONSENT, mapping); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); @@ -549,7 +550,7 @@ void numericAgeTranslation() throws Exception { ] } """); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(AGE)); + var conceptTree = createTreeWithoutChildren(AGE); var mappings = Map.of(AGE, mapping); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); @@ -625,7 +626,7 @@ void ageRangeTranslation() throws Exception { ] } """); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(AGE)); + var conceptTree = createTreeWithoutChildren(AGE); var mappings = Map.of(AGE, mapping); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); @@ -701,7 +702,7 @@ void numericAgeTranslationInHours() throws Exception { ] } """); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(AGE)); + var conceptTree = createTreeWithoutChildren(AGE); var mappings = Map.of(AGE, mapping); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); @@ -777,7 +778,7 @@ void patientGender() throws Exception { ] } """); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(GENDER)); + var conceptTree = createTreeWithoutChildren(GENDER); var mappings = Map.of(GENDER, mapping); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); @@ -844,7 +845,7 @@ void consent() throws Exception { ] } """); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(CONSENT_MDAT)); + var conceptTree = createTreeWithoutChildren(CONSENT_MDAT); var mappings = Map.of(CONSENT_MDAT, mapping); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); @@ -938,7 +939,7 @@ void bloodPressure() throws Exception { ] } """); - var conceptTree = TermCodeNode.of(ROOT, TermCodeNode.of(BLOOD_PRESSURE)); + var conceptTree = createTreeWithoutChildren(BLOOD_PRESSURE); var mappings = Map.of(BLOOD_PRESSURE, mapping); var mappingContext = MappingContext.of(mappings, conceptTree, CODE_SYSTEM_ALIASES); diff --git a/src/test/java/de/numcodex/sq2cql/Util.java b/src/test/java/de/numcodex/sq2cql/Util.java index 8afcd58..46f305f 100644 --- a/src/test/java/de/numcodex/sq2cql/Util.java +++ b/src/test/java/de/numcodex/sq2cql/Util.java @@ -2,13 +2,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Functions; -import de.numcodex.sq2cql.model.Mapping; -import de.numcodex.sq2cql.model.MappingContext; -import de.numcodex.sq2cql.model.TermCodeNode; +import de.numcodex.sq2cql.model.*; import de.numcodex.sq2cql.model.structured_query.ContextualTermCode; import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.zip.ZipFile; @@ -38,17 +37,18 @@ public interface Util { entry("http://fhir.de/CodeSystem/bfarm/ops", "oops")); private static Map readMappings(ZipFile zipFile) throws IOException { - try (var in = zipFile.getInputStream(zipFile.getEntry("ontology/mapping/mapping_cql.json"))) { + try (var in = zipFile.getInputStream(zipFile.getEntry("mapping/cql/mapping_cql.json"))) { var mapper = new ObjectMapper(); return Arrays.stream(mapper.readValue(in, Mapping[].class)) .collect(Collectors.toMap(Mapping::key, Functions.identity())); } } - private static TermCodeNode readConceptTree(ZipFile zipFile) throws IOException { - try (var in = zipFile.getInputStream(zipFile.getEntry("ontology/mapping/mapping_tree.json"))) { + private static MappingTreeBase readConceptTree(ZipFile zipFile) throws IOException { + try (var in = zipFile.getInputStream(zipFile.getEntry("mapping/mapping_tree.json"))) { var mapper = new ObjectMapper(); - return mapper.readValue(in, TermCodeNode.class); + return new MappingTreeBase( + Arrays.stream(mapper.readValue(in, MappingTreeModuleRoot[].class)).toList()); } } @@ -60,4 +60,25 @@ static Translator createTranslator() throws Exception { return Translator.of(mappingContext); } } + + static MappingTreeBase createTreeWithoutChildren(ContextualTermCode c) { + return new MappingTreeBase(List.of(new MappingTreeModuleRoot(c.context(), c.termCode().system(), Map.of(c.termCode().code(), + new MappingTreeModuleEntry(c.termCode().code(), List.of()))))); + } + + static MappingTreeBase createTreeWithChildren(ContextualTermCode c, ContextualTermCode child1, ContextualTermCode child2) { + return new MappingTreeBase(List.of(createTreeRootWithChildren(c, child1, child2))); + } + + static MappingTreeModuleRoot createTreeRootWithChildren(ContextualTermCode c, ContextualTermCode child1, ContextualTermCode child2) { + return new MappingTreeModuleRoot(c.context(), c.termCode().system(), Map.of( + c.termCode().code(), new MappingTreeModuleEntry(c.termCode().code(), List.of(child1.termCode().code(), child2.termCode().code())), + child1.termCode().code(), new MappingTreeModuleEntry(child1.termCode().code(), List.of()), + child2.termCode().code(), new MappingTreeModuleEntry(child2.termCode().code(), List.of()))); + } + + static MappingTreeModuleRoot createTreeRootWithoutChildren(ContextualTermCode c) { + return new MappingTreeModuleRoot(c.context(), c.termCode().system(), Map.of( + c.termCode().code(), new MappingTreeModuleEntry(c.termCode().code(), List.of()))); + } } diff --git a/src/test/java/de/numcodex/sq2cql/model/MappingContextTest.java b/src/test/java/de/numcodex/sq2cql/model/MappingContextTest.java index 917b451..9163eac 100644 --- a/src/test/java/de/numcodex/sq2cql/model/MappingContextTest.java +++ b/src/test/java/de/numcodex/sq2cql/model/MappingContextTest.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; +import static de.numcodex.sq2cql.Util.createTreeWithoutChildren; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -38,7 +39,7 @@ void expandConcept_EmptyTree() { @Test void expandConcept_MissingMapping() { - var context = MappingContext.of(Map.of(), TermCodeNode.of(C1), Map.of()); + var context = MappingContext.of(Map.of(), createTreeWithoutChildren(C1), Map.of()); var termCodes = context.expandConcept(ContextualConcept.of(C1)).toList(); diff --git a/src/test/java/de/numcodex/sq2cql/model/MappingTreeBaseTest.java b/src/test/java/de/numcodex/sq2cql/model/MappingTreeBaseTest.java new file mode 100644 index 0000000..53cc47d --- /dev/null +++ b/src/test/java/de/numcodex/sq2cql/model/MappingTreeBaseTest.java @@ -0,0 +1,288 @@ +package de.numcodex.sq2cql.model; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.numcodex.sq2cql.model.common.TermCode; +import de.numcodex.sq2cql.model.structured_query.ContextualTermCode; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class MappingTreeBaseTest { + static final TermCode CONTEXT_1 = TermCode.of("context1", "context1", "context1"); + static final TermCode CONTEXT_2 = TermCode.of("context2", "context2", "context2"); + static final String SYSTEM_1 = "sys1"; + static final String SYSTEM_2 = "sys2"; + static final String C1 = "c1"; + static final String C2 = "c2"; + static final String C3 = "c3"; + static final String C4 = "c4"; + static final String C5 = "c5"; + + + private static ContextualTermCode contextualTermCodeOf(TermCode context, String system, String code) { + return new ContextualTermCode(context, new TermCode(system, code, "display")); + } + + @Test + void expand_empty() { + var base = new MappingTreeBase(List.of(new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, Map.of()))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)).toList(); + + assertThat(result).isEmpty(); + } + + @Test + void expand_noMatch_differentCode() { + var base = new MappingTreeBase(List.of(new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2)).toList(); + + assertThat(result).isEmpty(); + } + + @Test + void expand_noMatch_differentContext() { + var base = new MappingTreeBase(List.of(new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of()))))); + + var result = base.expand( + contextualTermCodeOf(new TermCode("", "different-context", ""), SYSTEM_1, C2)) + .toList(); + + assertThat(result).isEmpty(); + } + + @Test + void expand_noMatch_differentSystem() { + var base = new MappingTreeBase(List.of(new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, "system2", C2)).toList(); + + assertThat(result).isEmpty(); + } + + @Test + void expand_oneModule_twoEntries_withoutChildren() { + var base = new MappingTreeBase(List.of(new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of()), + C2, new MappingTreeModuleEntry(C2, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)).toList(); + + assertThat(result).containsExactlyInAnyOrder(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)); + } + + @Test + void expand_twoModules_sameContext_differentSystem_withoutChildren() { + var base = new MappingTreeBase(List.of( + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of()), + C2, new MappingTreeModuleEntry(C2, List.of()))), + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_2, + Map.of(C3, new MappingTreeModuleEntry(C3, List.of()), + C4, new MappingTreeModuleEntry(C4, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2)).toList(); + + assertThat(result).containsExactlyInAnyOrder(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2)); + } + + @Test + void expand_twoModules_sameSystem_differentContext_withoutChildren() { + var base = new MappingTreeBase(List.of( + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of()), + C2, new MappingTreeModuleEntry(C2, List.of()))), + new MappingTreeModuleRoot(CONTEXT_2, SYSTEM_1, + Map.of(C3, new MappingTreeModuleEntry(C3, List.of()), + C4, new MappingTreeModuleEntry(C4, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2)).toList(); + + assertThat(result).containsExactlyInAnyOrder(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2)); + } + + @Test + void expand_oneChild_withNoReference() { + var base = new MappingTreeBase(List.of( + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of(C2)))))); + + assertThatThrownBy(() -> base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)).toList()) + .isInstanceOf(NullPointerException.class); + } + + @Test + void expand_oneChild_onFirstLayer() { + var base = new MappingTreeBase(List.of( + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of(C2)), + C2, new MappingTreeModuleEntry(C2, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)).toList(); + + assertThat(result).containsExactlyInAnyOrder( + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2)); + } + + @Test + void expand_twoChildren_onFirstLayer() { + var base = new MappingTreeBase(List.of( + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of(C2, C3)), + C2, new MappingTreeModuleEntry(C2, List.of()), + C3, new MappingTreeModuleEntry(C3, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)).toList(); + + assertThat(result).containsExactlyInAnyOrder( + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C3)); + } + + @Test + void expand_twoChildren_onFirstAndSecondLayer() { + var base = new MappingTreeBase(List.of( + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of(C2, C3)), + C2, new MappingTreeModuleEntry(C2, List.of(C4)), + C3, new MappingTreeModuleEntry(C3, List.of(C5)), + C4, new MappingTreeModuleEntry(C4, List.of()), + C5, new MappingTreeModuleEntry(C5, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)).toList(); + + assertThat(result).containsExactlyInAnyOrder( + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C3), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C4), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C5)); + } + + @Test + void expand_oneChild_onThreeLayers() { + var base = new MappingTreeBase(List.of( + new MappingTreeModuleRoot(CONTEXT_1, SYSTEM_1, + Map.of(C1, new MappingTreeModuleEntry(C1, List.of(C2)), + C2, new MappingTreeModuleEntry(C2, List.of(C3)), + C3, new MappingTreeModuleEntry(C3, List.of(C4, C5)), + C4, new MappingTreeModuleEntry(C4, List.of()), + C5, new MappingTreeModuleEntry(C5, List.of()))))); + + var result = base.expand(contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1)).toList(); + + assertThat(result).containsExactlyInAnyOrder( + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C1), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C2), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C3), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C4), + contextualTermCodeOf(CONTEXT_1, SYSTEM_1, C5)); + } + + @Test + void fromJson() throws Exception { + var base = parse(""" + [ + { + "entries": [ + { + "key": "C1", + "parents": [], + "children": [] + } + ], + "context": { + "system": "sys1", + "code": "code1", + "display": "display" + }, + "system": "module-system" + } + ] + """); + + assertThat(base.moduleRoots().get(0).context()) + .isEqualTo(new TermCode("sys1", "code1", "display")); + assertThat(base.moduleRoots().get(0).system()).isEqualTo("module-system"); + assertThat(base.moduleRoots().get(0).entries().get("C1")).isNotNull(); + } + + @Test + void fromJson_AdditionalPropertyIsIgnored() throws Exception { + var base = parse(""" + [ + { + "foo-133831": "bar-133841", + "entries": [ + { + "key": "C1", + "parents": [], + "children": [] + } + ], + "context": { + "system": "sys1", + "code": "code1", + "display": "display" + }, + "system": "module-system" + } + ] + """); + + assertThat(base.moduleRoots().get(0).context()) + .isEqualTo(new TermCode("sys1", "code1", "display")); + assertThat(base.moduleRoots().get(0).system()).isEqualTo("module-system"); + assertThat(base.moduleRoots().get(0).entries().get("C1")).isNotNull(); + } + + @Test + void fromJson_withChildren() throws Exception { + var base = parse(""" + [ + { + "entries": [ + { + "key": "C1", + "parents": [], + "children": ["C2"] + }, + { + "key": "C2", + "parents": [], + "children": [] + } + ], + "context": { + "system": "sys1", + "code": "code1", + "display": "display" + }, + "system": "module-system" + } + ] + """); + + assertThat(base.moduleRoots().get(0).context()) + .isEqualTo(new TermCode("sys1", "code1", "display")); + assertThat(base.moduleRoots().get(0).system()).isEqualTo("module-system"); + assertThat(base.moduleRoots().get(0).entries().get("C1").children()).containsExactly("C2"); + assertThat(base.moduleRoots().get(0).entries().get("C2")).isNotNull(); + } + + static MappingTreeBase parse(String s) throws JsonProcessingException { + return new MappingTreeBase(Arrays.stream(new ObjectMapper().readValue(s, MappingTreeModuleRoot[].class)).toList()); + } +} \ No newline at end of file diff --git a/src/test/java/de/numcodex/sq2cql/model/TermCodeNodeTest.java b/src/test/java/de/numcodex/sq2cql/model/TermCodeNodeTest.java deleted file mode 100644 index b1d9894..0000000 --- a/src/test/java/de/numcodex/sq2cql/model/TermCodeNodeTest.java +++ /dev/null @@ -1,163 +0,0 @@ -package de.numcodex.sq2cql.model; - -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.sq2cql.model.common.TermCode; -import de.numcodex.sq2cql.model.structured_query.ContextualTermCode; -import org.junit.jupiter.api.Test; - -import java.util.Set; -import java.util.stream.Collectors; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * @author Alexander Kiel - */ -class TermCodeNodeTest { - - - static final TermCode CONTEXT = TermCode.of("context", "context", "context"); - static final ContextualTermCode ROOT = ContextualTermCode.of(CONTEXT, TermCode.of("foo", "root", "root")); - static final ContextualTermCode C1 = ContextualTermCode.of(CONTEXT, TermCode.of("foo", "c1", "c1")); - static final ContextualTermCode C2 = ContextualTermCode.of(CONTEXT, TermCode.of("foo", "c2", "c2")); - static final ContextualTermCode C11 = ContextualTermCode.of(CONTEXT, TermCode.of("foo", "c11", "c11")); - static final ContextualTermCode C12 = ContextualTermCode.of(CONTEXT, TermCode.of("foo", "c12", "c12")); - static final ContextualTermCode C111 = ContextualTermCode.of(CONTEXT, TermCode.of("foo", "c111", "c111")); - static final ContextualTermCode C112 = ContextualTermCode.of(CONTEXT, TermCode.of("foo", "c112", "c112")); - - @Test - void noChildren() { - var node = TermCodeNode.of(ROOT); - - assertTrue(node.children().isEmpty()); - } - - @Test - void expandSelfLeaf() { - var node = TermCodeNode.of(ROOT); - - assertEquals(Set.of(ROOT), node.expand(ROOT).collect(Collectors.toSet())); - } - - @Test - void expandSelf() { - var node = TermCodeNode.of(ROOT, TermCodeNode.of(C1), TermCodeNode.of(C2)); - - assertEquals(Set.of(ROOT, C1, C2), node.expand(ROOT).collect(Collectors.toSet())); - } - - @Test - void expandChildAndSelf() { - var c1 = TermCodeNode.of(C1, TermCodeNode.of(C11), TermCodeNode.of(C12)); - var node = TermCodeNode.of(ROOT, c1, TermCodeNode.of(C2)); - - assertEquals(Set.of(C1, C11, C12), node.expand(C1).collect(Collectors.toSet())); - } - - @Test - void expandChildDeep() { - var c11 = TermCodeNode.of(C11, TermCodeNode.of(C111), TermCodeNode.of(C112)); - var c1 = TermCodeNode.of(C1, c11, TermCodeNode.of(C12)); - var node = TermCodeNode.of(ROOT, c1, TermCodeNode.of(C2)); - - assertEquals(Set.of(C1, C11, C12, C111, C112), node.expand(C1).collect(Collectors.toSet())); - } - - @Test - void fromJson() throws Exception { - var mapper = new ObjectMapper(); - - var conceptNode = mapper.readValue(""" - { - "context": { - "system": "context-152133", - "code": "context-152136", - "display": "context-152144" - }, - "termCode": { - "system": "system-143705", - "code": "code-143708", - "display": "display-143716" - }, - "children": [] - } - """, TermCodeNode.class); - assertEquals(ContextualTermCode.of(TermCode.of("context-152133", "context-152136", "context-152144"), - TermCode.of("system-143705", "code-143708", "display-143716")), conceptNode.contextualTermCode()); - } - - @Test - void fromJson_AdditionalPropertyIsIgnored() throws Exception { - var mapper = new ObjectMapper(); - - var conceptNode = mapper.readValue(""" - {"foo-152133": "bar-152136", - "termCode": { - "system": "system-143705", - "code": "code-143708", - "display": "display-143716" - }, - "children": [] - } - """, TermCodeNode.class); - - assertEquals("system-143705", conceptNode.contextualTermCode().termCode().system()); - } - - @Test - void fromJson_WithChildren() throws Exception { - var mapper = new ObjectMapper(); - - var conceptNode = mapper.readValue(""" - { - "context": { - "system": "context-152133", - "code": "context-152136", - "display": "context-152144" - }, - "termCode": { - "system": "system-143705", - "code": "code-143708", - "display": "display-143716" - }, - "children": [ - {"context": { - "system": "child-1-context-155856", - "code": "child-1-context-155858", - "display": "child-1-context-155900" - }, - "termCode": { - "system": "child-1-system-155856", - "code": "child-1-code-155858", - "display": "child-1-display-155900" - }}, - { - "context": { - "system": "child-2-context-155956", - "code": "child-2-context-155958", - "display": "child-2-context-160000" - }, - "termCode": { - "system": "child-2-system-155958", - "code": "child-2-code-160000", - "display": "child-2-display-160002" - }} - ] - } - """, TermCodeNode.class); - - assertEquals(ContextualTermCode.of( - TermCode.of("context-152133", "context-152136", "context-152144"), - TermCode.of("system-143705", "code-143708", "display-143716")), conceptNode.contextualTermCode()); - assertEquals(ContextualTermCode.of( - TermCode.of("child-1-context-155856", "child-1-context-155858", "child-1-context-155900"), - TermCode.of("child-1-system-155856", "child-1-code-155858", "child-1-display-155900")), - conceptNode.children().get(0).contextualTermCode()); - assertEquals(ContextualTermCode.of( - TermCode.of("child-2-context-155956", "child-2-context-155958", "child-2-context-160000"), - TermCode.of("child-2-system-155958", "child-2-code-160000", "child-2-display-160002")), - conceptNode.children().get(1).contextualTermCode()); - - } -} diff --git a/src/test/java/de/numcodex/sq2cql/model/structured_query/ConceptCriterionTest.java b/src/test/java/de/numcodex/sq2cql/model/structured_query/ConceptCriterionTest.java index bc51885..1c14cf5 100644 --- a/src/test/java/de/numcodex/sq2cql/model/structured_query/ConceptCriterionTest.java +++ b/src/test/java/de/numcodex/sq2cql/model/structured_query/ConceptCriterionTest.java @@ -1,10 +1,7 @@ package de.numcodex.sq2cql.model.structured_query; import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.sq2cql.model.AttributeMapping; -import de.numcodex.sq2cql.model.Mapping; -import de.numcodex.sq2cql.model.MappingContext; -import de.numcodex.sq2cql.model.TermCodeNode; +import de.numcodex.sq2cql.model.*; import de.numcodex.sq2cql.model.common.TermCode; import de.numcodex.sq2cql.model.cql.CodeSystemDefinition; import org.junit.jupiter.api.Test; @@ -15,6 +12,8 @@ import java.util.Set; import static de.numcodex.sq2cql.Assertions.assertThat; +import static de.numcodex.sq2cql.Util.createTreeWithChildren; +import static de.numcodex.sq2cql.Util.createTreeWithoutChildren; import static de.numcodex.sq2cql.model.common.Comparator.LESS_THAN; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -209,7 +208,7 @@ void fromJson_BloodPressureRange() throws Exception { void toCql() { var criterion = ConceptCriterion.of(ContextualConcept.of(C71)); var mappingContext = MappingContext.of(Map.of(C71, Mapping.of(C71, "Condition")), - TermCodeNode.of(C71), CODE_SYSTEM_ALIASES); + createTreeWithoutChildren(C71), CODE_SYSTEM_ALIASES); var container = criterion.toCql(mappingContext); @@ -233,7 +232,7 @@ void toCql_WithMultipleTermCodes() { ContextualConcept.of(CONTEXT, Concept.of(C71_1_TC, C71_2_TC))); var mappings = Map.of(C71_1, Mapping.of(C71_1, "Condition"), C71_2, Mapping.of(C71_2, "Condition")); - var mappingContext = MappingContext.of(mappings, TermCodeNode.of(C71), CODE_SYSTEM_ALIASES); + var mappingContext = MappingContext.of(mappings, createTreeWithoutChildren(C71), CODE_SYSTEM_ALIASES); var container = criterion.toCql(mappingContext); @@ -258,7 +257,7 @@ void toCql_WithAttributeFilter() { .appendAttributeFilter(ValueSetAttributeFilter.of(VERIFICATION_STATUS, CONFIRMED)); var mapping = Mapping.of(C71, "Condition", null, null, List.of(), List.of(AttributeMapping.of("Coding", VERIFICATION_STATUS, "verificationStatus.coding"))); - var mappingContext = MappingContext.of(Map.of(C71, mapping), TermCodeNode.of(C71), + var mappingContext = MappingContext.of(Map.of(C71, mapping), createTreeWithoutChildren(C71), CODE_SYSTEM_ALIASES); var container = criterion.toCql(mappingContext); @@ -290,7 +289,7 @@ void toCql_Expanded_WithAttributeFilter() { var mapping2 = Mapping.of(C71_2, "Condition", null, null, List.of(), List.of(AttributeMapping.of("Coding", VERIFICATION_STATUS, "verificationStatus.coding"))); var mappingContext = MappingContext.of(Map.of(C71_1, mapping1, C71_2, mapping2), - TermCodeNode.of(C71, TermCodeNode.of(C71_1), TermCodeNode.of(C71_2)), CODE_SYSTEM_ALIASES); + createTreeWithChildren(C71, C71_1, C71_2), CODE_SYSTEM_ALIASES); var container = criterion.toCql(mappingContext); @@ -373,7 +372,7 @@ void toCql_FixedCriteria_Coding() { var criterion = ConceptCriterion.of(ContextualConcept.of(C71)); var mappingContext = MappingContext.of(Map.of(C71, Mapping.of(C71, "Condition", null, null, List.of(CodingModifier.of("verificationStatus.coding", CONFIRMED)), List.of())), - TermCodeNode.of(C71), CODE_SYSTEM_ALIASES); + createTreeWithoutChildren(C71), CODE_SYSTEM_ALIASES); var container = criterion.toCql(mappingContext);