diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java index eabe909b40..38cb2cb54c 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java @@ -82,7 +82,7 @@ public Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) { } private R4MeasureService buildMeasureService() { - return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator, measureServiceUtils); + return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator); } public When when() { diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java index 60c6566054..55d3540f4d 100644 --- a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -25,6 +26,7 @@ import org.hl7.fhir.instance.model.api.IBaseParameters; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; +import org.opencds.cqf.cql.engine.execution.EvaluationResultsForMultiLib; import org.opencds.cqf.fhir.cql.engine.parameters.CqlFhirParametersConverter; import org.opencds.cqf.fhir.cql.engine.parameters.CqlParameterDefinition; import org.opencds.cqf.fhir.utility.CqfExpression; @@ -325,8 +327,8 @@ public List resolveExpression( return result; } - public EvaluationResult getEvaluationResult( - VersionedIdentifier id, + public EvaluationResultsForMultiLib getEvaluationResult( + List ids, String patientId, IBaseParameters parameters, Map rawParameters, @@ -336,25 +338,53 @@ public EvaluationResult getEvaluationResult( @Nullable ZonedDateTime zonedDateTime, CqlEngine engine) { - if (cqlFhirParametersConverter == null) { - cqlFhirParametersConverter = Engines.getCqlFhirParametersConverter(repository.fhirContext()); - } + var cqlFhirParametersConverterToUse = Objects.requireNonNullElseGet( + cqlFhirParametersConverter, () -> Engines.getCqlFhirParametersConverter(repository.fhirContext())); + // engine context built externally of LibraryEngine? - if (engine == null) { - engine = Engines.forRepository(repository, settings, additionalData); - } + var engineToUse = Objects.requireNonNullElseGet( + engine, () -> Engines.forRepository(repository, settings, additionalData)); - var evaluationParameters = cqlFhirParametersConverter.toCqlParameters(parameters); + var evaluationParameters = cqlFhirParametersConverterToUse.toCqlParameters(parameters); if (rawParameters != null && !rawParameters.isEmpty()) { evaluationParameters.putAll(rawParameters); } - return engine.evaluate( - new VersionedIdentifier().withId(id.getId()), + var versionlessIdentifiers = ids.stream() + .map(id -> new VersionedIdentifier().withId(id.getId())) + .toList(); + + return engineToUse.evaluate( + versionlessIdentifiers, expressions, buildContextParameter(patientId), evaluationParameters, null, zonedDateTime); } + + public EvaluationResult getEvaluationResult( + VersionedIdentifier id, + String patientId, + IBaseParameters parameters, + Map rawParameters, + IBaseBundle additionalData, + Set expressions, + CqlFhirParametersConverter cqlFhirParametersConverter, + @Nullable ZonedDateTime zonedDateTime, + CqlEngine engine) { + + var evaluationResultsForMultiLib = getEvaluationResult( + List.of(id), + patientId, + parameters, + rawParameters, + additionalData, + expressions, + cqlFhirParametersConverter, + zonedDateTime, + engine); + + return evaluationResultsForMultiLib.getOnlyResultOrThrow(); + } } diff --git a/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java index 30407a90c1..f48af72171 100644 --- a/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java +++ b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java @@ -14,11 +14,11 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.util.BundleBuilder; +import jakarta.annotation.Nonnull; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.stream.StreamSupport; -import javax.annotation.Nonnull; import org.cqframework.cql.cql2elm.StringLibrarySourceProvider; import org.cqframework.fhir.npm.NpmPackageManager; import org.cqframework.fhir.npm.NpmProcessor; diff --git a/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java b/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java index 574436e25c..86a60e7db7 100644 --- a/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java +++ b/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java @@ -26,10 +26,8 @@ import org.opencds.cqf.fhir.cr.cli.argument.MeasureCommandArgument; import org.opencds.cqf.fhir.cr.cli.command.CqlCommand.SubjectAndResult; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; -import org.opencds.cqf.fhir.cr.measure.SubjectProviderOptions; +import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor; -import org.opencds.cqf.fhir.cr.measure.r4.R4RepositorySubjectProvider; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; @@ -139,11 +137,7 @@ private static R4MeasureProcessor getR4MeasureProcessor( evaluationOptions.setApplyScoringSetMembership(false); evaluationOptions.setEvaluationSettings(evaluationSettings); - return new R4MeasureProcessor( - repository, - evaluationOptions, - new R4RepositorySubjectProvider(new SubjectProviderOptions()), - new R4MeasureServiceUtils(repository)); + return new R4MeasureProcessor(repository, evaluationOptions, new MeasureProcessorUtils()); } private void writeJsonToFile(String json, String patientId, Path path) { diff --git a/cqf-fhir-cr-cli/src/test/java/org/opencds/cqf/fhir/cr/cli/CliTest.java b/cqf-fhir-cr-cli/src/test/java/org/opencds/cqf/fhir/cr/cli/CliTest.java index 1dfbfe5571..9e409a2462 100644 --- a/cqf-fhir-cr-cli/src/test/java/org/opencds/cqf/fhir/cr/cli/CliTest.java +++ b/cqf-fhir-cr-cli/src/test/java/org/opencds/cqf/fhir/cr/cli/CliTest.java @@ -327,7 +327,7 @@ void optionsFailure() { Main.run(args); String errOutput = errContent.toString(); - assertTrue(errOutput.contains("library FluentFunctions loaded, but had errors")); + assertTrue(errOutput.contains("Library FluentFunctions loaded, but had errors")); } @Test diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java index 5f230aa24b..9c3711cc6d 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java @@ -48,13 +48,8 @@ public class CrR4Config { R4MeasureEvaluatorSingleFactory r4MeasureServiceFactory( IRepositoryFactory repositoryFactory, MeasureEvaluationOptions evaluationOptions, - MeasurePeriodValidator measurePeriodValidator, - R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory) { - return rd -> new R4MeasureService( - repositoryFactory.create(rd), - evaluationOptions, - measurePeriodValidator, - r4MeasureServiceUtilsFactory.create(rd)); + MeasurePeriodValidator measurePeriodValidator) { + return rd -> new R4MeasureService(repositoryFactory.create(rd), evaluationOptions, measurePeriodValidator); } @Bean @@ -90,8 +85,7 @@ ICollectDataServiceFactory collectDataServiceFactory( IRepositoryFactory repositoryFactory, MeasureEvaluationOptions measureEvaluationOptions, R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory) { - return rd -> new R4CollectDataService( - repositoryFactory.create(rd), measureEvaluationOptions, r4MeasureServiceUtilsFactory.create(rd)); + return rd -> new R4CollectDataService(repositoryFactory.create(rd), measureEvaluationOptions); } @Bean diff --git a/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java b/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java index 642b28e555..7e093c1377 100644 --- a/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java +++ b/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java @@ -2,10 +2,10 @@ import ca.uhn.fhir.repository.IRepository; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider; import org.opencds.cqf.fhir.cr.measure.dstu3.Dstu3MeasureProcessor; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -26,8 +26,7 @@ Dstu3MeasureProcessor dstu3MeasureProcessor( R4MeasureProcessor r4MeasureProcessor( IRepository repository, MeasureEvaluationOptions measureEvaluationOptions, - SubjectProvider subjectProvider, - R4MeasureServiceUtils measureServiceUtils) { - return new R4MeasureProcessor(repository, measureEvaluationOptions, subjectProvider, measureServiceUtils); + MeasureProcessorUtils measureProcessorUtils) { + return new R4MeasureProcessor(repository, measureEvaluationOptions, measureProcessorUtils); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java new file mode 100644 index 0000000000..dd750c76f2 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/CompositeEvaluationResultsPerMeasure.java @@ -0,0 +1,110 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.hl7.fhir.instance.model.api.IIdType; +import org.opencds.cqf.cql.engine.execution.EvaluationResult; + +/** + * Container meant to hold the results or an early and cached CQL measure evaluation that holds + * two data points: + *
    + *
  1. To hold the results of a measure evaluation, grouped by measure ID, with each measure evaluation grouped by Subject ID
  2. + *
  3. To hold any errors that occurred during the evaluation, grouped by measure ID
  4. + *
+ * These data points are not mutually exclusive, meaning a measure may have both successful results and errors. + *

+ * This class also allows the caller to mutate a {@link MeasureDef} with the errors that occurred during the evaluation + */ +public class CompositeEvaluationResultsPerMeasure { + // The same measure may have successful results AND errors, so account for both + private final Map> resultsPerMeasure; + // We may get several errors for a given measure + private final Map> errorsPerMeasure; + + private CompositeEvaluationResultsPerMeasure(Builder builder) { + + var resultsBuilder = ImmutableMap.>builder(); + builder.resultsPerMeasure.forEach((key, value) -> resultsBuilder.put(key, ImmutableMap.copyOf(value))); + resultsPerMeasure = resultsBuilder.build(); + + var errorsBuilder = ImmutableMap.>builder(); + builder.errorsPerMeasure.forEach((key, value) -> errorsBuilder.put(key, List.copyOf(value))); + errorsPerMeasure = errorsBuilder.build(); + } + + /** + * Retrieves results and populates errors for a given measure. + * This method uses direct map lookups for efficient data retrieval. + * measureDef will occasionally be prepended with the version, which means we need to parse it into an IIdType which + * is too much work, so pass in the measureId directly + * + * @param measureId the ID of the measure to process + * @param measureDef the MeasureDef to populate with errors + * + * @return a map of evaluation results per subject, or an empty map if none exist + */ + public Map processMeasureForSuccessOrFailure(IIdType measureId, MeasureDef measureDef) { + var unqualifiedMeasureId = measureId.toUnqualifiedVersionless(); + + errorsPerMeasure.getOrDefault(unqualifiedMeasureId, List.of()).forEach(measureDef::addError); + + // We are explicitly maintaining the logic of accepting the lack of any sort of results, + // either errors or successes, and returning an empty map. + return resultsPerMeasure.getOrDefault(unqualifiedMeasureId, Map.of()); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final Map> resultsPerMeasure = new HashMap<>(); + private final Map> errorsPerMeasure = new HashMap<>(); + + public CompositeEvaluationResultsPerMeasure build() { + return new CompositeEvaluationResultsPerMeasure(this); + } + + public void addResults(List measureIds, String subjectId, EvaluationResult evaluationResult) { + for (IIdType measureId : measureIds) { + addResult(measureId, subjectId, evaluationResult); + } + } + + public void addResult(IIdType measureId, String subjectId, EvaluationResult evaluationResult) { + // if we have no results, we don't need to add anything + if (evaluationResult == null || evaluationResult.expressionResults.isEmpty()) { + return; + } + + var resultPerMeasure = + resultsPerMeasure.computeIfAbsent(measureId.toUnqualifiedVersionless(), k -> new HashMap<>()); + + resultPerMeasure.put(subjectId, evaluationResult); + } + + public void addErrors(List measureIds, String error) { + if (error == null || error.isEmpty()) { + return; + } + + for (IIdType measureId : measureIds) { + addError(measureId, error); + } + } + + public void addError(IIdType measureId, String error) { + if (error == null || error.isBlank()) { + return; + } + + errorsPerMeasure + .computeIfAbsent(measureId.toUnqualifiedVersionless(), k -> new ArrayList<>()) + .add(error); + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java index 20578e1b50..d2c3a75f1a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorUtils.java @@ -2,6 +2,7 @@ import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.MEASUREPOPULATION; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -9,7 +10,6 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -22,12 +22,12 @@ import org.hl7.elm.r1.VersionedIdentifier; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; +import org.opencds.cqf.cql.engine.execution.EvaluationResultsForMultiLib; import org.opencds.cqf.cql.engine.execution.Libraries; import org.opencds.cqf.cql.engine.execution.Variable; import org.opencds.cqf.cql.engine.runtime.Date; import org.opencds.cqf.cql.engine.runtime.DateTime; import org.opencds.cqf.cql.engine.runtime.Interval; -import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.helper.DateHelper; import org.slf4j.Logger; @@ -35,6 +35,8 @@ public class MeasureProcessorUtils { private static final Logger logger = LoggerFactory.getLogger(MeasureProcessorUtils.class); + private static final String EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE = "Exception for subjectId: %s, Message: %s"; + /** * Method that processes CQL Results into Measure defined fields that reference associated CQL expressions * @param results criteria expression results @@ -66,7 +68,7 @@ public void processResults( } catch (Exception e) { // Catch Exceptions from evaluation per subject, but allow rest of subjects to be processed (if // applicable) - var error = "Exception for subjectId: %s, Message: %s".formatted(subjectId, e.getMessage()); + var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted(subjectId, e.getMessage()); // Capture error for MeasureReportBuilder measureDef.addError(error); logger.error(error, e); @@ -147,12 +149,11 @@ public ParameterDef getMeasurementPeriodParameterDef(CqlEngine context) { /** * method to set measurement period on cql engine context. * Priority is operation parameter defined value, otherwise default CQL value is used - * @param measureDef Measure defined objects to populate with criteria results * @param measurementPeriod Interval defined by operation parameters to override default CQL value * @param context cql engine context used to set measurement period parameter */ @SuppressWarnings({"deprecation", "removal"}) - public void setMeasurementPeriod(MeasureDef measureDef, Interval measurementPeriod, CqlEngine context) { + public void setMeasurementPeriod(Interval measurementPeriod, CqlEngine context, List measureUrls) { ParameterDef pd = this.getMeasurementPeriodParameterDef(context); if (pd == null) { logger.warn( @@ -194,7 +195,7 @@ public void setMeasurementPeriod(MeasureDef measureDef, Interval measurementPeri NamedTypeSpecifier pointType = (NamedTypeSpecifier) intervalTypeSpecifier.getPointType(); String targetType = pointType.getName().getLocalPart(); - Interval convertedPeriod = convertInterval(measureDef, measurementPeriod, targetType); + Interval convertedPeriod = convertInterval(measurementPeriod, targetType, measureUrls); context.getState().setParameter(null, MeasureConstants.MEASUREMENT_PERIOD_PARAMETER_NAME, convertedPeriod); } @@ -247,7 +248,7 @@ private static DateTime cloneDateTimeWithUtc(DateTime dateTime) { return newDateTime; } - public Interval convertInterval(MeasureDef measureDef, Interval interval, String targetType) { + public Interval convertInterval(Interval interval, String targetType, List measureUrls) { String sourceTypeQualified = interval.getPointType().getTypeName(); String sourceType = sourceTypeQualified.substring(sourceTypeQualified.lastIndexOf(".") + 1); if (sourceType.equals(targetType)) { @@ -265,8 +266,11 @@ public Interval convertInterval(MeasureDef measureDef, Interval interval, String } throw new InvalidRequestException( - "The interval type of %s did not match the expected type of %s and no conversion was possible for MeasureDef: %s." - .formatted(sourceType, targetType, measureDef.url())); + "The interval type of %s did not match the expected type of %s and no conversion was possible for measure URLs (first 5 only shown): %s." + .formatted( + sourceType, + targetType, + measureUrls.stream().limit(5).toList())); } public Date truncateDateTime(DateTime dateTime) { @@ -337,49 +341,95 @@ public Object evaluateObservationCriteria( /** * method used to execute generate CQL results via Library $evaluate + * * @param subjectIds subjects to generate results for - * @param measureDef Measure definition object used to store results of criteria expressions * @param zonedMeasurementPeriod offset defined measurement period for evaluation * @param context cql engine context - * @param libraryEngine library engine to use for evaluation of cql - * @param id library Version identifier used by library engine + * @param multiLibraryIdMeasureEngineDetails container for engine, library and measure IDs * @return CQL results for Library defined in the Measure resource */ - public Map getEvaluationResults( + public CompositeEvaluationResultsPerMeasure getEvaluationResults( List subjectIds, - MeasureDef measureDef, ZonedDateTime zonedMeasurementPeriod, CqlEngine context, - LibraryEngine libraryEngine, - VersionedIdentifier id) { + MultiLibraryIdMeasureEngineDetails multiLibraryIdMeasureEngineDetails) { - Map result = new HashMap<>(); + // measure -> subject -> results + var resultsBuilder = CompositeEvaluationResultsPerMeasure.builder(); // Library $evaluate each subject + // The goal here is to do each measure/library evaluation within the context of a single subject. + // This means that we will not switch between subject contexts while evaluating measures. + // Once we've switched to a different subject context, the previous expression cache is dropped. for (String subjectId : subjectIds) { if (subjectId == null) { - throw new NullPointerException("SubjectId is required in order to calculate."); + throw new InternalErrorException("SubjectId is required in order to calculate."); } Pair subjectInfo = this.getSubjectTypeAndId(subjectId); String subjectTypePart = subjectInfo.getLeft(); String subjectIdPart = subjectInfo.getRight(); context.getState().setContextValue(subjectTypePart, subjectIdPart); try { - result.put( - subjectId, - libraryEngine.getEvaluationResult( - id, subjectId, null, null, null, null, null, zonedMeasurementPeriod, context)); + var libraryIdentifiers = multiLibraryIdMeasureEngineDetails.getLibraryIdentifiers(); + + var evaluationResultsForMultiLib = multiLibraryIdMeasureEngineDetails + .getLibraryEngine() + .getEvaluationResult( + libraryIdentifiers, + subjectId, + null, + null, + null, + null, + null, + zonedMeasurementPeriod, + context); + + for (var libraryVersionedIdentifier : libraryIdentifiers) { + validateEvaluationResultExistsForIdentifier( + libraryVersionedIdentifier, evaluationResultsForMultiLib); + + var evaluationResult = evaluationResultsForMultiLib.getResultFor(libraryVersionedIdentifier); + + var measureIds = + multiLibraryIdMeasureEngineDetails.getMeasureIdsForLibrary(libraryVersionedIdentifier); + + resultsBuilder.addResults(measureIds, subjectId, evaluationResult); + + Optional.ofNullable(evaluationResultsForMultiLib.getExceptionFor(libraryVersionedIdentifier)) + .ifPresent(exception -> { + var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted( + subjectId, exception.getMessage()); + resultsBuilder.addErrors(measureIds, error); + logger.error(error, exception); + }); + } + } catch (Exception e) { - // Catch Exceptions from evaluation per subject, but allow rest of subjects to be processed (if - // applicable) - var error = "Exception for subjectId: %s, Message: %s".formatted(subjectId, e.getMessage()); - // Capture error for MeasureReportBuilder - measureDef.addError(error); + // If there's any error we didn't anticipate, catch it here: + var error = EXCEPTION_FOR_SUBJECT_ID_MESSAGE_TEMPLATE.formatted(subjectId, e.getMessage()); + var measureIds = multiLibraryIdMeasureEngineDetails.getAllMeasureIds(); + + resultsBuilder.addErrors(measureIds, error); logger.error(error, e); } } - return result; + return resultsBuilder.build(); + } + + private void validateEvaluationResultExistsForIdentifier( + VersionedIdentifier versionedIdentifierFromQuery, + EvaluationResultsForMultiLib evaluationResultsForMultiLib) { + + var containsResults = evaluationResultsForMultiLib.containsResultsFor(versionedIdentifierFromQuery); + var containsExceptions = evaluationResultsForMultiLib.containsExceptionsFor(versionedIdentifierFromQuery); + + if (!containsResults && !containsExceptions) { + throw new InternalErrorException( + "Evaluation result in versionless search not found for identifier with ID: %s" + .formatted(versionedIdentifierFromQuery.getId())); + } } public Pair getSubjectTypeAndId(String subjectId) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MultiLibraryIdMeasureEngineDetails.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MultiLibraryIdMeasureEngineDetails.java new file mode 100644 index 0000000000..10ed0f0ae8 --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MultiLibraryIdMeasureEngineDetails.java @@ -0,0 +1,62 @@ +package org.opencds.cqf.fhir.cr.measure.common; + +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; +import java.util.List; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.IIdType; +import org.opencds.cqf.fhir.cql.LibraryEngine; + +/** + * Convenience class to hold a library engine and a mapping of library IDs to measure IDs for + * library and measure evaluation. + */ +public class MultiLibraryIdMeasureEngineDetails { + private final LibraryEngine libraryEngine; + private final ListMultimap libraryIdToMeasureIds; + + private MultiLibraryIdMeasureEngineDetails(Builder builder) { + this.libraryEngine = builder.libraryEngine; + this.libraryIdToMeasureIds = builder.libraryIdToMeasureIdsBuilder.build(); + } + + public LibraryEngine getLibraryEngine() { + return libraryEngine; + } + + public List getLibraryIdentifiers() { + // Assuming we want the first library identifier + return List.copyOf(libraryIdToMeasureIds.keySet()); + } + + public List getMeasureIdsForLibrary(VersionedIdentifier libraryId) { + return libraryIdToMeasureIds.get(libraryId); + } + + public static Builder builder(LibraryEngine engine) { + return new Builder(engine); + } + + public List getAllMeasureIds() { + return List.copyOf(libraryIdToMeasureIds.values()); + } + + public static class Builder { + private final LibraryEngine libraryEngine; + private final ImmutableListMultimap.Builder libraryIdToMeasureIdsBuilder = + ImmutableListMultimap.builder(); + + public Builder(LibraryEngine libraryEngine) { + this.libraryEngine = libraryEngine; + } + + public Builder addLibraryIdToMeasureId(VersionedIdentifier libraryId, IIdType measureId) { + libraryIdToMeasureIdsBuilder.put(libraryId, measureId); + return this; + } + + public MultiLibraryIdMeasureEngineDetails build() { + return new MultiLibraryIdMeasureEngineDetails(this); + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index c5fb762fb2..3b569c79a5 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -3,12 +3,13 @@ import ca.uhn.fhir.repository.IRepository; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.Optional; import org.cqframework.cql.cql2elm.CqlIncludeException; import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.VersionedIdentifier; @@ -28,10 +29,12 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; +import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider; import org.opencds.cqf.fhir.utility.repository.FederatedRepository; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; +@SuppressWarnings("squid:S1135") public class Dstu3MeasureProcessor { private final IRepository repository; private final MeasureEvaluationOptions measureEvaluationOptions; @@ -89,15 +92,20 @@ protected MeasureReport evaluateMeasure( actualRepo = new FederatedRepository( this.repository, new InMemoryFhirRepository(this.repository.fhirContext(), additionalData)); } - var subjects = subjectProvider.getSubjects(actualRepo, subjectIds).collect(Collectors.toList()); + var subjects = subjectProvider.getSubjects(actualRepo, subjectIds).toList(); var evalType = getMeasureEvalType(reportType, subjects); var context = Engines.forRepository( this.repository, this.measureEvaluationOptions.getEvaluationSettings(), additionalData); - var libraryVersionIdentifier = getLibraryVersionIdentifier(measure); - var libraryEngine = getLibraryEngine(parameters, libraryVersionIdentifier, context); + // Note that we must build the LibraryEngine BEFORE we call + // measureProcessorUtils.setMeasurementPeriod(), otherwise, we get an NPE. + var measureLibraryIdEngineDetails = buildLibraryIdEngineDetails(measure, parameters, context); + // set measurement Period from CQL if operation parameters are empty - measureProcessorUtils.setMeasurementPeriod(measureDef, measurementPeriodParams, context); + measureProcessorUtils.setMeasurementPeriod( + measurementPeriodParams, + context, + Optional.ofNullable(measure.getUrl()).map(List::of).orElse(List.of("Unknown Measure URL"))); // extract measurement Period from CQL to pass to report Builder Interval measurementPeriod = measureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context); @@ -106,11 +114,11 @@ protected MeasureReport evaluateMeasure( // populate results from Library $evaluate if (!subjects.isEmpty()) { var results = measureProcessorUtils.getEvaluationResults( - subjectIds, measureDef, zonedMeasurementPeriod, context, libraryEngine, libraryVersionIdentifier); + subjectIds, zonedMeasurementPeriod, context, measureLibraryIdEngineDetails); // Process Criteria Expression Results measureProcessorUtils.processResults( - results, + results.processMeasureForSuccessOrFailure(measure.getIdElement(), measureDef), measureDef, evalType, measureEvaluationOptions.getApplyScoringSetMembership(), @@ -124,20 +132,26 @@ protected MeasureReport evaluateMeasure( .build(measure, measureDef, evalTypeToReportType(evalType), measurementPeriod, subjects); } + // Ideally this would be done in MeasureProcessorUtils, but it's too much work to change for now + private MultiLibraryIdMeasureEngineDetails buildLibraryIdEngineDetails( + Measure measure, Parameters parameters, CqlEngine context) { + + var libraryVersionIdentifier = getLibraryVersionIdentifier(measure); + + final LibraryEngine libraryEngine = getLibraryEngine(parameters, libraryVersionIdentifier, context); + + return MultiLibraryIdMeasureEngineDetails.builder(libraryEngine) + .addLibraryIdToMeasureId( + new VersionedIdentifier().withId(libraryVersionIdentifier.getId()), measure.getIdElement()) + .build(); + } + protected MeasureReportType evalTypeToReportType(MeasureEvalType measureEvalType) { - switch (measureEvalType) { - case PATIENT: - case SUBJECT: - return MeasureReportType.INDIVIDUAL; - case PATIENTLIST: - case SUBJECTLIST: - return MeasureReportType.PATIENTLIST; - case POPULATION: - return MeasureReportType.SUMMARY; - default: - throw new InvalidRequestException( - "Unsupported MeasureEvalType: %s".formatted(measureEvalType.toCode())); - } + return switch (measureEvalType) { + case PATIENT, SUBJECT -> MeasureReportType.INDIVIDUAL; + case PATIENTLIST, SUBJECTLIST -> MeasureReportType.PATIENTLIST; + case POPULATION -> MeasureReportType.SUMMARY; + }; } protected LibraryEngine getLibraryEngine(Parameters parameters, VersionedIdentifier id, CqlEngine context) { @@ -219,7 +233,10 @@ private Map resolveParameterMap(Parameters parameters) { list.add(value); } } else { - parameterMap.put(param.getName(), Arrays.asList(parameterMap.get(param.getName()), value)); + // We need a mutable list here, otherwise, retrieving the list above will fail with + // UnsupportedOperationException + parameterMap.put( + param.getName(), new ArrayList<>(Arrays.asList(parameterMap.get(param.getName()), value))); } } else { parameterMap.put(param.getName(), value); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java index b8cedd31b2..46b3a0aa75 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java @@ -15,26 +15,27 @@ import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Resource; +import org.opencds.cqf.cql.engine.execution.CqlEngine; +import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; +import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.monad.Eithers; +@SuppressWarnings("squid:S107") public class R4CollectDataService { private final IRepository repository; private final MeasureEvaluationOptions measureEvaluationOptions; private final R4RepositorySubjectProvider subjectProvider; - private final R4MeasureServiceUtils measureServiceUtils; + private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); - public R4CollectDataService( - IRepository repository, - MeasureEvaluationOptions measureEvaluationOptions, - R4MeasureServiceUtils measureServiceUtils) { + public R4CollectDataService(IRepository repository, MeasureEvaluationOptions measureEvaluationOptions) { this.repository = repository; this.measureEvaluationOptions = measureEvaluationOptions; this.subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); - this.measureServiceUtils = measureServiceUtils; } /** @@ -65,23 +66,46 @@ public Parameters collectData( String practitioner) { Parameters parameters = new Parameters(); - var processor = new R4MeasureProcessor( - this.repository, this.measureEvaluationOptions, this.subjectProvider, this.measureServiceUtils); + var processor = + new R4MeasureProcessor(this.repository, this.measureEvaluationOptions, this.measureProcessorUtils); - // getSubjects List subjectList = getSubjects(subject, practitioner, subjectProvider); - // if(subjectList.isEmpty() || subjectList == null){ - // } - // loop over subjects + var context = + Engines.forRepository(this.repository, this.measureEvaluationOptions.getEvaluationSettings(), null); + + var foldedMeasure = R4MeasureServiceUtils.foldMeasure(Eithers.forMiddle3(measureId), repository); + if (!subjectList.isEmpty()) { for (String patient : subjectList) { var subjects = Collections.singletonList(patient); + + var compositeEvaluationResultsPerMeasure = processor.evaluateMeasureWithCqlEngine( + subjects, foldedMeasure, periodStart, periodEnd, parameters, context); + // add resources per subject to Parameters - addReports(processor, measureId, periodStart, periodEnd, subjects, parameters); + addReports( + processor, + measureId, + periodStart, + periodEnd, + subjects, + parameters, + context, + compositeEvaluationResultsPerMeasure); } } else { - addReports(processor, measureId, periodStart, periodEnd, subjectList, parameters); + var compositeEvaluationResultsPerMeasure = processor.evaluateMeasureWithCqlEngine( + subjectList, foldedMeasure, periodStart, periodEnd, parameters, context); + addReports( + processor, + measureId, + periodStart, + periodEnd, + subjectList, + parameters, + context, + compositeEvaluationResultsPerMeasure); } return parameters; } @@ -92,7 +116,10 @@ private void addReports( @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, List subjects, - Parameters parameters) { + Parameters parameters, + CqlEngine context, + CompositeEvaluationResultsPerMeasure evaluateMeasureResults) { + MeasureReport report = processor.evaluateMeasure( Eithers.forMiddle3(measureId), periodStart, @@ -100,8 +127,8 @@ private void addReports( MeasureEvalType.SUBJECT.toCode(), subjects, null, - null, - null); + context, + evaluateMeasureResults); report.setType(MeasureReport.MeasureReportType.DATACOLLECTION); report.setGroup(null); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java index beeaf12911..c0d6dbba65 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureDefBuilder.java @@ -149,6 +149,25 @@ && checkPopulationForCode(populations, DATEOFCOMPLIANCE) == null) { return new MeasureDef(measure.getId(), measure.getUrl(), measure.getVersion(), groups, sdes); } + public static void triggerFirstPassValidation(List measures) { + measures.forEach(R4MeasureDefBuilder::triggerFirstPassValidation); + } + + private static void triggerFirstPassValidation(Measure measure) { + + checkId(measure); + + // SDES + for (MeasureSupplementalDataComponent s : measure.getSupplementalData()) { + checkId(s); + checkSDEUsage(measure, s); + } + // scoring + getMeasureScoring(measure); + + validateMeasureImprovementNotation(measure); + } + private PopulationDef checkPopulationForCode( List populations, MeasurePopulationType measurePopType) { return populations.stream() @@ -164,7 +183,8 @@ private ConceptDef totalConceptDefCreator(MeasurePopulationType measurePopulatio null); } - private void checkSDEUsage(Measure measure, MeasureSupplementalDataComponent measureSupplementalDataComponent) { + private static void checkSDEUsage( + Measure measure, MeasureSupplementalDataComponent measureSupplementalDataComponent) { var hasUsage = measureSupplementalDataComponent.getUsage().stream() .filter(t -> t.getCodingFirstRep().getCode().equals(SDE_USAGE_CODE)) .collect(Collectors.toList()); @@ -191,19 +211,19 @@ private CodeDef codeToCodeDef(Coding coding) { return new CodeDef(coding.getSystem(), coding.getVersion(), coding.getCode(), coding.getDisplay()); } - private void checkId(Element e) { + private static void checkId(Element e) { if (e.getId() == null || StringUtils.isBlank(e.getId())) { throw new NullPointerException("id is required on all Elements of type: " + e.fhirType()); } } - private void checkId(Resource r) { + private static void checkId(Resource r) { if (r.getId() == null || StringUtils.isBlank(r.getId())) { throw new NullPointerException("id is required on all Resources of type: " + r.fhirType()); } } - private MeasureScoring getMeasureScoring(Measure measure, @Nullable String scoringCode) { + private static MeasureScoring getMeasureScoring(Measure measure, @Nullable String scoringCode) { if (scoringCode != null) { var code = MeasureScoring.fromCode(scoringCode); if (code == null) { @@ -217,12 +237,12 @@ private MeasureScoring getMeasureScoring(Measure measure, @Nullable String scori return null; } - private MeasureScoring getMeasureScoring(Measure measure) { + private static MeasureScoring getMeasureScoring(Measure measure) { var scoringCode = measure.getScoring().getCodingFirstRep().getCode(); return getMeasureScoring(measure, scoringCode); } - private void validateImprovementNotationCode(Measure measure, CodeDef improvementNotation) { + private static void validateImprovementNotationCode(Measure measure, CodeDef improvementNotation) { var code = improvementNotation.code(); var system = improvementNotation.system(); boolean hasValidSystem = system.equals(MEASUREREPORT_IMPROVEMENT_NOTATION_SYSTEM); @@ -264,6 +284,16 @@ public CodeDef getMeasureImprovementNotation(Measure measure) { return null; } + private static void validateMeasureImprovementNotation(Measure measure) { + if (measure.hasImprovementNotation()) { + var improvementNotationValue = measure.getImprovementNotation(); + var codeDef = new CodeDef( + improvementNotationValue.getCodingFirstRep().getSystem(), + improvementNotationValue.getCodingFirstRep().getCode()); + validateImprovementNotationCode(measure, codeDef); + } + } + public CodeDef getGroupImpNotation(Measure measure, MeasureGroupComponent group) { var ext = group.getExtensionByUrl(MEASUREREPORT_IMPROVEMENT_NOTATION_EXTENSION); if (ext != null) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index bc6673ec85..608eb32463 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -3,20 +3,22 @@ import ca.uhn.fhir.repository.IRepository; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import com.google.common.collect.ImmutableListMultimap; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.time.ZonedDateTime; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.Optional; +import org.cqframework.cql.cql2elm.CqlCompilerException; import org.cqframework.cql.cql2elm.CqlIncludeException; import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.hl7.elm.r1.VersionedIdentifier; -import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CanonicalType; @@ -25,14 +27,15 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Resource; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver; import org.opencds.cqf.cql.engine.runtime.Interval; -import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cql.VersionedIdentifiers; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; @@ -40,54 +43,32 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; -import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider; +import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4DateHelper; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; -import org.opencds.cqf.fhir.utility.Canonicals; import org.opencds.cqf.fhir.utility.monad.Either3; -import org.opencds.cqf.fhir.utility.repository.FederatedRepository; -import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.search.Searches; public class R4MeasureProcessor { private final IRepository repository; private final MeasureEvaluationOptions measureEvaluationOptions; - private final SubjectProvider subjectProvider; - private final R4MeasureServiceUtils r4MeasureServiceUtils; - private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); + private final MeasureProcessorUtils measureProcessorUtils; public R4MeasureProcessor( IRepository repository, MeasureEvaluationOptions measureEvaluationOptions, - SubjectProvider subjectProvider, - R4MeasureServiceUtils r4MeasureServiceUtils) { + MeasureProcessorUtils measureProcessorUtils) { + this.repository = Objects.requireNonNull(repository); this.measureEvaluationOptions = measureEvaluationOptions != null ? measureEvaluationOptions : MeasureEvaluationOptions.defaultOptions(); - this.subjectProvider = subjectProvider; - this.r4MeasureServiceUtils = r4MeasureServiceUtils; + this.measureProcessorUtils = measureProcessorUtils; } - public MeasureReport evaluateMeasure( - Either3 measure, - @Nullable ZonedDateTime periodStart, - @Nullable ZonedDateTime periodEnd, - String reportType, - List subjectIds, - IBaseBundle additionalData, - Parameters parameters) { - - var evalType = r4MeasureServiceUtils.getMeasureEvalType(reportType, subjectIds); - - var actualRepo = this.repository; - if (additionalData != null) { - actualRepo = new FederatedRepository( - this.repository, new InMemoryFhirRepository(this.repository.fhirContext(), additionalData)); - } - var subjects = subjectProvider.getSubjects(actualRepo, subjectIds).collect(Collectors.toList()); - - return this.evaluateMeasure( - measure, periodStart, periodEnd, reportType, subjects, additionalData, parameters, evalType); + // Expose this so CQL measure evaluation can use the same Repository as the one passed to the + // processor: this may be some sort of federated proxy repository initialized at runtime + public IRepository getRepository() { + return repository; } public MeasureReport evaluateMeasure( @@ -96,12 +77,18 @@ public MeasureReport evaluateMeasure( @Nullable ZonedDateTime periodEnd, String reportType, List subjectIds, - IBaseBundle additionalData, - Parameters parameters, - MeasureEvalType evalType) { - var m = measure.fold(this::resolveByUrl, this::resolveById, Function.identity()); + MeasureEvalType evalType, + CqlEngine context, + CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure) { return this.evaluateMeasure( - m, periodStart, periodEnd, reportType, subjectIds, additionalData, parameters, evalType); + R4MeasureServiceUtils.foldMeasure(measure, this.repository), + periodStart, + periodEnd, + reportType, + subjectIds, + evalType, + context, + compositeEvaluationResultsPerMeasure); } /** @@ -160,22 +147,18 @@ public MeasureReport evaluateMeasureResults( * @param periodEnd end date of Measurement Period * @param reportType type of report that defines MeasureReport Type * @param subjectIds the subjectIds to process - * @param additionalData external bundle to process with results - * @param parameters cql parameters specified in parameters resource * @param evalType the type of evaluation to process, this is an output of reportType param * @return Measure Report resource */ - protected MeasureReport evaluateMeasure( + public MeasureReport evaluateMeasure( Measure measure, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, String reportType, List subjectIds, - IBaseBundle additionalData, - Parameters parameters, - MeasureEvalType evalType) { - - checkMeasureLibrary(measure); + MeasureEvalType evalType, + CqlEngine context, + CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure) { MeasureEvalType evaluationType = measureProcessorUtils.getEvalType(evalType, reportType, subjectIds); // Measurement Period: operation parameter defined measurement period @@ -184,28 +167,23 @@ protected MeasureReport evaluateMeasure( // setup MeasureDef var measureDef = new R4MeasureDefBuilder().build(measure); - // CQL Engine context - var context = Engines.forRepository( - this.repository, this.measureEvaluationOptions.getEvaluationSettings(), additionalData); + measureProcessorUtils.setMeasurementPeriod( + measurementPeriodParams, + context, + Optional.ofNullable(measure.getUrl()).map(List::of).orElse(List.of("Unknown Measure URL"))); - var libraryVersionIdentifier = getLibraryVersionIdentifier(measure); - // library engine setup - var libraryEngine = getLibraryEngine(parameters, libraryVersionIdentifier, context); - - // set measurement Period from CQL if operation parameters are empty - measureProcessorUtils.setMeasurementPeriod(measureDef, measurementPeriodParams, context); // extract measurement Period from CQL to pass to report Builder Interval measurementPeriod = measureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context); - // set offset of operation parameter measurement period - ZonedDateTime zonedMeasurementPeriod = MeasureProcessorUtils.getZonedTimeZoneForEval(measurementPeriod); - // populate results from Library $evaluate - var results = measureProcessorUtils.getEvaluationResults( - subjectIds, measureDef, zonedMeasurementPeriod, context, libraryEngine, libraryVersionIdentifier); // Process Criteria Expression Results + final IIdType measureId = measure.getIdElement().toUnqualifiedVersionless(); + // populate results from Library $evaluate + final Map resultForThisMeasure = + compositeEvaluationResultsPerMeasure.processMeasureForSuccessOrFailure(measureId, measureDef); + measureProcessorUtils.processResults( - results, + resultForThisMeasure, measureDef, evaluationType, this.measureEvaluationOptions.getApplyScoringSetMembership(), @@ -224,6 +202,128 @@ protected MeasureReport evaluateMeasure( subjectIds); } + public CompositeEvaluationResultsPerMeasure evaluateMeasureWithCqlEngine( + List subjects, + Either3 measureEither, + @Nullable ZonedDateTime periodStart, + @Nullable ZonedDateTime periodEnd, + Parameters parameters, + CqlEngine context) { + + return evaluateMultiMeasuresWithCqlEngine( + subjects, + List.of(R4MeasureServiceUtils.foldMeasure(measureEither, repository)), + periodStart, + periodEnd, + parameters, + context); + } + + public CompositeEvaluationResultsPerMeasure evaluateMeasureIdWithCqlEngine( + List subjects, + IIdType measureId, + @Nullable ZonedDateTime periodStart, + @Nullable ZonedDateTime periodEnd, + Parameters parameters, + CqlEngine context) { + + return evaluateMultiMeasuresWithCqlEngine( + subjects, + List.of(R4MeasureServiceUtils.resolveById(measureId, repository)), + periodStart, + periodEnd, + parameters, + context); + } + + public CompositeEvaluationResultsPerMeasure evaluateMeasureWithCqlEngine( + List subjects, + Measure measure, + @Nullable ZonedDateTime periodStart, + @Nullable ZonedDateTime periodEnd, + Parameters parameters, + CqlEngine context) { + + return evaluateMultiMeasuresWithCqlEngine( + subjects, List.of(measure), periodStart, periodEnd, parameters, context); + } + + public CompositeEvaluationResultsPerMeasure evaluateMultiMeasureIdsWithCqlEngine( + List subjects, + List measureIds, + @Nullable ZonedDateTime periodStart, + @Nullable ZonedDateTime periodEnd, + Parameters parameters, + CqlEngine context) { + return evaluateMultiMeasuresWithCqlEngine( + subjects, + measureIds.stream() + .map(IIdType::toUnqualifiedVersionless) + .map(id -> R4MeasureServiceUtils.resolveById(id, repository)) + .toList(), + periodStart, + periodEnd, + parameters, + context); + } + + public CompositeEvaluationResultsPerMeasure evaluateMultiMeasuresWithCqlEngine( + List subjects, + List measures, + @Nullable ZonedDateTime periodStart, + @Nullable ZonedDateTime periodEnd, + Parameters parameters, + CqlEngine context) { + + measures.forEach(this::checkMeasureLibrary); + + var measurementPeriodParams = buildMeasurementPeriod(periodStart, periodEnd); + var zonedMeasurementPeriod = MeasureProcessorUtils.getZonedTimeZoneForEval( + measureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context)); + + // Do this to be backwards compatible with the previous single-library evaluation: + // Trigger first-pass validation on measure scoring as well as other aspects of the Measures + R4MeasureDefBuilder.triggerFirstPassValidation(measures); + + // Note that we must build the LibraryEngine BEFORE we call + // measureProcessorUtils.setMeasurementPeriod(), otherwise, we get an NPE. + var multiLibraryIdMeasureEngineDetails = getMultiLibraryIdMeasureEngineDetails(measures, parameters, context); + + // set measurement Period from CQL if operation parameters are empty + measureProcessorUtils.setMeasurementPeriod( + measurementPeriodParams, + context, + measures.stream() + .map(Measure::getUrl) + .map(url -> Optional.ofNullable(url).orElse("Unknown Measure URL")) + .toList()); + + // populate results from Library $evaluate + return measureProcessorUtils.getEvaluationResults( + subjects, zonedMeasurementPeriod, context, multiLibraryIdMeasureEngineDetails); + } + + private MultiLibraryIdMeasureEngineDetails getMultiLibraryIdMeasureEngineDetails( + List measures, Parameters parameters, CqlEngine context) { + + var libraryIdentifiersToMeasureIds = measures.stream() + .collect(ImmutableListMultimap.toImmutableListMultimap( + this::getLibraryVersionIdentifier, // Key function + Resource::getIdElement // Value function + )); + + var libraryEngine = getLibraryEngine(parameters, List.copyOf(libraryIdentifiersToMeasureIds.keySet()), context); + + var builder = MultiLibraryIdMeasureEngineDetails.builder(libraryEngine); + + libraryIdentifiersToMeasureIds.entries().forEach(entry -> { + builder.addLibraryIdToMeasureId( + new VersionedIdentifier().withId(entry.getKey().getId()), entry.getValue()); + }); + + return builder.build(); + } + /** Temporary check for Measures that are being blocked from use by evaluateResults method * * @param measureDef defined measure definition object used to capture criteria expression results @@ -302,6 +402,65 @@ protected LibraryEngine getLibraryEngine(Parameters parameters, VersionedIdentif return new LibraryEngine(repository, this.measureEvaluationOptions.getEvaluationSettings()); } + protected LibraryEngine getLibraryEngine(Parameters parameters, List ids, CqlEngine context) { + + var compiledLibraries = getCompiledLibraries(ids, context); + + var libraries = + compiledLibraries.stream().map(CompiledLibrary::getLibrary).toList(); + + context.getState().init(libraries); + + // if we comment this out MeasureScorerTest and other tests will fail with NPEs + setArgParameters(parameters, context, compiledLibraries); + + return new LibraryEngine(repository, this.measureEvaluationOptions.getEvaluationSettings()); + } + + private List getCompiledLibraries(List ids, CqlEngine context) { + try { + var resolvedLibraryResults = + context.getEnvironment().getLibraryManager().resolveLibraries(ids); + + var allErrors = resolvedLibraryResults.allErrors(); + if (resolvedLibraryResults.hasErrors() || ids.size() > allErrors.size()) { + return resolvedLibraryResults.allCompiledLibraries(); + } + + if (ids.size() == 1) { + final List cqlCompilerExceptions = + resolvedLibraryResults.getErrorsFor(ids.get(0)); + + if (cqlCompilerExceptions.size() == 1) { + throw new IllegalStateException( + "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." + .formatted(ids.get(0).getId()), + cqlCompilerExceptions.get(0)); + } else { + throw new IllegalStateException( + "Unable to load CQL/ELM for library: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" + .formatted( + ids.get(0).getId(), + cqlCompilerExceptions.stream() + .map(CqlCompilerException::getMessage) + .reduce((s1, s2) -> s1 + "; " + s2) + .orElse("No error messages found."))); + } + } + + throw new IllegalStateException( + "Unable to load CQL/ELM for libraries: %s Verify that the Library resource is available in your environment and has CQL/ELM content embedded. Errors: %s" + .formatted(ids, allErrors)); + + } catch (CqlIncludeException exception) { + throw new IllegalStateException( + "Unable to load CQL/ELM for libraries: %s. Verify that the Library resource is available in your environment and has CQL/ELM content embedded." + .formatted( + ids.stream().map(VersionedIdentifier::getId).toList()), + exception); + } + } + protected void checkMeasureLibrary(Measure measure) { if (!measure.hasLibrary()) { throw new InvalidRequestException( @@ -317,31 +476,33 @@ protected void checkMeasureLibrary(Measure measure) { * @param context CQL engine generated */ protected void setArgParameters(Parameters parameters, CqlEngine context, CompiledLibrary lib) { + setArgParameters(parameters, context, List.of(lib)); + } + + /** + * Set parameters for included libraries, which may be multiple + * Note: this may not be the optimal method (e.g. libraries with the same + * parameter name, but different values) + * @param parameters CQL parameters passed in from operation + * @param context CQL engine generated + */ + protected void setArgParameters(Parameters parameters, CqlEngine context, List libs) { if (parameters != null) { Map paramMap = resolveParameterMap(parameters); - context.getState().setParameters(lib.getLibrary(), paramMap); - - if (lib.getLibrary().getIncludes() != null) { - lib.getLibrary() - .getIncludes() - .getDef() - .forEach(includeDef -> paramMap.forEach((paramKey, paramValue) -> context.getState() - .setParameter(includeDef.getLocalIdentifier(), paramKey, paramValue))); + for (CompiledLibrary lib : libs) { + context.getState().setParameters(lib.getLibrary(), paramMap); + + if (lib.getLibrary().getIncludes() != null) { + lib.getLibrary() + .getIncludes() + .getDef() + .forEach(includeDef -> paramMap.forEach((paramKey, paramValue) -> context.getState() + .setParameter(includeDef.getLocalIdentifier(), paramKey, paramValue))); + } } } } - protected Measure resolveByUrl(CanonicalType url) { - var parts = Canonicals.getParts(url); - var result = this.repository.search( - Bundle.class, Measure.class, Searches.byNameAndVersion(parts.idPart(), parts.version())); - return (Measure) result.getEntryFirstRep().getResource(); - } - - protected Measure resolveById(IdType id) { - return this.repository.read(Measure.class, id); - } - /** * convert Parameters resource for injection into format for cql evaluation * @param parameters resource used to store cql parameters @@ -370,7 +531,10 @@ private Map resolveParameterMap(Parameters parameters) { list.add(value); } } else { - parameterMap.put(param.getName(), Arrays.asList(parameterMap.get(param.getName()), value)); + // We need a mutable list here, otherwise, retrieving the list above will fail with + // UnsupportedOperationException + parameterMap.put( + param.getName(), new ArrayList<>(Arrays.asList(parameterMap.get(param.getName()), value))); } } else { parameterMap.put(param.getName(), value); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java index 78630e44b3..06be1e14e8 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java @@ -1,9 +1,11 @@ package org.opencds.cqf.fhir.cr.measure.r4; import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.time.ZonedDateTime; -import java.util.Collections; +import java.util.List; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CanonicalType; @@ -12,10 +14,14 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; +import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; +import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.repository.FederatedRepository; +import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.repository.Repositories; public class R4MeasureService implements R4MeasureEvaluatorSingle { @@ -23,18 +29,16 @@ public class R4MeasureService implements R4MeasureEvaluatorSingle { private final MeasureEvaluationOptions measureEvaluationOptions; private final MeasurePeriodValidator measurePeriodValidator; private final R4RepositorySubjectProvider subjectProvider; - private final R4MeasureServiceUtils measureServiceUtils; + private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); public R4MeasureService( IRepository repository, MeasureEvaluationOptions measureEvaluationOptions, - MeasurePeriodValidator measurePeriodValidator, - R4MeasureServiceUtils measureServiceUtils) { + MeasurePeriodValidator measurePeriodValidator) { this.repository = repository; this.measureEvaluationOptions = measureEvaluationOptions; this.measurePeriodValidator = measurePeriodValidator; this.subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); - this.measureServiceUtils = measureServiceUtils; } @Override @@ -55,9 +59,10 @@ public MeasureReport evaluate( measurePeriodValidator.validatePeriodStartAndEnd(periodStart, periodEnd); - var repo = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); + var proxyRepoForMeasureProcessor = + Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); var processor = new R4MeasureProcessor( - repo, this.measureEvaluationOptions, this.subjectProvider, this.measureServiceUtils); + proxyRepoForMeasureProcessor, this.measureEvaluationOptions, measureProcessorUtils); R4MeasureServiceUtils r4MeasureServiceUtils = new R4MeasureServiceUtils(repository); r4MeasureServiceUtils.ensureSupplementalDataElementSearchParameter(); @@ -71,14 +76,21 @@ public MeasureReport evaluate( subjectId = practitioner; } + var evalType = r4MeasureServiceUtils.getMeasureEvalType( + reportType, Optional.ofNullable(subjectId).map(List::of).orElse(List.of())); + + var subjects = getSubjects(subjectId, proxyRepoForMeasureProcessor, additionalData); + + // Replicate the old logic of using the repository used to initialize the measure processor + // as the repository for the CQL engine context. + var context = Engines.forRepository( + proxyRepoForMeasureProcessor, this.measureEvaluationOptions.getEvaluationSettings(), additionalData); + + var evaluationResults = + processor.evaluateMeasureWithCqlEngine(subjects, measure, periodStart, periodEnd, parameters, context); + measureReport = processor.evaluateMeasure( - measure, - periodStart, - periodEnd, - reportType, - Collections.singletonList(subjectId), - additionalData, - parameters); + measure, periodStart, periodEnd, reportType, subjects, evalType, context, evaluationResults); // add ProductLine after report is generated measureReport = r4MeasureServiceUtils.addProductLineExtension(measureReport, productLine); @@ -86,4 +98,22 @@ public MeasureReport evaluate( // add subject reference for non-individual reportTypes return r4MeasureServiceUtils.addSubjectReference(measureReport, practitioner, subjectId); } + + @Nonnull + private List getSubjects( + String subjectId, IRepository proxyRepoForMeasureProcessor, Bundle additionalData) { + final IRepository repoToUseForSubjectProvider; + if (additionalData != null) { + repoToUseForSubjectProvider = new FederatedRepository( + this.repository, new InMemoryFhirRepository(this.repository.fhirContext(), additionalData)); + } else { + repoToUseForSubjectProvider = proxyRepoForMeasureProcessor; + } + + return subjectProvider + .getSubjects( + repoToUseForSubjectProvider, + Optional.ofNullable(subjectId).map(List::of).orElse(List.of())) + .toList(); + } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java index 340915fa5f..14c3a85902 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java @@ -9,7 +9,6 @@ import java.util.Collections; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleType; @@ -19,9 +18,13 @@ import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Resource; +import org.opencds.cqf.cql.engine.execution.CqlEngine; +import org.opencds.cqf.fhir.cql.Engines; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; +import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.builder.BundleBuilder; @@ -31,18 +34,19 @@ * Alternate MeasureService call to Process MeasureEvaluation for the selected population of subjects against n-number * of measure resources. The output of this operation would be a bundle of MeasureReports instead of MeasureReport. */ +@SuppressWarnings("squid:S107") public class R4MultiMeasureService implements R4MeasureEvaluatorMultiple { + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(R4MultiMeasureService.class); - private IRepository repository; + + private final IRepository repository; private final MeasureEvaluationOptions measureEvaluationOptions; private final MeasurePeriodValidator measurePeriodValidator; - private String serverBase; - + private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); + private final String serverBase; private final R4RepositorySubjectProvider subjectProvider; - - private R4MeasureProcessor r4Processor; - - private R4MeasureServiceUtils r4MeasureServiceUtils; + private final R4MeasureProcessor r4MeasureProcessorStandardRepository; + private final R4MeasureServiceUtils r4MeasureServiceUtilsStandardRepository; public R4MultiMeasureService( IRepository repository, @@ -53,13 +57,10 @@ public R4MultiMeasureService( this.measureEvaluationOptions = measureEvaluationOptions; this.measurePeriodValidator = measurePeriodValidator; this.serverBase = serverBase; - - subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); - - r4Processor = new R4MeasureProcessor( - repository, this.measureEvaluationOptions, subjectProvider, r4MeasureServiceUtils); - - r4MeasureServiceUtils = new R4MeasureServiceUtils(repository); + this.subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); + this.r4MeasureProcessorStandardRepository = + new R4MeasureProcessor(repository, this.measureEvaluationOptions, this.measureProcessorUtils); + this.r4MeasureServiceUtilsStandardRepository = new R4MeasureServiceUtils(repository); } @Override @@ -81,20 +82,27 @@ public Bundle evaluate( measurePeriodValidator.validatePeriodStartAndEnd(periodStart, periodEnd); + final R4MeasureProcessor r4ProcessorToUse; + final R4MeasureServiceUtils r4MeasureServiceUtilsToUse; if (dataEndpoint != null && contentEndpoint != null && terminologyEndpoint != null) { - // if needing to use proxy repository, override constructors - repository = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); + // if needing to use proxy repository, initialize new R4MeasureProcessor and R4MeasureServiceUtils + var repositoryToUse = + Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); - r4Processor = new R4MeasureProcessor( - repository, this.measureEvaluationOptions, subjectProvider, r4MeasureServiceUtils); + r4ProcessorToUse = + new R4MeasureProcessor(repositoryToUse, this.measureEvaluationOptions, this.measureProcessorUtils); - r4MeasureServiceUtils = new R4MeasureServiceUtils(repository); + r4MeasureServiceUtilsToUse = new R4MeasureServiceUtils(repositoryToUse); + } else { + r4ProcessorToUse = r4MeasureProcessorStandardRepository; + r4MeasureServiceUtilsToUse = r4MeasureServiceUtilsStandardRepository; } - r4MeasureServiceUtils.ensureSupplementalDataElementSearchParameter(); - List measures = r4MeasureServiceUtils.getMeasures(measureId, measureIdentifier, measureUrl); + + r4MeasureServiceUtilsToUse.ensureSupplementalDataElementSearchParameter(); + List measures = r4MeasureServiceUtilsToUse.getMeasures(measureId, measureIdentifier, measureUrl); log.info("multi-evaluate-measure, measures to evaluate: {}", measures.size()); - var evalType = r4MeasureServiceUtils.getMeasureEvalType(reportType, subject); + var evalType = r4MeasureServiceUtilsToUse.getMeasureEvalType(reportType, subject); // get subjects var subjects = getSubjects(subjectProvider, subject); @@ -104,9 +112,22 @@ public Bundle evaluate( .withType(BundleType.SEARCHSET.toString()) .build(); + var context = Engines.forRepository( + r4ProcessorToUse.getRepository(), + this.measureEvaluationOptions.getEvaluationSettings(), + additionalData); + + // This is basically a Map of measure -> subject -> EvaluationResult + var compositeEvaluationResultsPerMeasure = r4ProcessorToUse.evaluateMultiMeasuresWithCqlEngine( + subjects, measures, periodStart, periodEnd, parameters, context); + // evaluate Measures if (evalType.equals(MeasureEvalType.POPULATION) || evalType.equals(MeasureEvalType.SUBJECTLIST)) { populationMeasureReport( + r4ProcessorToUse, + r4MeasureServiceUtilsToUse, + compositeEvaluationResultsPerMeasure, + context, bundle, measures, periodStart, @@ -115,12 +136,14 @@ public Bundle evaluate( evalType, subject, subjects, - parameters, - additionalData, productLine, reporter); } else { subjectMeasureReport( + r4ProcessorToUse, + r4MeasureServiceUtilsToUse, + compositeEvaluationResultsPerMeasure, + context, bundle, measures, periodStart, @@ -128,8 +151,6 @@ public Bundle evaluate( reportType, evalType, subjects, - parameters, - additionalData, productLine, reporter); } @@ -138,6 +159,10 @@ public Bundle evaluate( } protected void populationMeasureReport( + R4MeasureProcessor r4Processor, + R4MeasureServiceUtils r4MeasureServiceUtils, + CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure, + CqlEngine context, Bundle bundle, List measures, @Nullable ZonedDateTime periodStart, @@ -146,18 +171,22 @@ protected void populationMeasureReport( MeasureEvalType evalType, String subjectParam, List subjects, - Parameters parameters, - Bundle additionalData, String productLine, String reporter) { - // one aggregated MeasureReport per Measure var totalMeasures = measures.size(); for (Measure measure : measures) { MeasureReport measureReport; // evaluate each measure measureReport = r4Processor.evaluateMeasure( - measure, periodStart, periodEnd, reportType, subjects, additionalData, parameters, evalType); + measure, + periodStart, + periodEnd, + reportType, + subjects, + evalType, + context, + compositeEvaluationResultsPerMeasure); // add ProductLine after report is generated measureReport = r4MeasureServiceUtils.addProductLineExtension(measureReport, productLine); @@ -188,6 +217,10 @@ protected void populationMeasureReport( } protected void subjectMeasureReport( + R4MeasureProcessor r4Processor, + R4MeasureServiceUtils r4MeasureServiceUtils, + CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure, + CqlEngine context, Bundle bundle, List measures, @Nullable ZonedDateTime periodStart, @@ -195,8 +228,6 @@ protected void subjectMeasureReport( String reportType, MeasureEvalType evalType, List subjects, - Parameters parameters, - Bundle additionalData, String productLine, String reporter) { @@ -217,9 +248,9 @@ protected void subjectMeasureReport( periodEnd, reportType, Collections.singletonList(subject), - additionalData, - parameters, - evalType); + evalType, + context, + compositeEvaluationResultsPerMeasure); // add ProductLine after report is generated measureReport = r4MeasureServiceUtils.addProductLineExtension(measureReport, productLine); @@ -251,8 +282,7 @@ protected void subjectMeasureReport( } protected List getSubjects(R4RepositorySubjectProvider subjectProvider, String subjectId) { - - return subjectProvider.getSubjects(repository, subjectId).collect(Collectors.toList()); + return subjectProvider.getSubjects(repository, subjectId).toList(); } protected void initializeReport(MeasureReport measureReport) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java index cd9cab0496..fdaaae8e4c 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidator.java @@ -10,6 +10,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Enumeration; @@ -175,17 +176,26 @@ private List> extractClassesFromSingleOrListResult(Object result) { return Collections.emptyList(); } - if (!(result instanceof List list)) { + if (!(result instanceof Iterable iterable)) { return Collections.singletonList(result.getClass()); } // Need to this to return List> and get rid of Sonar warnings. final Stream> classStream = - list.stream().filter(Objects::nonNull).map(Object::getClass); + getStream(iterable).filter(Objects::nonNull).map(Object::getClass); return classStream.toList(); } + private Stream getStream(Iterable iterable) { + if (iterable instanceof List list) { + return list.stream(); + } + + // It's entirely possible CQL returns an Iterable that is not a List, so we need to handle that case + return StreamSupport.stream(iterable.spliterator(), false); + } + private List prettyClassNames(List> classes) { return classes.stream().map(Class::getSimpleName).toList(); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java index 463c6e8126..a00edf8452 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; @@ -33,10 +34,13 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import javax.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.ContactDetail; @@ -53,7 +57,10 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvalType; +import org.opencds.cqf.fhir.utility.Canonicals; import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.search.Searches; public class R4MeasureServiceUtils { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(R4MeasureServiceUtils.class); @@ -341,4 +348,40 @@ public MeasureEvalType convertToNonVersionedMeasureEvalTypeOrDefault(R4MeasureEv public boolean isSubjectListEffectivelyEmpty(List subjectIds) { return subjectIds == null || subjectIds.isEmpty() || subjectIds.get(0) == null; } + + public static List foldMeasures( + List> measures, IRepository repository) { + return measures.stream() + .map(measure -> foldMeasure(measure, repository)) + .toList(); + } + + public static Measure foldMeasure(Either3 measure, IRepository repository) { + return measure.fold( + measureCanonicalType -> resolveByUrl(measureCanonicalType, repository), + measureIdType -> resolveById(measureIdType, repository), + Function.identity()); + } + + private static Measure resolveByUrl(CanonicalType url, IRepository repository) { + var parts = Canonicals.getParts(url); + var result = repository.search( + Bundle.class, Measure.class, Searches.byNameAndVersion(parts.idPart(), parts.version())); + return (Measure) result.getEntryFirstRep().getResource(); + } + + public static List resolveByIds(List ids, IRepository repository) { + var idStringArray = ids.stream().map(IPrimitiveType::getValueAsString).toArray(String[]::new); + var searchParameters = Searches.byId(idStringArray); + + return repository.search(Bundle.class, Measure.class, searchParameters).getEntry().stream() + .map(BundleEntryComponent::getResource) + .filter(Measure.class::isInstance) + .map(Measure.class::cast) + .toList(); + } + + public static Measure resolveById(IIdType id, IRepository repository) { + return repository.read(Measure.class, id); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/cpg/r4/LibraryEvaluationServiceComplexDepsTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/cpg/r4/LibraryEvaluationServiceComplexDepsTest.java new file mode 100644 index 0000000000..8c9fc54f80 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/cpg/r4/LibraryEvaluationServiceComplexDepsTest.java @@ -0,0 +1,211 @@ +package org.opencds.cqf.fhir.cr.cpg.r4; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opencds.cqf.fhir.utility.r4.Parameters.parameters; +import static org.opencds.cqf.fhir.utility.r4.Parameters.stringPart; + +import java.util.Set; +import java.util.stream.Collectors; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("squid:S1135") +class LibraryEvaluationServiceComplexDepsTest { + + @Test + void singleLibraryTest_1A() { + + var params = parameters(stringPart("subject", "Patient/patient1")); + var libId = new IdType("Library", "Level1A"); + var when = Library.given() + .repositoryFor("libraryevalcomplexdeps") + .when() + .id(libId) + .parameters(params) + .evaluateLibrary(); + var report = when.then().parameters(); + + assertNotNull(report); + assertTrue(report.hasParameter("Initial Population")); + assertTrue(report.hasParameter("Numerator")); + assertTrue(report.hasParameter("Denominator")); + + var initialPopulations = report.getParameters("Initial Population"); + assertNotNull(initialPopulations); + assertFalse(initialPopulations.isEmpty()); + assertEquals(10, initialPopulations.size()); + + initialPopulations.forEach(component -> { + assertNotNull(component); + assertNull(component.getValue()); + assertEquals("Initial Population", component.getName()); + assertInstanceOf(Encounter.class, component.getResource()); + }); + + var expectedInitialPopulationEncounterIds = Set.of( + "enc_planned_cat1", + "enc_cancelled_cat3", + "enc_planned_cat2", + "enc_triaged_cat3", + "enc_planned_cat3", + "enc_arrived_cat1_v2", + "enc_arrived_cat3", + "enc_arrived_cat1_v1", + "enc_arrived_cat2", + "enc_finished_cat3"); + + assertEquals( + expectedInitialPopulationEncounterIds, + initialPopulations.stream() + .map(Parameters.ParametersParameterComponent::getResource) + .map(r -> r.getIdElement().getIdPart()) + .collect(Collectors.toUnmodifiableSet())); + + var denominators = report.getParameters("Denominator"); + assertNotNull(denominators); + assertFalse(denominators.isEmpty()); + assertEquals(7, denominators.size()); + + denominators.forEach(component -> { + assertNotNull(component); + assertNull(component.getValue()); + assertEquals("Denominator", component.getName()); + assertInstanceOf(Encounter.class, component.getResource()); + }); + + var expectedDenominatorEncounterIds = Set.of( + "enc_planned_cat1", + "enc_planned_cat2", + "enc_planned_cat3", + "enc_arrived_cat1_v2", + "enc_arrived_cat3", + "enc_arrived_cat1_v1", + "enc_arrived_cat2"); + + assertEquals( + expectedDenominatorEncounterIds, + denominators.stream() + .map(Parameters.ParametersParameterComponent::getResource) + .map(r -> r.getIdElement().getIdPart()) + .collect(Collectors.toUnmodifiableSet())); + + var numerators = report.getParameters("Numerator"); + assertNotNull(numerators); + assertFalse(numerators.isEmpty()); + assertEquals(3, numerators.size()); + + numerators.forEach(component -> { + assertNotNull(component); + assertNull(component.getValue()); + assertEquals("Numerator", component.getName()); + assertInstanceOf(Encounter.class, component.getResource()); + }); + + var expectedNumeratorEncounterIds = Set.of("enc_planned_cat1", "enc_planned_cat2", "enc_planned_cat3"); + + assertEquals( + expectedNumeratorEncounterIds, + numerators.stream() + .map(Parameters.ParametersParameterComponent::getResource) + .map(r -> r.getIdElement().getIdPart()) + .collect(Collectors.toUnmodifiableSet())); + } + + @Test + void singleLibraryTest_1B() { + var params = parameters(stringPart("subject", "Patient/patient1")); + var libId = new IdType("Library", "Level1B"); + var when = Library.given() + .repositoryFor("libraryevalcomplexdeps") + .when() + .id(libId) + .parameters(params) + .evaluateLibrary(); + var report = when.then().parameters(); + + assertNotNull(report); + assertTrue(report.hasParameter("Initial Population")); + assertTrue(report.hasParameter("Numerator")); + assertTrue(report.hasParameter("Denominator")); + + var initialPopulations = report.getParameters("Initial Population"); + assertNotNull(initialPopulations); + assertFalse(initialPopulations.isEmpty()); + assertEquals(10, initialPopulations.size()); + + initialPopulations.forEach(component -> { + assertNotNull(component); + assertNull(component.getValue()); + assertEquals("Initial Population", component.getName()); + assertInstanceOf(Encounter.class, component.getResource()); + }); + + var expectedInitialPopulationEncounterIds = Set.of( + "enc_planned_cat1", + "enc_cancelled_cat3", + "enc_planned_cat2", + "enc_triaged_cat3", + "enc_planned_cat3", + "enc_arrived_cat1_v2", + "enc_arrived_cat3", + "enc_arrived_cat1_v1", + "enc_arrived_cat2", + "enc_finished_cat3"); + + assertEquals( + expectedInitialPopulationEncounterIds, + initialPopulations.stream() + .map(Parameters.ParametersParameterComponent::getResource) + .map(r -> r.getIdElement().getIdPart()) + .collect(Collectors.toUnmodifiableSet())); + + var denominators = report.getParameters("Denominator"); + assertNotNull(denominators); + assertFalse(denominators.isEmpty()); + assertEquals(3, denominators.size()); + + denominators.forEach(component -> { + assertNotNull(component); + assertNull(component.getValue()); + assertEquals("Denominator", component.getName()); + assertInstanceOf(Encounter.class, component.getResource()); + }); + + var expectedDenominatorEncounterIds = Set.of("enc_planned_cat1", "enc_arrived_cat1_v2", "enc_arrived_cat1_v1"); + + assertEquals( + expectedDenominatorEncounterIds, + denominators.stream() + .map(Parameters.ParametersParameterComponent::getResource) + .map(r -> r.getIdElement().getIdPart()) + .collect(Collectors.toUnmodifiableSet())); + + var numerators = report.getParameters("Numerator"); + assertNotNull(numerators); + assertFalse(numerators.isEmpty()); + assertEquals(1, numerators.size()); + + numerators.forEach(component -> { + assertNotNull(component); + assertNull(component.getValue()); + assertEquals("Numerator", component.getName()); + assertInstanceOf(Encounter.class, component.getResource()); + }); + + var expectedNumeratorEncounterIds = Set.of("enc_planned_cat1"); + + assertEquals( + expectedNumeratorEncounterIds, + numerators.stream() + .map(Parameters.ParametersParameterComponent::getResource) + .map(r -> r.getIdElement().getIdPart()) + .collect(Collectors.toUnmodifiableSet())); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelperTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelperTest.java index 288a678430..017766d1ae 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelperTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/helper/DateHelperTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import jakarta.annotation.Nonnull; import java.time.LocalDateTime; import java.time.Month; import java.time.OffsetDateTime; @@ -10,7 +11,6 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.stream.Stream; -import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java index fc0fcb7503..960c2ac810 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java @@ -97,7 +97,7 @@ public Given repositoryFor(String repositoryPath) { } private R4CollectDataService buildR4CollectDataService() { - return new R4CollectDataService(repository, evaluationOptions, measureServiceUtils); + return new R4CollectDataService(repository, evaluationOptions); } public When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectDataTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectDataTest.java index aeab724192..cb45cdbbc2 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectDataTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectDataTest.java @@ -16,10 +16,10 @@ void collectData_booleanBasisMeasure_subject() { .subject("Patient/eNeMVHWfNoTsMTbrwWQQ30A3") .collectData() .then() - .hasParameterCount(16) // 3 more? + .hasParameterCount(13) .measureReport() .hasDataCollectionReportType() - .hasEvaluatedResourceCount(15) // 3 more? + .hasEvaluatedResourceCount(12) .up() .report(); } @@ -34,7 +34,7 @@ void collectData_booleanBasisMeasure_population() { .periodEnd("2022-06-29") .collectData() .then() - .hasParameterCount(16) + .hasParameterCount(13) .hasMeasureReportCount(1) .report(); } @@ -66,7 +66,7 @@ void collectData_resourceBasisMeasure_population() { .periodEnd("2020-01-01") .collectData() .then() - .hasParameterCount(18) + .hasParameterCount(17) .hasMeasureReportCount(4) .report(); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CqlExpressionDefCachingOptimizationTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CqlExpressionDefCachingOptimizationTest.java new file mode 100644 index 0000000000..e888b2eb5f --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CqlExpressionDefCachingOptimizationTest.java @@ -0,0 +1,60 @@ +package org.opencds.cqf.fhir.cr.measure.r4; + +import org.cqframework.cql.cql2elm.CqlCompilerOptions; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cql.CqlOptions; +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given; + +public class CqlExpressionDefCachingOptimizationTest { + private static final Given GIVEN_REPO = + MultiMeasure.given().repositoryFor("CqlOptimize").evaluationOptions(getEvaluationOptions()); + + @Test + void MultiMeasure_AllSubjects_MeasureIdentifier() { + var when = GIVEN_REPO + .when() + .measureId("MeasureA") + .measureId("MeasureB") + .reportType("population") + .evaluate(); + + when.then() + .hasMeasureReportCount(2) + .getFirstMeasureReport() + .hasMeasure("http://example.com/Measure/MeasureA") + .firstGroup() + .population("initial-population") + .hasCount(1) + .up() + .up() + .up() + .getSecondMeasureReport() + .hasMeasure("http://example.com/Measure/MeasureB") + .firstGroup() + .population("initial-population") + .hasCount(1); + } + + // The point of this chain of methods is to set HEDIS options and to disable ELM library compilation. + private static MeasureEvaluationOptions getEvaluationOptions() { + return MeasureEvaluationOptions.defaultOptions().setEvaluationSettings(getEvaluationSettings()); + } + + private static EvaluationSettings getEvaluationSettings() { + return EvaluationSettings.getDefault().withCqlOptions(getCqlOptions()); + } + + private static CqlOptions getCqlOptions() { + final CqlOptions cqlOptions = CqlOptions.defaultOptions(); + cqlOptions.setCqlCompilerOptions(getCqlCompilerOptions()); + return cqlOptions; + } + + private static CqlCompilerOptions getCqlCompilerOptions() { + final CqlCompilerOptions cqlCompilerOptions = new CqlCompilerOptions(); + cqlCompilerOptions.setEnableCqlOnly(true); + return cqlCompilerOptions; + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/InvalidMeasureTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/InvalidMeasureTest.java index 1aa2117a82..235a204172 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/InvalidMeasureTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/InvalidMeasureTest.java @@ -37,6 +37,6 @@ void evaluateThrowsErrorWhenLibraryIsMissingContent() { .measureId("LibraryMissingContent") .evaluate(); var e = assertThrows(IllegalStateException.class, when::then); - assertTrue(e.getMessage().contains("Unable to load CQL/ELM for library")); + assertTrue(e.getMessage().contains("Unable to load CQL/ELM for libraries")); } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java index 95a27ea108..ba056e380b 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java @@ -17,6 +17,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nullable; import java.nio.file.Path; import java.time.LocalDate; import java.time.ZoneId; @@ -30,7 +31,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; -import javax.annotation.Nullable; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeableConcept; @@ -76,7 +76,7 @@ interface Validator { } @FunctionalInterface - interface Selector { + public interface Selector { T select(S from); } @@ -167,7 +167,7 @@ public Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) { } private R4MeasureService buildMeasureService() { - return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator, measureServiceUtils); + return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator); } public When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java index f4b2fe3d24..842f1ea3c5 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureDefBuilderTest.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.Arrays; import java.util.List; @@ -21,7 +22,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; -import javax.annotation.Nonnull; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.CodeableConcept; diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluatedResourcesTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluatedResourcesTest.java index 1bb2226a88..0379f762ae 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluatedResourcesTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureEvaluatedResourcesTest.java @@ -59,11 +59,9 @@ void cohortBooleanSDE() { .hasNoDuplicateExtensions() .up() .evaluatedResource("Encounter/patient-9-encounter-1") - .referenceHasExtension("sde-patient-sex") .hasNoDuplicateExtensions() .up() .evaluatedResource("Encounter/patient-9-encounter-2") - .referenceHasExtension("sde-patient-sex") .hasNoDuplicateExtensions() .up() .report(); @@ -86,9 +84,6 @@ void ratioBooleanPopulationCheck() { .hasEvaluatedResourceCount(3) .evaluatedResourceHasNoDuplicateReferences() .evaluatedResource("Patient/patient-9") - .referenceHasExtension("initial-population") - .referenceHasExtension("denominator") - .referenceHasExtension("numerator") .hasNoDuplicateExtensions() .up() .evaluatedResource("Encounter/patient-9-encounter-1") diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureReportTypeIndividualTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureReportTypeIndividualTest.java index 96f4a5ddbb..e35099cb13 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureReportTypeIndividualTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MeasureReportTypeIndividualTest.java @@ -55,7 +55,7 @@ void proportionResourceWithReportTypeParameter() { .hasMeasureReportPeriod() .hasStatus(MeasureReportStatus.COMPLETE) .hasMeasureUrl("http://example.com/Measure/ProportionResourceAllPopulations") - .hasEvaluatedResourceCount(3) + .hasEvaluatedResourceCount(2) .firstGroup() .population("initial-population") .hasCount(2) @@ -94,7 +94,7 @@ void proportionResourceWithNoReportType() { .hasMeasureReportPeriod() .hasStatus(MeasureReportStatus.COMPLETE) .hasMeasureUrl("http://example.com/Measure/ProportionResourceAllPopulations") - .hasEvaluatedResourceCount(3) + .hasEvaluatedResourceCount(2) .firstGroup() .population("initial-population") .hasCount(2) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCqlTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCqlTest.java new file mode 100644 index 0000000000..a19ff607b3 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCqlTest.java @@ -0,0 +1,118 @@ +package org.opencds.cqf.fhir.cr.measure.r4; + +import org.cqframework.cql.cql2elm.CqlCompilerOptions; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.cql.engine.execution.CqlEngine; +import org.opencds.cqf.fhir.cql.CqlOptions; +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given; + +@SuppressWarnings({"java:S2699"}) +class MultiLibEvalComplexCqlTest { + private static final Given GIVEN_REPO = + MultiMeasure.given().repositoryFor("MultiLibEvalComplexCql").evaluationOptions(getEvaluationOptions()); + + @Test + void singleLibraryTest_1A() { + + GIVEN_REPO + .when() + .measureId("Level1A") + .reportType("subject") + .evaluate() + .then() + .hasMeasureReportCount(1) + .getFirstMeasureReport() + .firstGroup() + .population("initial-population") + .hasCount(10) + .up() + .population("denominator") + .hasCount(7) + .up() + .population("numerator") + .hasCount(3); + } + + @Test + void singleLibraryTest_1B() { + + GIVEN_REPO + .when() + .measureId("Level1B") + .reportType("subject") + .evaluate() + .then() + .hasMeasureReportCount(1) + .getFirstMeasureReport() + .firstGroup() + .population("initial-population") + .hasCount(10) + .up() + .population("denominator") + .hasCount(3) + .up() + .population("numerator") + .hasCount(1); + } + + @Test + void multipleLibraryTest_1A_and_1B() { + + GIVEN_REPO + .when() + .measureId("Level1A") + .measureId("Level1B") + .reportType("subject") + .evaluate() + .then() + .hasMeasureReportCount(2) + .getFirstMeasureReport() + .firstGroup() + .population("initial-population") + .hasCount(10) + .up() + .population("denominator") + .hasCount(7) + .up() + .population("numerator") + .hasCount(3) + .up() + .up() + .up() + .getSecondMeasureReport() + .firstGroup() + .population("initial-population") + .hasCount(10) + .up() + .population("denominator") + .hasCount(3) + .up() + .population("numerator") + .hasCount(1); + } + + private static MeasureEvaluationOptions getEvaluationOptions() { + return MeasureEvaluationOptions.defaultOptions() + .setApplyScoringSetMembership(false) + .setEvaluationSettings(getEvaluationSettings()); + } + + private static EvaluationSettings getEvaluationSettings() { + return EvaluationSettings.getDefault().withCqlOptions(getCqlOptions()); + } + + private static CqlOptions getCqlOptions() { + final CqlOptions cqlOptions = CqlOptions.defaultOptions(); + cqlOptions.getCqlEngineOptions().getOptions().add(CqlEngine.Options.EnableHedisCompatibilityMode); + cqlOptions.setCqlCompilerOptions(getCqlCompilerOptions()); + return cqlOptions; + } + + private static CqlCompilerOptions getCqlCompilerOptions() { + final CqlCompilerOptions cqlCompilerOptions = new CqlCompilerOptions(); + cqlCompilerOptions.setEnableCqlOnly(true); + return cqlCompilerOptions; + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java index 67c579a3ae..7bd4e2f45c 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.opencds.cqf.fhir.test.Resources.getResourcePath; import ca.uhn.fhir.context.FhirContext; @@ -50,7 +51,7 @@ class MultiMeasure { public static final String CLASS_PATH = "org/opencds/cqf/fhir/cr/measure/r4"; @FunctionalInterface - interface Validator { + public interface Validator { void validate(T value); } @@ -137,6 +138,16 @@ public MultiMeasure.Given serverBase(String serverBase) { return this; } + // Exposed for unit tests that only want to use part of this framework when passing in + // an IgRepository + public IRepository getRepository() { + if (this.repository == null) { + throw new IllegalStateException( + "Repository has not been set. Use 'repository' or 'repositoryFor' to set it."); + } + return this.repository; + } + private R4MultiMeasureService buildMeasureService() { return new R4MultiMeasureService(repository, evaluationOptions, serverBase, measurePeriodValidator); } @@ -309,6 +320,19 @@ public SelectedMeasureReport getFirstMeasureReport() { return this.measureReport(g -> mr); } + public SelectedMeasureReport getSecondMeasureReport() { + var entries = report().getEntry(); + if (entries.size() < 2) { + fail("There are not enough entries in the report to get the second one."); + } + var secondEntryResource = entries.get(1).getResource(); + if (!(secondEntryResource instanceof MeasureReport measureReport)) { + fail("The second entry in the report is not a MeasureReport resource."); + return null; + } + return this.measureReport(g -> measureReport); + } + public MeasureReport resourceToMeasureReport(List entries, String measureUrl) { IParser parser = FhirContext.forR4Cached().newJsonParser(); MeasureReport matchedReport = null; @@ -368,6 +392,11 @@ public SelectedMeasureReport measureReportTypeIndividual() { return this; } + public SelectedMeasureReport hasMeasure(String measureUrl) { + assertEquals(measureUrl, report().getMeasure()); + return this; + } + public SelectedReference reference( org.opencds.cqf.fhir.cr.measure.r4.Measure.Selector referenceSelector) { var r = referenceSelector.select(value()); @@ -480,7 +509,8 @@ public SelectedMeasureReport hasPeriodEnd(Date periodEnd) { } } - static class SelectedGroup extends MultiMeasure.Selected { + public static class SelectedGroup + extends MultiMeasure.Selected { public SelectedGroup(MeasureReportGroupComponent value, SelectedMeasureReport parent) { super(value, parent); @@ -512,7 +542,7 @@ public SelectedPopulation firstPopulation() { } public SelectedGroup hasStratifierCount(int count) { - assertEquals(this.value().getStratifier().size(), count); + assertEquals(count, this.value().getStratifier().size()); return this; } @@ -558,7 +588,7 @@ public SelectedReference

hasPopulations(String... population) { } } - static class SelectedPopulation + public static class SelectedPopulation extends MultiMeasure.Selected { public SelectedPopulation(MeasureReportGroupPopulationComponent value, SelectedGroup parent) { @@ -582,7 +612,7 @@ public SelectedPopulation passes( } } - static class SelectedStratifier + public static class SelectedStratifier extends MultiMeasure.Selected { public SelectedStratifier(MeasureReportGroupStratifierComponent value, SelectedGroup parent) { @@ -616,7 +646,7 @@ public SelectedStratum stratum( } } - static class SelectedStratum extends MultiMeasure.Selected { + public static class SelectedStratum extends MultiMeasure.Selected { public SelectedStratum(MeasureReport.StratifierGroupComponent value, SelectedStratifier parent) { super(value, parent); @@ -627,6 +657,11 @@ public SelectedStratum hasScore(String score) { return this; } + public SelectedStratum hasValue(String text) { + assertEquals(text, value().getValue().getText()); + return this; + } + public SelectedStratumPopulation firstPopulation() { return population(MeasureReport.StratifierGroupComponent::getPopulationFirstRep); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasureServiceTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasureServiceTest.java index 775c5a994d..99ac6052fb 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasureServiceTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasureServiceTest.java @@ -559,7 +559,7 @@ void MultiMeasure_EightMeasures_Patient() { .measureReport("http://example.com/Measure/MinimalCohortResourceBasisSingleGroup") .hasReportType("Individual") .hasSubjectReference("Patient/female-1988") - .hasEvaluatedResourceCount(3) + .hasEvaluatedResourceCount(2) .firstGroup() .population("initial-population") .hasCount(2) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java new file mode 100644 index 0000000000..6fb6d298e4 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java @@ -0,0 +1,60 @@ +package org.opencds.cqf.fhir.cr.measure.r4; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; +import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; +import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given; + +class R4MeasureProcessorTest { + private static final Given GIVEN_REPO = MultiMeasure.given().repositoryFor("MinimalMeasureEvaluation"); + private static final IdType MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP = + new IdType(ResourceType.Measure.name(), "MinimalCohortBooleanBasisSingleGroup"); + private static final String SUBJECT_ID = "Patient/female-1914"; + + // This test could probably be improved with better data and more assertions, but it's to + // confirm that a method exposed for downstream works with reasonable sanity. + @Test + void evaluateMultiMeasureIdsWithCqlEngine() { + var repository = GIVEN_REPO.getRepository(); + var r4MeasureProcessor = new R4MeasureProcessor( + repository, MeasureEvaluationOptions.defaultOptions(), new MeasureProcessorUtils()); + + var cqlEngine = Engines.forRepository(repository); + + var results = r4MeasureProcessor.evaluateMultiMeasureIdsWithCqlEngine( + List.of(SUBJECT_ID), + List.of(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP), + null, + null, + new Parameters(), + cqlEngine); + + assertNotNull(results); + var measureDef = new MeasureDef("", "", "", List.of(), List.of()); + var evaluationResults = + results.processMeasureForSuccessOrFailure(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP, measureDef); + + assertNotNull(evaluationResults); + + var evaluationResult = evaluationResults.get(SUBJECT_ID); + assertNotNull(evaluationResult); + + var expressionResults = evaluationResult.expressionResults; + assertNotNull(expressionResults); + + var expressionResult = expressionResults.get("Initial Population"); + assertNotNull(expressionResult); + + var evaluatedResources = expressionResult.evaluatedResources(); + assertEquals(1, evaluatedResources.size()); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java index e3c29a38cc..d39b269699 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureReportBuilderTest.java @@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.fail; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; @@ -16,8 +18,6 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.Measure.MeasureGroupComponent; diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java index 20114495b3..b1f1652d8e 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4PopulationBasisValidatorTest.java @@ -7,12 +7,12 @@ import static org.opencds.cqf.fhir.cr.measure.common.MeasurePopulationType.NUMERATOR; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nonnull; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Stream; -import javax.annotation.Nonnull; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Encounter; diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level1A.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level1A.cql new file mode 100644 index 0000000000..b0417b9dc3 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level1A.cql @@ -0,0 +1,25 @@ +library Level1A + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +// Level 3A > { 'arrived', 'in-progress', 'planned', 'triaged' } +// Level 2 > { 'arrived', 'in-progress', 'planned' } +include Level2 called Level2 + +parameter "Measurement Period" Interval default Interval[@2021-01-01T00:00:00.000, @2022-01-01T00:00:00.000) + +context Patient + +define "Initial Population": + [Encounter] E + +define "Denominator": + Level2."Encounters" E + where E.status.value in { 'arrived', 'cancelled', 'in-progress', 'planned' } + +define "Numerator": + Level2."Encounters" E + where E.status.value in { 'cancelled', 'finished', 'in-progress', 'planned' } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level1B.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level1B.cql new file mode 100644 index 0000000000..0407621cb6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level1B.cql @@ -0,0 +1,23 @@ +library Level1B + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +include Level2 called Level2 +include Level3B called Level3B + +parameter "Measurement Period" Interval default Interval[@2021-07-01T00:00:00.000, @2022-07-01T00:00:00.000) + +define "Initial Population": + [Encounter] + +define "Denominator": + Level2."Encounters" E + where E.status.value in { 'arrived', 'cancelled', 'in-progress', 'planned' } and + E.meta.profile[0] in Level3B."Encounter Profiles" + +define "Numerator": + Level2."Encounters" E + where E.status.value in { 'finished', 'cancelled', 'in-progress', 'planned' } and + E.meta.profile[0] in Level3B."Encounter Profiles" diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level2.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level2.cql new file mode 100644 index 0000000000..1f463516be --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level2.cql @@ -0,0 +1,14 @@ +library Level2 + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +// Level 3A > { 'arrived', 'in-progress', 'planned', 'triaged' } +include Level3A called Level3A + +// Level 2 > { 'arrived', 'in-progress', 'planned' } +define "Encounters": + Level3A."Encounters" E + where not(E.status.value in { 'triaged' }) \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level3A.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level3A.cql new file mode 100644 index 0000000000..6d29c55108 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level3A.cql @@ -0,0 +1,13 @@ +library Level3A + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +include Level5 called Level5 + +// Level 3A > { 'arrived', 'in-progress', 'planned', 'triaged' } +define "Encounters": + Level5."Encounters" E + where not(E.status.value in { 'finished' }) diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level3B.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level3B.cql new file mode 100644 index 0000000000..aa0737aba6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level3B.cql @@ -0,0 +1,11 @@ +library Level3B + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +include Level4 called Level4 + +define "Encounter Profiles": + Level4."Encounter Profiles" P + where not (P in { 'http://example.com/category2' }) diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level4.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level4.cql new file mode 100644 index 0000000000..c581fcc63a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level4.cql @@ -0,0 +1,12 @@ +library Level4 + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +include Level5 called Level5 + +define "Encounter Profiles": + Level5."Encounters" E + where not (E.meta.profile[0] in { 'http://example.com/category3' }) + return E.meta.profile[0] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level5.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level5.cql new file mode 100644 index 0000000000..7a8292979f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/cql/Level5.cql @@ -0,0 +1,16 @@ +library Level5 + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +context Patient + +//// Level 5 > { 'cat1', cat2', 'cat3' } +//define "Encounters": +// [Encounter] E +// where E.meta.profile[0] in { 'http://example.com/category1', 'http://example.com/category2', 'http://example.com/category3' } +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +define "Encounters": + [Encounter] E + where E.status.value in { 'arrived', 'planned', 'triaged', 'finished', 'in-progress' } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level1A.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level1A.json new file mode 100644 index 0000000000..0d0a36c46e --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level1A.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level1A", + "url": "http://example.com/Library/Level1A", + "name": "Level1A", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level1A.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level1B.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level1B.json new file mode 100644 index 0000000000..95896df10c --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level1B.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level1B", + "url": "http://example.com/Library/Level1B", + "name": "Level1B", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level1B.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level2.json new file mode 100644 index 0000000000..0ddf2d4277 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level2", + "url": "http://example.com/Library/Level2", + "name": "Level2", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level2.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level3A.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level3A.json new file mode 100644 index 0000000000..6b07d64303 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level3A.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level3A", + "url": "http://example.com/Library/Level3A", + "name": "Level3A", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level3A.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level3B.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level3B.json new file mode 100644 index 0000000000..0d56d7abee --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level3B.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level3B", + "url": "http://example.com/Library/Level3B", + "name": "Level3B", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level3B.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level4.json new file mode 100644 index 0000000000..66aa069b51 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level4.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level4", + "url": "http://example.com/Library/Level4", + "name": "Level4", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level4.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level5.json new file mode 100644 index 0000000000..f55154f74b --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/resources/library/Level5.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level5", + "url": "http://example.com/Library/Level5", + "name": "Level5", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level5.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat1_v1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat1_v1.json new file mode 100644 index 0000000000..88fb8c559d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat1_v1.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat1_v1", + "meta": { + "profile": [ + "http://example.com/category1" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat1_v2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat1_v2.json new file mode 100644 index 0000000000..ed8dbc3ffc --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat1_v2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat1_v2", + "meta": { + "profile": [ + "http://example.com/category1" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat2.json new file mode 100644 index 0000000000..8a84193482 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat2", + "meta": { + "profile": [ + "http://example.com/category2" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat3.json new file mode 100644 index 0000000000..4bb869bfe9 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_arrived_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_cancelled_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_cancelled_cat3.json new file mode 100644 index 0000000000..a4d312f17f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_cancelled_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_cancelled_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "cancelled", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_finished_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_finished_cat3.json new file mode 100644 index 0000000000..e13acc6d31 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_finished_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_finished_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "finished", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat1.json new file mode 100644 index 0000000000..a4bf19ee8f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat1.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_cat1", + "meta": { + "profile": [ + "http://example.com/category1" + ] + }, + "status": "planned", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat2.json new file mode 100644 index 0000000000..69e8a2ee74 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_cat2", + "meta": { + "profile": [ + "http://example.com/category2" + ] + }, + "status": "planned", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat3.json new file mode 100644 index 0000000000..6e0fdf3ee3 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_planned_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "planned", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_triaged_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_triaged_cat3.json new file mode 100644 index 0000000000..bc4d8cbf34 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/encounter/enc_triaged_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_triaged_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "triaged", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/patient/patient1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/patient/patient1.json new file mode 100644 index 0000000000..179f54afb8 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/cpg/r4/libraryevalcomplexdeps/input/tests/patient/patient1.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient1", + "gender": "female", + "birthDate": "1904-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryA.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryA.cql new file mode 100644 index 0000000000..f98d0c84c7 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryA.cql @@ -0,0 +1,11 @@ +library libraryA + +using FHIR version '4.0.1' + +include libraryCommon called lc + +context Patient + +define "Initial Population": + exists (lc."All Encounters") + diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryB.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryB.cql new file mode 100644 index 0000000000..aca7e785fa --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryB.cql @@ -0,0 +1,13 @@ +library libraryB + +using FHIR version '4.0.1' + +include libraryCommon called lc + +context Patient + +define "Initial Population": + exists ( + lc."finished encounters" + ) + diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryCommon.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryCommon.cql new file mode 100644 index 0000000000..421286d2b2 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/cql/libraryCommon.cql @@ -0,0 +1,14 @@ +library libraryCommon + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' called FHIRHelpers + +context Patient + +define "All Encounters": + ["Encounter"] + +define "finished encounters": + ("All Encounters") e + where e.status = 'finished' \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryA.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryA.json new file mode 100644 index 0000000000..060e640bda --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryA.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "libraryA", + "url": "http://example.com/Library/libraryA", + "name": "libraryA", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/libraryA.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryB.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryB.json new file mode 100644 index 0000000000..1edd58913f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryB.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "libraryB", + "url": "http://example.com/Library/libraryB", + "name": "libraryB", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/libraryB.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryCommon.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryCommon.json new file mode 100644 index 0000000000..bfe9797749 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/library/libraryCommon.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "libraryCommon", + "url": "http://example.com/Library/libraryCommon", + "name": "libraryCommon", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/libraryCommon.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/measure/MeasureA.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/measure/MeasureA.json new file mode 100644 index 0000000000..a88ff8bf9b --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/measure/MeasureA.json @@ -0,0 +1,43 @@ + +{ + "id": "MeasureA", + "resourceType": "Measure", + "url": "http://example.com/Measure/MeasureA", + "library": [ + "http://example.com/Library/libraryA" + ], + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/measure/MeasureB.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/measure/MeasureB.json new file mode 100644 index 0000000000..05964b1665 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/resources/measure/MeasureB.json @@ -0,0 +1,43 @@ + +{ + "id": "MeasureB", + "resourceType": "Measure", + "url": "http://example.com/Measure/MeasureB", + "library": [ + "http://example.com/Library/libraryB" + ], + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "boolean" + } ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "cohort" + } + ] + }, + "group": [ + { + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-1.json new file mode 100644 index 0000000000..854fe4a7f8 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-1988-encounter-1", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-1988" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-2.json new file mode 100644 index 0000000000..8e003b9cd0 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-2.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-1988-encounter-2", + "status": "cancelled", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-1988" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-3.json new file mode 100644 index 0000000000..4353f33b01 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-3.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-1988-encounter-3", + "status": "cancelled", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-1988" + }, + "period": { + "start": "2025-01-16T08:30:00-07:00", + "end": "2025-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-4.json new file mode 100644 index 0000000000..2892c8bd52 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/encounter/male-1988-encounter-4.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-1988-encounter-4", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-1988" + }, + "period": { + "start": "2025-01-16T08:30:00-07:00", + "end": "2025-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/patient/male-1988.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/patient/male-1988.json new file mode 100644 index 0000000000..eb88176d97 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/CqlOptimize/input/tests/patient/male-1988.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "male-1988", + "gender": "male", + "birthDate": "1988-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level1A.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level1A.cql new file mode 100644 index 0000000000..5b01e7616c --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level1A.cql @@ -0,0 +1,25 @@ +library Level1A + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +// Level 3A > { 'arrived', 'in-progress', 'planned', 'triaged' } +// Level 2 > { 'arrived', 'in-progress', 'planned' } +include Level2 called Level2 + +parameter "Measurement Period" Interval default Interval[@2021-01-01T00:00:00.000, @2022-01-01T00:00:00.000) + +context Patient + +define "Initial Population": + [Encounter] + +define "Denominator": + Level2."Encounters" E + where E.status.value in { 'arrived', 'cancelled', 'in-progress', 'planned' } + +define "Numerator": + Level2."Encounters" E + where E.status.value in { 'cancelled', 'finished', 'in-progress', 'planned' } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level1B.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level1B.cql new file mode 100644 index 0000000000..0407621cb6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level1B.cql @@ -0,0 +1,23 @@ +library Level1B + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +include Level2 called Level2 +include Level3B called Level3B + +parameter "Measurement Period" Interval default Interval[@2021-07-01T00:00:00.000, @2022-07-01T00:00:00.000) + +define "Initial Population": + [Encounter] + +define "Denominator": + Level2."Encounters" E + where E.status.value in { 'arrived', 'cancelled', 'in-progress', 'planned' } and + E.meta.profile[0] in Level3B."Encounter Profiles" + +define "Numerator": + Level2."Encounters" E + where E.status.value in { 'finished', 'cancelled', 'in-progress', 'planned' } and + E.meta.profile[0] in Level3B."Encounter Profiles" diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level2.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level2.cql new file mode 100644 index 0000000000..1f463516be --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level2.cql @@ -0,0 +1,14 @@ +library Level2 + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +// Level 3A > { 'arrived', 'in-progress', 'planned', 'triaged' } +include Level3A called Level3A + +// Level 2 > { 'arrived', 'in-progress', 'planned' } +define "Encounters": + Level3A."Encounters" E + where not(E.status.value in { 'triaged' }) \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level3A.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level3A.cql new file mode 100644 index 0000000000..6d29c55108 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level3A.cql @@ -0,0 +1,13 @@ +library Level3A + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +include Level5 called Level5 + +// Level 3A > { 'arrived', 'in-progress', 'planned', 'triaged' } +define "Encounters": + Level5."Encounters" E + where not(E.status.value in { 'finished' }) diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level3B.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level3B.cql new file mode 100644 index 0000000000..aa0737aba6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level3B.cql @@ -0,0 +1,11 @@ +library Level3B + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +include Level4 called Level4 + +define "Encounter Profiles": + Level4."Encounter Profiles" P + where not (P in { 'http://example.com/category2' }) diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level4.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level4.cql new file mode 100644 index 0000000000..c581fcc63a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level4.cql @@ -0,0 +1,12 @@ +library Level4 + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +include Level5 called Level5 + +define "Encounter Profiles": + Level5."Encounters" E + where not (E.meta.profile[0] in { 'http://example.com/category3' }) + return E.meta.profile[0] diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level5.cql b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level5.cql new file mode 100644 index 0000000000..7a8292979f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/cql/Level5.cql @@ -0,0 +1,16 @@ +library Level5 + +using FHIR version '4.0.1' + +include FHIRHelpers version '4.0.1' + +context Patient + +//// Level 5 > { 'cat1', cat2', 'cat3' } +//define "Encounters": +// [Encounter] E +// where E.meta.profile[0] in { 'http://example.com/category1', 'http://example.com/category2', 'http://example.com/category3' } +// Level 5 > { 'arrived', 'finished', 'in-progress', 'planned', 'triaged' } +define "Encounters": + [Encounter] E + where E.status.value in { 'arrived', 'planned', 'triaged', 'finished', 'in-progress' } diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level1A.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level1A.json new file mode 100644 index 0000000000..0d0a36c46e --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level1A.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level1A", + "url": "http://example.com/Library/Level1A", + "name": "Level1A", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level1A.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level1B.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level1B.json new file mode 100644 index 0000000000..95896df10c --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level1B.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level1B", + "url": "http://example.com/Library/Level1B", + "name": "Level1B", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level1B.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level2.json new file mode 100644 index 0000000000..0ddf2d4277 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level2", + "url": "http://example.com/Library/Level2", + "name": "Level2", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level2.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level3A.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level3A.json new file mode 100644 index 0000000000..6b07d64303 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level3A.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level3A", + "url": "http://example.com/Library/Level3A", + "name": "Level3A", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level3A.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level3B.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level3B.json new file mode 100644 index 0000000000..0d56d7abee --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level3B.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level3B", + "url": "http://example.com/Library/Level3B", + "name": "Level3B", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level3B.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level4.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level4.json new file mode 100644 index 0000000000..66aa069b51 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level4.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level4", + "url": "http://example.com/Library/Level4", + "name": "Level4", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level4.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level5.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level5.json new file mode 100644 index 0000000000..f55154f74b --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/library/Level5.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Library", + "id": "Level5", + "url": "http://example.com/Library/Level5", + "name": "Level5", + "status": "active", + "type": { + "coding": [ { + "system": "http://terminology.hl7.org/CodeSystem/library-type", + "code": "logic-library" + } ] + }, + "content": [ { + "contentType": "text/cql", + "url": "../../cql/Level5.cql" + } ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/measure/Level1A.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/measure/Level1A.json new file mode 100644 index 0000000000..bd9a38edee --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/measure/Level1A.json @@ -0,0 +1,80 @@ + +{ + "id": "Level1A", + "resourceType": "Measure", + "name": "Level1A", + "url": "http://example.com/Measure/Level1A", + "library": [ + "http://example.com/Library/Level1A" + ], + "identifier": [ { + "use": "official", + "value": "124" + } ], + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "proportion" + } + ] + }, + "group": [ + { + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population" + } + }, + { + "id": "numerator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator", + "display": "Numerator" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Numerator" + } + }, + { + "id": "denominator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Denominator" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/measure/Level1B.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/measure/Level1B.json new file mode 100644 index 0000000000..26c014417a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/resources/measure/Level1B.json @@ -0,0 +1,80 @@ + +{ + "id": "Level1B", + "resourceType": "Measure", + "name": "Level1B", + "url": "http://example.com/Measure/Level1B", + "library": [ + "http://example.com/Library/Level1B" + ], + "identifier": [ { + "use": "official", + "value": "124" + } ], + "extension": [ { + "url": "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-populationBasis", + "valueCode": "Encounter" + } ], + "scoring": { + "coding": [ + { + "system": "http://hl7.org/fhir/measure-scoring", + "code": "proportion" + } + ] + }, + "group": [ + { + "population": [ + { + "id": "initial-population", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "initial-population", + "display": "Initial Population" + } + ] + }, + "criteria": { + "language": "text/cql-identifier", + "expression": "Initial Population" + } + }, + { + "id": "numerator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "numerator", + "display": "Numerator" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Numerator" + } + }, + { + "id": "denominator", + "code": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/measure-population", + "code": "denominator", + "display": "Denominator" + } + ] + }, + "criteria": { + "language": "text/cql", + "expression": "Denominator" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat1_v1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat1_v1.json new file mode 100644 index 0000000000..88fb8c559d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat1_v1.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat1_v1", + "meta": { + "profile": [ + "http://example.com/category1" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat1_v2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat1_v2.json new file mode 100644 index 0000000000..ed8dbc3ffc --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat1_v2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat1_v2", + "meta": { + "profile": [ + "http://example.com/category1" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat2.json new file mode 100644 index 0000000000..8a84193482 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat2", + "meta": { + "profile": [ + "http://example.com/category2" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat3.json new file mode 100644 index 0000000000..4bb869bfe9 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_arrived_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_arrived_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "arrived", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2000-01-01T00:00:00-05:00", + "end": "2000-12-31T00:00:00-05:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_cancelled_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_cancelled_cat3.json new file mode 100644 index 0000000000..a4d312f17f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_cancelled_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_cancelled_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "cancelled", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_finished_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_finished_cat3.json new file mode 100644 index 0000000000..e13acc6d31 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_finished_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_finished_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "finished", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat1.json new file mode 100644 index 0000000000..a4bf19ee8f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat1.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_cat1", + "meta": { + "profile": [ + "http://example.com/category1" + ] + }, + "status": "planned", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat2.json new file mode 100644 index 0000000000..69e8a2ee74 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat2.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_cat2", + "meta": { + "profile": [ + "http://example.com/category2" + ] + }, + "status": "planned", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat3.json new file mode 100644 index 0000000000..6e0fdf3ee3 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_planned_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_planned_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "planned", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_triaged_cat3.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_triaged_cat3.json new file mode 100644 index 0000000000..bc4d8cbf34 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/encounter/enc_triaged_cat3.json @@ -0,0 +1,17 @@ +{ + "resourceType": "Encounter", + "id": "enc_triaged_cat3", + "meta": { + "profile": [ + "http://example.com/category3" + ] + }, + "status": "triaged", + "subject": { + "reference": "Patient/patient1" + }, + "period": { + "start": "2001-01-01T00:00:00-05:00", + "end": "2001-12-31T00:00:00-05:00" + } +} diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/patient/patient1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/patient/patient1.json new file mode 100644 index 0000000000..179f54afb8 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/MultiLibEvalComplexCql/input/tests/patient/patient1.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "patient1", + "gender": "female", + "birthDate": "1904-01-01" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 81b8dd5de3..5a9e90e501 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 5.10.2 2.0.4 - 3.27.0 + 3.28.0 1.36 6.1.14 8.2.0