diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30f20cb..8e062cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ on: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Check out Git repository @@ -42,7 +42,7 @@ jobs: run: mvn -B verify deploy: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: build if: startsWith(github.ref, 'refs/tags/') @@ -63,7 +63,7 @@ jobs: key: maven-${{ hashFiles('pom.xml') }} - name: Maven Settings - uses: s4u/maven-settings-action@v3.0.0 + uses: s4u/maven-settings-action@v3.1.0 with: servers: | [{"id": "github", "username": "${{ github.actor }}", "password": "${{ secrets.GITHUB_TOKEN }}"}] diff --git a/pom.xml b/pom.xml index 94985c7..10279bc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ de.medizininformatik-initiative sq2cql - 0.5.0 + 0.6.0 sq2cql @@ -28,10 +28,10 @@ UTF-8 17 - 5.10.3 - 7.2.1 - 1.20.0 - 2.0.13 + 5.11.3 + 7.6.0 + 1.20.4 + 2.0.16 3.0.0 @@ -94,7 +94,7 @@ ch.qos.logback logback-classic - 1.5.6 + 1.5.12 test @@ -128,13 +128,13 @@ org.apache.maven.plugins maven-surefire-plugin - 3.3.1 + 3.5.2 org.apache.maven.plugins maven-failsafe-plugin - 3.3.1 + 3.5.2 @@ -148,7 +148,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.2 + 3.1.3 diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java index c3446f2..698b5d6 100644 --- a/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java +++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/AbstractCriterion.java @@ -76,6 +76,11 @@ private static String referenceName(TermCode termCode) { public abstract T appendAttributeFilter(AttributeFilter attributeFilter); + @Override + public List attributeFilters() { + return attributeFilters; + } + @Override public ContextualConcept getConcept() { return concept; diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java index fcd9a25..4997f00 100644 --- a/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java +++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/Criterion.java @@ -48,6 +48,11 @@ public Container toReferencesCql(MappingContext mappingContex throw new UnsupportedOperationException(); } + @Override + public List attributeFilters() { + return List.of(); + } + @Override public TimeRestriction timeRestriction() { return null; @@ -74,6 +79,11 @@ public Container toReferencesCql(MappingContext mappingContex throw new UnsupportedOperationException(); } + @Override + public List attributeFilters() { + return List.of(); + } + @Override public TimeRestriction timeRestriction() { return null; @@ -172,5 +182,7 @@ private static T getAndMap(JsonNode node, String name, Function Container toReferencesCql(MappingContext mappingContext); + List attributeFilters(); + TimeRestriction timeRestriction(); } diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java index aea4db2..03d4222 100644 --- a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java +++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestriction.java @@ -5,24 +5,41 @@ import com.fasterxml.jackson.databind.JsonNode; import de.numcodex.sq2cql.model.Mapping; -public record TimeRestriction(String afterDate, String beforeDate) { +import java.time.LocalDate; - public static TimeRestriction of(String afterDate, String beforeDate) { +import static java.util.Objects.requireNonNull; + +public record TimeRestriction(LocalDate afterDate, LocalDate beforeDate) { + + public TimeRestriction { + requireNonNull(afterDate); + requireNonNull(beforeDate); + if (beforeDate.isBefore(afterDate)) { + throw new IllegalArgumentException("Invalid time restriction: beforeDate `%s` is before afterDate `%s` but should not be." + .formatted(beforeDate, afterDate)); + } + } + + public static TimeRestriction of(LocalDate afterDate, LocalDate beforeDate) { return new TimeRestriction(afterDate, beforeDate); } @JsonCreator public static TimeRestriction create(@JsonProperty("afterDate") String afterDate, @JsonProperty("beforeDate") String beforeDate) { - //FIXME: quick and dirty for empty timeRestriction if (afterDate == null && beforeDate == null) { return null; + } else if (afterDate == null) { + return TimeRestriction.of(LocalDate.of(1900, 1, 1), LocalDate.parse(beforeDate)); + } else if (beforeDate == null) { + return TimeRestriction.of(LocalDate.parse(afterDate), LocalDate.of(2040, 1, 1)); + } else { + return TimeRestriction.of(LocalDate.parse(afterDate), LocalDate.parse(beforeDate)); } - return TimeRestriction.of(afterDate, beforeDate); } public static TimeRestriction fromJsonNode(JsonNode node) { - return TimeRestriction.of(node.get("afterDate").asText(), node.get("beforeDate").asText()); + return TimeRestriction.create(node.get("afterDate").asText(), node.get("beforeDate").asText()); } public Modifier toModifier(Mapping mapping) { diff --git a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java index 557846d..da1c063 100644 --- a/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java +++ b/src/main/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifier.java @@ -3,19 +3,20 @@ import de.numcodex.sq2cql.model.MappingContext; import de.numcodex.sq2cql.model.cql.*; +import java.time.LocalDate; import java.util.List; import static java.util.Objects.requireNonNull; -public record TimeRestrictionModifier(String path, String afterDate, String beforeDate) implements SimpleModifier { +public record TimeRestrictionModifier(String path, LocalDate afterDate, LocalDate beforeDate) implements SimpleModifier { public TimeRestrictionModifier { requireNonNull(path); - afterDate = afterDate == null ? "1900-01-01T" : afterDate; - beforeDate = beforeDate == null ? "2040-01-01T" : beforeDate; + requireNonNull(afterDate); + requireNonNull(beforeDate); } - public static TimeRestrictionModifier of(String path, String afterDate, String beforeDate) { + public static TimeRestrictionModifier of(String path, LocalDate afterDate, LocalDate beforeDate) { return new TimeRestrictionModifier(path, afterDate, beforeDate); } @@ -24,8 +25,13 @@ public Container expression(MappingContext mappingContext, Id var invocationExpr = InvocationExpression.of(sourceAlias, path); var castExp = TypeExpression.of(invocationExpr, "dateTime"); var toDateFunction = FunctionInvocation.of("ToDate", List.of(castExp)); - var intervalSelector = IntervalSelector.of(DateTimeExpression.of(afterDate), DateTimeExpression.of(beforeDate)); + var intervalSelector = IntervalSelector.of(DateTimeExpression.of(afterDate.toString()), DateTimeExpression.of(beforeDate.toString())); var dateTimeInExpr = MembershipExpression.in(toDateFunction, intervalSelector); + + if ("recordedDate".equals(path)) { + return Container.of(dateTimeInExpr); + } + var intervalOverlapExpr = OverlapsIntervalOperatorPhrase.of(invocationExpr, intervalSelector); return Container.of(OrExpression.of(dateTimeInExpr, intervalOverlapExpr)); } diff --git a/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java b/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java index 8efc183..4c68b6b 100644 --- a/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java +++ b/src/test/java/de/numcodex/sq2cql/AcceptanceTest.java @@ -34,7 +34,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.stream.Stream; @@ -53,7 +52,7 @@ public class AcceptanceTest { private static final Logger logger = LoggerFactory.getLogger(AcceptanceTest.class); private final GenericContainer blaze = new GenericContainer<>( - DockerImageName.parse("samply/blaze:0.28")) + DockerImageName.parse("samply/blaze:0.30")) .withImagePullPolicy(PullPolicy.alwaysPull()) .withEnv("LOG_LEVEL", "debug") .withExposedPorts(8080) @@ -80,16 +79,8 @@ private static Bundle createBundle(Library library, Measure measure) { return bundle; } - public static List getTestQueriesReturningOnePatient() throws URISyntaxException, IOException { - try (Stream paths = Files.list(resourcePath("returningOnePatient"))) { - return paths.map(path -> { - try { - return new ObjectMapper().readValue(Files.readString(path), StructuredQuery.class); - } catch (IOException e) { - throw new RuntimeException(e); - } - }).toList(); - } + public static Stream getTestQueriesReturningOnePatient() throws IOException, URISyntaxException { + return Files.list(resourcePath("returningOnePatient")); } @BeforeAll @@ -106,7 +97,8 @@ public void setUp() throws Exception { @ParameterizedTest @MethodSource("de.numcodex.sq2cql.AcceptanceTest#getTestQueriesReturningOnePatient") - public void runTestCase(StructuredQuery structuredQuery) throws Exception { + public void runTestCase(Path path) throws Exception { + var structuredQuery = new ObjectMapper().readValue(Files.readString(path), StructuredQuery.class); var cql = translator.toCql(structuredQuery).print(); var measureUri = createMeasureAndLibrary(cql); var report = evaluateMeasure(measureUri); diff --git a/src/test/java/de/numcodex/sq2cql/EvaluationIT.java b/src/test/java/de/numcodex/sq2cql/EvaluationIT.java index 4db9a2c..63da521 100644 --- a/src/test/java/de/numcodex/sq2cql/EvaluationIT.java +++ b/src/test/java/de/numcodex/sq2cql/EvaluationIT.java @@ -50,7 +50,7 @@ public class EvaluationIT { static final Map CODE_SYSTEM_ALIASES = Map.of("http://loinc.org", "loinc"); @Container - private final GenericContainer blaze = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.28")) + private final GenericContainer blaze = new GenericContainer<>(DockerImageName.parse("samply/blaze:0.30")) .withImagePullPolicy(PullPolicy.alwaysPull()) .withExposedPorts(8080) .waitingFor(Wait.forHttp("/health").forStatusCode(200)) diff --git a/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java b/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java index 7b174e7..03de66f 100644 --- a/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java +++ b/src/test/java/de/numcodex/sq2cql/MedicationAdministrationTest.java @@ -38,21 +38,21 @@ public void translateMedicationAdministration() throws Exception { library Retrieve version '1.0.0' using FHIR version '4.0.0' include FHIRHelpers version '4.0.0' - + codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc' - + context Unfiltered - + define B01AB01Ref: from [Medication: Code 'B01AB01' from atc] M return 'Medication/' + M.id - + context Patient - + define Criterion: exists (from [MedicationAdministration] M where M.medication.reference in B01AB01Ref) - + define InInitialPopulation: Criterion """); @@ -69,23 +69,23 @@ public void translateMedicationAdministrationTimeRestriction() throws Exception library Retrieve version '1.0.0' using FHIR version '4.0.0' include FHIRHelpers version '4.0.0' - + codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc' - + context Unfiltered - + define B01AB01Ref: from [Medication: Code 'B01AB01' from atc] M return 'Medication/' + M.id - + context Patient - + define Criterion: exists (from [MedicationAdministration] M where M.medication.reference in B01AB01Ref and (ToDate(M.effective as dateTime) in Interval[@2024-01-01, @2024-02-01] or M.effective overlaps Interval[@2024-01-01, @2024-02-01])) - + define InInitialPopulation: Criterion """); @@ -102,29 +102,29 @@ public void translateMedicationAdministrationDoubleCriteria() throws Exception { library Retrieve version '1.0.0' using FHIR version '4.0.0' include FHIRHelpers version '4.0.0' - + codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc' - + context Unfiltered - + define B01AB01Ref: from [Medication: Code 'B01AB01' from atc] M return 'Medication/' + M.id - + context Patient - + define "Criterion 1": exists (from [MedicationAdministration] M where M.medication.reference in B01AB01Ref and (ToDate(M.effective as dateTime) in Interval[@2024-01-01, @2024-02-01] or M.effective overlaps Interval[@2024-01-01, @2024-02-01])) - + define "Criterion 2": exists (from [MedicationAdministration] M where M.medication.reference in B01AB01Ref and (ToDate(M.effective as dateTime) in Interval[@2023-01-01, @2023-02-01] or M.effective overlaps Interval[@2023-01-01, @2023-02-01])) - + define InInitialPopulation: "Criterion 1" and "Criterion 2" @@ -142,29 +142,29 @@ public void translateMedicationAdministrationTwoCriteria() throws Exception { library Retrieve version '1.0.0' using FHIR version '4.0.0' include FHIRHelpers version '4.0.0' - + codesystem atc: 'http://fhir.de/CodeSystem/bfarm/atc' - + context Unfiltered - + define B01AB01Ref: from [Medication: Code 'B01AB01' from atc] M return 'Medication/' + M.id - + define B01AC06Ref: from [Medication: Code 'B01AC06' from atc] M return 'Medication/' + M.id - + context Patient - + define "Criterion 1": exists (from [MedicationAdministration] M where M.medication.reference in B01AB01Ref) - + define "Criterion 2": exists (from [MedicationAdministration] M where M.medication.reference in B01AC06Ref) - + define InInitialPopulation: "Criterion 1" and "Criterion 2" diff --git a/src/test/java/de/numcodex/sq2cql/TranslatorTest.java b/src/test/java/de/numcodex/sq2cql/TranslatorTest.java index a079034..6f14f2a 100644 --- a/src/test/java/de/numcodex/sq2cql/TranslatorTest.java +++ b/src/test/java/de/numcodex/sq2cql/TranslatorTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -35,7 +36,6 @@ class TranslatorTest { TermCode.of("urn:oid:2.16.840.1.113883.3.1937.777.24.5.3", "2.16.840.1.113883.3.1937.777.24.5.3.8", "MDAT wissenschaftlich nutzen EU DSGVO NIVEAU")); - static final ContextualTermCode ROOT = ContextualTermCode.of(CONTEXT, TermCode.of("", "", "")); static final ContextualTermCode C71 = ContextualTermCode.of(CONTEXT, TermCode.of("http://fhir.de/CodeSystem/bfarm/icd-10-gm", "C71", "Malignant neoplasm of brain")); @@ -171,7 +171,7 @@ void timeRestriction() { var library = Translator.of(mappingContext).toCql(StructuredQuery.of(List.of(List.of( ConceptCriterion.of(ContextualConcept.of(c71_1), - TimeRestriction.of("2020-01-01T", "2020-01-02T")))))); + TimeRestriction.of(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 2))))))); assertThat(library).printsTo(""" library Retrieve version '1.0.0' @@ -184,8 +184,8 @@ void timeRestriction() { define Criterion: exists (from [Condition: Code 'C71.1' from icd10] C - where ToDate(C.onset as dateTime) in Interval[@2020-01-01T, @2020-01-02T] or - C.onset overlaps Interval[@2020-01-01T, @2020-01-02T]) + where ToDate(C.onset as dateTime) in Interval[@2020-01-01, @2020-01-02] or + C.onset overlaps Interval[@2020-01-01, @2020-01-02]) define InInitialPopulation: Criterion @@ -203,7 +203,7 @@ void timeRestriction_missingPathInMapping() { 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), - TimeRestriction.of("2020-01-01T", "2020-01-02T"))))); + TimeRestriction.of(LocalDate.of(2020, 1, 1), LocalDate.of(2020, 1, 2)))))); var translator = Translator.of(mappingContext); assertThatIllegalStateException().isThrownBy(() -> translator.toCql(query)).withMessage( diff --git a/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java b/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java index 7770b93..7801907 100644 --- a/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java +++ b/src/test/java/de/numcodex/sq2cql/model/structured_query/CriterionTest.java @@ -2,34 +2,34 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Alexander Kiel */ class CriterionTest { - @Test - void fromJson_WithoutTermCodes() { - var mapper = new ObjectMapper(); + @Nested + class FromJson { - try { - mapper.readValue("{}", Criterion.class); - fail(); - } catch (JsonProcessingException e) { - assertEquals("missing JSON property: context", e.getCause().getMessage()); - } - } + ObjectMapper mapper = new ObjectMapper(); - @Test - void fromJson_UnknownValueFilterType() { - var mapper = new ObjectMapper(); + @Test + void emptyObject() { + assertThatThrownBy(() -> mapper.readValue("{}", Criterion.class)) + .isInstanceOf(JsonProcessingException.class) + .hasRootCauseMessage("missing JSON property: context"); + } - try { - mapper.readValue(""" + @Test + void unknownValueFilterType() { + assertThatThrownBy(() -> mapper.readValue(""" { "context": { "system": "context", @@ -41,10 +41,222 @@ void fromJson_UnknownValueFilterType() { "type": "foo" } } - """, Criterion.class); - fail(); - } catch (JsonProcessingException e) { - assertEquals("unknown valueFilter type: foo", e.getCause().getMessage()); + """, Criterion.class)) + .isInstanceOf(JsonProcessingException.class) + .hasRootCauseMessage("unknown valueFilter type: foo"); + } + + @Test + void emptyTimeRestrictionIsIgnored() throws JsonProcessingException { + assertThat(mapper.readValue(""" + { + "context": { + "system": "context", + "code": "context", + "display": "context" + }, + "termCodes": [], + "timeRestriction": {} + } + """, Criterion.class).timeRestriction()).isNull(); + } + + @Test + void invalidTimeRestrictionAfterDate() { + assertThatThrownBy(() -> mapper.readValue(""" + { + "context": { + "system": "context", + "code": "context", + "display": "context" + }, + "termCodes": [], + "timeRestriction": { + "afterDate": "2023-02-29" + } + } + """, Criterion.class)) + .isInstanceOf(JsonProcessingException.class) + .hasRootCauseMessage("Invalid date 'February 29' as '2023' is not a leap year"); + } + + @Test + void invalidTimeRestrictionBeforeDate() { + assertThatThrownBy(() -> mapper.readValue(""" + { + "context": { + "system": "context", + "code": "context", + "display": "context" + }, + "termCodes": [], + "timeRestriction": { + "afterDate": "2023-02-28", + "beforeDate": "2023-02-29" + } + } + """, Criterion.class)) + .isInstanceOf(JsonProcessingException.class) + .hasRootCauseMessage("Invalid date 'February 29' as '2023' is not a leap year"); + } + + @Test + void invalidTimeRestriction() { + assertThatThrownBy(() -> mapper.readValue(""" + { + "context": { + "system": "context", + "code": "context", + "display": "context" + }, + "termCodes": [], + "timeRestriction": { + "afterDate": "2024-11-20", + "beforeDate": "2024-11-19" + } + } + """, Criterion.class)) + .isInstanceOf(JsonProcessingException.class) + .hasRootCauseMessage("Invalid time restriction: beforeDate `2024-11-19` is before afterDate `2024-11-20` but should not be."); + } + + @Test + void timeRestrictionSameDay() throws JsonProcessingException { + assertThat(mapper.readValue(""" + { + "context": { + "system": "context", + "code": "context", + "display": "context" + }, + "termCodes": [], + "timeRestriction": { + "afterDate": "2024-11-20", + "beforeDate": "2024-11-20" + } + } + """, Criterion.class).timeRestriction()) + .isEqualTo(TimeRestriction.of(LocalDate.of(2024, 11, 20), LocalDate.of(2024, 11, 20))); + } + + @Nested + class WithReferenceAttributeFilter { + + @Test + void invalidTimeRestriction() { + assertThatThrownBy(() -> mapper.readValue(""" + { + "context": { + "code": "Specimen", + "system": "fdpg.mii.cds", + "version": "1.0.0", + "display": "Bioprobe" + }, + "termCodes": [ + { + "code": "119364003", + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20220930", + "display": "Serum specimen" + } + ], + "attributeFilters": [ + { + "type": "reference", + "attributeCode": { + "code": "festgestellteDiagnose", + "display": "Festgestellte Diagnose", + "system": "http://hl7.org/fhir/StructureDefinition" + }, + "criteria": [ + { + "termCodes": [ + { + "code": "E13.9", + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "version": "2023", + "display": "Sonstiger näher bezeichneter Diabetes mellitus : Ohne Komplikationen" + } + ], + "context": { + "code": "Diagnose", + "system": "fdpg.mii.cds", + "version": "1.0.0", + "display": "Diagnose" + }, + "timeRestriction": { + "afterDate": "2024-11-20", + "beforeDate": "2024-11-19" + } + } + ] + } + ] + } + """, Criterion.class)) + .isInstanceOf(JsonProcessingException.class) + .hasRootCauseMessage("Invalid time restriction: beforeDate `2024-11-19` is before afterDate `2024-11-20` but should not be."); + } + + @Test + void timeRestrictionSameDay() throws JsonProcessingException { + var criterion = mapper.readValue(""" + { + "context": { + "code": "Specimen", + "system": "fdpg.mii.cds", + "version": "1.0.0", + "display": "Bioprobe" + }, + "termCodes": [ + { + "code": "119364003", + "system": "http://snomed.info/sct", + "version": "http://snomed.info/sct/900000000000207008/version/20220930", + "display": "Serum specimen" + } + ], + "attributeFilters": [ + { + "type": "reference", + "attributeCode": { + "code": "festgestellteDiagnose", + "display": "Festgestellte Diagnose", + "system": "http://hl7.org/fhir/StructureDefinition" + }, + "criteria": [ + { + "termCodes": [ + { + "code": "E13.9", + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", + "version": "2023", + "display": "Sonstiger näher bezeichneter Diabetes mellitus : Ohne Komplikationen" + } + ], + "context": { + "code": "Diagnose", + "system": "fdpg.mii.cds", + "version": "1.0.0", + "display": "Diagnose" + }, + "timeRestriction": { + "afterDate": "2024-11-20", + "beforeDate": "2024-11-20" + } + } + ] + } + ] + } + """, Criterion.class); + + var attributeFilter = (ReferenceAttributeFilter) criterion.attributeFilters().get(0); + var referencedCriterion = attributeFilter.criteria().get(0); + + assertThat(referencedCriterion.timeRestriction()) + .isEqualTo(TimeRestriction.of(LocalDate.of(2024, 11, 20), LocalDate.of(2024, 11, 20))); + } } } } diff --git a/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java b/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java index a492000..83c3e12 100644 --- a/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java +++ b/src/test/java/de/numcodex/sq2cql/model/structured_query/StructuredQueryTest.java @@ -5,7 +5,6 @@ import de.numcodex.sq2cql.model.common.TermCode; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -176,39 +175,4 @@ void fromJson_OneInclusionCriteria_OneExclusionCriteria() throws Exception { assertEquals(ContextualConcept.of(TC_1), structuredQuery.inclusionCriteria().get(0).get(0).getConcept()); assertEquals(ContextualConcept.of(TC_2), structuredQuery.exclusionCriteria().get(0).get(0).getConcept()); } - - @Test - void fromJson_EmptyTimeRestriction() throws Exception { - var mapper = new ObjectMapper(); - - var structuredQuery = mapper.readValue(""" - { - "version": "https://medizininformatik-initiative.de/fdpg/StructuredQuery/v3/schema", - "display": "", - "inclusionCriteria": [ - [ - { - "context": { - "system": "context", - "code": "context", - "display": "context" - }, - "termCodes": [ - { - "code": "Q50", - "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm", - "version": "2021", - "display": "Angeborene Fehlbildungen der Ovarien, der Tubae uterinae und der Ligg. lata uteri" - } - ], - "attributeFilters": [], - "timeRestriction": {} - } - ] - ] - } - """, StructuredQuery.class); - - assertThat(structuredQuery.inclusionCriteria().get(0).get(0).timeRestriction()).isNull(); - } } diff --git a/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java b/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java index 2dda279..fca7e23 100644 --- a/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java +++ b/src/test/java/de/numcodex/sq2cql/model/structured_query/TimeRestrictionModifierTest.java @@ -2,71 +2,57 @@ import de.numcodex.sq2cql.model.MappingContext; import de.numcodex.sq2cql.model.cql.StandardIdentifierExpression; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.time.LocalDate; import static de.numcodex.sq2cql.Assertions.assertThat; class TimeRestrictionModifierTest { - @Test - void expression_before() { - var timeRestriction = TimeRestrictionModifier.of("effective", null, "2021-01-01T"); - var identifier = StandardIdentifierExpression.of("O"); - - var expression = timeRestriction.expression(MappingContext.of(), identifier); - - assertThat(expression.moveToPatientContext("Criterion")).printsTo(""" - library Retrieve version '1.0.0' - using FHIR version '4.0.0' - include FHIRHelpers version '4.0.0' - - context Patient - - define Criterion: - ToDate(O.effective as dateTime) in Interval[@1900-01-01T, @2021-01-01T] or - O.effective overlaps Interval[@1900-01-01T, @2021-01-01T] - """); - } - - - @Test - void expression_after() { - var timeRestriction = TimeRestrictionModifier.of("effective", "2020-01-01T", null); - var identifier = StandardIdentifierExpression.of("O"); - - var expression = timeRestriction.expression(MappingContext.of(), identifier); - - assertThat(expression.moveToPatientContext("Criterion")).printsTo(""" - library Retrieve version '1.0.0' - using FHIR version '4.0.0' - include FHIRHelpers version '4.0.0' - - context Patient - - define Criterion: - ToDate(O.effective as dateTime) in Interval[@2020-01-01T, @2040-01-01T] or - O.effective overlaps Interval[@2020-01-01T, @2040-01-01T] - """); - } - - - @Test - void expression_beforeAfter() { - var timeRestriction = TimeRestrictionModifier.of("effective", "2021-01-01T", "2021-01-01T"); - var identifier = StandardIdentifierExpression.of("O"); - - var expression = timeRestriction.expression(MappingContext.of(), identifier); - - assertThat(expression.moveToPatientContext("Criterion")).printsTo(""" - library Retrieve version '1.0.0' - using FHIR version '4.0.0' - include FHIRHelpers version '4.0.0' - - context Patient - - define Criterion: - ToDate(O.effective as dateTime) in Interval[@2021-01-01T, @2021-01-01T] or - O.effective overlaps Interval[@2021-01-01T, @2021-01-01T] - """); + @Nested + class Expression { + + @Test + void effective() { + var timeRestriction = TimeRestrictionModifier.of("effective", LocalDate.of(2021, 1, 1), LocalDate.of(2021, 1, 2)); + var identifier = StandardIdentifierExpression.of("O"); + + var expression = timeRestriction.expression(MappingContext.of(), identifier); + + assertThat(expression.moveToPatientContext("Criterion")).printsTo(""" + library Retrieve version '1.0.0' + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define Criterion: + ToDate(O.effective as dateTime) in Interval[@2021-01-01, @2021-01-02] or + O.effective overlaps Interval[@2021-01-01, @2021-01-02] + """); + } + + @Test + @DisplayName("recordedDate has no Period type") + void recordedDate() { + var timeRestriction = TimeRestrictionModifier.of("recordedDate", LocalDate.of(2021, 1, 1), LocalDate.of(2021, 1, 2)); + var identifier = StandardIdentifierExpression.of("C"); + + var expression = timeRestriction.expression(MappingContext.of(), identifier); + + assertThat(expression.moveToPatientContext("Criterion")).printsTo(""" + library Retrieve version '1.0.0' + using FHIR version '4.0.0' + include FHIRHelpers version '4.0.0' + + context Patient + + define Criterion: + ToDate(C.recordedDate as dateTime) in Interval[@2021-01-01, @2021-01-02] + """); + } } } diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-TimeRestriction.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-TimeRestriction.json new file mode 100644 index 0000000..81fe8ad --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-TimeRestriction.json @@ -0,0 +1,26 @@ +{ + "version": "http://to_be_decided.com/draft-1/schema#", + "inclusionCriteria": [ + [ + { + "context": { + "code": "Diagnose", + "display": "Diagnose", + "system": "fdpg.mii.cds", + "version": "1.0.0" + }, + "termCodes": [ + { + "code": "S14.11", + "display": "", + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm" + } + ], + "timeRestriction": { + "afterDate": "2023-02-01", + "beforeDate": "2023-02-28" + } + } + ] + ] +} diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-f728b4fa-4248-5e3a-0a5d-2f346baa9455 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-f728b4fa-4248-5e3a-0a5d-2f346baa9455 deleted file mode 100644 index cc10a80..0000000 --- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose-f728b4fa-4248-5e3a-0a5d-2f346baa9455 +++ /dev/null @@ -1 +0,0 @@ -{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "I08.0", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm"}], "context": {"code": "Diagnose", "display": "Diagnose", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"} \ No newline at end of file diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose.json new file mode 100644 index 0000000..74045ac --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Diagnose.json @@ -0,0 +1,23 @@ +{ + "version": "http://to_be_decided.com/draft-1/schema#", + "inclusionCriteria": [ + [ + { + "context": { + "code": "Diagnose", + "display": "Diagnose", + "system": "fdpg.mii.cds", + "version": "1.0.0" + }, + "termCodes": [ + { + "code": "I08.0", + "display": "", + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm" + } + ], + "attributeFilters": [] + } + ] + ] +} diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration-aedab7b5-e2aa-55a7-4951-03edfd05a5f5 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration-aedab7b5-e2aa-55a7-4951-03edfd05a5f5 deleted file mode 100644 index c17b868..0000000 --- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration-aedab7b5-e2aa-55a7-4951-03edfd05a5f5 +++ /dev/null @@ -1 +0,0 @@ -{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "P01CA", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/atc"}], "context": {"code": "Medikamentenverabreichung", "display": "Verabreichung von Medikamenten", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"} \ No newline at end of file diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration.json new file mode 100644 index 0000000..8d7a3b8 --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/MedicationAdministration.json @@ -0,0 +1,23 @@ +{ + "inclusionCriteria": [ + [ + { + "attributeFilters": [], + "termCodes": [ + { + "code": "P01CA", + "display": "", + "system": "http://fhir.de/CodeSystem/bfarm/atc" + } + ], + "context": { + "code": "Medikamentenverabreichung", + "display": "Verabreichung von Medikamenten", + "system": "fdpg.mii.cds", + "version": "1.0.0" + } + } + ] + ], + "version": "http://to_be_decided.com/draft-1/schema#" +} diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-0a02da60-749d-4601-df98-377981bd5336 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-0a02da60-749d-4601-df98-377981bd5336 deleted file mode 100644 index f3e8de4..0000000 --- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-0a02da60-749d-4601-df98-377981bd5336 +++ /dev/null @@ -1 +0,0 @@ -{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "800-3", "display": "", "system": "http://loinc.org"}], "context": {"code": "Laboruntersuchung", "display": "Laboruntersuchung", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"} \ No newline at end of file diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-TimeRestriction.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-TimeRestriction.json new file mode 100644 index 0000000..36f3593 --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab-TimeRestriction.json @@ -0,0 +1,26 @@ +{ + "version": "http://to_be_decided.com/draft-1/schema#", + "inclusionCriteria": [ + [ + { + "context": { + "code": "Laboruntersuchung", + "display": "Laboruntersuchung", + "system": "fdpg.mii.cds", + "version": "1.0.0" + }, + "termCodes": [ + { + "code": "800-3", + "display": "", + "system": "http://loinc.org" + } + ], + "timeRestriction": { + "afterDate": "2004-07-24", + "beforeDate": "2004-07-24" + } + } + ] + ] +} diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab.json new file mode 100644 index 0000000..50335a5 --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/ObservationLab.json @@ -0,0 +1,23 @@ +{ + "version": "http://to_be_decided.com/draft-1/schema#", + "inclusionCriteria": [ + [ + { + "context": { + "code": "Laboruntersuchung", + "display": "Laboruntersuchung", + "system": "fdpg.mii.cds", + "version": "1.0.0" + }, + "termCodes": [ + { + "code": "800-3", + "display": "", + "system": "http://loinc.org" + } + ], + "attributeFilters": [] + } + ] + ] +} diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure-d7de701d-8eaf-b323-5f65-380029c836e0 b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure-d7de701d-8eaf-b323-5f65-380029c836e0 deleted file mode 100644 index d2c20ad..0000000 --- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure-d7de701d-8eaf-b323-5f65-380029c836e0 +++ /dev/null @@ -1 +0,0 @@ -{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "5-403.05", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/ops"}], "context": {"code": "Procedure", "display": "Prozedur", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"} \ No newline at end of file diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure.json new file mode 100644 index 0000000..659f5b8 --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Procedure.json @@ -0,0 +1,23 @@ +{ + "inclusionCriteria": [ + [ + { + "attributeFilters": [], + "termCodes": [ + { + "code": "5-403.05", + "display": "", + "system": "http://fhir.de/CodeSystem/bfarm/ops" + } + ], + "context": { + "code": "Procedure", + "display": "Prozedur", + "system": "fdpg.mii.cds", + "version": "1.0.0" + } + } + ] + ], + "version": "http://to_be_decided.com/draft-1/schema#" +} diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen-e3e70682-c209-4cac-629f-6fbed82c07cd b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen-e3e70682-c209-4cac-629f-6fbed82c07cd deleted file mode 100644 index 3b9734e..0000000 --- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen-e3e70682-c209-4cac-629f-6fbed82c07cd +++ /dev/null @@ -1 +0,0 @@ -{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "396997002", "display": "", "system": "http://snomed.info/sct"}], "context": {"code": "Specimen", "display": "Bioprobe", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"} \ No newline at end of file diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen.json new file mode 100644 index 0000000..5a80208 --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Specimen.json @@ -0,0 +1,23 @@ +{ + "inclusionCriteria": [ + [ + { + "attributeFilters": [], + "termCodes": [ + { + "code": "396997002", + "display": "", + "system": "http://snomed.info/sct" + } + ], + "context": { + "code": "Specimen", + "display": "Bioprobe", + "system": "fdpg.mii.cds", + "version": "1.0.0" + } + } + ] + ], + "version": "http://to_be_decided.com/draft-1/schema#" +} diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache-31307e46-af79-7546-7e51-68dc5690f74d b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache-31307e46-af79-7546-7e51-68dc5690f74d deleted file mode 100644 index 8f72173..0000000 --- a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache-31307e46-af79-7546-7e51-68dc5690f74d +++ /dev/null @@ -1 +0,0 @@ -{"inclusionCriteria": [[{"attributeFilters": [], "termCodes": [{"code": "S14.11", "display": "", "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm"}], "context": {"code": "Diagnose", "display": "Diagnose", "system": "fdpg.mii.cds", "version": "1.0.0"}}]], "version": "http://to_be_decided.com/draft-1/schema#"} \ No newline at end of file diff --git a/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache.json b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache.json new file mode 100644 index 0000000..725e0f3 --- /dev/null +++ b/src/test/resources/de/numcodex/sq2cql/returningOnePatient/Todesursache.json @@ -0,0 +1,23 @@ +{ + "inclusionCriteria": [ + [ + { + "attributeFilters": [], + "termCodes": [ + { + "code": "S14.11", + "display": "", + "system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm" + } + ], + "context": { + "code": "Diagnose", + "display": "Diagnose", + "system": "fdpg.mii.cds", + "version": "1.0.0" + } + } + ] + ], + "version": "http://to_be_decided.com/draft-1/schema#" +}