From db631e636f5abd2b12155939b390e70d910c9cf6 Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Thu, 26 Mar 2026 00:41:09 +1300 Subject: [PATCH 01/21] WIP --- .../engine-fhir/config/detekt-baseline.xml | 79 +--- .../fhir/model/Dstu2FhirModelResolver.kt | 210 --------- .../fhir/model/Dstu3FhirModelResolver.kt | 211 --------- .../engine/fhir/model/FhirModelResolver.kt | 333 ++++++-------- .../engine/fhir/model/R4FhirModelResolver.kt | 249 ---------- .../engine/fhir/model/R5FhirModelResolver.kt | 247 ---------- .../fhir/retrieve/RestFhirRetrieveProvider.kt | 4 +- .../SearchParamFhirRetrieveProvider.kt | 6 +- .../hl7/fhirpath/CQLOperationsDstu3Test.kt | 106 +++-- .../org/hl7/fhirpath/CQLOperationsR4Test.kt | 109 ++--- .../kotlin/org/hl7/fhirpath/TestFhirPath.kt | 9 +- .../fhir/data/EvaluatedResourceTestUtils.kt | 56 ++- ...rcesMultiLibComplexDepsRetrieveProvider.kt | 15 +- ...aluatedResourcesMultiLibComplexDepsTest.kt | 8 +- ...valuatedResourcesMultiLibLinearDepsTest.kt | 2 +- .../fhir/data/EvaluatedResourcesTest.kt | 5 +- .../cqf/cql/engine/fhir/data/Issue1225.kt | 2 +- .../cqf/cql/engine/fhir/data/Issue1226.kt | 9 +- .../cqf/cql/engine/fhir/data/Issue1558.kt | 4 +- .../cqf/cql/engine/fhir/data/Issue1577.kt | 16 +- .../fhir/data/IssueSortByFluentFunction.kt | 15 +- .../TestCqlEngineRelatedContextSupport.kt | 223 +++++---- .../engine/fhir/data/TestPrimitiveProfiles.kt | 101 +--- .../fhir/model/TestDstu2ModelResolver.kt | 21 +- .../fhir/model/TestDstu3ModelResolver.kt | 55 +-- .../engine/fhir/model/TestR4ModelResolver.kt | 109 +++-- .../engine/fhir/model/TestR5ModelResolver.kt | 92 ++-- Src/java/engine/detekt-baseline.xml | 49 +- .../cql/engine/data/CompositeDataProvider.kt | 47 +- .../cqf/cql/engine/data/SystemDataProvider.kt | 215 +-------- .../cql/engine/elm/executing/AsEvaluator.kt | 14 +- .../engine/elm/executing/ConvertEvaluator.kt | 37 +- .../elm/executing/DescendentsEvaluator.kt | 3 +- .../engine/elm/executing/EqualEvaluator.kt | 19 +- .../elm/executing/EquivalentEvaluator.kt | 19 +- .../elm/executing/FunctionRefEvaluator.kt | 13 +- .../cql/engine/elm/executing/IsEvaluator.kt | 13 +- .../engine/elm/executing/MessageEvaluator.kt | 20 +- .../engine/elm/executing/RetrieveEvaluator.kt | 14 +- .../cqf/cql/engine/execution/Environment.kt | 432 +++++++++++------- .../cqf/cql/engine/model/BaseModelResolver.kt | 33 -- .../model/CachingModelResolverDecorator.kt | 114 +---- .../cqf/cql/engine/model/ModelResolver.kt | 104 +---- .../cql/engine/runtime/CqlClassInstance.kt | 6 + .../cqf/cql/engine/runtime/CqlValue.kt | 35 ++ .../cql/engine/data/SystemDataProviderTest.kt | 12 - .../elm/executing/FunctionRefEvaluatorTest.kt | 12 +- .../CqlErrorsAndMessagingOperatorsTest.kt | 6 +- .../CqlListDistinguishedOverloadsTest.kt | 13 +- .../CachingModelResolverDecoratorTest.kt | 21 - 50 files changed, 1053 insertions(+), 2494 deletions(-) delete mode 100644 Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/BaseModelResolver.kt create mode 100644 Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlClassInstance.kt create mode 100644 Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt diff --git a/Src/java/engine-fhir/config/detekt-baseline.xml b/Src/java/engine-fhir/config/detekt-baseline.xml index 5a5fbde19..2e45e6c8a 100644 --- a/Src/java/engine-fhir/config/detekt-baseline.xml +++ b/Src/java/engine-fhir/config/detekt-baseline.xml @@ -3,45 +3,25 @@ ComplexCondition:BaseFhirQueryGenerator.kt$BaseFhirQueryGenerator$context != null && context == "Patient" && contextValue != null && contextPath != null - ComplexCondition:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.Companion.<no name provided>$PATIENT == dataType && PATIENT == context && ID == contextPath && _PATIENT_123 == contextValue - ComplexCondition:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.Companion.<no name provided>$PATIENT == dataType && PRACTITIONER == context && GENERAL_PRACTITIONER == contextPath && equals - ComplexCondition:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.Companion.<no name provided>$PRACTITIONER == dataType && PATIENT == context && ID == codePath && codesEqual(codes, PRACTITIONER_SLASH + XYZ) + ComplexCondition:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.<no name provided>$PATIENT == dataType && PATIENT == context && ID == contextPath && _PATIENT_123 == contextValue + ComplexCondition:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.<no name provided>$PATIENT == dataType && PRACTITIONER == context && GENERAL_PRACTITIONER == contextPath && equals + ComplexCondition:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.<no name provided>$PRACTITIONER == dataType && PATIENT == context && ID == codePath && codesEqual(codes, PRACTITIONER_SLASH + XYZ) CyclomaticComplexMethod:BaseFhirQueryGenerator.kt$BaseFhirQueryGenerator$protected fun getCodeParams( codes: Iterable<Code?>?, valueSet: String?, ): MutableList<TokenOrListParam> CyclomaticComplexMethod:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$override fun toCqlType(value: Any?): Any? CyclomaticComplexMethod:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$override fun toFhirType(value: Any?): IBase? - CyclomaticComplexMethod:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - CyclomaticComplexMethod:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? - CyclomaticComplexMethod:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - CyclomaticComplexMethod:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? CyclomaticComplexMethod:Dstu3FhirQueryGenerator.kt$Dstu3FhirQueryGenerator$override fun generateFhirQueries( dataRequirement: ICompositeType?, evaluationDateTime: DateTime?, contextValues: MutableMap<String, Any?>?, parameters: MutableMap<String, Any?>?, capabilityStatement: IBaseConformance?, ): MutableList<String> - CyclomaticComplexMethod:FhirModelResolver.kt$FhirModelResolver$override fun resolveType(typeName: String?): Class<*>? - CyclomaticComplexMethod:FhirModelResolver.kt$FhirModelResolver$protected open fun resolveProperty(target: Any?, path: String): Any? - CyclomaticComplexMethod:R4FhirModelResolver.kt$R4FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - CyclomaticComplexMethod:R4FhirModelResolver.kt$R4FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? + CyclomaticComplexMethod:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): Any? + CyclomaticComplexMethod:FhirModelResolver.kt$FhirModelResolver$open fun resolveType(typeName: String?): Class<*>? CyclomaticComplexMethod:R4FhirModelResolver.kt$R4FhirModelResolver$override fun resolveType(typeName: String?): Class<*>? CyclomaticComplexMethod:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$override fun generateFhirQueries( dataRequirement: ICompositeType?, evaluationDateTime: DateTime?, contextValues: MutableMap<String, Any?>?, parameters: MutableMap<String, Any?>?, capabilityStatement: IBaseConformance?, ): MutableList<String> - CyclomaticComplexMethod:R5FhirModelResolver.kt$R5FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - CyclomaticComplexMethod:R5FhirModelResolver.kt$R5FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? CyclomaticComplexMethod:R5FhirModelResolver.kt$R5FhirModelResolver$override fun resolveType(typeName: String?): Class<*>? CyclomaticComplexMethod:SearchParameterMap.kt$SearchParameterMap$fun toNormalizedQueryString(theCtx: FhirContext): String CyclomaticComplexMethod:TestFhirPath.kt$TestFhirPath$protected fun runTest( test: Test, basePathInput: String?, fhirContext: FhirContext, provider: CompositeDataProvider?, resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, ) CyclomaticComplexMethod:TestR4ModelResolver.kt$TestR4ModelResolver$@Test @Throws(Exception::class) fun modelInfo401Tests() ForbiddenComment:BaseFhirQueryGenerator.kt$BaseFhirQueryGenerator$// TODO: Think about how to best handle the decision to expand value sets... Should it be part ForbiddenComment:BaseFhirQueryGenerator.kt$BaseFhirQueryGenerator$// TODO: This assumes the code path will always be a token param. - ForbiddenComment:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$// TODO: Ensure age constraints are met, else return null (Except that we can't - ForbiddenComment:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$// TODO: Ensure count constraints are met, else return null - ForbiddenComment:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$// TODO: Ensure distance constraints are met, else return null - ForbiddenComment:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$// TODO: Ensure duration constraints are met, else return null - ForbiddenComment:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$// TODO: These should not return true unless the constraints that are used in the as logic - ForbiddenComment:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$// TODO: These should really be using profile validation ForbiddenComment:Dstu2FhirTypeConverter.kt$Dstu2FhirTypeConverter$// TODO: This will construct DateTimeType values in FHIR with the system timezone id, - ForbiddenComment:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$// TODO: Ensure age constraints are met, else return null (Except that we can't - ForbiddenComment:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$// TODO: Ensure count constraints are met, else return null - ForbiddenComment:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$// TODO: Ensure distance constraints are met, else return null - ForbiddenComment:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$// TODO: Ensure duration constraints are met, else return null ForbiddenComment:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$// TODO: Might be able to patch some of these by registering custom types in HAPI. - ForbiddenComment:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$// TODO: These should not return true unless the constraints that are used in the as logic - ForbiddenComment:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$// TODO: These should really be using profile validation ForbiddenComment:Dstu3FhirQueryGenerator.kt$Dstu3FhirQueryGenerator$// TODO: Deal with the case that the value is expressed as an expression extension ForbiddenComment:Dstu3FhirQueryGenerator.kt$Dstu3FhirQueryGenerator$// TODO: What to do if/when System is not provided... ForbiddenComment:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$// TODO: This will construct DateTimeType values in FHIR with the system timezone id, @@ -50,26 +30,11 @@ ForbiddenComment:FhirHelpersDstu3Test.kt$FhirHelpersDstu3Test$// TODO: ModelInfo bug. Not aware of SimpleQuantity ForbiddenComment:FhirHelpersDstu3Test.kt$FhirHelpersDstu3Test$// TODO: Resolve Error: Could not load model information for model FHIR, version ForbiddenComment:FhirModelResolver.kt$FhirModelResolver$/* * // TODO: Find HAPI registry of Primitive Type conversions public Object * fromJavaPrimitive(Object value, Object target) { String simpleName = * target.getClass().getSimpleName(); switch(simpleName) { case "DateTimeType": * case "InstantType": return ((DateTime)value).toJavaDate(); case "DateType": * return ((org.opencds.cqf.cql.engine.runtime.Date)value).toJavaDate(); case * "TimeType": return ((Time) value).toString(); } * * if (value instanceof Time) { return ((Time) value).toString(); } else { * return value; } } */ - ForbiddenComment:FhirModelResolver.kt$FhirModelResolver$// TODO: Consider using getResourceType everywhere? ForbiddenComment:FhirModelResolver.kt$FhirModelResolver$// TODO: Probably quite a bit of redundancy here. Probably only really need the BaseType and the - ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: Ensure age constraints are met, else return null (Except that we can't - ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: Ensure count constraints are met, else return null - ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: Ensure distance constraints are met, else return null - ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: Ensure duration constraints are met, else return null - ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: Ensure money constraints are met, else return null ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: Might be able to patch some of these by registering custom types in HAPI. - ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: These should not return true unless the constraints that are used in the as logic - ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: These should really be using profile validation ForbiddenComment:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$// TODO: Deal with the case that the value is expressed as an expression extension ForbiddenComment:R4FhirTypeConverter.kt$R4FhirTypeConverter$// TODO: This will construct DateTimeType values in FHIR with the system - ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: Ensure age constraints are met, else return null (Except that we can't - ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: Ensure count constraints are met, else return null - ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: Ensure distance constraints are met, else return null - ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: Ensure duration constraints are met, else return null - ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: Ensure money constraints are met, else return null ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: Might be able to patch some of these by registering custom types in - ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: These should not return true unless the constraints that are used in the as logic - ForbiddenComment:R5FhirModelResolver.kt$R5FhirModelResolver$// TODO: These should really be using profile validation ForbiddenComment:R5FhirTypeConverter.kt$R5FhirTypeConverter$// TODO: This will construct DateTimeType values in FHIR with the system timezone id, ForbiddenComment:RestFhirRetrieveProvider.kt$RestFhirRetrieveProvider$// TODO: evaluate this lazily in case the engine only needs the first element ForbiddenComment:SearchParameterResolver.kt$SearchParameterResolver$// TODO: All the others like "_language" @@ -94,8 +59,6 @@ LargeClass:R4TypeConverterTests.kt$R4TypeConverterTests LargeClass:R5TypeConverterTests.kt$R5TypeConverterTests LongMethod:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$protected fun toDateTime(calendar: Calendar, calendarConstant: Int): DateTime - LongMethod:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - LongMethod:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? LongMethod:Dstu3FhirQueryGenerator.kt$Dstu3FhirQueryGenerator$override fun generateFhirQueries( dataRequirement: ICompositeType?, evaluationDateTime: DateTime?, contextValues: MutableMap<String, Any?>?, parameters: MutableMap<String, Any?>?, capabilityStatement: IBaseConformance?, ): MutableList<String> LongMethod:EvaluatedResourcesMultiLibComplexDepsTest.kt$EvaluatedResourcesMultiLibComplexDepsTest.Companion$@JvmStatic private fun multiLibraryEvaluationParams(): Stream<Arguments?> LongMethod:EvaluatedResourcesMultiLibComplexDepsTest.kt$EvaluatedResourcesMultiLibComplexDepsTest.Companion$@JvmStatic private fun multiLibrarySingleEvaluationAtATimeParams(): Stream<Arguments?> @@ -103,17 +66,15 @@ LongMethod:EvaluatedResourcesMultiLibLinearDepsTest.kt$EvaluatedResourcesMultiLibLinearDepsTest$@ParameterizedTest @MethodSource("multiLibEnsurePartialCacheAllowsUncachedLibsToBeCompiledParams") fun multiLibEnsurePartialCacheAllowsUncachedLibsToBeCompiled(expressionCaching: Boolean) LongMethod:EvaluatedResourcesMultiLibLinearDepsTest.kt$EvaluatedResourcesMultiLibLinearDepsTest.Companion$@JvmStatic private fun multiLibParams(): List<Arguments> LongMethod:EvaluatedResourcesMultiLibLinearDepsTest.kt$EvaluatedResourcesMultiLibLinearDepsTest.Companion$@JvmStatic private fun singleLibParams(): List<Arguments> + LongMethod:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): Any? LongMethod:FhirModelResolver.kt$FhirModelResolver$protected fun toDateTime( value: BaseDateTimeType, calendarConstant: Int = this.getCalendarConstant(value), ): DateTime - LongMethod:R4FhirModelResolver.kt$R4FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? LongMethod:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$override fun generateFhirQueries( dataRequirement: ICompositeType?, evaluationDateTime: DateTime?, contextValues: MutableMap<String, Any?>?, parameters: MutableMap<String, Any?>?, capabilityStatement: IBaseConformance?, ): MutableList<String> - LongMethod:R5FhirModelResolver.kt$R5FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? LongMethod:SearchParameterMap.kt$SearchParameterMap$fun toNormalizedQueryString(theCtx: FhirContext): String LongMethod:TestFHIR2Helpers.kt$TestFHIR2Helpers$fun test() LongMethod:TestFHIR3Helpers.kt$TestFHIR3Helpers$fun test() LongMethod:TestFHIRHelpers.kt$TestFHIRHelpers$@Test fun testFhirHelpers() LongMethod:TestFhirDataProviderDstu3.kt$TestFhirDataProviderDstu3$fun testContained() LongMethod:TestFhirPath.kt$TestFhirPath$protected fun runTest( test: Test, basePathInput: String?, fhirContext: FhirContext, provider: CompositeDataProvider?, resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, ) - LongMethod:TestPrimitiveProfiles.kt$TestPrimitiveProfiles$@Test fun profileCast() LongParameterList:BaseFhirQueryGenerator.kt$BaseFhirQueryGenerator$( context: String?, contextPath: String?, contextValue: Any?, dataType: String?, templateId: String?, codeFilters: MutableList<CodeFilter>?, dateFilters: MutableList<DateFilter>?, ) LongParameterList:BaseFhirQueryGenerator.kt$BaseFhirQueryGenerator$( context: String?, contextPath: String?, contextValue: Any?, dataType: String?, templateId: String?, codePath: String?, codes: Iterable<Code?>?, valueSet: String?, datePath: String?, dateLowPath: String?, dateHighPath: String?, dateRange: Interval?, ) LongParameterList:FhirQueryGeneratorFactory.kt$FhirQueryGeneratorFactory$( modelResolver: ModelResolver, searchParameterResolver: SearchParameterResolver, terminologyProvider: TerminologyProvider?, shouldExpandValueSets: Boolean?, maxCodesPerQuery: Int?, pageSize: Int?, queryBatchThreshold: Int?, ) @@ -133,6 +94,7 @@ MaxLineLength:CQLOperationsR4Test.kt$CQLOperationsR4Test.Companion$"r4/tests-fhir-r4/testInvariants/extension('http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring').exists() and extension('http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-scoring').value = 'ratio' implies group.population.where(code.coding.where(system = 'http://terminology.hl7.org/CodeSystem/measure-population').code = 'initial-population').count() in (1 | 2)" MaxLineLength:Dstu3FhirTerminologyProvider.kt$Dstu3FhirTerminologyProvider$"Could not expand value set ${valueSet.id}; version and code system bindings are not supported at this time." MaxLineLength:FhirModelResolver.kt$FhirModelResolver$"Could not resolve type $typeName. Primary package(s) for this resolver are ${this.packageNames.joinToString(",")}" + MaxLineLength:FhirModelResolver.kt$FhirModelResolver$"Unable to convert an instance of ${target.javaClass.name} to a CQL value. Expected an instance of IBase." MaxLineLength:FhirModelResolver.kt$FhirModelResolver$/* * type-to-class and contextPath resolutions are potentially expensive and can be cached * for improved performance. * * See <a href="https://github.com/DBCG/cql-evaluator/blob/master/evaluator.engine/src/main/java/org/opencds/cqf/cql/evaluator/engine/model/CachingModelResolverDecorator.java"/> * for a decorator that adds caching logic for ModelResolvers. */ MaxLineLength:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$"Cannot process data requirements with subjects not specified using a code from http://hl7.org/fhir/resource-types" MaxLineLength:R4FhirTerminologyProvider.kt$R4FhirTerminologyProvider$"Could not expand value set ${valueSet.id}; version and code system bindings are not supported at this time." @@ -150,6 +112,7 @@ MaxLineLength:TestR4FhirQueryGenerator.kt$TestR4FhirQueryGenerator$"Observation?category=http://myterm.com/fhir/CodeSystem/MyValueSet|code4,http://myterm.com/fhir/CodeSystem/MyValueSet|code5,http://myterm.com/fhir/CodeSystem/MyValueSet|code6,http://myterm.com/fhir/CodeSystem/MyValueSet|code7&subject=Patient/{{context.patientId}}" MaxLineLength:TestR4FhirQueryGenerator.kt$TestR4FhirQueryGenerator$"Observation?category=http://myterm.com/fhir/CodeSystem/MyValueSet|code5,http://myterm.com/fhir/CodeSystem/MyValueSet|code6,http://myterm.com/fhir/CodeSystem/MyValueSet|code7,http://myterm.com/fhir/CodeSystem/MyValueSet|code8,http://myterm.com/fhir/CodeSystem/MyValueSet|code9&subject=Patient/{{context.patientId}}" MaxLineLength:TestR4FhirQueryGenerator.kt$TestR4FhirQueryGenerator$"Observation?date=ge${simpleDateFormatter.format(expectedRangeStartDateTime)}&date=le${dateTimeFormatter.format(evaluationDateTimeAsLocal)}&subject=Patient/{{context.patientId}}" + MemberNameEqualsClassName:HapiToCqlValueTest.kt$HapiToCqlValueTest$@Test fun hapiToCqlValueTest() MemberNameEqualsClassName:TestFHIRHelpers.kt$TestFHIRHelpers$@Test fun testFhirHelpers() NestedBlockDepth:CQLOperationsDstu3Test.kt$CQLOperationsDstu3Test.Companion$@JvmStatic fun dataMethod(): Array<Array<Any>> NestedBlockDepth:CQLOperationsR4Test.kt$CQLOperationsR4Test.Companion$@JvmStatic fun dataMethod(): Array<Array<Any>> @@ -173,51 +136,39 @@ ReturnCount:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$override fun toFhirType(value: Any?): IBase? ReturnCount:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$protected fun toDateTime(calendar: Calendar, calendarConstant: Int): DateTime ReturnCount:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$protected fun toTime(calendar: Calendar, calendarConstant: Int): Time - ReturnCount:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - ReturnCount:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? ReturnCount:Dstu2FhirModelResolver.kt$Dstu2FhirModelResolver$override fun getContextPath(contextType: String?, targetType: String?): Any? ReturnCount:Dstu2FhirTypeConverter.kt$Dstu2FhirTypeConverter$override fun toCqlInterval(value: ICompositeType?): Interval? ReturnCount:Dstu2FhirTypeConverter.kt$Dstu2FhirTypeConverter$override fun toCqlTemporal(value: IPrimitiveType<Date>?): BaseTemporal? ReturnCount:Dstu2FhirTypeConverter.kt$Dstu2FhirTypeConverter$override fun toFhirPeriod(value: Interval?): ICompositeType? ReturnCount:Dstu2TypeConverterTests.kt$Dstu2TypeConverterTests$private fun compareObjects(left: Any?, right: Any?): Boolean - ReturnCount:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - ReturnCount:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? ReturnCount:Dstu3FhirModelResolver.kt$Dstu3FhirModelResolver$override fun getContextPath(contextType: String?, targetType: String?): Any? ReturnCount:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$override fun toCqlInterval(value: ICompositeType?): Interval? ReturnCount:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$override fun toCqlTemporal(value: IPrimitiveType<Date>?): BaseTemporal? ReturnCount:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$override fun toFhirPeriod(value: Interval?): ICompositeType? ReturnCount:Dstu3TypeConverterTests.kt$Dstu3TypeConverterTests$private fun compareObjects(left: Any?, right: Any?): Boolean + ReturnCount:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): Any? + ReturnCount:FhirModelResolver.kt$FhirModelResolver$open fun resolveType(typeName: String?): Class<*>? + ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun `is`(valueType: String, type: QName): Boolean? ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun getContextPath(contextType: String?, targetType: String?): Any? - ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun objectEqual(left: Any?, right: Any?): Boolean? - ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun objectEquivalent(left: Any?, right: Any?): Boolean - ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun resolveType(typeName: String?): Class<*>? - ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun resolveType(value: Any?): Class<*>? - ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun setValue(target: Any?, path: String?, value: Any?) + ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun resolveId(target: Any?): String? ReturnCount:FhirModelResolver.kt$FhirModelResolver$protected fun innerGetContextPath( visitedElements: MutableSet<String?>, child: BaseRuntimeChildDefinition, type: Class<out IBase?>?, ): String? ReturnCount:FhirModelResolver.kt$FhirModelResolver$protected fun resolveRuntimeDefinition(base: IBase): BaseRuntimeElementCompositeDefinition<*> ReturnCount:FhirModelResolver.kt$FhirModelResolver$protected fun toDateTime( value: BaseDateTimeType, calendarConstant: Int = this.getCalendarConstant(value), ): DateTime - ReturnCount:FhirModelResolver.kt$FhirModelResolver$protected open fun resolveProperty(target: Any?, path: String): Any? ReturnCount:Issue1226.kt$Issue1226.<no name provided>$override fun retrieve( context: String?, contextPath: String?, contextValue: Any?, dataType: String, templateId: String?, codePath: String?, codes: Iterable<Code>?, valueSet: String?, datePath: String?, dateLowPath: String?, dateHighPath: String?, dateRange: Interval?, ): Iterable<Any?> - ReturnCount:R4FhirModelResolver.kt$R4FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - ReturnCount:R4FhirModelResolver.kt$R4FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? ReturnCount:R4FhirModelResolver.kt$R4FhirModelResolver$override fun getContextPath(contextType: String?, targetType: String?): Any? - ReturnCount:R4FhirModelResolver.kt$R4FhirModelResolver$override fun resolveProperty(target: Any?, path: String): Any? ReturnCount:R4FhirTypeConverter.kt$R4FhirTypeConverter$override fun toCqlInterval(value: ICompositeType?): Interval? ReturnCount:R4FhirTypeConverter.kt$R4FhirTypeConverter$override fun toCqlTemporal(value: IPrimitiveType<Date>?): BaseTemporal? ReturnCount:R4FhirTypeConverter.kt$R4FhirTypeConverter$override fun toFhirPeriod(value: Interval?): ICompositeType? ReturnCount:R4TypeConverterTests.kt$R4TypeConverterTests$private fun compareObjects(left: Any?, right: Any?): Boolean - ReturnCount:R5FhirModelResolver.kt$R5FhirModelResolver$override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? - ReturnCount:R5FhirModelResolver.kt$R5FhirModelResolver$override fun `is`(value: Any?, type: Class<*>?): Boolean? ReturnCount:R5FhirModelResolver.kt$R5FhirModelResolver$override fun getContextPath(contextType: String?, targetType: String?): Any? - ReturnCount:R5FhirModelResolver.kt$R5FhirModelResolver$override fun resolveProperty(target: Any?, path: String): Any? ReturnCount:R5FhirTypeConverter.kt$R5FhirTypeConverter$override fun toCqlInterval(value: ICompositeType?): Interval? ReturnCount:R5FhirTypeConverter.kt$R5FhirTypeConverter$override fun toCqlTemporal(value: IPrimitiveType<Date>?): BaseTemporal? ReturnCount:R5FhirTypeConverter.kt$R5FhirTypeConverter$override fun toFhirPeriod(value: Interval?): ICompositeType? ReturnCount:R5TypeConverterTests.kt$R5TypeConverterTests$private fun compareObjects(left: Any?, right: Any?): Boolean ReturnCount:SearchParameterResolver.kt$SearchParameterResolver$fun createSearchParameter( context: String?, dataType: String?, path: String?, value: String?, ): Pair<String, IQueryParameterType>? ReturnCount:SearchParameterResolver.kt$SearchParameterResolver$fun getSearchParameterDefinition( dataType: String?, path: String?, paramType: RestSearchParameterTypeEnum?, ): RuntimeSearchParam? + ReturnCount:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.<no name provided>$override fun retrieve( context: String?, contextPath: String?, contextValue: Any?, dataType: String, templateId: String?, codePath: String?, codes: Iterable<Code>?, valueSet: String?, datePath: String?, dateLowPath: String?, dateHighPath: String?, dateRange: Interval?, ): Iterable<Any?>? ReturnCount:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.Companion$private fun codesEqual(codes: Iterable<*>?, equalTo: String): Boolean - ReturnCount:TestCqlEngineRelatedContextSupport.kt$TestCqlEngineRelatedContextSupport.Companion.<no name provided>$override fun retrieve( context: String?, contextPath: String?, contextValue: Any?, dataType: String, templateId: String?, codePath: String?, codes: Iterable<Code>?, valueSet: String?, datePath: String?, dateLowPath: String?, dateHighPath: String?, dateRange: Interval?, ): Iterable<Any?>? ReturnCount:TestFhirPath.kt$TestFhirPath$protected fun runTest( test: Test, basePathInput: String?, fhirContext: FhirContext, provider: CompositeDataProvider?, resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, ) SpreadOperator:RestFhirRetrieveProvider.kt$RestFhirRetrieveProvider$(*codings) SwallowedException:Dstu3FhirQueryGenerator.kt$Dstu3FhirQueryGenerator$ex: Exception @@ -226,13 +177,12 @@ SwallowedException:FhirModelResolver.kt$FhirModelResolver$e: InstantiationException SwallowedException:FhirModelResolver.kt$FhirModelResolver$e: InvocationTargetException SwallowedException:FhirModelResolver.kt$FhirModelResolver$e: NoSuchMethodException - SwallowedException:FhirModelResolver.kt$FhirModelResolver$le: IllegalArgumentException SwallowedException:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$ex: Exception SwallowedException:R4FhirTerminologyProvider.kt$R4FhirTerminologyProvider$rnfe: ResourceNotFoundException SwallowedException:SearchParamFhirRetrieveProvider.kt$SearchParamFhirRetrieveProvider$exception: FhirVersionMisMatchException SwallowedException:TestDstu2ModelResolver.kt$TestDstu2ModelResolver$e: Exception ThrowsCount:EvaluatedResourceTestUtils.kt$EvaluatedResourceTestUtils$fun setupCql( classToUse: Class<*>, librariesToPopulate: MutableList<Library?>, libraryManagerToUse: LibraryManager, ) - ThrowsCount:FhirModelResolver.kt$FhirModelResolver$protected fun createInstance(clazz: Class<*>): Any + ThrowsCount:FhirModelResolver.kt$FhirModelResolver$protected fun createHapiInstance(clazz: Class<*>): Any ThrowsCount:TestFhirPath.kt$TestFhirPath$protected fun runTest( test: Test, basePathInput: String?, fhirContext: FhirContext, provider: CompositeDataProvider?, resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, ) TooGenericExceptionCaught:Dstu3FhirQueryGenerator.kt$Dstu3FhirQueryGenerator$ex: Exception TooGenericExceptionCaught:Dstu3FhirTerminologyProvider.kt$Dstu3FhirTerminologyProvider$e: Exception @@ -271,6 +221,7 @@ UseRequire:Dstu2FhirTypeConverter.kt$Dstu2FhirTypeConverter$throw IllegalArgumentException("value is not a FHIR Range or Period") UseRequire:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$throw IllegalArgumentException("value is not a FHIR Instant or DateTime") UseRequire:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$throw IllegalArgumentException("value is not a FHIR Range or Period") + UseRequire:FhirModelResolver.kt$FhirModelResolver$throw IllegalArgumentException( "Unable to convert an instance of ${target.javaClass.name} to a CQL value. Expected an instance of IBase." ) UseRequire:R4FhirTypeConverter.kt$R4FhirTypeConverter$throw IllegalArgumentException("value is not a FHIR Instant or DateTime") UseRequire:R4FhirTypeConverter.kt$R4FhirTypeConverter$throw IllegalArgumentException("value is not a FHIR Range or Period") UseRequire:R5FhirTypeConverter.kt$R5FhirTypeConverter$throw IllegalArgumentException("value is not a FHIR Instant or DateTime") diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu2FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu2FhirModelResolver.kt index 343c50a77..25fff4c57 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu2FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu2FhirModelResolver.kt @@ -3,30 +3,17 @@ package org.opencds.cqf.cql.engine.fhir.model import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import java.util.* -import org.hl7.fhir.dstu2.model.Age import org.hl7.fhir.dstu2.model.AnnotatedUuidType import org.hl7.fhir.dstu2.model.Base import org.hl7.fhir.dstu2.model.BaseDateTimeType -import org.hl7.fhir.dstu2.model.Count -import org.hl7.fhir.dstu2.model.Distance -import org.hl7.fhir.dstu2.model.Duration import org.hl7.fhir.dstu2.model.EnumFactory import org.hl7.fhir.dstu2.model.Enumeration import org.hl7.fhir.dstu2.model.Enumerations import org.hl7.fhir.dstu2.model.IdType -import org.hl7.fhir.dstu2.model.IntegerType -import org.hl7.fhir.dstu2.model.OidType -import org.hl7.fhir.dstu2.model.PositiveIntType -import org.hl7.fhir.dstu2.model.Quantity import org.hl7.fhir.dstu2.model.Resource import org.hl7.fhir.dstu2.model.SimpleQuantity -import org.hl7.fhir.dstu2.model.StringType import org.hl7.fhir.dstu2.model.TimeType -import org.hl7.fhir.dstu2.model.UnsignedIntType -import org.hl7.fhir.dstu2.model.UriType -import org.hl7.fhir.dstu2.model.UuidType import org.hl7.fhir.instance.model.api.IBaseResource -import org.opencds.cqf.cql.engine.exception.InvalidCast import org.opencds.cqf.cql.engine.runtime.BaseTemporal open class Dstu2FhirModelResolver(fhirContext: FhirContext) : @@ -103,10 +90,6 @@ open class Dstu2FhirModelResolver(fhirContext: FhirContext) : } } - override fun equalsDeep(left: Base, right: Base): Boolean { - return left.equalsDeep(right) - } - override fun castToSimpleQuantity(base: Base): SimpleQuantity { return base.castToSimpleQuantity(base) } @@ -156,199 +139,6 @@ open class Dstu2FhirModelResolver(fhirContext: FhirContext) : } } - /* - Casting of derived primitives: - Datatypes that derive from datatypes other than Element are actually profiles - // Types that exhibit this behavior are: - // url: uri - // canonical: uri - // uuid: uri - // oid: uri - // positiveInt: integer - // unsignedInt: integer - // code: string - // markdown: string - // id: string - - */ - override fun `is`(value: Any?, type: Class<*>?): Boolean? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return true - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is UriType) { - when (type.getSimpleName()) { - "UrlType" -> return true - "CanonicalType" -> return true - "AnnotatedUuidType", - "UuidType" -> return true - "OidType" -> return true - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> return true - "UnsignedIntType" -> return true - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> return true - "MarkdownType" -> return true - "IdType" -> return true - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is Quantity) { - when (type.getSimpleName()) { - "Age", - "Distance", - "Duration", - "Count", - "SimpleQuantity" -> return true - } - } - - return false - } - - override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return value - } - - if (value is UriType) { - when (type.getSimpleName()) { - "AnnotatedUuidType", - "UuidType" -> - return if (value.hasValue() && value.value.startsWith("urn:uuid:")) - UuidType(value.primitiveValue()) - else null - - "OidType" -> - return if (value.hasValue() && value.value.startsWith("urn:oid:")) - OidType(value.primitiveValue()) - else null // castToOid(uriType); Throws an exception, not implemented - } - } - - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> - return if (value.hasValue() && value.value > 0) - PositiveIntType(value.primitiveValue()) - else - null // integerType.castToPositiveInt(integerType); Throws an exception, not - // implemented - "UnsignedIntType" -> - return if (value.hasValue() && value.value >= 0) - UnsignedIntType(value.primitiveValue()) - else - null // castToUnsignedInt(integerType); Throws an exception, not implemented - } - } - - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> return value.castToCode(value) - "MarkdownType" -> return value.castToMarkdown(value) - "IdType" -> - return if (value.hasValue()) IdType(value.primitiveValue()) - else null // stringType.castToId(stringType); Throws an exception, not - // implemented - } - } - - if (value is Quantity) { - when (type.getSimpleName()) { - "Age" -> { - val age = Age() - age.setValue(value.getValue()) - age.setCode(value.getCode()) - age.setUnit(value.getUnit()) - age.setSystem(value.getSystem()) - age.setComparator(value.getComparator()) - // TODO: Ensure age constraints are met, else return null (Except that we can't - // do this - // unless we can be assured that the same constraints have resulted in true when - // we called 'is' - // As it's written now, any Quantity will return true if you ask if it's an Age - // Same is true for all the subtypes, and so that's a type-system level issue - // waiting to bite - return age - } - - "Distance" -> { - val distance = Distance() - distance.setValue(value.getValue()) - distance.setCode(value.getCode()) - distance.setUnit(value.getUnit()) - distance.setSystem(value.getSystem()) - distance.setComparator(value.getComparator()) - // TODO: Ensure distance constraints are met, else return null - return distance - } - - "Duration" -> { - val duration = Duration() - duration.setValue(value.getValue()) - duration.setCode(value.getCode()) - duration.setUnit(value.getUnit()) - duration.setSystem(value.getSystem()) - duration.setComparator(value.getComparator()) - // TODO: Ensure duration constraints are met, else return null - return duration - } - - "Count" -> { - val count = Count() - count.setValue(value.getValue()) - count.setCode(value.getCode()) - count.setUnit(value.getUnit()) - count.setSystem(value.getSystem()) - count.setComparator(value.getComparator()) - // TODO: Ensure count constraints are met, else return null - return count - } - - "SimpleQuantity" -> - return value.castToSimpleQuantity( - value - ) // NOTE: This is wrong in that it is copying the comparator, it should be - } - } - - if (isStrict) { - throw InvalidCast( - "Cannot cast a value of type ${value.javaClass.name} as ${type.name}." - ) - } - - return null - } - override fun getContextPath(contextType: String?, targetType: String?): Any? { if (targetType == null || contextType == null) { return null diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu3FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu3FhirModelResolver.kt index cc20d724a..dda6ab99a 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu3FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/Dstu3FhirModelResolver.kt @@ -3,30 +3,17 @@ package org.opencds.cqf.cql.engine.fhir.model import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import java.util.* -import org.hl7.fhir.dstu3.model.Age import org.hl7.fhir.dstu3.model.AnnotatedUuidType import org.hl7.fhir.dstu3.model.Base import org.hl7.fhir.dstu3.model.BaseDateTimeType -import org.hl7.fhir.dstu3.model.Count -import org.hl7.fhir.dstu3.model.Distance -import org.hl7.fhir.dstu3.model.Duration import org.hl7.fhir.dstu3.model.EnumFactory import org.hl7.fhir.dstu3.model.Enumeration import org.hl7.fhir.dstu3.model.Enumerations import org.hl7.fhir.dstu3.model.IdType -import org.hl7.fhir.dstu3.model.IntegerType -import org.hl7.fhir.dstu3.model.OidType -import org.hl7.fhir.dstu3.model.PositiveIntType -import org.hl7.fhir.dstu3.model.Quantity import org.hl7.fhir.dstu3.model.Resource import org.hl7.fhir.dstu3.model.SimpleQuantity -import org.hl7.fhir.dstu3.model.StringType import org.hl7.fhir.dstu3.model.TimeType -import org.hl7.fhir.dstu3.model.UnsignedIntType -import org.hl7.fhir.dstu3.model.UriType -import org.hl7.fhir.dstu3.model.UuidType import org.hl7.fhir.instance.model.api.IBaseResource -import org.opencds.cqf.cql.engine.exception.InvalidCast import org.opencds.cqf.cql.engine.runtime.BaseTemporal open class Dstu3FhirModelResolver(fhirContext: FhirContext) : @@ -98,10 +85,6 @@ open class Dstu3FhirModelResolver(fhirContext: FhirContext) : } } - override fun equalsDeep(left: Base, right: Base): Boolean { - return left.equalsDeep(right) - } - override fun castToSimpleQuantity(base: Base): SimpleQuantity { return base.castToSimpleQuantity(base) } @@ -159,200 +142,6 @@ open class Dstu3FhirModelResolver(fhirContext: FhirContext) : return super.resolveType(typeName) } - /* - Casting of derived primitives: - Datatypes that derive from datatypes other than Element are actually profiles - // Types that exhibit this behavior are: - // url: uri - // canonical: uri - // uuid: uri - // oid: uri - // positiveInt: integer - // unsignedInt: integer - // code: string - // markdown: string - // id: string - - */ - override fun `is`(value: Any?, type: Class<*>?): Boolean? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return true - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is UriType) { - when (type.getSimpleName()) { - "UrlType" -> return true - "CanonicalType" -> return true - "AnnotatedUuidType", - "UuidType" -> return true - "OidType" -> return true - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> return true - "UnsignedIntType" -> return true - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> return true - "MarkdownType" -> return true - "IdType" -> return true - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is Quantity) { - when (type.getSimpleName()) { - "Age", - "Distance", - "Duration", - "Count", - "SimpleQuantity", - "MoneyQuantity" -> return true - } - } - - return false - } - - override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return value - } - - if (value is UriType) { - when (type.getSimpleName()) { - "AnnotatedUuidType", - "UuidType" -> - return if (value.hasPrimitiveValue() && value.value.startsWith("urn:uuid:")) - UuidType(value.primitiveValue()) - else null - - "OidType" -> - return if (value.hasPrimitiveValue() && value.value.startsWith("urn:oid:")) - OidType(value.primitiveValue()) - else null // castToOid(uriType); Throws an exception, not implemented - } - } - - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> - return if (value.hasPrimitiveValue() && value.value > 0) - PositiveIntType(value.primitiveValue()) - else - null // integerType.castToPositiveInt(integerType); Throws an exception, not - // implemented - "UnsignedIntType" -> - return if (value.hasPrimitiveValue() && value.value >= 0) - UnsignedIntType(value.primitiveValue()) - else - null // castToUnsignedInt(integerType); Throws an exception, not implemented - } - } - - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> return value.castToCode(value) - "MarkdownType" -> return value.castToMarkdown(value) - "IdType" -> - return if (value.hasPrimitiveValue()) IdType(value.primitiveValue()) - else null // stringType.castToId(stringType); Throws an exception, not - // implemented - } - } - - if (value is Quantity) { - when (type.getSimpleName()) { - "Age" -> { - val age = Age() - age.setValue(value.getValue()) - age.setCode(value.getCode()) - age.setUnit(value.getUnit()) - age.setSystem(value.getSystem()) - age.setComparator(value.getComparator()) - // TODO: Ensure age constraints are met, else return null (Except that we can't - // do this - // unless we can be assured that the same constraints have resulted in true when - // we called 'is' - // As it's written now, any Quantity will return true if you ask if it's an Age - // Same is true for all the subtypes, and so that's a type-system level issue - // waiting to bite - return age - } - - "Distance" -> { - val distance = Distance() - distance.setValue(value.getValue()) - distance.setCode(value.getCode()) - distance.setUnit(value.getUnit()) - distance.setSystem(value.getSystem()) - distance.setComparator(value.getComparator()) - // TODO: Ensure distance constraints are met, else return null - return distance - } - - "Duration" -> { - val duration = Duration() - duration.setValue(value.getValue()) - duration.setCode(value.getCode()) - duration.setUnit(value.getUnit()) - duration.setSystem(value.getSystem()) - duration.setComparator(value.getComparator()) - // TODO: Ensure duration constraints are met, else return null - return duration - } - - "Count" -> { - val count = Count() - count.setValue(value.getValue()) - count.setCode(value.getCode()) - count.setUnit(value.getUnit()) - count.setSystem(value.getSystem()) - count.setComparator(value.getComparator()) - // TODO: Ensure count constraints are met, else return null - return count - } - - "SimpleQuantity" -> - return value.castToSimpleQuantity( - value - ) // NOTE: This is wrong in that it is copying the comparator, it should be - } - } - - if (isStrict) { - throw InvalidCast( - "Cannot cast a value of type ${value.javaClass.name} as ${type.name}." - ) - } - - return null - } - override fun getContextPath(contextType: String?, targetType: String?): Any? { if (targetType == null || contextType == null) { return null diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt index b415edb04..0cbed4cdd 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt @@ -4,35 +4,35 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.RuntimeChildChoiceDefinition -import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition import ca.uhn.fhir.context.RuntimeChildResourceBlockDefinition import ca.uhn.fhir.context.RuntimeChildResourceDefinition import ca.uhn.fhir.model.api.TemporalPrecisionEnum import java.lang.reflect.InvocationTargetException import java.util.Calendar import java.util.GregorianCalendar +import javax.xml.namespace.QName import kotlin.Any import kotlin.Boolean import kotlin.Exception import kotlin.IllegalArgumentException import kotlin.Int -import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.instance.model.api.IAnyResource import org.hl7.fhir.instance.model.api.IBase import org.hl7.fhir.instance.model.api.IBaseBackboneElement import org.hl7.fhir.instance.model.api.IBaseElement import org.hl7.fhir.instance.model.api.IBaseEnumFactory import org.hl7.fhir.instance.model.api.IBaseEnumeration +import org.hl7.fhir.instance.model.api.IBaseHasExtensions +import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions import org.hl7.fhir.instance.model.api.IBaseResource import org.hl7.fhir.instance.model.api.ICompositeType import org.hl7.fhir.instance.model.api.IIdType import org.hl7.fhir.instance.model.api.IPrimitiveType -import org.opencds.cqf.cql.engine.exception.DataProviderException -import org.opencds.cqf.cql.engine.exception.InvalidCast import org.opencds.cqf.cql.engine.exception.InvalidPrecision import org.opencds.cqf.cql.engine.fhir.exception.UnknownType import org.opencds.cqf.cql.engine.model.ModelResolver import org.opencds.cqf.cql.engine.runtime.BaseTemporal +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance import org.opencds.cqf.cql.engine.runtime.Date import org.opencds.cqf.cql.engine.runtime.DateTime import org.opencds.cqf.cql.engine.runtime.Precision @@ -64,8 +64,6 @@ abstract class FhirModelResolver< ) : ModelResolver { protected abstract fun initialize() - protected abstract fun equalsDeep(left: BaseType, right: BaseType): Boolean - abstract fun castToSimpleQuantity(base: BaseType): SimpleQuantityType protected abstract fun getCalendar(dateTime: BaseDateTimeType): Calendar @@ -91,8 +89,12 @@ abstract class FhirModelResolver< } override fun resolveId(target: Any?): String? { - if (target is IBaseResource) { - return target.idElement.idPart + if (target is CqlClassInstance && target.type.namespaceURI == "http://hl7.org/fhir") { + val clazz = this.resolveType(target.type.localPart) ?: return null + if (IBaseResource::class.java.isAssignableFrom(clazz)) { + val id = target.elements["id"] as? CqlClassInstance ?: return null + return id.elements["value"] as? String + } } return null } @@ -115,7 +117,7 @@ abstract class FhirModelResolver< } val resourceDefinition = this.fhirContext.getResourceDefinition(targetType) - val theValue = this.createInstance(contextType) + val theValue = this.createHapiInstance(contextType) // Because we created this instance from the local FhirContext, we know it is an IBase // The model resolver interface is not generic, so we have to cast here @@ -174,71 +176,37 @@ abstract class FhirModelResolver< return null } - override fun objectEqual(left: Any?, right: Any?): Boolean? { - if (left == null) { - return null - } - - if (right == null) { - return null - } - - @Suppress("UNCHECKED_CAST") - return this.equalsDeep(left as BaseType, right as BaseType) - } - - override fun objectEquivalent(left: Any?, right: Any?): Boolean { - if (left == null && right == null) { + override fun `is`(valueType: String, type: QName): Boolean? { + // System.Any is a supertype of all types + if (type == QName("urn:hl7-org:elm-types:r1", "Any")) { return true } - if (left == null || right == null) { + if (type.namespaceURI != "http://hl7.org/fhir") { return false } - @Suppress("UNCHECKED_CAST") - return this.equalsDeep(left as BaseType, right as BaseType) + val valueTypeClass = resolveType(valueType) ?: return null + val typeClass = resolveType(type.localPart) ?: return null + + return typeClass.isAssignableFrom(valueTypeClass) } override fun createInstance(typeName: String?): Any { - return createInstance(resolveType(typeName)!!) + return toCqlValue(createHapiInstance(typeName!!), true)!! } - @get:Deprecated("Deprecated in Java") - @set:Deprecated("Deprecated in Java") - override var packageName: String? - get() { - if (packageNames.isEmpty()) { - return null - } - return packageNames[0] - } - set(packageName) { - this.packageNames = - if (packageName != null) mutableListOf(packageName) else mutableListOf() - } - - override var packageNames = mutableListOf() + /** The package names of Java objects supported by this model */ + open var packageNames = mutableListOf() - override fun resolvePath(target: Any?, path: String?): Any? { - var target = target - val identifiers = - path!!.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - for (identifier in identifiers) { - // handling indexes: i.e. item[0].code - if (identifier.contains("[")) { - val index = Character.getNumericValue(identifier[identifier.indexOf("[") + 1]) - target = resolveProperty(target, identifier.replace("\\[\\d]".toRegex(), ""))!! - target = (target as ArrayList<*>)[index] - } else { - target = resolveProperty(target, identifier) - } - } - - return target - } - - override fun resolveType(typeName: String?): Class<*>? { + /** + * Resolve the Java class that corresponds to the given model type. + * + * @param typeName E.g. "Patient" + * @return The Java class that corresponds to the given model type, e.g. + * `org.hl7.fhir.r4.model.Patient`. + */ + open fun resolveType(typeName: String?): Class<*>? { // For Dstu2 var typeName = typeName if (typeName!!.startsWith("FHIR.")) { @@ -334,147 +302,7 @@ abstract class FhirModelResolver< return null } - override fun resolveType(value: Any?): Class<*>? { - if (value == null) { - return Any::class.java - } - - // For FHIR enumerations, return the type of the backing Enum - if (this.enumChecker(value)) { - @Suppress("UNCHECKED_CAST") - val factoryName = this.enumFactoryTypeGetter(value as EnumerationType).getSimpleName() - return this.resolveType(factoryName.substringBefore("EnumFactory")) - } - - return value.javaClass - } - - override fun setValue(target: Any?, path: String?, value: Any?) { - var value = value - if (target == null) { - return - } - - if (target is IBaseEnumeration<*> && path == "value") { - target.valueAsString = value as String? - return - } - - val base = target as IBase - val definition: BaseRuntimeElementCompositeDefinition<*> - if (base is IPrimitiveType<*>) { - setPrimitiveValue(value, base) - return - } else { - definition = resolveRuntimeDefinition(base) - } - - var child = definition.getChildByName(path) - if (child == null) { - child = resolveChoiceProperty(definition, path) - } - - if (child == null) { - throw DataProviderException("Unable to resolve path $path.") - } - - try { - if (value is Iterable<*>) { - for (`val` in value) { - child.mutator.addValue(base, setBaseValue(`val`, base)) - } - } else { - child.mutator.setValue(base, setBaseValue(value, base)) - } - } catch (le: IllegalArgumentException) { - if (value!!.javaClass.getSimpleName() == "Quantity") { - try { - @Suppress("UNCHECKED_CAST") - value = this.castToSimpleQuantity(value as BaseType) - } catch (_: FHIRException) { - throw InvalidCast("Unable to cast Quantity to SimpleQuantity") - } - child.mutator.setValue(base, setBaseValue(value, base)) - } else { - throw DataProviderException("Configuration error encountered: ${le.message}") - } - } - } - // Resolutions - protected open fun resolveProperty(target: Any?, path: String): Any? { - if (target == null) { - return null - } - - if (target is IBaseEnumeration<*> && path == "value") { - return target.valueAsString - } - - // TODO: Consider using getResourceType everywhere? - @Suppress("UNCHECKED_CAST") - if (target is IAnyResource && this.getResourceType(target as ResourceType) == path) { - return target - } - - val base = target as IBase - val definition: BaseRuntimeElementCompositeDefinition<*> - if (base is IPrimitiveType<*>) { - return toJavaPrimitive( - if (path == "value") (target as IPrimitiveType<*>).getValue() else target, - base, - ) - } else { - definition = resolveRuntimeDefinition(base) - } - - var child = definition.getChildByName(path) - if (child == null) { - child = resolveChoiceProperty(definition, path) - } - - if (child == null) { - return null - } - - val values = child.accessor.getValues(base) - - if (values == null || values.isEmpty()) { - return null - } - - // If the instance is a primitive (including (or even especially an enumeration), and it has - // no value, return - // null - if (child is RuntimeChildPrimitiveDatatypeDefinition) { - val value = values[0] - if (value is IPrimitiveType<*>) { - if (!value.hasValue()) { - return null - } - } - } - - if ( - child is RuntimeChildChoiceDefinition && - !child.elementName.equals(path, ignoreCase = true) - ) { - if ( - !values[0]!! - .javaClass - .getSimpleName() - .equals( - child.getChildByName(path).getImplementingClass().getSimpleName(), - ignoreCase = true, - ) - ) { - return null - } - } - - return toJavaPrimitive(if (child.max < 1) values else values[0], base) - } - protected fun resolveRuntimeDefinition(base: IBase): BaseRuntimeElementCompositeDefinition<*> { when (base) { is IAnyResource -> { @@ -535,13 +363,17 @@ abstract class FhirModelResolver< } // Creators - protected fun createInstance(clazz: Class<*>): Any { + internal fun createHapiInstance(typeName: String): Any { + return createHapiInstance(this.resolveType(typeName)!!) + } + + protected fun createHapiInstance(clazz: Class<*>): Any { try { if (clazz.isEnum) { val factoryClass = this.resolveType(clazz.getName() + "EnumFactory") @Suppress("UNCHECKED_CAST") - val factory = this.createInstance(factoryClass!!) as EnumFactoryType + val factory = this.createHapiInstance(factoryClass!!) as EnumFactoryType return this.enumConstructor(factory) } @@ -752,4 +584,97 @@ abstract class FhirModelResolver< else -> result } } + + /** Recursively converts a HAPI FHIR construct to a CQL-native equivalent. */ + fun toCqlValue( + target: Any?, + expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, + ): Any? { + if (target == null) { + return null + } + + val elements = mutableMapOf() + + if (target is IBaseHasExtensions) { + val extensionsAsCqlValues = target.extension.map { toCqlValue(it) } + elements["extension"] = + if (extensionsAsCqlValues.isEmpty()) null else extensionsAsCqlValues + } + + if (target is IBaseHasModifierExtensions) { + val modifierExtensionsAsCqlValues = target.modifierExtension.map { toCqlValue(it) } + elements["extension"] = + if (modifierExtensionsAsCqlValues.isEmpty()) null else modifierExtensionsAsCqlValues + } + + if (target is IBaseElement) { + elements["id"] = target.id + } + + if (this.enumChecker(target)) { + @Suppress("UNCHECKED_CAST") + target as EnumerationType + + // If the instance is a primitive (including or even especially an enumeration), and it + // has no value, return null + if ( + !expandPrimitivesAndEnumerationsWithNoValues && + !target.hasValue() && + elements.all { it.value == null } + ) { + return null + } + + // For FHIR enumerations, use the type of the backing Enum + val factoryName = this.enumFactoryTypeGetter(target).simpleName + val typeName = factoryName.substringBefore("EnumFactory") + + elements["value"] = target.valueAsString + + return CqlClassInstance(QName("http://hl7.org/fhir", typeName), elements) + } + + if (target is IPrimitiveType<*>) { + // If the instance is a primitive (including or even especially an enumeration), and it + // has no value, return null + if ( + !expandPrimitivesAndEnumerationsWithNoValues && + !target.hasValue() && + elements.all { it.value == null } + ) { + return null + } + + val elementDefinition = this.fhirContext.getElementDefinition(target.javaClass) + + elements["value"] = toJavaPrimitive(target.value, target) + + return CqlClassInstance(QName("http://hl7.org/fhir", elementDefinition.name), elements) + } + + if (target !is IBase) { + throw IllegalArgumentException( + "Unable to convert an instance of ${target.javaClass.name} to a CQL value. Expected an instance of IBase." + ) + } + + val definition = resolveRuntimeDefinition(target) + + for (child in definition.children) { + val childName = child.elementName + val hapiValues = child.accessor.getValues(target) + + val hapiValuesAsCqlValues = hapiValues.map { toCqlValue(it) } + + if (child.max == 1) { + elements[childName] = hapiValuesAsCqlValues.firstOrNull() + } else { + elements[childName] = + if (hapiValuesAsCqlValues.isEmpty()) null else hapiValuesAsCqlValues + } + } + + return CqlClassInstance(QName("http://hl7.org/fhir", definition.name), elements) + } } diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R4FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R4FhirModelResolver.kt index 4de5cb766..1a5bfb8c4 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R4FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R4FhirModelResolver.kt @@ -4,31 +4,16 @@ import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import java.util.* import org.hl7.fhir.instance.model.api.IBaseResource -import org.hl7.fhir.r4.model.Age import org.hl7.fhir.r4.model.AnnotatedUuidType import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.BaseDateTimeType -import org.hl7.fhir.r4.model.Count -import org.hl7.fhir.r4.model.Distance -import org.hl7.fhir.r4.model.Duration import org.hl7.fhir.r4.model.EnumFactory import org.hl7.fhir.r4.model.Enumeration import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.IdType -import org.hl7.fhir.r4.model.IntegerType -import org.hl7.fhir.r4.model.MoneyQuantity -import org.hl7.fhir.r4.model.OidType -import org.hl7.fhir.r4.model.PositiveIntType -import org.hl7.fhir.r4.model.PrimitiveType -import org.hl7.fhir.r4.model.Quantity import org.hl7.fhir.r4.model.Resource import org.hl7.fhir.r4.model.SimpleQuantity -import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.TimeType -import org.hl7.fhir.r4.model.UnsignedIntType -import org.hl7.fhir.r4.model.UriType -import org.hl7.fhir.r4.model.UuidType -import org.opencds.cqf.cql.engine.exception.InvalidCast import org.opencds.cqf.cql.engine.runtime.BaseTemporal open class R4FhirModelResolver(fhirContext: FhirContext) : @@ -100,25 +85,6 @@ open class R4FhirModelResolver(fhirContext: FhirContext) : } } - override fun resolveProperty(target: Any?, path: String): Any? { - // This is kind of a hack to get around contained resources - HAPI doesn't have - // ResourceContainer type for R4 - if (target is Resource && target.fhirType() == path) { - return target - } - - // Account for extensions on primitives - if (target is PrimitiveType<*> && path == "extension") { - return target.getExtension() - } - - return super.resolveProperty(target, path) - } - - override fun equalsDeep(left: Base, right: Base): Boolean { - return left.equalsDeep(right) - } - override fun castToSimpleQuantity(base: Base): SimpleQuantity { return base.castToSimpleQuantity(base) } @@ -185,221 +151,6 @@ open class R4FhirModelResolver(fhirContext: FhirContext) : return super.resolveType(typeName) } - /* - Casting of derived primitives: - Datatypes that derive from datatypes other than Element are actually profiles - // Types that exhibit this behavior are: - // url: uri - // canonical: uri - // uuid: uri - // oid: uri - // positiveInt: integer - // unsignedInt: integer - // code: string - // markdown: string - // id: string - - */ - override fun `is`(value: Any?, type: Class<*>?): Boolean? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return true - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is UriType) { - when (type.getSimpleName()) { - "UrlType" -> return true - "CanonicalType" -> return true - "AnnotatedUuidType", - "UuidType" -> return true - "OidType" -> return true - else -> {} - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> return true - "UnsignedIntType" -> return true - else -> {} - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> return true - "MarkdownType" -> return true - "IdType" -> return true - else -> {} - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is Quantity) { - when (type.getSimpleName()) { - "Age", - "Distance", - "Duration", - "Count", - "SimpleQuantity", - "MoneyQuantity" -> return true - else -> {} - } - } - - return false - } - - override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return value - } - - if (value is UriType) { - when (type.getSimpleName()) { - "UrlType" -> return value.castToUrl(value) - "CanonicalType" -> return value.castToCanonical(value) - "AnnotatedUuidType", - "UuidType" -> - return if (value.hasPrimitiveValue() && value.value.startsWith("urn:uuid:")) - UuidType(value.primitiveValue()) - else null - - "OidType" -> - return if (value.hasPrimitiveValue() && value.value.startsWith("urn:oid:")) - OidType(value.primitiveValue()) - else null // castToOid(uriType); Throws an exception, not implemented - else -> {} - } - } - - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> - return if (value.hasPrimitiveValue() && value.value > 0) - PositiveIntType(value.primitiveValue()) - else - null // integerType.castToPositiveInt(integerType); Throws an exception, not - // implemented - "UnsignedIntType" -> - return if (value.hasPrimitiveValue() && value.value >= 0) - UnsignedIntType(value.primitiveValue()) - else - null // castToUnsignedInt(integerType); Throws an exception, not implemented - else -> {} - } - } - - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> return value.castToCode(value) - "MarkdownType" -> return value.castToMarkdown(value) - "IdType" -> - return if (value.hasPrimitiveValue()) IdType(value.primitiveValue()) - else null // stringType.castToId(stringType); Throws an exception, not - // implemented - else -> {} - } - } - - if (value is Quantity) { - when (type.getSimpleName()) { - "Age" -> { - val age = Age() - age.setValue(value.getValue()) - age.setCode(value.getCode()) - age.setUnit(value.getUnit()) - age.setSystem(value.getSystem()) - age.setComparator(value.getComparator()) - // TODO: Ensure age constraints are met, else return null (Except that we can't - // do this - // unless we can be assured that the same constraints have resulted in true when - // we called 'is' - // As it's written now, any Quantity will return true if you ask if it's an Age - // Same is true for all the subtypes, and so that's a type-system level issue - // waiting to bite - return age - } - - "Distance" -> { - val distance = Distance() - distance.setValue(value.getValue()) - distance.setCode(value.getCode()) - distance.setUnit(value.getUnit()) - distance.setSystem(value.getSystem()) - distance.setComparator(value.getComparator()) - // TODO: Ensure distance constraints are met, else return null - return distance - } - - "Duration" -> { - val duration = Duration() - duration.setValue(value.getValue()) - duration.setCode(value.getCode()) - duration.setUnit(value.getUnit()) - duration.setSystem(value.getSystem()) - duration.setComparator(value.getComparator()) - // TODO: Ensure duration constraints are met, else return null - return duration - } - - "Count" -> { - val count = Count() - count.setValue(value.getValue()) - count.setCode(value.getCode()) - count.setUnit(value.getUnit()) - count.setSystem(value.getSystem()) - count.setComparator(value.getComparator()) - // TODO: Ensure count constraints are met, else return null - return count - } - - "SimpleQuantity" -> - return value.castToSimpleQuantity( - value - ) // NOTE: This is wrong in that it is copying the comparator, it should be - "MoneyQuantity" -> { - val moneyQuantity = MoneyQuantity() - moneyQuantity.setValue(value.getValue()) - moneyQuantity.setCode(value.getCode()) - moneyQuantity.setUnit(value.getUnit()) - moneyQuantity.setSystem(value.getSystem()) - moneyQuantity.setComparator(value.getComparator()) - // TODO: Ensure money constraints are met, else return null - return moneyQuantity - } - - else -> {} - } - } - - if (isStrict) { - throw InvalidCast( - "Cannot cast a value of type ${value.javaClass.name} as ${type.name}." - ) - } - - return null - } - override fun getContextPath(contextType: String?, targetType: String?): Any? { if (targetType == null || contextType == null) { return null diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R5FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R5FhirModelResolver.kt index 2c99edead..1fa97b44d 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R5FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/R5FhirModelResolver.kt @@ -5,34 +5,16 @@ import ca.uhn.fhir.context.FhirVersionEnum import java.util.* import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.instance.model.api.IBaseResource -import org.hl7.fhir.r5.model.Age import org.hl7.fhir.r5.model.Base import org.hl7.fhir.r5.model.BaseDateTimeType -import org.hl7.fhir.r5.model.CanonicalType -import org.hl7.fhir.r5.model.CodeType -import org.hl7.fhir.r5.model.Count -import org.hl7.fhir.r5.model.Distance -import org.hl7.fhir.r5.model.Duration import org.hl7.fhir.r5.model.EnumFactory import org.hl7.fhir.r5.model.Enumeration import org.hl7.fhir.r5.model.Enumerations.FHIRTypes import org.hl7.fhir.r5.model.IdType -import org.hl7.fhir.r5.model.IntegerType -import org.hl7.fhir.r5.model.MarkdownType -import org.hl7.fhir.r5.model.MoneyQuantity -import org.hl7.fhir.r5.model.OidType -import org.hl7.fhir.r5.model.PositiveIntType -import org.hl7.fhir.r5.model.PrimitiveType import org.hl7.fhir.r5.model.Quantity import org.hl7.fhir.r5.model.Resource import org.hl7.fhir.r5.model.SimpleQuantity -import org.hl7.fhir.r5.model.StringType import org.hl7.fhir.r5.model.TimeType -import org.hl7.fhir.r5.model.UnsignedIntType -import org.hl7.fhir.r5.model.UriType -import org.hl7.fhir.r5.model.UrlType -import org.hl7.fhir.r5.model.UuidType -import org.opencds.cqf.cql.engine.exception.InvalidCast import org.opencds.cqf.cql.engine.runtime.BaseTemporal open class R5FhirModelResolver(fhirContext: FhirContext) : @@ -102,25 +84,6 @@ open class R5FhirModelResolver(fhirContext: FhirContext) : } } - override fun equalsDeep(left: Base, right: Base): Boolean { - return left.equalsDeep(right) - } - - override fun resolveProperty(target: Any?, path: String): Any? { - // This is kind of a hack to get around contained resources - HAPI doesn't have - // ResourceContainer type for R5 - if (target is Resource && target.fhirType() == path) { - return target - } - - // Account for extensions on primitives - if (target is PrimitiveType<*> && path == "extension") { - return target.getExtension() - } - - return super.resolveProperty(target, path) - } - override fun castToSimpleQuantity(base: Base): SimpleQuantity { when (base) { is SimpleQuantity -> return base @@ -204,216 +167,6 @@ open class R5FhirModelResolver(fhirContext: FhirContext) : return super.resolveType(typeName) } - /* - * Casting of derived primitives: Datatypes that derive from datatypes other - * than Element are - * actually profiles // Types that exhibit this behavior are: // url: uri // - * canonical: uri // - * uuid: uri // oid: uri // positiveInt: integer // unsignedInt: integer // - * code: string // - * markdown: string // id: string - * - */ - override fun `is`(value: Any?, type: Class<*>?): Boolean? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return true - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is UriType) { - when (type.getSimpleName()) { - "UrlType" -> return true - "CanonicalType" -> return true - "AnnotatedUuidType", - "UuidType" -> return true - "OidType" -> return true - else -> {} - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> return true - "UnsignedIntType" -> return true - else -> {} - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> return true - "MarkdownType" -> return true - "IdType" -> return true - else -> {} - } - } - - // TODO: These should really be using profile validation - // TODO: These should not return true unless the constraints that are used in the as logic - // below return true - if (value is Quantity) { - when (type.getSimpleName()) { - "Age", - "Distance", - "Duration", - "Count", - "SimpleQuantity", - "MoneyQuantity" -> return true - else -> {} - } - } - - return false - } - - override fun `as`(value: Any?, type: Class<*>?, isStrict: Boolean): Any? { - if (value == null) { - return null - } - - if (type!!.isAssignableFrom(value.javaClass)) { - return value - } - - if (value is UriType) { - when (type.getSimpleName()) { - "UrlType" -> - return if (value.hasPrimitiveValue()) UrlType(value.primitiveValue()) else null - "CanonicalType" -> - return if (value.hasPrimitiveValue()) CanonicalType(value.primitiveValue()) - else null - "AnnotatedUuidType", - "UuidType" -> - return if (value.hasPrimitiveValue() && value.value.startsWith("urn:uuid:")) - UuidType(value.primitiveValue()) - else null - - "OidType" -> - return if (value.hasPrimitiveValue() && value.value.startsWith("urn:oid:")) - OidType(value.primitiveValue()) - else null // castToOid(uriType); Throws an exception, not implemented - else -> {} - } - } - - if (value is IntegerType) { - when (type.getSimpleName()) { - "PositiveIntType" -> - return if (value.hasPrimitiveValue() && value.value > 0) - PositiveIntType(value.primitiveValue()) - else null // integerType.castToPositiveInt(integerType); Throws an - "UnsignedIntType" -> - return if (value.hasPrimitiveValue() && value.value >= 0) - UnsignedIntType(value.primitiveValue()) - else null // castToUnsignedInt(integerType); Throws an exception, not - else -> {} - } - } - - if (value is StringType) { - when (type.getSimpleName()) { - "CodeType" -> - return if (value.hasPrimitiveValue()) CodeType(value.primitiveValue()) else null - "MarkdownType" -> - return if (value.hasPrimitiveValue()) MarkdownType(value.primitiveValue()) - else null - "IdType" -> - return if (value.hasPrimitiveValue()) IdType(value.primitiveValue()) - else null // stringType.castToId(stringType); - else -> {} - } - } - - if (value is Quantity) { - when (type.getSimpleName()) { - "Age" -> { - val age = Age() - age.setValue(value.getValue()) - age.setCode(value.getCode()) - age.setUnit(value.getUnit()) - age.setSystem(value.getSystem()) - age.setComparator(value.getComparator()) - // TODO: Ensure age constraints are met, else return null (Except that we can't - // do this - // unless we can be assured that the same constraints have resulted in true when - // we called 'is' - // As it's written now, any Quantity will return true if you ask if it's an Age - // Same is true for all the subtypes, and so that's a type-system level issue - // waiting to bite - return age - } - - "Distance" -> { - val distance = Distance() - distance.setValue(value.getValue()) - distance.setCode(value.getCode()) - distance.setUnit(value.getUnit()) - distance.setSystem(value.getSystem()) - distance.setComparator(value.getComparator()) - // TODO: Ensure distance constraints are met, else return null - return distance - } - - "Duration" -> { - val duration = Duration() - duration.setValue(value.getValue()) - duration.setCode(value.getCode()) - duration.setUnit(value.getUnit()) - duration.setSystem(value.getSystem()) - duration.setComparator(value.getComparator()) - // TODO: Ensure duration constraints are met, else return null - return duration - } - - "Count" -> { - val count = Count() - count.setValue(value.getValue()) - count.setCode(value.getCode()) - count.setUnit(value.getUnit()) - count.setSystem(value.getSystem()) - count.setComparator(value.getComparator()) - // TODO: Ensure count constraints are met, else return null - return count - } - - "SimpleQuantity" -> - return castToSimpleQuantity(value) // NOTE: This is wrong in that it is - "MoneyQuantity" -> { - val moneyQuantity = MoneyQuantity() - moneyQuantity.setValue(value.getValue()) - moneyQuantity.setCode(value.getCode()) - moneyQuantity.setUnit(value.getUnit()) - moneyQuantity.setSystem(value.getSystem()) - moneyQuantity.setComparator(value.getComparator()) - // TODO: Ensure money constraints are met, else return null - return moneyQuantity - } - - else -> {} - } - } - - if (isStrict) { - throw InvalidCast( - "Cannot cast a value of type ${value.javaClass.name} as ${type.name}." - ) - } - - return null - } - override fun getContextPath(contextType: String?, targetType: String?): Any? { if (targetType == null || contextType == null) { return null diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/RestFhirRetrieveProvider.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/RestFhirRetrieveProvider.kt index f29ab50f0..03aadb25b 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/RestFhirRetrieveProvider.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/RestFhirRetrieveProvider.kt @@ -12,13 +12,13 @@ import org.hl7.fhir.dstu3.model.Coding import org.hl7.fhir.instance.model.api.IBaseBundle import org.hl7.fhir.instance.model.api.IBaseCoding import org.hl7.fhir.instance.model.api.IBaseResource +import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterMap import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver -import org.opencds.cqf.cql.engine.model.ModelResolver class RestFhirRetrieveProvider( searchParameterResolver: SearchParameterResolver, - modelResolver: ModelResolver, + modelResolver: FhirModelResolver<*, *, *, *, *, *, *, *>, private val fhirClient: IGenericClient, ) : SearchParamFhirRetrieveProvider(searchParameterResolver, modelResolver) { var searchStyle: SearchStyleEnum = DEFAULT_SEARCH_STYLE diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/SearchParamFhirRetrieveProvider.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/SearchParamFhirRetrieveProvider.kt index 4b37e4e58..2bd1ea4cc 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/SearchParamFhirRetrieveProvider.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/retrieve/SearchParamFhirRetrieveProvider.kt @@ -3,9 +3,9 @@ package org.opencds.cqf.cql.engine.fhir.retrieve import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import org.opencds.cqf.cql.engine.fhir.exception.FhirVersionMisMatchException +import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterMap import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver -import org.opencds.cqf.cql.engine.model.ModelResolver import org.opencds.cqf.cql.engine.retrieve.TerminologyAwareRetrieveProvider import org.opencds.cqf.cql.engine.runtime.Code import org.opencds.cqf.cql.engine.runtime.Interval @@ -13,7 +13,7 @@ import org.opencds.cqf.cql.engine.runtime.Interval abstract class SearchParamFhirRetrieveProvider protected constructor( val searchParameterResolver: SearchParameterResolver, - val modelResolver: ModelResolver, + val modelResolver: FhirModelResolver<*, *, *, *, *, *, *, *>, ) : TerminologyAwareRetrieveProvider() { protected val fhirContext: FhirContext get() = searchParameterResolver.fhirContext @@ -105,6 +105,6 @@ protected constructor( dateRange, ) - return this.executeQueries(dataType, queries) + return this.executeQueries(dataType, queries)?.map { modelResolver.toCqlValue(it) } } } diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt index 073fceb49..704fc26b3 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt @@ -3,14 +3,6 @@ package org.hl7.fhirpath import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import com.google.common.collect.Sets -import org.hl7.fhir.dstu3.model.BaseDateTimeType -import org.hl7.fhir.dstu3.model.BooleanType -import org.hl7.fhir.dstu3.model.Coding -import org.hl7.fhir.dstu3.model.DecimalType -import org.hl7.fhir.dstu3.model.Enumeration -import org.hl7.fhir.dstu3.model.IntegerType -import org.hl7.fhir.dstu3.model.Quantity -import org.hl7.fhir.dstu3.model.StringType import org.hl7.fhirpath.tests.Group import org.hl7.fhirpath.tests.Test import org.junit.jupiter.api.Assumptions @@ -24,7 +16,7 @@ import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.fhir.retrieve.RestFhirRetrieveProvider import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver -import org.opencds.cqf.cql.engine.runtime.Code +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance class CQLOperationsDstu3Test : TestFhirPath() { @ParameterizedTest(name = "{0}") @@ -41,53 +33,59 @@ class CQLOperationsDstu3Test : TestFhirPath() { state: State?, resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, ): Boolean? { - // Perform FHIR system-defined type conversions - var actualResult = actualResult - when (actualResult) { - is Enumeration<*> -> { - actualResult = actualResult.valueAsString - } - - is BooleanType -> { - actualResult = actualResult.value - } - - is IntegerType -> { - actualResult = actualResult.value - } - - is DecimalType -> { - actualResult = actualResult.value - } - - is StringType -> { - actualResult = actualResult.value - } - - is BaseDateTimeType -> { - actualResult = resolver.toJavaPrimitive(actualResult, actualResult) - } - - is Quantity -> { - val quantity = actualResult - actualResult = - org.opencds.cqf.cql.engine.runtime - .Quantity() - .withValue(quantity.getValue()) - .withUnit(quantity.getUnit()) - } - - is Coding -> { - val coding = actualResult - actualResult = - Code() - .withCode(coding.getCode()) - .withDisplay(coding.getDisplay()) - .withSystem(coding.getSystem()) - .withVersion(coding.getVersion()) - } + if (actualResult is CqlClassInstance && actualResult.elements.containsKey("value")) { + return EqualEvaluator.equal(expectedResult, actualResult.elements["value"], state) } + return EqualEvaluator.equal(expectedResult, actualResult, state) + + // // Perform FHIR system-defined type conversions + // var actualResult = actualResult + // when (actualResult) { + // is Enumeration<*> -> { + // actualResult = actualResult.valueAsString + // } + // + // is BooleanType -> { + // actualResult = actualResult.value + // } + // + // is IntegerType -> { + // actualResult = actualResult.value + // } + // + // is DecimalType -> { + // actualResult = actualResult.value + // } + // + // is StringType -> { + // actualResult = actualResult.value + // } + // + // is BaseDateTimeType -> { + // actualResult = resolver.toJavaPrimitive(actualResult, actualResult) + // } + // + // is Quantity -> { + // val quantity = actualResult + // actualResult = + // org.opencds.cqf.cql.engine.runtime + // .Quantity() + // .withValue(quantity.getValue()) + // .withUnit(quantity.getUnit()) + // } + // + // is Coding -> { + // val coding = actualResult + // actualResult = + // Code() + // .withCode(coding.getCode()) + // .withDisplay(coding.getDisplay()) + // .withSystem(coding.getSystem()) + // .withVersion(coding.getVersion()) + // } + // } + // return EqualEvaluator.equal(expectedResult, actualResult, state) } companion object { diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt index 330a671b9..ebefb78d3 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt @@ -3,14 +3,6 @@ package org.hl7.fhirpath import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import com.google.common.collect.Sets -import org.hl7.fhir.r4.model.BaseDateTimeType -import org.hl7.fhir.r4.model.BooleanType -import org.hl7.fhir.r4.model.Coding -import org.hl7.fhir.r4.model.DecimalType -import org.hl7.fhir.r4.model.Enumeration -import org.hl7.fhir.r4.model.IntegerType -import org.hl7.fhir.r4.model.Quantity -import org.hl7.fhir.r4.model.StringType import org.hl7.fhirpath.tests.Group import org.hl7.fhirpath.tests.Test import org.junit.jupiter.api.Assumptions @@ -24,7 +16,7 @@ import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver import org.opencds.cqf.cql.engine.fhir.retrieve.RestFhirRetrieveProvider import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver -import org.opencds.cqf.cql.engine.runtime.Code +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance class CQLOperationsR4Test : TestFhirPath() { @ParameterizedTest(name = "{0}") @@ -40,53 +32,59 @@ class CQLOperationsR4Test : TestFhirPath() { state: State?, resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, ): Boolean? { - // Perform FHIR system-defined type conversions - var actualResult = actualResult - when (actualResult) { - is Enumeration<*> -> { - actualResult = actualResult.valueAsString - } - - is BooleanType -> { - actualResult = actualResult.value - } - - is IntegerType -> { - actualResult = actualResult.value - } - - is DecimalType -> { - actualResult = actualResult.value - } - - is StringType -> { - actualResult = actualResult.value - } - - is BaseDateTimeType -> { - actualResult = resolver.toJavaPrimitive(actualResult, actualResult) - } - - is Quantity -> { - val quantity = actualResult - actualResult = - org.opencds.cqf.cql.engine.runtime - .Quantity() - .withValue(quantity.getValue()) - .withUnit(quantity.getUnit()) - } - - is Coding -> { - val coding = actualResult - actualResult = - Code() - .withCode(coding.getCode()) - .withDisplay(coding.getDisplay()) - .withSystem(coding.getSystem()) - .withVersion(coding.getVersion()) - } + if (actualResult is CqlClassInstance && actualResult.elements.containsKey("value")) { + return EqualEvaluator.equal(expectedResult, actualResult.elements["value"], state) } + return EqualEvaluator.equal(expectedResult, actualResult, state) + + // // Perform FHIR system-defined type conversions + // var actualResult = actualResult + // when (actualResult) { + // is Enumeration<*> -> { + // actualResult = actualResult.valueAsString + // } + // + // is BooleanType -> { + // actualResult = actualResult.value + // } + // + // is IntegerType -> { + // actualResult = actualResult.value + // } + // + // is DecimalType -> { + // actualResult = actualResult.value + // } + // + // is StringType -> { + // actualResult = actualResult.value + // } + // + // is BaseDateTimeType -> { + // actualResult = resolver.toJavaPrimitive(actualResult, actualResult) + // } + // + // is Quantity -> { + // val quantity = actualResult + // actualResult = + // org.opencds.cqf.cql.engine.runtime + // .Quantity() + // .withValue(quantity.getValue()) + // .withUnit(quantity.getUnit()) + // } + // + // is Coding -> { + // val coding = actualResult + // actualResult = + // Code() + // .withCode(coding.getCode()) + // .withDisplay(coding.getDisplay()) + // .withSystem(coding.getSystem()) + // .withVersion(coding.getVersion()) + // } + // } + // return EqualEvaluator.equal(expectedResult, actualResult, state) } companion object { @@ -247,6 +245,9 @@ class CQLOperationsR4Test : TestFhirPath() { "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent13", "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent17", "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent21", + "r4/tests-fhir-r4/testObservations/testPolymorphismIsA3", // The engine correctly + // returns false but the + // tests expects null. "r4/tests-fhir-r4/testPower/testPower3", "r4/tests-fhir-r4/testPrecedence/testPrecedence3", "r4/tests-fhir-r4/testPrecedence/testPrecedence4", diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt index 3dc85692d..736f64ffe 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt @@ -100,10 +100,11 @@ abstract class TestFhirPath { engine.state.environment.registerDataProvider("http://hl7.org/fhir", provider) if (testCase is Pass && testCase.resource != null) { val resource = testCase.resource - engine.state.setParameter(null, resource.fhirType(), resource) - engine.state.setParameter(null, "%context", resource) - engine.state.setParameter(null, "%resource", resource) - engine.state.setParameter(null, "%rootResource", resource) + val resourceAsCqlValue = resolver.toCqlValue(resource) + engine.state.setParameter(null, resource.fhirType(), resourceAsCqlValue) + engine.state.setParameter(null, "%context", resourceAsCqlValue) + engine.state.setParameter(null, "%resource", resourceAsCqlValue) + engine.state.setParameter(null, "%rootResource", resourceAsCqlValue) } var result: EvaluationResult? diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourceTestUtils.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourceTestUtils.kt index 38f8ce66c..ea6aca5de 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourceTestUtils.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourceTestUtils.kt @@ -6,6 +6,7 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.util.stream.Collectors +import kotlin.test.assertEquals import kotlinx.io.asSource import kotlinx.io.buffered import org.cqframework.cql.cql2elm.CqlCompiler @@ -30,9 +31,11 @@ import org.opencds.cqf.cql.engine.execution.CqlEngine import org.opencds.cqf.cql.engine.execution.EvaluationResult import org.opencds.cqf.cql.engine.execution.EvaluationResults import org.opencds.cqf.cql.engine.execution.ExpressionResult +import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider import org.opencds.cqf.cql.engine.runtime.Code +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance import org.opencds.cqf.cql.engine.runtime.Interval import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -51,7 +54,9 @@ internal object EvaluatedResourceTestUtils { val PROCEDURE: Procedure = Procedure().setId(IdType(ResourceType.Procedure.name, "Procedure1")) as Procedure - val RETRIEVE_PROVIDER: RetrieveProvider = + fun getRetrieveProvider( + fhirModelResolver: FhirModelResolver<*, *, *, *, *, *, *, *> + ): RetrieveProvider = object : RetrieveProvider { override fun retrieve( context: String?, @@ -68,11 +73,11 @@ internal object EvaluatedResourceTestUtils { dateRange: Interval?, ): Iterable? { return when (dataType) { - "Encounter" -> mutableListOf(ENCOUNTER) - "Condition" -> mutableListOf(CONDITION) - "Patient" -> mutableListOf(PATIENT) - "Procedure" -> mutableListOf(PROCEDURE) - else -> mutableListOf() + "Encounter" -> mutableListOf(fhirModelResolver.toCqlValue(ENCOUNTER)) + "Condition" -> mutableListOf(fhirModelResolver.toCqlValue(CONDITION)) + "Patient" -> mutableListOf(fhirModelResolver.toCqlValue(PATIENT)) + "Procedure" -> mutableListOf(fhirModelResolver.toCqlValue(PROCEDURE)) + else -> mutableListOf() } } } @@ -296,11 +301,25 @@ internal object EvaluatedResourceTestUtils { return VersionedIdentifier().withId(id) } - private fun extractResourcesInOrder(resourceCandidates: Collection<*>): List { + private fun extractResourceTypesAndIdsInOrder( + resourceCandidates: Collection<*> + ): List> { return resourceCandidates - .filter { obj: Any? -> IBaseResource::class.java.isInstance(obj) } - .map { obj: Any? -> IBaseResource::class.java.cast(obj) } - .sortedBy { it.idElement.idPart } + .filterIsInstance() + .map { it.fhirType() to it.idElement.idPart } + .sortedBy { it.second } + } + + private fun extractCqlFhirClassInstanceTypesAndIdsInOrder( + candidates: Collection<*> + ): List> { + return candidates + .filterIsInstance() + .map { + it.type.localPart to + (it.elements["id"] as CqlClassInstance).elements["value"] as String + } + .sortedBy { it.second } } private fun assertValuesEqual(expectedValue: Collection, actualValue: Any?) { @@ -323,8 +342,8 @@ internal object EvaluatedResourceTestUtils { CoreMatchers.`is`(expectedResources.size), ) - val expectedResourcesList = extractResourcesInOrder(expectedResources) - val actualResourcesList = extractResourcesInOrder(actualResources) + val expectedResourcesList = extractResourceTypesAndIdsInOrder(expectedResources) + val actualResourcesList = extractCqlFhirClassInstanceTypesAndIdsInOrder(actualResources) for (index in expectedResourcesList.indices) { val expectedResource = expectedResourcesList[0] @@ -353,17 +372,10 @@ internal object EvaluatedResourceTestUtils { } private fun assertResourcesEqual( - expectedResource: IBaseResource, - actualResource: IBaseResource, + expectedResourceTypeAndId: Pair, + actualResourceTypeAndId: Pair, ) { - MatcherAssert.assertThat( - actualResource.javaClass, - CoreMatchers.equalTo?>(expectedResource.javaClass), - ) - MatcherAssert.assertThat( - actualResource.idElement, - CoreMatchers.equalTo(expectedResource.idElement), - ) + assertEquals(expectedResourceTypeAndId, actualResourceTypeAndId) } private class TestRetrieveProvider : RetrieveProvider { diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsRetrieveProvider.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsRetrieveProvider.kt index 00f48fe01..ea29f4c93 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsRetrieveProvider.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsRetrieveProvider.kt @@ -9,13 +9,14 @@ import org.hl7.fhir.r4.model.IdType import org.hl7.fhir.r4.model.Period import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.ResourceType +import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider import org.opencds.cqf.cql.engine.runtime.Code import org.opencds.cqf.cql.engine.runtime.Interval -internal enum class EvaluatedResourcesMultiLibComplexDepsRetrieveProvider : RetrieveProvider { - INSTANCE; - +class EvaluatedResourcesMultiLibComplexDepsRetrieveProvider( + val fhirModelResolver: FhirModelResolver<*, *, *, *, *, *, *, *> +) : RetrieveProvider { override fun retrieve( context: String?, contextPath: String?, @@ -31,10 +32,10 @@ internal enum class EvaluatedResourcesMultiLibComplexDepsRetrieveProvider : Retr dateRange: Interval?, ): Iterable? { return when (dataType) { - "Encounter" -> this.encounters - "Condition" -> this.conditions - "Patient" -> this.patients - "Procedure" -> this.procedures + "Encounter" -> this.encounters.map { fhirModelResolver.toCqlValue(it) } + "Condition" -> this.conditions?.map { fhirModelResolver.toCqlValue(it) } + "Patient" -> this.patients?.map { fhirModelResolver.toCqlValue(it) } + "Procedure" -> this.procedures.map { fhirModelResolver.toCqlValue(it) } else -> mutableListOf() } } diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsTest.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsTest.kt index f7655de56..045190d8a 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsTest.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibComplexDepsTest.kt @@ -10,7 +10,6 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import org.opencds.cqf.cql.engine.execution.CqlEngine import org.opencds.cqf.cql.engine.fhir.data.EvaluatedResourcesMultiLibComplexDepsRetrieveProvider.Companion.allEncounters -import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider /** * See EvaluatedResourcesMultiLibComplexDepsTest.md for a mermaid diagram of the library @@ -112,7 +111,7 @@ internal class EvaluatedResourcesMultiLibComplexDepsTest : FhirExecutionMultiLib engineWithExistingLibraryManager, expressionCaching, r4ModelResolver, - RETRIEVE_PROVIDER_COMPLEX, + EvaluatedResourcesMultiLibComplexDepsRetrieveProvider(r4ModelResolver!!), ) } @@ -122,14 +121,11 @@ internal class EvaluatedResourcesMultiLibComplexDepsTest : FhirExecutionMultiLib engineWithNewLibraryManager, expressionCaching, r4ModelResolver, - RETRIEVE_PROVIDER_COMPLEX, + EvaluatedResourcesMultiLibComplexDepsRetrieveProvider(r4ModelResolver!!), ) } companion object { - private val RETRIEVE_PROVIDER_COMPLEX: RetrieveProvider = - EvaluatedResourcesMultiLibComplexDepsRetrieveProvider.INSTANCE - private val LIB_1A: VersionedIdentifier = EvaluatedResourceTestUtils.forId("EvaluatedResourcesMultiLibComplexDepsTest_Level1A") private val LIB_1B: VersionedIdentifier = diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibLinearDepsTest.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibLinearDepsTest.kt index 4abe3d4d3..169aefa97 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibLinearDepsTest.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesMultiLibLinearDepsTest.kt @@ -374,7 +374,7 @@ internal class EvaluatedResourcesMultiLibLinearDepsTest : FhirExecutionMultiLibT engineWithNewLibraryManager, expressionCaching, r4ModelResolver, - EvaluatedResourceTestUtils.RETRIEVE_PROVIDER, + EvaluatedResourceTestUtils.getRetrieveProvider(r4ModelResolver!!), ) } diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesTest.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesTest.kt index 8864c0a4d..bcc5d797b 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesTest.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/EvaluatedResourcesTest.kt @@ -90,7 +90,10 @@ internal class EvaluatedResourcesTest : FhirExecutionTestBase() { private fun getCqlEngineForFhir(expressionCaching: Boolean): CqlEngine { engine.state.environment.registerDataProvider( "http://hl7.org/fhir", - CompositeDataProvider(r4ModelResolver!!, EvaluatedResourceTestUtils.RETRIEVE_PROVIDER), + CompositeDataProvider( + r4ModelResolver!!, + EvaluatedResourceTestUtils.getRetrieveProvider(r4ModelResolver!!), + ), ) engine.cache.setExpressionCaching(expressionCaching) return engine diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1225.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1225.kt index df1a728b3..f2a5d81ce 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1225.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1225.kt @@ -32,7 +32,7 @@ internal class Issue1225 : FhirExecutionTestBase() { if (dataType == "Patient") { val p = Patient() p.getAddress().add(Address().addLine("123").addLine("456")) - return mutableListOf(p) + return mutableListOf(r4ModelResolver!!.toCqlValue(p)) } return mutableListOf() diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1226.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1226.kt index 8cb9da1e3..341b39727 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1226.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1226.kt @@ -31,10 +31,15 @@ internal class Issue1226 : FhirExecutionTestBase() { dateRange: Interval?, ): Iterable { when (dataType) { - "Patient" -> return mutableListOf(Patient().setId("123")) + "Patient" -> + return mutableListOf( + r4ModelResolver!!.toCqlValue(Patient().setId("123")) + ) "MedicationRequest" -> return mutableListOf( - MedicationRequest().setMedication(Reference("Medication/456")) + r4ModelResolver!!.toCqlValue( + MedicationRequest().setMedication(Reference("Medication/456")) + ) ) } diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1558.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1558.kt index 8440bfd85..e0e184d33 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1558.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1558.kt @@ -36,8 +36,8 @@ internal class Issue1558 : FhirExecutionTestBase() { dateRange: Interval?, ): Iterable? { return when (dataType) { - "Patient" -> mutableListOf(patient) - "CareTeam" -> mutableListOf(careTeam) + "Patient" -> mutableListOf(r4ModelResolver!!.toCqlValue(patient)) + "CareTeam" -> mutableListOf(r4ModelResolver!!.toCqlValue(careTeam)) else -> mutableListOf() } } diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1577.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1577.kt index ce2051304..0966948a3 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1577.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/Issue1577.kt @@ -33,6 +33,8 @@ class Issue1577 { .trimIndent() ) + val r4ModelResolver = CachedR4FhirModelResolver() + val retrieveProvider = object : RetrieveProvider { override fun retrieve( @@ -50,19 +52,25 @@ class Issue1577 { dateRange: Interval?, ): Iterable<*> = when (dataType) { - "Patient" -> listOf(Patient().setId("pat1")) + "Patient" -> listOf(r4ModelResolver.toCqlValue(Patient().setId("pat1"))) // Note: returning an Iterable implementation and not a list to test the // handling of different Iterable types "Condition" -> object : Iterable { override fun iterator(): Iterator<*> { - return listOf(Condition().setId("cond1")).iterator() + return listOf( + r4ModelResolver.toCqlValue(Condition().setId("cond1")) + ) + .iterator() } } "Observation" -> object : Iterable { override fun iterator(): Iterator<*> { - return listOf(Observation().setId("obs1")).iterator() + return listOf( + r4ModelResolver.toCqlValue(Observation().setId("obs1")) + ) + .iterator() } } else -> listOf() @@ -70,7 +78,7 @@ class Issue1577 { } engine.state.environment.registerDataProvider( "http://hl7.org/fhir", - CompositeDataProvider(CachedR4FhirModelResolver(), retrieveProvider), + CompositeDataProvider(r4ModelResolver, retrieveProvider), ) val evaluationResult = engine.evaluate { library("Issue1577") }.onlyResultOrThrow diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/IssueSortByFluentFunction.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/IssueSortByFluentFunction.kt index eaba154f2..57f12a678 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/IssueSortByFluentFunction.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/IssueSortByFluentFunction.kt @@ -18,6 +18,8 @@ internal class IssueSortByFluentFunction : FhirExecutionTestBase() { @Test fun observationsSortedByFluentFunctionAreSorted() { val patient = Patient().setId("123") + val patientCqlValue = r4ModelResolver!!.toCqlValue(patient) + val obs1 = Observation() obs1.setId("A") val period1 = @@ -25,6 +27,7 @@ internal class IssueSortByFluentFunction : FhirExecutionTestBase() { .setStartElement(DateTimeType("2020-01-01")) .setEndElement(DateTimeType("2020-01-02")) obs1.setEffective(period1) + val obs1CqlValue = r4ModelResolver!!.toCqlValue(obs1) val obs2 = Observation() obs2.setId("B") @@ -33,6 +36,7 @@ internal class IssueSortByFluentFunction : FhirExecutionTestBase() { .setStartElement(DateTimeType("2020-01-03")) .setEndElement(DateTimeType("2020-01-04")) obs2.setEffective(period2) + val obs2CqlValue = r4ModelResolver!!.toCqlValue(obs2) val r = object : RetrieveProvider { @@ -51,9 +55,12 @@ internal class IssueSortByFluentFunction : FhirExecutionTestBase() { dateRange: Interval?, ): Iterable? { return when (dataType) { - "Patient" -> mutableListOf(patient) + "Patient" -> mutableListOf(patientCqlValue) "Observation" -> - listOf(obs2, obs1) // Intentionally out of order to test sorting + listOf( + obs2CqlValue, + obs1CqlValue, + ) // Intentionally out of order to test sorting else -> mutableListOf() } } @@ -70,7 +77,7 @@ internal class IssueSortByFluentFunction : FhirExecutionTestBase() { .value val obs = Assertions.assertInstanceOf(MutableList::class.java, result) - Assertions.assertEquals(obs1, obs!![0]) - Assertions.assertEquals(obs2, obs[1]) + Assertions.assertEquals(obs1CqlValue, obs!![0]) + Assertions.assertEquals(obs2CqlValue, obs[1]) } } diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.kt index 27c74801e..bcb543c53 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestCqlEngineRelatedContextSupport.kt @@ -4,8 +4,9 @@ import java.time.LocalDate import java.time.Month import java.time.ZoneId import java.util.* -import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert +import kotlin.test.assertEquals +import kotlin.test.assertIs +import org.cqframework.cql.shared.QName import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Practitioner @@ -17,6 +18,7 @@ import org.opencds.cqf.cql.engine.data.CompositeDataProvider import org.opencds.cqf.cql.engine.execution.CqlEngine import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider import org.opencds.cqf.cql.engine.runtime.Code +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance import org.opencds.cqf.cql.engine.runtime.Interval import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -36,55 +38,42 @@ internal class TestCqlEngineRelatedContextSupport : FhirExecutionTestBase() { val resultPatient = evaluate(cqlEngine, PATIENT, initialContext) - MatcherAssert.assertThat( - resultPatient, - CoreMatchers.instanceOf(Patient::class.java), - ) - val resultPatientCasted = resultPatient as Patient - MatcherAssert.assertThat( - resultPatientCasted.getId(), - CoreMatchers.`is`(_PATIENT_123), + assertIs(resultPatient) + assertEquals(QName("http://hl7.org/fhir", "Patient"), resultPatient.type) + assertEquals( + _PATIENT_123, + (resultPatient.elements["id"] as CqlClassInstance).elements["value"], ) + cqlEngine.state.clearEvaluatedResources() val resultPrimaryCareDoctor = evaluate(cqlEngine, PRIMARY_CARE_DOCTOR, initialContext) - - MatcherAssert.assertThat( - resultPrimaryCareDoctor, - CoreMatchers.instanceOf(Practitioner::class.java), - ) - val resultPractitioner = resultPrimaryCareDoctor as Practitioner - MatcherAssert.assertThat( - resultPractitioner, - CoreMatchers.instanceOf(Practitioner::class.java), - ) - MatcherAssert.assertThat( - resultPractitioner.getId(), - CoreMatchers.`is`(XYZ), + assertIs(resultPrimaryCareDoctor) + assertEquals(QName("http://hl7.org/fhir", "Practitioner"), resultPrimaryCareDoctor.type) + assertEquals( + XYZ, + (resultPrimaryCareDoctor.elements["id"] as CqlClassInstance).elements["value"], ) + cqlEngine.state.clearEvaluatedResources() val resultAllPatientForGp = evaluate(cqlEngine, ALL_PATIENT_FOR_GP, initialContext) cqlEngine.state.clearEvaluatedResources() - MatcherAssert.assertThat( - resultAllPatientForGp, - CoreMatchers.instanceOf(MutableList::class.java), - ) + assertIs>(resultAllPatientForGp) val patientsForPractitioner = - (resultAllPatientForGp as MutableList<*>) - .filter { obj: Any? -> Patient::class.java.isInstance(obj) } - .map { obj: Any? -> Patient::class.java.cast(obj) } - - MatcherAssert.assertThat(patientsForPractitioner.size, CoreMatchers.`is`(3)) - MatcherAssert.assertThat( - patientsForPractitioner.map { obj: Patient? -> obj!!.getId() }.toSet(), - CoreMatchers.`is`( - setOf(PATIENT_123, PATIENT_456, PATIENT_789) - .map { obj: Patient? -> obj!!.getId() } - .toSet() - ), + resultAllPatientForGp.filterIsInstance().filter { + it.type == QName("http://hl7.org/fhir", "Patient") + } + + assertEquals(3, patientsForPractitioner.size) + + assertEquals( + setOf(PATIENT_123, PATIENT_456, PATIENT_789).map { it.getId() }.toSet(), + patientsForPractitioner + .map { (it.elements["id"] as CqlClassInstance).elements["value"] } + .toSet(), ) } @@ -103,6 +92,87 @@ internal class TestCqlEngineRelatedContextSupport : FhirExecutionTestBase() { return evaluateResult[expression]!!.value } + private val retrieveProvider = + object : RetrieveProvider { + override fun retrieve( + context: String?, + contextPath: String?, + contextValue: Any?, + dataType: String, + templateId: String?, + codePath: String?, + codes: Iterable?, + valueSet: String?, + datePath: String?, + dateLowPath: String?, + dateHighPath: String?, + dateRange: Interval?, + ): Iterable? { + val allPatients = + setOf(PATIENT_123, PATIENT_456, PATIENT_789, PATIENT_ABC, PATIENT_DEF) + val allPractitioners = setOf(PRACTITIONER_XYZ, PRACTITIONER_ZULU) + + // a) All matching patients for the patient being searched by ID=123 + if ( + PATIENT == dataType && + PATIENT == context && + ID == contextPath && + _PATIENT_123 == contextValue + ) { + return allPatients + .filter { patient -> _PATIENT_123 == patient.getId() } + .map { r4ModelResolver!!.toCqlValue(it) } + } + + // b) All practitioners matching XYZ and patient 123 + if ( + PRACTITIONER == dataType && + PATIENT == context && + ID == codePath && + codesEqual(codes, PRACTITIONER_SLASH + XYZ) + ) { + val optPatient123 = + allPatients.firstOrNull { patient -> _PATIENT_123 == patient.getId() } + + if (optPatient123 != null) { + val generalPractitionerIds = + optPatient123 + .getGeneralPractitioner() + .map { obj -> obj!!.getReference() } + .map { ref -> + ref!! + .split(PRACTITIONER_SLASH.toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray()[1] + } + + return allPractitioners + .filter { practitioner -> + generalPractitionerIds.contains(practitioner.getId()) + } + .map { r4ModelResolver!!.toCqlValue(it) } + } + } + + // c) All patients belonging to Patient 123'd generalPractitioner + val equals = "xyz" == contextValue.toString() + if ( + PATIENT == dataType && + PRACTITIONER == context && + GENERAL_PRACTITIONER == contextPath && + equals + ) { + logger.info(">>> patients for practitioner xyz") + return allPatients + .filter { patient -> + getMatchingPractitioners(patient).contains(PRACTITIONER_XYZ.getId()) + } + .map { r4ModelResolver!!.toCqlValue(it) } + } + return null + } + } + companion object { private val logger: Logger = LoggerFactory.getLogger(TestCqlEngineRelatedContextSupport::class.java) @@ -134,81 +204,6 @@ internal class TestCqlEngineRelatedContextSupport : FhirExecutionTestBase() { private val PATIENT_DEF: Patient = getPatient("def", LocalDate.of(1975, Month.AUGUST, 21), PRACTITIONER_ZULU) - private val retrieveProvider = - object : RetrieveProvider { - override fun retrieve( - context: String?, - contextPath: String?, - contextValue: Any?, - dataType: String, - templateId: String?, - codePath: String?, - codes: Iterable?, - valueSet: String?, - datePath: String?, - dateLowPath: String?, - dateHighPath: String?, - dateRange: Interval?, - ): Iterable? { - val allPatients = - setOf(PATIENT_123, PATIENT_456, PATIENT_789, PATIENT_ABC, PATIENT_DEF) - val allPractitioners = setOf(PRACTITIONER_XYZ, PRACTITIONER_ZULU) - - // a) All matching patients for the patient being searched by ID=123 - if ( - PATIENT == dataType && - PATIENT == context && - ID == contextPath && - _PATIENT_123 == contextValue - ) { - return allPatients.filter { patient -> _PATIENT_123 == patient.getId() } - } - - // b) All practitioners matching XYZ and patient 123 - if ( - PRACTITIONER == dataType && - PATIENT == context && - ID == codePath && - codesEqual(codes, PRACTITIONER_SLASH + XYZ) - ) { - val optPatient123 = - allPatients.firstOrNull { patient -> _PATIENT_123 == patient.getId() } - - if (optPatient123 != null) { - val generalPractitionerIds = - optPatient123 - .getGeneralPractitioner() - .map { obj -> obj!!.getReference() } - .map { ref -> - ref!! - .split(PRACTITIONER_SLASH.toRegex()) - .dropLastWhile { it.isEmpty() } - .toTypedArray()[1] - } - - return allPractitioners.filter { practitioner -> - generalPractitionerIds.contains(practitioner.getId()) - } - } - } - - // c) All patients belonging to Patient 123'd generalPractitioner - val equals = "xyz" == contextValue.toString() - if ( - PATIENT == dataType && - PRACTITIONER == context && - GENERAL_PRACTITIONER == contextPath && - equals - ) { - logger.info(">>> patients for practitioner xyz") - return allPatients.filter { patient -> - getMatchingPractitioners(patient).contains(PRACTITIONER_XYZ.getId()) - } - } - return null - } - } - // TODO: LD: Due to a type erasure and the CQL compiler historically being in separate // repositories, two different // code paths were merged, resulting in an insidious condition where type erasure has diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestPrimitiveProfiles.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestPrimitiveProfiles.kt index e426972b8..794638a73 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestPrimitiveProfiles.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/data/TestPrimitiveProfiles.kt @@ -1,24 +1,6 @@ package org.opencds.cqf.cql.engine.fhir.data -import java.math.BigDecimal -import org.hamcrest.CoreMatchers -import org.hamcrest.MatcherAssert -import org.hl7.fhir.r4.model.Age -import org.hl7.fhir.r4.model.CanonicalType -import org.hl7.fhir.r4.model.CodeType -import org.hl7.fhir.r4.model.Count -import org.hl7.fhir.r4.model.Distance -import org.hl7.fhir.r4.model.Duration -import org.hl7.fhir.r4.model.IdType -import org.hl7.fhir.r4.model.MarkdownType -import org.hl7.fhir.r4.model.MoneyQuantity -import org.hl7.fhir.r4.model.OidType -import org.hl7.fhir.r4.model.PositiveIntType -import org.hl7.fhir.r4.model.SimpleQuantity -import org.hl7.fhir.r4.model.UnsignedIntType -import org.hl7.fhir.r4.model.UriType -import org.hl7.fhir.r4.model.UrlType -import org.hl7.fhir.r4.model.UuidType +import kotlin.test.assertNull import org.junit.jupiter.api.Test internal class TestPrimitiveProfiles : FhirExecutionTestBase() { @@ -32,155 +14,104 @@ internal class TestPrimitiveProfiles : FhirExecutionTestBase() { define CastToUrl: UrlUri as FHIR.url */ var value = results["CastToUrl"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(UrlType::class.java)) - MatcherAssert.assertThat((value as UriType).value, CoreMatchers.`is`("http://example.org")) + assertNull(value) /* define CanonicalUri: FHIR.uri { value: 'http://example.org/StructureDefinition/profile' } define CastToCanonical: CanonicalUri as FHIR.canonical */ value = results["CastToCanonical"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(CanonicalType::class.java)) - MatcherAssert.assertThat( - (value as CanonicalType).value, - CoreMatchers.`is`("http://example.org/StructureDefinition/profile"), - ) + assertNull(value) /* define UuidUri: FHIR.uri { value: 'urn:uuid:d27ceea4-e506-42a4-8111-f01c003a11c4' } define CastToUuid: UuidUri as FHIR.uuid */ value = results["CastToUuid"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(UuidType::class.java)) - MatcherAssert.assertThat( - (value as UuidType).value, - CoreMatchers.`is`("urn:uuid:d27ceea4-e506-42a4-8111-f01c003a11c4"), - ) + assertNull(value) /* define OidUri: FHIR.uri { value: 'urn:oid:2.16.840.1.113883.3.464.1004.1116' } define CastToOid: OidUri as FHIR.oid */ value = results["CastToOid"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(OidType::class.java)) - MatcherAssert.assertThat( - (value as OidType).value, - CoreMatchers.`is`("urn:oid:2.16.840.1.113883.3.464.1004.1116"), - ) + assertNull(value) /* define PositiveInt: FHIR.integer { value: 12 } define CastToPositiveInt: PositiveInt as FHIR.positiveInt */ value = results["CastToPositiveInt"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(PositiveIntType::class.java)) - MatcherAssert.assertThat((value as PositiveIntType).value, CoreMatchers.`is`(12)) + assertNull(value) /* define UnsignedInt: FHIR.integer { value: 12 } define CastToUnsignedInt: UnsignedInt as FHIR.unsignedInt */ value = results["CastToUnsignedInt"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(UnsignedIntType::class.java)) - MatcherAssert.assertThat((value as UnsignedIntType).value, CoreMatchers.`is`(12)) + assertNull(value) /* define CodeString: FHIR.string { value: '12345' } define CastToCode: CodeString as FHIR.code */ value = results["CastToCode"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(CodeType::class.java)) - MatcherAssert.assertThat((value as CodeType).value, CoreMatchers.`is`("12345")) + assertNull(value) /* define MarkdownString: FHIR.string { value: '# Markdown is [good](http://example.org)' } define CastToMarkdown: MarkdownString as FHIR.markdown */ value = results["CastToMarkdown"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(MarkdownType::class.java)) - MatcherAssert.assertThat( - (value as MarkdownType).value, - CoreMatchers.`is`("# Markdown is [good](http://example.org)"), - ) + assertNull(value) /* define IdString: FHIR.string { value: 'fhir-string' } define CastToId: IdString as FHIR.id */ value = results["CastToId"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(IdType::class.java)) - MatcherAssert.assertThat((value as IdType).value, CoreMatchers.`is`("fhir-string")) + assertNull(value) /* define SimpleQuantity: FHIR.Quantity { value: FHIR.decimal { value: 1.0 }, code: FHIR.code { value: 'mg' } } define CastToSimpleQuantity: FHIRQuantity as FHIR.SimpleQuantity */ value = results["CastToSimpleQuantity"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(SimpleQuantity::class.java)) - MatcherAssert.assertThat( - (value as SimpleQuantity).getValue().compareTo(BigDecimal("1.0")), - CoreMatchers.`is`(0), - ) - MatcherAssert.assertThat(value.getCode(), CoreMatchers.`is`("mg")) + assertNull(value) /* define AgeQuantity: FHIR.Quantity { value: FHIR.decimal { value: 10.0 }, code: FHIR.code { value: 'a' } } define CastToAge: AgeQuantity as FHIR.Age */ value = results["CastToAge"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(Age::class.java)) - MatcherAssert.assertThat( - (value as Age).getValue().compareTo(BigDecimal("10.0")), - CoreMatchers.`is`(0), - ) - MatcherAssert.assertThat(value.getCode(), CoreMatchers.`is`("a")) + assertNull(value) /* define DistanceQuantity: FHIR.Quantity { value: FHIR.decimal { value: 1200.0 }, code: FHIR.code { value: 'km' } } define CastToDistance: DistanceQuantity as FHIR.Distance */ value = results["CastToDistance"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(Distance::class.java)) - MatcherAssert.assertThat( - (value as Distance).getValue().compareTo(BigDecimal("1200.0")), - CoreMatchers.`is`(0), - ) - MatcherAssert.assertThat(value.getCode(), CoreMatchers.`is`("km")) + assertNull(value) /* define DurationQuantity: FHIR.Quantity { value: FHIR.decimal { value: 12.0 }, code: FHIR.code { value: 'a' } } define CastToDuration: DurationQuantity as FHIR.Duration */ value = results["CastToDuration"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(Duration::class.java)) - MatcherAssert.assertThat( - (value as Duration).getValue().compareTo(BigDecimal("12.0")), - CoreMatchers.`is`(0), - ) - MatcherAssert.assertThat(value.getCode(), CoreMatchers.`is`("a")) + assertNull(value) /* define CountQuantity: FHIR.Quantity { value: FHIR.decimal { value: 100 }, code: FHIR.code { value: '1' } } define CastToCount: CountQuantity as FHIR.Count */ value = results["CastToCount"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(Count::class.java)) - MatcherAssert.assertThat( - (value as Count).getValue().compareTo(BigDecimal("100")), - CoreMatchers.`is`(0), - ) - MatcherAssert.assertThat(value.getCode(), CoreMatchers.`is`("1")) + assertNull(value) /* define MoneyQuantity: FHIR.Quantity { value: FHIR.decimal { value: 12000.00 }, code: FHIR.code { value: '$' } } define CastToMoney: MoneyQuantity as FHIR.MoneyQuantity */ value = results["CastToMoney"]!!.value - MatcherAssert.assertThat(value, CoreMatchers.instanceOf(MoneyQuantity::class.java)) - MatcherAssert.assertThat( - (value as MoneyQuantity).getValue().compareTo(BigDecimal("12000.00")), - CoreMatchers.`is`(0), - ) - MatcherAssert.assertThat(value.getCode(), CoreMatchers.`is`("$")) + assertNull(value) } } diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu2ModelResolver.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu2ModelResolver.kt index e165537a1..b380e7f25 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu2ModelResolver.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu2ModelResolver.kt @@ -13,13 +13,11 @@ import org.hl7.fhir.dstu2.model.Enumerations import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.opencds.cqf.cql.engine.fhir.exception.UnknownType -import org.opencds.cqf.cql.engine.model.ModelResolver class TestDstu2ModelResolver { @Test fun resolverThrowsExceptionForUnknownType() { - val resolver: ModelResolver = - Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) + val resolver = Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) Assertions.assertThrows(UnknownType::class.java) { resolver.resolveType("ImpossibleTypeThatDoesn'tExistAndShouldBlowUp") } @@ -27,7 +25,7 @@ class TestDstu2ModelResolver { @Test fun resolveModelInfoTests() { - val resolver: ModelResolver = Dstu2FhirModelResolver() + val resolver = Dstu2FhirModelResolver() val mm = ModelManager() val m = mm.resolveModel(ModelIdentifier("FHIR", null, "1.0.2")) @@ -51,8 +49,7 @@ class TestDstu2ModelResolver { @Test fun resolveTypeTests() { - val resolver: ModelResolver = - Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) + val resolver = Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) for (type in Enumerations.DataType.entries) { // These are abstract types that should never be resolved directly. @@ -84,8 +81,7 @@ class TestDstu2ModelResolver { @Test fun createInstanceTests() { - val resolver: ModelResolver = - Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) + val resolver = Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) for (type in Enumerations.DataType.entries) { // These are abstract types that should never be resolved directly. @@ -96,7 +92,7 @@ class TestDstu2ModelResolver { else -> {} } - val instance = resolver.createInstance(type.toCode()) + val instance = resolver.createHapiInstance(type.toCode()) Assertions.assertNotNull(instance) } @@ -110,7 +106,7 @@ class TestDstu2ModelResolver { else -> {} } - val instance = resolver.createInstance(type.toCode()) + val instance = resolver.createHapiInstance(type.toCode()) Assertions.assertNotNull(instance) } @@ -118,7 +114,7 @@ class TestDstu2ModelResolver { for (enumType in enums) { // For the enums we actually expect an Enumeration with a factory of the correct // type to be created. - val instance = resolver.createInstance(enumType.getSimpleName()) as Enumeration<*>? + val instance = resolver.createHapiInstance(enumType.getSimpleName()) as Enumeration<*>? Assertions.assertNotNull(instance) val enumFactory: Field? @@ -139,8 +135,7 @@ class TestDstu2ModelResolver { @Test fun contextPathTests() { - val resolver: ModelResolver = - Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) + val resolver = Dstu2FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU2)) var path = resolver.getContextPath("Patient", "Patient") as String? Assertions.assertNotNull(path) diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu3ModelResolver.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu3ModelResolver.kt index 2da7add58..15d7473f6 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu3ModelResolver.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestDstu3ModelResolver.kt @@ -3,27 +3,27 @@ package org.opencds.cqf.cql.engine.fhir.model import ca.uhn.fhir.context.FhirContext import ca.uhn.fhir.context.FhirVersionEnum import java.math.BigDecimal +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull import org.cqframework.cql.cql2elm.ModelManager import org.hl7.cql.model.ModelIdentifier import org.hl7.elm_modelinfo.r1.ClassInfo import org.hl7.elm_modelinfo.r1.TypeInfo -import org.hl7.fhir.dstu3.model.Base import org.hl7.fhir.dstu3.model.DateTimeType import org.hl7.fhir.dstu3.model.Enumeration import org.hl7.fhir.dstu3.model.Enumerations import org.hl7.fhir.dstu3.model.Patient import org.hl7.fhir.dstu3.model.Quantity -import org.hl7.fhir.dstu3.model.SimpleQuantity import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.opencds.cqf.cql.engine.fhir.exception.UnknownType -import org.opencds.cqf.cql.engine.model.ModelResolver +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance internal class TestDstu3ModelResolver { @Test fun resolverThrowsExceptionForUnknownType() { - val resolver: ModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) Assertions.assertThrows(UnknownType::class.java) { resolver.resolveType("ImpossibleTypeThatDoesn'tExistAndShouldBlowUp") } @@ -32,8 +32,7 @@ internal class TestDstu3ModelResolver { // This tests all the top-level types HAPI knows about. @Test fun resolveTypeTests() { - val resolver: ModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) for (type in Enumerations.DataType.entries) { // These are abstract types that should never be resolved directly. @@ -66,8 +65,7 @@ internal class TestDstu3ModelResolver { // This tests all the types that are present in the ModelInfo. @Test fun resolveModelInfoTests() { - val resolver: ModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) val mm = ModelManager() val m = mm.resolveModel(ModelIdentifier("FHIR", null, "3.0.0")) @@ -94,8 +92,7 @@ internal class TestDstu3ModelResolver { // on the FhirContext or generalized logic, or fixed-up ModelInfos @Test fun modelInfoSpecialCaseTests() { - val resolver: ModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) // This tests resolution of inner classes. They aren't registered directly. resolver.resolveType("TestScriptRequestMethodCode") @@ -117,8 +114,7 @@ internal class TestDstu3ModelResolver { @Test fun createInstanceTests() { - val resolver: ModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) for (type in Enumerations.DataType.entries) { // These are abstract types that should never be resolved directly. @@ -129,7 +125,7 @@ internal class TestDstu3ModelResolver { else -> {} } - val instance = resolver.createInstance(type.toCode()) + val instance = resolver.createHapiInstance(type.toCode()) Assertions.assertNotNull(instance) } @@ -143,7 +139,7 @@ internal class TestDstu3ModelResolver { else -> {} } - val instance = resolver.createInstance(type.toCode()) + val instance = resolver.createHapiInstance(type.toCode()) Assertions.assertNotNull(instance) } @@ -151,7 +147,7 @@ internal class TestDstu3ModelResolver { for (enumType in enums) { // For the enums we actually expect an Enumeration with a factory of the correct // type to be created. - val instance = resolver.createInstance(enumType.getSimpleName()) as Enumeration<*>? + val instance = resolver.createHapiInstance(enumType.getSimpleName()) as Enumeration<*>? Assertions.assertNotNull(instance) Assertions.assertEquals( @@ -163,17 +159,16 @@ internal class TestDstu3ModelResolver { // These are some inner classes that don't appear in the enums above // This list is not exhaustive. It's meant as a spot check for the resolution // code. - var instance = resolver.createInstance("TestScriptRequestMethodCode") + var instance = resolver.createHapiInstance("TestScriptRequestMethodCode") Assertions.assertNotNull(instance) - instance = resolver.createInstance("FHIRDeviceStatus") + instance = resolver.createHapiInstance("FHIRDeviceStatus") Assertions.assertNotNull(instance) } @Test fun contextPathTests() { - val resolver: ModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) var path = resolver.getContextPath("Patient", "Patient") as String? Assertions.assertNotNull(path) @@ -225,27 +220,27 @@ internal class TestDstu3ModelResolver { @Test fun resolveMissingPropertyReturnsNull() { - val resolver: ModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) val p = Patient() - val value = resolver.resolvePath(p, "not-a-path") - Assertions.assertNull(value) + val patientAsCqlValue = resolver.toCqlValue(p) + assertIs(patientAsCqlValue) + assertFalse(patientAsCqlValue.elements.containsKey("not-a-path")) } @Test fun resolveNullEnumerationReturnsNull() { - val resolver: FhirModelResolver = - Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) + val resolver = Dstu3FhirModelResolver(FhirContext.forCached(FhirVersionEnum.DSTU3)) val q = Quantity() q.setValue(BigDecimal("10.0")) q.setUnit("1") val sq = resolver.castToSimpleQuantity(q) - val value = resolver.resolvePath(sq, "comparator") - Assertions.assertNull(value) + val value = resolver.toCqlValue(sq) + assertIs(value) + assertNull(value.elements["comparator"]) } @Test @@ -254,8 +249,8 @@ internal class TestDstu3ModelResolver { val dt = DateTimeType() - val value = resolver.resolvePath(dt, "value") - Assertions.assertNull(value) + val value = resolver.toCqlValue(dt) + assertNull(value) } companion object { diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.kt index 95addf54d..679fed109 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR4ModelResolver.kt @@ -5,37 +5,36 @@ import ca.uhn.fhir.context.FhirVersionEnum import java.math.BigDecimal import java.text.ParseException import java.util.* +import javax.xml.namespace.QName +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull import org.apache.commons.lang3.time.DateUtils import org.cqframework.cql.cql2elm.ModelManager -import org.hamcrest.MatcherAssert -import org.hamcrest.Matchers import org.hl7.cql.model.ModelIdentifier import org.hl7.elm_modelinfo.r1.ClassInfo import org.hl7.elm_modelinfo.r1.TypeInfo -import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.DateTimeType -import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Enumeration import org.hl7.fhir.r4.model.Enumerations import org.hl7.fhir.r4.model.Enumerations.DefinitionResourceType import org.hl7.fhir.r4.model.Enumerations.EventResourceType import org.hl7.fhir.r4.model.Enumerations.KnowledgeResourceType -import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Procedure import org.hl7.fhir.r4.model.Quantity -import org.hl7.fhir.r4.model.SimpleQuantity import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.VisionPrescription import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.opencds.cqf.cql.engine.fhir.exception.UnknownType -import org.opencds.cqf.cql.engine.model.ModelResolver +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance internal class TestR4ModelResolver { @Test fun resolverThrowsExceptionForUnknownType() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) Assertions.assertThrows(UnknownType::class.java) { resolver.resolveType("ImpossibleTypeThatDoesn'tExistAndShouldBlowUp") } @@ -43,7 +42,7 @@ internal class TestR4ModelResolver { @Test fun resolveTypeTests() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) for (type in Enumerations.DataType.entries) { // These are abstract types that should never be resolved directly. @@ -75,7 +74,7 @@ internal class TestR4ModelResolver { @Test fun modelInfoSpecialCaseTests() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) // This tests resolution of inner classes. They aren't registered directly. resolver.resolveType("TestScriptRequestMethodCode") @@ -113,7 +112,7 @@ internal class TestR4ModelResolver { @Test fun modelInfo400Tests() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) val mm = ModelManager() val m = mm.resolveModel(ModelIdentifier("FHIR", null, "4.0.1")) @@ -145,7 +144,7 @@ internal class TestR4ModelResolver { @Test @Throws(Exception::class) fun modelInfo401Tests() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) val mm = ModelManager() val m = mm.resolveModel(ModelIdentifier("FHIR", null, "4.0.1")) @@ -184,7 +183,7 @@ internal class TestR4ModelResolver { @Test fun createInstanceTests() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) for (type in Enumerations.DataType.entries) { // These are abstract types that should never be resolved directly. @@ -195,7 +194,7 @@ internal class TestR4ModelResolver { else -> {} } - val instance = resolver.createInstance(type.toCode()) + val instance = resolver.createHapiInstance(type.toCode()) Assertions.assertNotNull(instance) } @@ -209,7 +208,7 @@ internal class TestR4ModelResolver { else -> {} } - val instance = resolver.createInstance(type.toCode()) + val instance = resolver.createHapiInstance(type.toCode()) Assertions.assertNotNull(instance) } @@ -217,7 +216,7 @@ internal class TestR4ModelResolver { for (enumType in enums) { // For the enums we actually expect an Enumeration with a factory of the correct type to // be created. - val instance = resolver.createInstance(enumType.getSimpleName()) as Enumeration<*>? + val instance = resolver.createHapiInstance(enumType.getSimpleName()) as Enumeration<*>? Assertions.assertNotNull(instance) Assertions.assertEquals( @@ -228,16 +227,16 @@ internal class TestR4ModelResolver { // These are some inner classes that don't appear in the enums above // This list is not exhaustive. It's meant as a spot check for the resolution code. - var instance = resolver.createInstance("TestScriptRequestMethodCode") + var instance = resolver.createHapiInstance("TestScriptRequestMethodCode") Assertions.assertNotNull(instance) - instance = resolver.createInstance("FHIRDeviceStatus") + instance = resolver.createHapiInstance("FHIRDeviceStatus") Assertions.assertNotNull(instance) } @Test fun contextPathTests() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) var path = resolver.getContextPath("Patient", "Patient") as String? Assertions.assertNotNull(path) @@ -292,58 +291,59 @@ internal class TestR4ModelResolver { @Test fun resolveMissingPropertyReturnsNull() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) val p = Patient() - val value = resolver.resolvePath(p, "not-a-path") - Assertions.assertNull(value) + val patientAsCqlValue = resolver.toCqlValue(p) + assertIs(patientAsCqlValue) + assertFalse(patientAsCqlValue.elements.containsKey("not-a-path")) } @Test fun resolveIdPropertyReturnsString() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) val p = Patient() p.setId("5") - val idType = p.idElement - var value = resolver.resolvePath(p, "id") - Assertions.assertNotNull(value) - MatcherAssert.assertThat(value, Matchers.`is`(idType)) + val patientAsCqlValue = resolver.toCqlValue(p) + assertIs(patientAsCqlValue) - value = resolver.resolvePath(p, "id.value") - Assertions.assertNotNull(value) - MatcherAssert.assertThat(value, Matchers.`is`("5")) + val id = patientAsCqlValue.elements["id"] + assertIs(id) + assertEquals(QName("http://hl7.org/fhir", "id"), id.type) + + assertEquals("5", id.elements["value"]) } @Test fun resolveDateTimeProviderReturnsDate() { - val resolver: ModelResolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) val vp = VisionPrescription() val time = GregorianCalendar(1999, 3, 31).getTime() vp.setDateWritten(time) - val dateTimeType = vp.dateWrittenElement - - val value = resolver.resolvePath(vp, "dateWritten") - Assertions.assertNotNull(value) - MatcherAssert.assertThat(value, Matchers.`is`(dateTimeType)) + val value = resolver.toCqlValue(vp) + assertIs(value) + val dateWritten = value.elements["dateWritten"] + assertIs(dateWritten) + assertEquals(QName("http://hl7.org/fhir", "dateTime"), dateWritten.type) } @Test fun resolveNullEnumerationReturnsNull() { - val resolver: FhirModelResolver = - R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) + val resolver = R4FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R4)) val q = Quantity() q.setValue(BigDecimal("10.0")) q.setUnit("1") val sq = resolver.castToSimpleQuantity(q) - val value = resolver.resolvePath(sq, "comparator") - Assertions.assertNull(value) + val value = resolver.toCqlValue(sq) + assertIs(value) + assertNull(value.elements["comparator"]) } @Test @@ -352,8 +352,8 @@ internal class TestR4ModelResolver { val dt = DateTimeType() - val value = resolver.resolvePath(dt, "value") - Assertions.assertNull(value) + val value = resolver.toCqlValue(dt) + assertNull(value) } @Test @@ -364,7 +364,7 @@ internal class TestR4ModelResolver { val patient = Patient() patient.setId(expectedId) - Assertions.assertEquals(resolver.resolveId(patient), expectedId) + Assertions.assertEquals(resolver.resolveId(resolver.toCqlValue(patient)), expectedId) } @Test @@ -379,12 +379,21 @@ internal class TestR4ModelResolver { "http://hl7.org/fhir/StructureDefinition/patient-birthTime", DateTimeType("1974-12-25T14:35:45-05:00"), ) - var result = resolver.resolvePath(patient, "birthDate") - Assertions.assertInstanceOf(DateType::class.java, result) - result = resolver.resolvePath(patient, "birthDate.extension") - Assertions.assertInstanceOf(MutableList::class.java, result) - Assertions.assertEquals(1, (result as MutableList<*>).size) - Assertions.assertInstanceOf(Extension::class.java, result[0]) + + val patientAsCqlValue = resolver.toCqlValue(patient) + assertIs(patientAsCqlValue) + + var result = patientAsCqlValue.elements["birthDate"] + assertIs(result) + assertEquals(QName("http://hl7.org/fhir", "date"), result.type) + + result = result.elements["extension"] + assertIs>(result) + assertEquals(1, result.count()) + + val extension = result.first() + assertIs(extension) + assertEquals(QName("http://hl7.org/fhir", "Extension"), extension.type) } @Test @@ -395,7 +404,7 @@ internal class TestR4ModelResolver { val procedure = Procedure() procedure.setId(expectedId) - Assertions.assertEquals(resolver.resolveId(procedure), expectedId) + Assertions.assertEquals(resolver.resolveId(resolver.toCqlValue(procedure)), expectedId) } @Test diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR5ModelResolver.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR5ModelResolver.kt index 40ab9347a..be4ef6c3b 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR5ModelResolver.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/TestR5ModelResolver.kt @@ -5,27 +5,28 @@ import ca.uhn.fhir.context.FhirVersionEnum import java.math.BigDecimal import java.text.ParseException import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull import org.apache.commons.lang3.time.DateUtils -import org.hamcrest.MatcherAssert -import org.hamcrest.Matchers +import org.cqframework.cql.shared.QName import org.hl7.fhir.r5.model.DateTimeType -import org.hl7.fhir.r5.model.DateType import org.hl7.fhir.r5.model.Enumeration import org.hl7.fhir.r5.model.Enumerations import org.hl7.fhir.r5.model.Enumerations.FHIRTypes -import org.hl7.fhir.r5.model.Extension import org.hl7.fhir.r5.model.Patient import org.hl7.fhir.r5.model.Quantity import org.hl7.fhir.r5.model.VisionPrescription import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.opencds.cqf.cql.engine.fhir.exception.UnknownType -import org.opencds.cqf.cql.engine.model.ModelResolver +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance internal class TestR5ModelResolver { @Test fun resolverThrowsExceptionForUnknownType() { - val resolver: ModelResolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) + val resolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) Assertions.assertThrows(UnknownType::class.java) { resolver.resolveType("ImpossibleTypeThatDoesn'tExistAndShouldBlowUp") @@ -34,7 +35,7 @@ internal class TestR5ModelResolver { @Test fun resolveTypeTests() { - val resolver: ModelResolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) + val resolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) for (type in FHIRTypes.entries) { // These are abstract types that should never be resolved directly. @@ -127,7 +128,7 @@ internal class TestR5ModelResolver { // } @Test fun createInstanceTests() { - val resolver: ModelResolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) + val resolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) for (type in FHIRTypes.entries) { // These are abstract types that should never be resolved directly. @@ -146,7 +147,7 @@ internal class TestR5ModelResolver { else -> {} } - val instance = resolver.createInstance(type.toCode()) + val instance = resolver.createHapiInstance(type.toCode()) Assertions.assertNotNull(instance) } @@ -154,7 +155,7 @@ internal class TestR5ModelResolver { for (enumType in enums) { // For the enums we actually expect an Enumeration with a factory of the correct type to // be created. - val instance = resolver.createInstance(enumType.getSimpleName()) as Enumeration<*>? + val instance = resolver.createHapiInstance(enumType.getSimpleName()) as Enumeration<*>? Assertions.assertNotNull(instance) Assertions.assertEquals( @@ -165,16 +166,16 @@ internal class TestR5ModelResolver { // These are some inner classes that don't appear in the enums above // This list is not exhaustive. It's meant as a spot check for the resolution code. - var instance = resolver.createInstance("TestScriptRequestMethodCode") + var instance = resolver.createHapiInstance("TestScriptRequestMethodCode") Assertions.assertNotNull(instance) - instance = resolver.createInstance("FHIRDeviceStatus") + instance = resolver.createHapiInstance("FHIRDeviceStatus") Assertions.assertNotNull(instance) } @Test fun contextPathTests() { - val resolver: ModelResolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) + val resolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) var path = resolver.getContextPath("Patient", "Patient") as String? Assertions.assertNotNull(path) @@ -229,44 +230,45 @@ internal class TestR5ModelResolver { @Test fun resolveMissingPropertyReturnsNull() { - val resolver: ModelResolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) + val resolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) val p = Patient() - val value = resolver.resolvePath(p, "not-a-path") - Assertions.assertNull(value) + val patientAsCqlValue = resolver.toCqlValue(p) + assertIs(patientAsCqlValue) + assertFalse(patientAsCqlValue.elements.containsKey("not-a-path")) } @Test fun resolveIdPropertyReturnsString() { - val resolver: ModelResolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) + val resolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) val p = Patient() p.setId("5") - val idType = p.idElement - var value = resolver.resolvePath(p, "id") - Assertions.assertNotNull(value) - MatcherAssert.assertThat(value, Matchers.`is`(idType)) + val patientAsCqlValue = resolver.toCqlValue(p) + assertIs(patientAsCqlValue) + + val id = patientAsCqlValue.elements["id"] + assertIs(id) + assertEquals(QName("http://hl7.org/fhir", "id"), id.type) - value = resolver.resolvePath(p, "id.value") - Assertions.assertNotNull(value) - MatcherAssert.assertThat(value, Matchers.`is`("5")) + assertEquals("5", id.elements["value"]) } @Test fun resolveDateTimeProviderReturnsDate() { - val resolver: ModelResolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) + val resolver = R5FhirModelResolver(FhirContext.forCached(FhirVersionEnum.R5)) val vp = VisionPrescription() val time = GregorianCalendar(1999, 3, 31).getTime() vp.setDateWritten(time) - val dateTimeType = vp.dateWrittenElement - - val value = resolver.resolvePath(vp, "dateWritten") - Assertions.assertNotNull(value) - MatcherAssert.assertThat(value, Matchers.`is`(dateTimeType)) + val value = resolver.toCqlValue(vp) + assertIs(value) + val dateWritten = value.elements["dateWritten"] + assertIs(dateWritten) + assertEquals(QName("http://hl7.org/fhir", "dateTime"), dateWritten.type) } @Test @@ -278,8 +280,9 @@ internal class TestR5ModelResolver { q.setUnit("1") val sq = resolver.castToSimpleQuantity(q) - val value = resolver.resolvePath(sq, "comparator") - Assertions.assertNull(value) + val value = resolver.toCqlValue(sq) + assertIs(value) + assertNull(value.elements["comparator"]) } @Test @@ -288,8 +291,8 @@ internal class TestR5ModelResolver { val dt = DateTimeType() - val value = resolver.resolvePath(dt, "value") - Assertions.assertNull(value) + val value = resolver.toCqlValue(dt) + assertNull(value) } @Test @@ -304,12 +307,21 @@ internal class TestR5ModelResolver { "http://hl7.org/fhir/StructureDefinition/patient-birthTime", DateTimeType("1974-12-25T14:35:45-05:00"), ) - var result = resolver.resolvePath(patient, "birthDate") - Assertions.assertInstanceOf(DateType::class.java, result) - result = resolver.resolvePath(patient, "birthDate.extension") - Assertions.assertInstanceOf(MutableList::class.java, result) - Assertions.assertEquals(1, (result as MutableList<*>).size) - Assertions.assertInstanceOf(Extension::class.java, result[0]) + + val patientAsCqlValue = resolver.toCqlValue(patient) + assertIs(patientAsCqlValue) + + var result = patientAsCqlValue.elements["birthDate"] + assertIs(result) + assertEquals(QName("http://hl7.org/fhir", "date"), result.type) + + result = result.elements["extension"] + assertIs>(result) + assertEquals(1, result.count()) + + val extension = result.first() + assertIs(extension) + assertEquals(QName("http://hl7.org/fhir", "Extension"), extension.type) } companion object { diff --git a/Src/java/engine/detekt-baseline.xml b/Src/java/engine/detekt-baseline.xml index 8ae009dd9..be0af9349 100644 --- a/Src/java/engine/detekt-baseline.xml +++ b/Src/java/engine/detekt-baseline.xml @@ -21,8 +21,12 @@ ComplexCondition:UnionEvaluator.kt$UnionEvaluator$leftStart == null || leftEnd == null || rightStart == null || rightEnd == null ComplexCondition:ValueSet.kt$ValueSet$id == cs.id && ((version == null && cs.version == null) || (version != null && version == cs.version)) CyclomaticComplexMethod:CollapseEvaluator.kt$CollapseEvaluator$fun collapse(list: Iterable<Interval?>?, per: Quantity?, state: State?): List<Interval?>? + CyclomaticComplexMethod:CqlValue.kt$fun getNamedTypeForCqlValue(value: Any?): QName? CyclomaticComplexMethod:DifferenceBetweenEvaluator.kt$DifferenceBetweenEvaluator$@JvmStatic fun difference(left: Any?, right: Any?, precision: Precision): Any? CyclomaticComplexMethod:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? + CyclomaticComplexMethod:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? + CyclomaticComplexMethod:Environment.kt$Environment$fun resolveProperty(target: Any?, property: String): Any? + CyclomaticComplexMethod:Environment.kt$Environment$fun setValue(target: Any?, path: String, value: Any?) CyclomaticComplexMethod:EquivalentEvaluator.kt$EquivalentEvaluator$@JvmStatic @JvmOverloads fun equivalent(left: Any?, right: Any?, state: State? = null): Boolean? CyclomaticComplexMethod:ExceptEvaluator.kt$ExceptEvaluator$@JvmStatic fun except(left: Any?, right: Any?, state: State?): Any? CyclomaticComplexMethod:GreaterEvaluator.kt$GreaterEvaluator$@JvmStatic fun greater(left: Any?, right: Any?, state: State?): Boolean? @@ -40,9 +44,6 @@ CyclomaticComplexMethod:QueryEvaluator.kt$QueryEvaluator$fun internalEvaluate( elm: Query, state: State?, visitor: ElmLibraryVisitor<Any?, State?>, ): Any? CyclomaticComplexMethod:SameAsEvaluator.kt$SameAsEvaluator$@JvmStatic fun sameAs(left: Any?, right: Any?, precision: String?, state: State?): Boolean? CyclomaticComplexMethod:SuccessorEvaluator.kt$SuccessorEvaluator$@JvmStatic fun successor(value: Any?): Any? - CyclomaticComplexMethod:SystemDataProvider.kt$SystemDataProvider$override fun resolvePath(target: Any?, path: String?): Any? - CyclomaticComplexMethod:SystemDataProvider.kt$SystemDataProvider$override fun resolveType(typeName: String?): JavaClass<*> - CyclomaticComplexMethod:SystemDataProvider.kt$SystemDataProvider$override fun setValue(target: Any?, path: String?, value: Any?) CyclomaticComplexMethod:TemporalHelper.kt$TemporalHelper$fun truncateValueToTargetPrecision( value: Long, precision: Precision, targetPrecision: Precision?, ): Long CyclomaticComplexMethod:TimeJs.kt$ChronoUnitJs$fun between(start: TemporalJs, end: TemporalJs): Long CyclomaticComplexMethod:ToBooleanEvaluator.kt$ToBooleanEvaluator$@JvmStatic fun toBoolean(operand: Any?): Any? @@ -57,11 +58,6 @@ ForbiddenComment:CqlMainSuiteTest.kt$CqlMainSuiteTest$// TODO: It'd be interesting to be able to inspect the ForbiddenComment:CqlPerformanceIT.kt$CqlPerformanceIT$// TODO: Ratio type not implemented error ForbiddenComment:DateTimeTest.kt$DateTimeTest$// TODO: It'd be good to extend these across different types of constructors, for example, - ForbiddenComment:Environment.kt$Environment$// TODO: Path may include .'s and []'s. - ForbiddenComment:Environment.kt$Environment$// TODO: This doesn't allow for choice-distinguished overloads... - ForbiddenComment:Environment.kt$Environment$// TODO: This doesn't allow for interval-distinguished overloads - ForbiddenComment:Environment.kt$Environment$// TODO: This doesn't allow for list-distinguished overloads... - ForbiddenComment:Environment.kt$Environment$// TODO: This doesn't allow for tuple-distinguished overloads.... ForbiddenComment:FilterEvaluator.kt$FilterEvaluator$// TODO: verify this works for all cases -> will scope always be present? ForbiddenComment:GeometricMeanEvaluator.kt$GeometricMeanEvaluator$// remove nulls - operation is on non-null list elements ... TODO: generify and move this to ForbiddenComment:IntervalEvaluator.kt$IntervalEvaluator$// TODO: the spec states that it is possible to have an interval with null boundaries, but @@ -73,11 +69,10 @@ ForbiddenComment:RetrieveEvaluator.kt$RetrieveEvaluator$// TODO: We probably shouldn't eagerly load this, but we need to track ForbiddenComment:SystemExternalFunctionProvider.kt$SystemExternalFunctionProvider$// TODO: Support adding more functions to an existing provider object. ForbiddenComment:TerminologyAwareRetrieveProvider.kt$TerminologyAwareRetrieveProvider$// TODO: Think about how to best handle the decision to expand value sets... Should it be part - FunctionNaming:Environment.kt$Environment$fun `as`(operand: Any?, type: JavaClass<*>, isStrict: Boolean): Any? - FunctionNaming:Environment.kt$Environment$fun `is`(operand: Any?, type: JavaClass<*>): Boolean? + FunctionNaming:Environment.kt$Environment$fun `as`(operand: Any?, type: TypeSpecifier, isStrict: Boolean): Any? + FunctionNaming:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? FunctionNaming:InEvaluator.kt$InEvaluator$fun `in`(left: Any?, right: Any?, precision: String?, state: State?): Boolean? - FunctionNaming:ModelResolver.kt$ModelResolver$fun `as`(value: Any?, type: JavaClass<*>?, isStrict: Boolean): Any? - FunctionNaming:ModelResolver.kt$ModelResolver$fun `is`(value: Any?, type: JavaClass<*>?): Boolean? + FunctionNaming:ModelResolver.kt$ModelResolver$fun `is`(valueType: String, type: QName): Boolean? FunctionNaming:TerminologyProvider.kt$TerminologyProvider$fun `in`(code: Code, valueSet: ValueSetInfo): Boolean FunctionOnlyReturningConstant:NullEvaluator.kt$NullEvaluator$@JvmStatic fun internalEvaluate(state: State?): Any? FunctionParameterNaming:AsEvaluator.kt$AsEvaluator$`as`: As @@ -85,9 +80,9 @@ ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format( "400: This source should be redacted\n%s", RedactingPHIObfuscator.REDACTED_MESSAGE, ) ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("1: This is a message\n") ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("1: This is a message\nnull") - ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("1: null\n") - ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("400: This is an error!\n") - ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("This is a message\n") + ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("1: null\n1") + ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("400: This is an error!\n4") + ImplicitDefaultLocale:CqlErrorsAndMessagingOperatorsTest.kt$CqlErrorsAndMessagingOperatorsTest$String.format("This is a message\n1") ImplicitDefaultLocale:CqlMainSuiteTest.kt$CqlMainSuiteTest$String.format( "Test library compiled with the following errors : %s", this.toString(errors), ) ImplicitDefaultLocale:CqlPerformanceIT.kt$CqlPerformanceIT$String.format( "%s took longer per iteration than allowed. max: %3.2f, actual: %3.2f", libraryId.id, maxPerIterationMs, perIteration, ) ImplicitDefaultLocale:ListOperatorsTest.kt$ListOperatorsTest$String.format( "Test library compiled with the following errors : %s", this.toString(errors), ) @@ -115,6 +110,10 @@ LongMethod:DateTimeTest.kt$DateTimeTest$@Test fun offsetPrecisionsTest() LongMethod:DifferenceBetweenEvaluator.kt$DifferenceBetweenEvaluator$@JvmStatic fun difference(left: Any?, right: Any?, precision: Precision): Any? LongMethod:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? + LongMethod:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? + LongMethod:Environment.kt$Environment$fun resolveProperty(target: Any?, property: String): Any? + LongMethod:Environment.kt$Environment$fun setValue(target: Any?, path: String, value: Any?) + LongMethod:EqualEvaluator.kt$EqualEvaluator$@JvmStatic @JvmOverloads fun equal(left: Any?, right: Any?, state: State? = null): Boolean? LongMethod:ExceptEvaluator.kt$ExceptEvaluator$@JvmStatic fun except(left: Any?, right: Any?, state: State?): Any? LongMethod:HighBoundaryEvaluator.kt$HighBoundaryEvaluator$@JvmStatic fun highBoundary(input: Any?, precision: Any?): Any? LongMethod:InEvaluator.kt$InEvaluator$private fun intervalIn( left: Any?, right: Interval, precision: String?, state: State?, ): Boolean? @@ -128,8 +127,6 @@ LongMethod:RetrieveEvaluator.kt$RetrieveEvaluator$fun internalEvaluate( elm: Retrieve?, state: State?, visitor: ElmLibraryVisitor<Any?, State?>, ): Any LongMethod:SameAsEvaluator.kt$SameAsEvaluator$@JvmStatic fun sameAs(left: Any?, right: Any?, precision: String?, state: State?): Boolean? LongMethod:SuccessorEvaluator.kt$SuccessorEvaluator$@JvmStatic fun successor(value: Any?): Any? - LongMethod:SystemDataProvider.kt$SystemDataProvider$override fun resolvePath(target: Any?, path: String?): Any? - LongMethod:SystemDataProvider.kt$SystemDataProvider$override fun setValue(target: Any?, path: String?, value: Any?) LongMethod:TemporalHelper.kt$TemporalHelper$fun truncateValueToTargetPrecision( value: Long, precision: Precision, targetPrecision: Precision?, ): Long LongParameterList:DateTimeEvaluator.kt$DateTimeEvaluator$( year: Int?, month: Int?, day: Int?, hour: Int?, minute: Int?, second: Int?, milliSecond: Int?, timeZoneOffset: BigDecimal?, ) LongParameterList:MessageEvaluator.kt$MessageEvaluator$( state: State?, sourceLocator: SourceLocator?, source: Any?, condition: Boolean?, code: String?, severity: String?, message: String?, ) @@ -239,7 +236,7 @@ MaxLineLength:FirstEvaluator.kt$FirstEvaluator$/* First(argument List<T>) T The First operator returns the first element in a list. The operator is equivalent to invoking the indexer with an index of 0. If the argument is null, the result is null. */ MaxLineLength:FunctionRefEvaluator.kt$FunctionRefEvaluator$"Ambiguous call to operator '${name}(${typesToString(state, types)})' in library '${state!!.getCurrentLibrary()!!.identifier!!.id}'." MaxLineLength:FunctionRefEvaluator.kt$FunctionRefEvaluator$"Could not resolve call to operator '${name}(${typesToString(state, types)})' in library '${state!!.getCurrentLibrary()!!.identifier!!.id}'." - MaxLineLength:FunctionRefEvaluatorTest.kt$FunctionRefEvaluatorTest$"Could not resolve call to operator 'func(java.lang.Integer, java.lang.Integer, java.lang.Integer)' in library 'lib'." + MaxLineLength:FunctionRefEvaluatorTest.kt$FunctionRefEvaluatorTest$"Could not resolve call to operator 'func({urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer)' in library 'lib'." MaxLineLength:GreaterEvaluator.kt$GreaterEvaluator$"Greater(Integer, Integer), Greater(Long, Long), Greater(Decimal, Decimal), Greater(Quantity, Quantity), Greater(Date, Date), Greater(DateTime, DateTime), Greater(Time, Time) or Greater(String, String)" MaxLineLength:GreaterEvaluator.kt$GreaterEvaluator$/* >(left Integer, right Integer) Boolean >(left Long, right Long) Boolean >(left Decimal, right Decimal) Boolean >(left Quantity, right Quantity) Boolean >(left Date, right Date) Boolean >(left DateTime, right DateTime) Boolean >(left Time, right Time) Boolean >(left String, right String) Boolean The greater (>) operator returns true if the first argument is greater than the second argument. String comparisons are strictly lexical based on the Unicode value of the individual characters in the string. For comparisons involving quantities, the dimensions of each quantity must be the same, but not necessarily the unit. For example, units of 'cm' and 'm' are comparable, but units of 'cm2' and 'cm' are not. Attempting to operate on quantities with invalid units will result in a null. When a quantity has no units specified, it is treated as a quantity with the default unit ('1'). For date/time values, the comparison is performed by considering each precision in order, beginning with years (or hours for time values). If the values are the same, comparison proceeds to the next precision; if the first value is greater than the second, the result is true; if the first value is less than the second, the result is false; if one input has a value for the precision and the other does not, the comparison stops and the result is null; if neither input has a value for the precision or the last precision has been reached, the comparison stops and the result is false. For example: define DateTimeGreaterIsNull: @2012-01-01 > @2012-01-01T12 If either argument is null, the result is null. */ MaxLineLength:GreaterOrEqualEvaluator.kt$GreaterOrEqualEvaluator$"GreaterOrEqual(Integer, Integer), GreaterOrEqual(Long, Long), GreaterOrEqual(Decimal, Decimal), GreaterOrEqual(Quantity, Quantity), GreaterOrEqual(Date, Date), GreaterOrEqual(DateTime, DateTime), GreaterOrEqual(Time, Time) or GreaterOrEqual(String, String)" @@ -323,6 +320,7 @@ NestedBlockDepth:DebugLibraryMapEntry.kt$DebugLibraryMapEntry$@Suppress("ReturnCount") fun shouldDebug(node: Element?): DebugAction? NestedBlockDepth:DifferenceBetweenEvaluator.kt$DifferenceBetweenEvaluator$@JvmStatic fun difference(left: Any?, right: Any?, precision: Precision): Any? NestedBlockDepth:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? + NestedBlockDepth:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? NestedBlockDepth:FilterEvaluator.kt$FilterEvaluator$@JvmStatic fun filter(elm: Filter?, source: Any?, condition: Any?, state: State?): Any? NestedBlockDepth:FlattenEvaluator.kt$FlattenEvaluator$@JvmStatic fun flatten(operand: Any?): List<Any?>? NestedBlockDepth:InCodeSystemEvaluator.kt$InCodeSystemEvaluator$@JvmStatic fun inCodeSystem(code: Any?, codeSystem: Any?, state: State?): Any? @@ -359,11 +357,8 @@ ReturnCount:AnyInCodeSystemEvaluator.kt$AnyInCodeSystemEvaluator$@JvmStatic fun internalEvaluate( codes: Any?, codeSystemRef: CodeSystemRef?, codeSystem: Any?, state: State?, ): Any? ReturnCount:AnyInValueSetEvaluator.kt$AnyInValueSetEvaluator$@JvmStatic fun internalEvaluate( codes: Any?, valueSetRef: ValueSetRef?, valueset: Any?, state: State?, ): Any? ReturnCount:AnyTrueEvaluator.kt$AnyTrueEvaluator$@JvmStatic fun anyTrue(src: Any?): Boolean? - ReturnCount:BaseModelResolver.kt$BaseModelResolver$override fun `as`(value: Any?, type: JavaClass<*>?, isStrict: Boolean): Any? ReturnCount:BeforeEvaluator.kt$BeforeEvaluator$@JvmStatic fun before(left: Any?, right: Any?, precision: String?, state: State?): Boolean? ReturnCount:CachingModelResolverDecorator.kt$CachingModelResolverDecorator$override fun getContextPath(contextType: String?, targetType: String?): Any? - ReturnCount:CachingModelResolverDecorator.kt$CachingModelResolverDecorator$override fun resolveType(typeName: String?): JavaClass<*>? - ReturnCount:CachingModelResolverDecorator.kt$CachingModelResolverDecorator$override fun resolveType(value: Any?): JavaClass<*>? ReturnCount:CeilingEvaluator.kt$CeilingEvaluator$@JvmStatic fun ceiling(operand: Any?): Any? ReturnCount:CoalesceEvaluator.kt$CoalesceEvaluator$fun coalesce(operands: List<Any?>): Any? ReturnCount:CollapseEvaluator.kt$CollapseEvaluator$fun collapse(list: Iterable<Interval?>?, per: Quantity?, state: State?): List<Interval?>? @@ -394,10 +389,7 @@ ReturnCount:DivideEvaluator.kt$DivideEvaluator$@JvmStatic fun divide(left: Any?, right: Any?, state: State?): Any? ReturnCount:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? ReturnCount:EndsEvaluator.kt$EndsEvaluator$@JvmStatic fun ends(left: Any?, right: Any?, precision: String?, state: State?): Boolean? - ReturnCount:Environment.kt$Environment$fun `as`(operand: Any?, type: JavaClass<*>, isStrict: Boolean): Any? - ReturnCount:Environment.kt$Environment$fun `is`(operand: Any?, type: JavaClass<*>): Boolean? - ReturnCount:Environment.kt$Environment$fun objectEquivalent(left: Any?, right: Any?): Boolean? - ReturnCount:Environment.kt$Environment$fun resolveType(value: Any?): JavaClass<*>? + ReturnCount:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? ReturnCount:EvaluationVisitor.kt$EvaluationVisitor$override fun visitCase(elm: Case, context: State?): Any? ReturnCount:ExceptEvaluator.kt$ExceptEvaluator$@JvmStatic fun except(left: Any?, right: Any?, state: State?): Any? ReturnCount:ExpandEvaluator.kt$ExpandEvaluator$@JvmStatic fun expand(listOrInterval: Any?, per: Quantity?, state: State?): Any? @@ -477,8 +469,6 @@ ReturnCount:SubstringEvaluator.kt$SubstringEvaluator$@JvmStatic fun substring(stringValue: Any?, startIndexValue: Any?, lengthValue: Any?): Any? ReturnCount:SuccessorEvaluator.kt$SuccessorEvaluator$@JvmStatic fun successor(value: Any?): Any? ReturnCount:SuccessorEvaluator.kt$SuccessorEvaluator$@JvmStatic fun successor(value: Any?, quantity: Quantity?): Any? - ReturnCount:SystemDataProvider.kt$SystemDataProvider$override fun objectEqual(left: Any?, right: Any?): Boolean? - ReturnCount:SystemDataProvider.kt$SystemDataProvider$override fun objectEquivalent(left: Any?, right: Any?): Boolean? ReturnCount:TemporalHelper.kt$TemporalHelper$fun truncateValueToTargetPrecision( value: Long, precision: Precision, targetPrecision: Precision?, ): Long ReturnCount:Time.kt$Time$override fun compare(other: BaseTemporal, forSort: Boolean): Int? ReturnCount:Time.kt$Time$override fun compareToPrecision(other: BaseTemporal, p: Precision): Int? @@ -527,6 +517,7 @@ SwallowedException:ToDateEvaluator.kt$ToDateEvaluator$dtpe: Exception SwallowedException:ToDateTimeEvaluator.kt$ToDateTimeEvaluator$dtpe: Exception SwallowedException:ToTimeEvaluator.kt$ToTimeEvaluator$dtpe: Exception + ThrowsCount:Environment.kt$Environment$fun setValue(target: Any?, path: String, value: Any?) ThrowsCount:ExpEvaluator.kt$ExpEvaluator$@JvmStatic fun exp(operand: Any?): Any? ThrowsCount:LiteralEvaluator.kt$LiteralEvaluator$@JvmStatic fun internalEvaluate(valueT: QName?, value: String?, state: State?): Any? ThrowsCount:LnEvaluator.kt$LnEvaluator$@JvmStatic fun ln(operand: Any?): Any? @@ -554,14 +545,11 @@ TooGenericExceptionThrown:State.kt$State$throw RuntimeException("Not supported") TooGenericExceptionThrown:State.kt$State$throw RuntimeException("Stack underflow") TooGenericExceptionThrown:SystemExternalFunctionProvider.kt$SystemExternalFunctionProvider$throw RuntimeException( "Error when executing function [" + staticFunctionName + "]: \n" + e.toString() ) - TooManyFunctions:CachingModelResolverDecorator.kt$CachingModelResolverDecorator : ModelResolver - TooManyFunctions:CompositeDataProvider.kt$CompositeDataProvider : DataProvider TooManyFunctions:CqlEngine.kt$CqlEngine TooManyFunctions:Date.kt$Date : BaseTemporal TooManyFunctions:DateTime.kt$DateTime : BaseTemporal TooManyFunctions:Environment.kt$Environment TooManyFunctions:EvaluationVisitor.kt$EvaluationVisitor : BaseElmLibraryVisitor - TooManyFunctions:ModelResolver.kt$ModelResolver TooManyFunctions:Precision.kt$Precision$Companion TooManyFunctions:State.kt$State TooManyFunctions:TemporalHelper.kt$TemporalHelper @@ -575,6 +563,7 @@ TooManyFunctions:TimeJvm.kt$org.opencds.cqf.cql.engine.util.TimeJvm.kt TopLevelPropertyNaming:ReflectionJs.kt$const val unknownPackageName = "unknown" TopLevelPropertyNaming:ReflectionJs.kt$const val unknownSimpleName = "Unknown" + UnusedParameter:FunctionRefEvaluator.kt$FunctionRefEvaluator$state: State? UnusedParameter:LengthEvaluator.kt$LengthEvaluator$state: State? UnusedParameter:NullEvaluator.kt$NullEvaluator$state: State? UnusedParameter:QuantityEvaluator.kt$QuantityEvaluator$state: State? diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/CompositeDataProvider.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/CompositeDataProvider.kt index 462dbd821..40adb2219 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/CompositeDataProvider.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/CompositeDataProvider.kt @@ -1,68 +1,27 @@ package org.opencds.cqf.cql.engine.data +import org.cqframework.cql.shared.QName import org.opencds.cqf.cql.engine.model.ModelResolver import org.opencds.cqf.cql.engine.retrieve.RetrieveProvider import org.opencds.cqf.cql.engine.runtime.Code import org.opencds.cqf.cql.engine.runtime.Interval -import org.opencds.cqf.cql.engine.util.JavaClass open class CompositeDataProvider( protected var modelResolver: ModelResolver?, protected var retrieveProvider: RetrieveProvider?, ) : DataProvider { - @Deprecated("Use packageNames instead") - override var packageName: String? - get() = this.modelResolver!!.packageName - set(value) { - this.modelResolver!!.packageName = value - } - - override var packageNames: MutableList - get() = this.modelResolver!!.packageNames - set(value) { - this.modelResolver!!.packageNames = value - } - - override fun resolvePath(target: Any?, path: String?): Any? { - return this.modelResolver!!.resolvePath(target, path) - } - override fun getContextPath(contextType: String?, targetType: String?): Any? { return this.modelResolver!!.getContextPath(contextType, targetType) } - override fun resolveType(typeName: String?): JavaClass<*>? { - return this.modelResolver!!.resolveType(typeName) - } - - override fun resolveType(value: Any?): JavaClass<*>? { - return this.modelResolver!!.resolveType(value) - } - - override fun `is`(value: Any?, type: JavaClass<*>?): Boolean? { - return this.modelResolver!!.`is`(value, type) - } - - override fun `as`(value: Any?, type: JavaClass<*>?, isStrict: Boolean): Any? { - return this.modelResolver!!.`as`(value, type, isStrict) + override fun `is`(valueType: String, type: QName): Boolean? { + return this.modelResolver!!.`is`(valueType, type) } override fun createInstance(typeName: String?): Any? { return this.modelResolver!!.createInstance(typeName) } - override fun setValue(target: Any?, path: String?, value: Any?) { - this.modelResolver!!.setValue(target, path, value) - } - - override fun objectEqual(left: Any?, right: Any?): Boolean? { - return this.modelResolver!!.objectEqual(left, right) - } - - override fun objectEquivalent(left: Any?, right: Any?): Boolean? { - return this.modelResolver!!.objectEquivalent(left, right) - } - override fun resolveId(target: Any?): String? { return this.modelResolver!!.resolveId(target) } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt index 19deaebd2..a3ded270a 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt @@ -1,15 +1,9 @@ package org.opencds.cqf.cql.engine.data -import org.cqframework.cql.shared.BigDecimal -import org.opencds.cqf.cql.engine.model.BaseModelResolver +import org.cqframework.cql.shared.QName import org.opencds.cqf.cql.engine.runtime.* -import org.opencds.cqf.cql.engine.runtime.Date -import org.opencds.cqf.cql.engine.util.JavaClass -import org.opencds.cqf.cql.engine.util.javaClass -import org.opencds.cqf.cql.engine.util.javaClassName -import org.opencds.cqf.cql.engine.util.kotlinClassToJavaClass -open class SystemDataProvider : BaseModelResolver(), DataProvider { +open class SystemDataProvider : DataProvider { override fun retrieve( context: String?, contextPath: String?, @@ -27,182 +21,11 @@ open class SystemDataProvider : BaseModelResolver(), DataProvider { throw IllegalArgumentException("SystemDataProvider does not support retrieval.") } - @Deprecated("Use packageNames instead") - override var packageName: String? - get() = "org.opencds.cqf.cql.engine.runtime" - set(value) {} - - override fun resolvePath(target: Any?, path: String?): Any? { - if (target == null) { - return null - } - - return when (target) { - is Quantity -> { - when (path) { - "value" -> target.value - "unit" -> target.unit - else -> null - } - } - is Ratio -> { - when (path) { - "numerator" -> target.numerator - "denominator" -> target.denominator - else -> null - } - } - is Code -> { - when (path) { - "code" -> target.code - "display" -> target.display - "system" -> target.system - "version" -> target.version - else -> null - } - } - is Concept -> { - when (path) { - "display" -> target.display - "codes" -> target.codes - else -> null - } - } - is CodeSystem -> { - when (path) { - "id" -> target.id - "version" -> target.version - "name" -> target.name - else -> null - } - } - is ValueSet -> { - when (path) { - "id" -> target.id - "version" -> target.version - "name" -> target.name - "codesystems" -> target.codeSystems - else -> null - } - } - is Interval -> { - when (path) { - "low" -> target.low - "lowClosed" -> target.lowClosed - "high" -> target.high - "highClosed" -> target.highClosed - else -> null - } - } - is Tuple -> target.getElement(path) - else -> null - } - } - - override fun setValue(target: Any?, path: String?, value: Any?) { - if (target == null) { - return - } - - when (target) { - is Quantity -> { - when (path) { - "value" -> target.value = value as BigDecimal? - "unit" -> target.unit = value as String? - else -> throw IllegalArgumentException("Could not set ${path} on Quantity.") - } - } - is Ratio -> { - when (path) { - "numerator" -> target.numerator = value as Quantity? - "denominator" -> target.denominator = value as Quantity? - else -> throw IllegalArgumentException("Could not set ${path} on Ratio.") - } - } - is Code -> { - when (path) { - "code" -> target.code = value as String? - "display" -> target.display = value as String? - "system" -> target.system = value as String? - "version" -> target.version = value as String? - else -> throw IllegalArgumentException("Could not set ${path} on Code.") - } - } - is Concept -> { - when (path) { - "display" -> target.display = value as String? - "codes" -> - target.codes = @Suppress("UNCHECKED_CAST") (value as MutableList?) - else -> throw IllegalArgumentException("Could not set ${path} on Concept.") - } - } - is CodeSystem -> { - when (path) { - "id" -> target.id = value as String? - "version" -> target.version = value as String? - "name" -> target.name = value as String? - else -> throw IllegalArgumentException("Could not set ${path} on CodeSystem.") - } - } - is ValueSet -> { - when (path) { - "id" -> target.id = value as String? - "version" -> target.version = value as String? - "name" -> target.name = value as String? - "codesystems" -> - target.setCodeSystems( - @Suppress("UNCHECKED_CAST") (value as MutableList?) - ) - else -> throw IllegalArgumentException("Could not set ${path} on ValueSet.") - } - } - is Interval -> { - when (path) { - "low" -> target.low = value - "high" -> target.high = value - else -> throw IllegalArgumentException("Could not set ${path} on Interval.") - } - } - else -> - throw IllegalArgumentException( - "Could not set ${path} on type ${target.javaClassName}." - ) - } - } - - override fun resolveType(value: Any?): JavaClass<*> { - if (value == null) { - return kotlinClassToJavaClass(Any::class) - } - - return value.javaClass - } - - override fun resolveType(typeName: String?): JavaClass<*> { - return kotlinClassToJavaClass( - when (typeName) { - "Boolean" -> Boolean::class - "Integer" -> Int::class - "Long" -> Long::class - "Decimal" -> BigDecimal::class - "String" -> String::class - "Date" -> Date::class - "DateTime" -> DateTime::class - "Time" -> Time::class - "Quantity" -> Quantity::class - "Ratio" -> Ratio::class - "Code" -> Code::class - "Concept" -> Concept::class - "CodeSystem" -> CodeSystem::class - "ValueSet" -> ValueSet::class - "Interval" -> Interval::class - "Tuple" -> Tuple::class - else -> - throw IllegalArgumentException( - "Could not resolve type ${packageName}.${typeName}." - ) - } - ) + override fun `is`(valueType: String, type: QName): Boolean? { + return type.getNamespaceURI() == "urn:hl7-org:elm-types:r1" && + type.getLocalPart() == valueType || + valueType == "Any" || + type == QName("urn:hl7-org:elm-types:r1", "Any") } override fun createInstance(typeName: String?): Any? { @@ -217,30 +40,6 @@ open class SystemDataProvider : BaseModelResolver(), DataProvider { } } - override fun objectEqual(left: Any?, right: Any?): Boolean? { - if (left == null) { - return null - } - - if (right == null) { - return null - } - - return left == right - } - - override fun objectEquivalent(left: Any?, right: Any?): Boolean? { - if (left == null && right == null) { - return true - } - - if (left == null) { - return false - } - - return left == right - } - override fun resolveId(target: Any?): String? { return null } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/AsEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/AsEvaluator.kt index 9a4b7400c..1151c4592 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/AsEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/AsEvaluator.kt @@ -2,8 +2,8 @@ package org.opencds.cqf.cql.engine.elm.executing import kotlin.jvm.JvmStatic import org.hl7.elm.r1.As +import org.hl7.elm.r1.NamedTypeSpecifier import org.opencds.cqf.cql.engine.execution.State -import org.opencds.cqf.cql.engine.util.JavaClass /* as(argument Any) T @@ -25,17 +25,9 @@ define RuntimeError: return cast P as Observation */ object AsEvaluator { - private fun resolveType(`as`: As, state: State?): JavaClass<*>? { - if (`as`.asTypeSpecifier != null) { - return state!!.environment.resolveType(`as`.asTypeSpecifier) - } - - return state!!.environment.resolveType(`as`.asType) - } - @JvmStatic fun internalEvaluate(operand: Any?, `as`: As, isStrict: Boolean, state: State?): Any? { - val clazz = resolveType(`as`, state) - return state!!.environment.`as`(operand, clazz!!, isStrict) + val type = `as`.asTypeSpecifier ?: NamedTypeSpecifier().withName(`as`.asType) + return state!!.environment.`as`(operand, type, isStrict) } } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/ConvertEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/ConvertEvaluator.kt index 5eb9d893b..5b0e2ca05 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/ConvertEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/ConvertEvaluator.kt @@ -2,10 +2,10 @@ package org.opencds.cqf.cql.engine.elm.executing import kotlin.jvm.JvmStatic import org.cqframework.cql.shared.QName +import org.hl7.elm.r1.NamedTypeSpecifier import org.hl7.elm.r1.TypeSpecifier import org.opencds.cqf.cql.engine.exception.InvalidConversion import org.opencds.cqf.cql.engine.execution.State -import org.opencds.cqf.cql.engine.util.JavaClass /* convert to(argument Any) T @@ -40,33 +40,6 @@ For specific semantics for each conversion, refer to the explicit conversion ope */ object ConvertEvaluator { - private fun resolveType( - toType: QName?, - typeSpecifier: TypeSpecifier?, - state: State?, - ): JavaClass<*> { - if (typeSpecifier != null) { - return state!!.environment.resolveType(typeSpecifier)!! - } - return state!!.environment.resolveType(toType)!! - } - - private fun convert(operand: Any?, type: JavaClass<*>): Any? { - if (operand == null) { - return null - } - - try { - if (type.isInstance(operand)) { - return type.cast(operand) - } - } catch (e: Exception) { - throw InvalidConversion("Error during conversion: " + e.message) - } - - throw InvalidConversion(operand, type) - } - @JvmStatic fun internalEvaluate( operand: Any?, @@ -74,7 +47,11 @@ object ConvertEvaluator { typeSpecifier: TypeSpecifier?, state: State?, ): Any? { - val type = resolveType(toType, typeSpecifier, state) - return convert(operand, type) + val type = typeSpecifier ?: NamedTypeSpecifier().withName(toType) + try { + return state!!.environment.`as`(operand, type, true) + } catch (e: Exception) { + throw InvalidConversion("Error during conversion: " + e.message) + } } } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/DescendentsEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/DescendentsEvaluator.kt index 3526fbf7c..634b6fc97 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/DescendentsEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/DescendentsEvaluator.kt @@ -5,7 +5,6 @@ import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Tuple object DescendentsEvaluator { - private val descendents = mutableListOf() @JvmStatic fun descendents(source: Any?): Any? { @@ -17,6 +16,8 @@ object DescendentsEvaluator { } fun getDescendents(source: Any?): Any? { + val descendents = mutableListOf() + if (source is Iterable<*>) { for (element in source) { descendents.add(getDescendents(element)) diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt index 3b53b1503..fd04f1073 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EqualEvaluator.kt @@ -4,11 +4,9 @@ import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmStatic import org.cqframework.cql.shared.BigDecimal import org.opencds.cqf.cql.engine.elm.executing.OrEvaluator.or -import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.* import org.opencds.cqf.cql.engine.runtime.Quantity.Companion.unitsEqual -import org.opencds.cqf.cql.engine.util.javaClassName /* *** NOTES FOR CLINICAL OPERATORS *** @@ -112,15 +110,18 @@ object EqualEvaluator { return tuplesEqual(left, right, state) } - // Fallback to data provider's `objectEqual()` - - if (state != null) { - return state.environment.objectEqual(left, right) + if (left is CqlClassInstance && right is CqlClassInstance) { + if (left.type == right.type) { + return tuplesEqual( + Tuple().withElements(left.elements), + Tuple().withElements(right.elements), + state, + ) + } + return false } - throw InvalidOperatorArgument( - "Equal(${left.javaClassName}, ${right.javaClassName}) requires Context and state was null" - ) + return false } fun quantitiesEqual(left: Quantity, right: Quantity, state: State?): Boolean? { diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt index bf8b2ed1b..67c5c294a 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/EquivalentEvaluator.kt @@ -6,11 +6,9 @@ import kotlin.math.min import org.cqframework.cql.shared.BigDecimal import org.cqframework.cql.shared.RoundingMode import org.opencds.cqf.cql.engine.elm.executing.MultiplyEvaluator.multiply -import org.opencds.cqf.cql.engine.exception.InvalidOperatorArgument import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.* import org.opencds.cqf.cql.engine.runtime.Quantity.Companion.unitsEquivalent -import org.opencds.cqf.cql.engine.util.javaClassName /* https://cql.hl7.org/09-b-cqlreference.html#equivalent @@ -136,15 +134,18 @@ object EquivalentEvaluator { return tuplesEquivalent(left, right, state) } - // Fallback to data provider's `objectEquivalent()` - - if (state != null) { - return state.environment.objectEquivalent(left, right) + if (left is CqlClassInstance && right is CqlClassInstance) { + if (left.type == right.type) { + return tuplesEquivalent( + Tuple().withElements(left.elements), + Tuple().withElements(right.elements), + state, + ) + } + return false } - throw InvalidOperatorArgument( - "Equivalent(${left.javaClassName}, ${right.javaClassName}) requires Context and context was null" - ) + return false } fun quantitiesEquivalent(left: Quantity, right: Quantity, state: State?): Boolean? { diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt index f418569ee..421711fe8 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt @@ -10,6 +10,9 @@ import org.opencds.cqf.cql.engine.exception.CqlException import org.opencds.cqf.cql.engine.execution.Libraries import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.execution.Variable +import org.opencds.cqf.cql.engine.runtime.Interval +import org.opencds.cqf.cql.engine.runtime.Tuple +import org.opencds.cqf.cql.engine.runtime.getNamedTypeForCqlValue object FunctionRefEvaluator { private val logger = KotlinLogging.logger("FunctionRefEvaluator") @@ -178,8 +181,14 @@ object FunctionRefEvaluator { if (arguments != null) { arguments.forEach { a -> argStr.append(if (argStr.isNotEmpty()) ", " else "") - val type = state!!.environment.resolveType(a) - argStr.append(if (type == null) "null" else type.getName()) + argStr.append( + when (a) { + is Interval -> "Interval" + is Tuple -> "Tuple" + is Iterable<*> -> "List" + else -> getNamedTypeForCqlValue(a) + } + ) } } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/IsEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/IsEvaluator.kt index b5ff68841..6d0eb02c5 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/IsEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/IsEvaluator.kt @@ -2,8 +2,8 @@ package org.opencds.cqf.cql.engine.elm.executing import kotlin.jvm.JvmStatic import org.hl7.elm.r1.Is +import org.hl7.elm.r1.NamedTypeSpecifier import org.opencds.cqf.cql.engine.execution.State -import org.opencds.cqf.cql.engine.util.JavaClass /* is(argument Any) Boolean @@ -13,18 +13,11 @@ If the run-time type of the argument is of the type being tested, the result of otherwise, the result is false. */ object IsEvaluator { - private fun resolveType(`is`: Is?, state: State?): JavaClass<*>? { - if (`is`!!.isTypeSpecifier != null) { - return state!!.environment.resolveType(`is`.isTypeSpecifier) - } - - return state!!.environment.resolveType(`is`.isType) - } @JvmStatic fun internalEvaluate(`is`: Is?, operand: Any?, state: State?): Any? { - val type = resolveType(`is`, state) + val type = `is`?.isTypeSpecifier ?: NamedTypeSpecifier().withName(`is`?.isType) - return state!!.environment.`is`(operand, type!!) + return state!!.environment.`is`(operand, type) } } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt index ed6f7857a..16eb4a06e 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt @@ -7,7 +7,9 @@ import org.opencds.cqf.cql.engine.debug.SourceLocator import org.opencds.cqf.cql.engine.debug.SourceLocator.Companion.fromNode import org.opencds.cqf.cql.engine.exception.CqlException import org.opencds.cqf.cql.engine.execution.State -import org.opencds.cqf.cql.engine.util.javaClassPackageName +import org.opencds.cqf.cql.engine.runtime.Interval +import org.opencds.cqf.cql.engine.runtime.Tuple +import org.opencds.cqf.cql.engine.runtime.getNamedTypeForCqlValue object MessageEvaluator { val logger = KotlinLogging.logger("MessageEvaluator") @@ -74,7 +76,21 @@ object MessageEvaluator { } val dataProvider = - state!!.environment.resolveDataProvider(source.javaClassPackageName, false) + when (source) { + // Use the system data provider to obfuscate intervals, lists, and anonymous tuples + is Interval, + is Tuple, + is Iterable<*> -> + state!! + .environment + .resolveDataProviderByModelUriOrNull("urn:hl7-org:elm-types:r1") + else -> + state!! + .environment + .resolveDataProviderByModelUriOrNull( + getNamedTypeForCqlValue(source)?.getNamespaceURI() + ) + } return dataProvider?.phiObfuscationSupplier()?.invoke()?.obfuscate(source) ?: "" } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.kt index 1f3d751df..b77a956a1 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/RetrieveEvaluator.kt @@ -8,8 +8,8 @@ import org.opencds.cqf.cql.engine.runtime.Code import org.opencds.cqf.cql.engine.runtime.Concept import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.ValueSet +import org.opencds.cqf.cql.engine.runtime.getNamedTypeForCqlValue import org.opencds.cqf.cql.engine.util.javaClassName -import org.opencds.cqf.cql.engine.util.javaClassPackageName object RetrieveEvaluator { fun internalEvaluate( @@ -27,10 +27,14 @@ object RetrieveEvaluator { This whole block is a bit a hack in the sense that the need to switch to the context (e.g. Practitioner) identifies itself in a non-domain specific way */ val contextValue: Any = visitor.visitExpression(context, state)!! - val name = contextValue.javaClassPackageName - val dataProvider = state!!.environment.resolveDataProvider(name) - val contextTypeName = contextValue::class.simpleName!! - val contextId = dataProvider!!.resolveId(contextValue) + val dataProvider = + state!! + .environment + .resolveDataProviderByModelUri( + getNamedTypeForCqlValue(contextValue)?.getNamespaceURI() + ) + val contextTypeName = getNamedTypeForCqlValue(contextValue)!!.getLocalPart() + val contextId = dataProvider.resolveId(contextValue) state.setContextValue(contextTypeName, contextId!!) isEnteredContext = state.enterContext(contextTypeName) diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt index e1292bb93..939c93b1c 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt @@ -4,6 +4,7 @@ import kotlin.js.ExperimentalJsExport import kotlin.js.JsName import kotlin.jvm.JvmOverloads import org.cqframework.cql.cql2elm.LibraryManager +import org.cqframework.cql.shared.BigDecimal import org.cqframework.cql.shared.JsOnlyExport import org.cqframework.cql.shared.QName import org.hl7.elm.r1.* @@ -11,13 +12,18 @@ import org.opencds.cqf.cql.engine.data.DataProvider import org.opencds.cqf.cql.engine.data.ExternalFunctionProvider import org.opencds.cqf.cql.engine.data.SystemDataProvider import org.opencds.cqf.cql.engine.exception.CqlException +import org.opencds.cqf.cql.engine.exception.InvalidCast +import org.opencds.cqf.cql.engine.runtime.Code +import org.opencds.cqf.cql.engine.runtime.CodeSystem +import org.opencds.cqf.cql.engine.runtime.Concept +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance import org.opencds.cqf.cql.engine.runtime.Interval +import org.opencds.cqf.cql.engine.runtime.Quantity +import org.opencds.cqf.cql.engine.runtime.Ratio import org.opencds.cqf.cql.engine.runtime.Tuple +import org.opencds.cqf.cql.engine.runtime.ValueSet +import org.opencds.cqf.cql.engine.runtime.getNamedTypeForCqlValue import org.opencds.cqf.cql.engine.terminology.TerminologyProvider -import org.opencds.cqf.cql.engine.util.JavaClass -import org.opencds.cqf.cql.engine.util.javaClass -import org.opencds.cqf.cql.engine.util.javaClassPackageName -import org.opencds.cqf.cql.engine.util.kotlinClassToJavaClass /** * The Environment class represents the current CQL execution environment. Meaning, things that are @@ -35,8 +41,6 @@ constructor( ) { val dataProviders = mutableMapOf() - private val packageMap = mutableMapOf() - // -- ExternalFunctionProviders -- TODO the registration of these... Should be // part of the LibraryManager? // @@ -73,75 +77,102 @@ constructor( // -- DataProvider "Helpers" fun resolvePath(target: Any?, path: String): Any? { - if (target == null) { - return null - } - - // TODO: Path may include .'s and []'s. - // For now, assume no qualifiers or indexers... - val clazz: JavaClass<*> = target.javaClass - - if (clazz.getPackageName().startsWith("java.lang")) { - throw CqlException( - "Invalid path: $path for type: ${clazz.getName()} - this is likely an issue with the data model." - ) + var target = target + // The path attribute may include qualifiers (.) and indexers ([x]) + val qualifiersAndIndexers = + path.split('.', '[', ']').map { it.trim() }.filter { it.isNotEmpty() } + for (qualifierOrIndexer in qualifiersAndIndexers) { + val indexer = qualifierOrIndexer.toIntOrNull() + target = + if (indexer == null) { + resolveProperty(target, qualifierOrIndexer) + } else { + (target as Iterable<*>).elementAtOrNull(indexer) + } } - - val dataProvider = resolveDataProvider(clazz.getPackageName()) - return dataProvider!!.resolvePath(target, path) + return target } - fun `as`(operand: Any?, type: JavaClass<*>, isStrict: Boolean): Any? { - if (operand == null) { + fun resolveProperty(target: Any?, property: String): Any? { + if (target == null) { return null } - // Special case for Iterable instances being cast to CQL Lists. - // See https://github.com/cqframework/clinical_quality_language/issues/1577. - if ( - kotlinClassToJavaClass(Iterable::class).isAssignableFrom(type) && operand is Iterable<*> - ) { - return operand + return when (target) { + is Quantity -> { + when (property) { + "value" -> target.value + "unit" -> target.unit + else -> null + } + } + is Ratio -> { + when (property) { + "numerator" -> target.numerator + "denominator" -> target.denominator + else -> null + } + } + is Code -> { + when (property) { + "code" -> target.code + "display" -> target.display + "system" -> target.system + "version" -> target.version + else -> null + } + } + is Concept -> { + when (property) { + "display" -> target.display + "codes" -> target.codes + else -> null + } + } + is CodeSystem -> { + when (property) { + "id" -> target.id + "version" -> target.version + "name" -> target.name + else -> null + } + } + is ValueSet -> { + when (property) { + "id" -> target.id + "version" -> target.version + "name" -> target.name + "codesystems" -> target.codeSystems + else -> null + } + } + is Interval -> { + when (property) { + "low" -> target.low + "lowClosed" -> target.lowClosed + "high" -> target.high + "highClosed" -> target.highClosed + else -> null + } + } + is Tuple -> target.getElement(property) + is CqlClassInstance -> target.elements[property] + else -> throw IllegalArgumentException("Could not resolve path '$property' on $target.") } + } - if (type.isInstance(operand)) { + fun `as`(operand: Any?, type: TypeSpecifier, isStrict: Boolean): Any? { + if (`is`(operand, type) == true) { return operand } - val provider = resolveDataProvider(type.getPackageName(), false) - if (provider != null) { - return provider.`as`(operand, type, isStrict) + if (isStrict) { + throw InvalidCast("Cannot cast $operand to $type.") } return null } - fun objectEqual(left: Any?, right: Any?): Boolean? { - if (left == null) { - return null - } - - val clazz: JavaClass<*> = left.javaClass - - val dataProvider = resolveDataProvider(clazz.getPackageName()) - return dataProvider!!.objectEqual(left, right) - } - - fun objectEquivalent(left: Any?, right: Any?): Boolean? { - if ((left == null) && (right == null)) { - return true - } - - if (left == null) { - return false - } - - val clazz: JavaClass<*> = left.javaClass - - val dataProvider = resolveDataProvider(clazz.getPackageName()) - return dataProvider!!.objectEquivalent(left, right) - } - fun createInstance(typeName: QName): Any? { var typeName = typeName typeName = fixupQName(typeName) @@ -154,41 +185,182 @@ constructor( return } - val clazz: JavaClass<*> = target.javaClass - - val dataProvider = resolveDataProvider(clazz.getPackageName()) - dataProvider!!.setValue(target, path, value) - } - - fun `is`(operand: Any?, type: JavaClass<*>): Boolean? { - if (operand == null) { - return null + when (target) { + is Quantity -> { + when (path) { + "value" -> target.value = value as BigDecimal? + "unit" -> target.unit = value as String? + else -> throw IllegalArgumentException("Could not set $path on Quantity.") + } + } + is Ratio -> { + when (path) { + "numerator" -> target.numerator = value as Quantity? + "denominator" -> target.denominator = value as Quantity? + else -> throw IllegalArgumentException("Could not set $path on Ratio.") + } + } + is Code -> { + when (path) { + "code" -> target.code = value as String? + "display" -> target.display = value as String? + "system" -> target.system = value as String? + "version" -> target.version = value as String? + else -> throw IllegalArgumentException("Could not set $path on Code.") + } + } + is Concept -> { + when (path) { + "display" -> target.display = value as String? + "codes" -> + target.codes = @Suppress("UNCHECKED_CAST") (value as MutableList?) + else -> throw IllegalArgumentException("Could not set $path on Concept.") + } + } + is CodeSystem -> { + when (path) { + "id" -> target.id = value as String? + "version" -> target.version = value as String? + "name" -> target.name = value as String? + else -> throw IllegalArgumentException("Could not set $path on CodeSystem.") + } + } + is ValueSet -> { + when (path) { + "id" -> target.id = value as String? + "version" -> target.version = value as String? + "name" -> target.name = value as String? + "codesystems" -> + target.setCodeSystems( + @Suppress("UNCHECKED_CAST") (value as MutableList?) + ) + else -> throw IllegalArgumentException("Could not set $path on ValueSet.") + } + } + is Interval -> { + when (path) { + "low" -> target.low = value + "high" -> target.high = value + else -> throw IllegalArgumentException("Could not set $path on $target.") + } + } + is CqlClassInstance -> target.elements[path] = value + else -> throw IllegalArgumentException("Could not set $path on $target.") } + } - // Special case for Iterable instances being checked against CQL List type. - // See https://github.com/cqframework/clinical_quality_language/issues/1577. - if ( - kotlinClassToJavaClass(Iterable::class).isAssignableFrom(type) && operand is Iterable<*> - ) { + fun `is`(operand: Any?, type: TypeSpecifier): Boolean? { + // System.Any is a supertype of all types + if (type is NamedTypeSpecifier && type.name == QName("urn:hl7-org:elm-types:r1", "Any")) { return true } - if (type.isInstance(operand)) { - return true + if (operand == null) { + return false } - val provider = resolveDataProvider(type.getPackageName(), false) - if (provider != null) { - return provider.`is`(operand, type) - } + when (type) { + is NamedTypeSpecifier -> { + val operandNamedType = getNamedTypeForCqlValue(operand) + + if (operandNamedType == null) { + return false + } + + val provider = + resolveDataProviderByModelUriOrNull(operandNamedType.getNamespaceURI()) + + if (provider == null) { + return null + } - return false + return provider.`is`(operandNamedType.getLocalPart(), type.name!!) + } + is ListTypeSpecifier -> { + if (operand is Iterable<*>) { + if (operand.any()) { + for (item in operand) { + val result = `is`(item, type.elementType!!) + if (result == null) { + return null + } + if (result == false) { + return false + } + } + return true + } + // An empty list has type List + return type.elementType == QName("urn:hl7-org:elm-types:r1", "Any") + } + return false + } + is IntervalTypeSpecifier -> { + if (operand is Interval) { + val lowResult = `is`(operand.low, type.pointType!!) + if (lowResult == false) { + return false + } + + val highResult = `is`(operand.high, type.pointType!!) + if (highResult == false) { + return false + } + + if (lowResult == true || highResult == true) { + return true + } + + return null + } + return false + } + is TupleTypeSpecifier -> { + if ( + operand is Tuple && + operand.elements.keys == type.element.map { it.name!! }.toSet() + ) { + + for (elementDefinition in type.element) { + val elementValue = operand.elements[elementDefinition.name!!] + val result = `is`(elementValue, elementDefinition.elementType!!) + if (result == null) { + return null + } + if (result == false) { + return false + } + } + return true + } + return false + } + is ChoiceTypeSpecifier -> { + var foundNull = false + for (choice in type.choice) { + val result = `is`(operand, choice) + if (result == null) { + foundNull = true + } + if (result == true) { + return true + } + } + return if (foundNull) { + null + } else { + false + } + } + else -> { + return false + } + } } // -- DataProvider resolution fun registerDataProvider(modelUri: String?, dataProvider: DataProvider?) { dataProviders[modelUri] = dataProvider - dataProvider!!.packageNames.forEach { pn -> packageMap[pn] = dataProvider } } @JsName("resolveDataProviderByQName") @@ -199,98 +371,23 @@ constructor( } fun resolveDataProviderByModelUri(modelUri: String?): DataProvider { - val dataProvider = - dataProviders[modelUri] - ?: throw CqlException("Could not resolve data provider for model '${modelUri}'.") - - return dataProvider + return resolveDataProviderByModelUriOrNull(modelUri) + ?: throw CqlException("Could not resolve data provider for model '${modelUri}'.") } - @JvmOverloads - fun resolveDataProvider(packageName: String?, mustResolve: Boolean = true): DataProvider? { - val dataProvider = packageMap[packageName] - if (dataProvider == null && mustResolve) { - throw CqlException("Could not resolve data provider for package '${packageName}'.") - } - - return dataProvider - } - - @JsName("resolveTypeByQName") - fun resolveType(typeName: QName?): JavaClass<*>? { - var typeName = typeName - typeName = fixupQName(typeName!!) - val dataProvider = resolveDataProvider(typeName) - return dataProvider.resolveType(typeName.getLocalPart()) - } - - @JsName("resolveTypeByTypeSpecifier") - fun resolveType(typeSpecifier: TypeSpecifier?): JavaClass<*>? { - return when (typeSpecifier) { - is NamedTypeSpecifier -> resolveType(typeSpecifier.name) - is ListTypeSpecifier -> - // TODO: This doesn't allow for list-distinguished overloads... - kotlinClassToJavaClass(MutableList::class) - // return resolveType(((ListTypeSpecifier)typeSpecifier).getElementType()); - is IntervalTypeSpecifier -> - // TODO: This doesn't allow for interval-distinguished overloads - kotlinClassToJavaClass(Interval::class) - is ChoiceTypeSpecifier -> - // TODO: This doesn't allow for choice-distinguished overloads... - kotlinClassToJavaClass(Any::class) - else -> - // TODO: This doesn't allow for tuple-distinguished overloads.... - kotlinClassToJavaClass(Tuple::class) - } + fun resolveDataProviderByModelUriOrNull(modelUri: String?): DataProvider? { + return dataProviders[modelUri] } - fun resolveType(value: Any?): JavaClass<*>? { - if (value == null) { - return null - } - - if (value is TypeSpecifier) { - return resolveType(value) - } - - val packageName = value.javaClassPackageName - - // May not be necessary, idea is to sync with the use of List.class for - // ListTypeSpecifiers in the resolveType above - if (value is Iterable<*>) { - return kotlinClassToJavaClass(MutableList::class) - } - - if (value is Tuple) { - return kotlinClassToJavaClass(Tuple::class) - } - - // Primitives should just use the type - // BTR: Well, we should probably be explicit about all and only the types we - // expect - if (packageName.startsWith("java")) { - return value.javaClass - } - - val dataProvider = resolveDataProvider(value.javaClassPackageName) - return dataProvider!!.resolveType(value) - } - - fun resolveOperandType(operandDef: OperandDef): JavaClass<*>? { + fun resolveOperandType(operandDef: OperandDef): TypeSpecifier { return if (operandDef.operandTypeSpecifier != null) { - resolveType(operandDef.operandTypeSpecifier) + operandDef.operandTypeSpecifier!! } else { - resolveType(operandDef.operandType) + NamedTypeSpecifier().withName(operandDef.operandType) } } - fun isType(argumentType: JavaClass<*>?, operandType: JavaClass<*>): Boolean { - return argumentType == null || operandType.isAssignableFrom(argumentType) - } - fun matchesTypes(functionDef: FunctionDef, arguments: kotlin.collections.List<*>): Boolean { - var isMatch = true - val operands = functionDef.operand // if argument length is mismatched, don't compare @@ -298,14 +395,9 @@ constructor( return false } - for (i in arguments.indices) { - isMatch = isType(resolveType(arguments[i]), this.resolveOperandType(operands[i])!!) - if (!isMatch) { - break - } + return arguments.zip(operands).all { (argument, operand) -> + argument == null || `is`(argument, resolveOperandType(operand)) != false } - - return isMatch } fun fixupQName(typeName: QName): QName { diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/BaseModelResolver.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/BaseModelResolver.kt deleted file mode 100644 index 8b244b9a3..000000000 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/BaseModelResolver.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.opencds.cqf.cql.engine.model - -import org.opencds.cqf.cql.engine.exception.InvalidCast -import org.opencds.cqf.cql.engine.util.JavaClass -import org.opencds.cqf.cql.engine.util.javaClassName - -abstract class BaseModelResolver : ModelResolver { - override fun `is`(value: Any?, type: JavaClass<*>?): Boolean? { - if (value == null) { - return null - } - - return type!!.isInstance(value) - } - - override fun `as`(value: Any?, type: JavaClass<*>?, isStrict: Boolean): Any? { - if (value == null) { - return null - } - - if (type!!.isInstance(value)) { - return value - } - - if (isStrict) { - throw InvalidCast( - "Cannot cast a value of type ${value.javaClassName} as ${type.getName()}." - ) - } - - return null - } -} diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.kt index 7fb22a673..0b5628757 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecorator.kt @@ -1,93 +1,27 @@ package org.opencds.cqf.cql.engine.model -import org.opencds.cqf.cql.engine.util.JavaClass +import org.cqframework.cql.shared.QName import org.opencds.cqf.cql.engine.util.createConcurrentHashMap -import org.opencds.cqf.cql.engine.util.javaClass open class CachingModelResolverDecorator(val innerResolver: ModelResolver) : ModelResolver { - @Suppress("deprecation") - @Deprecated("use packageNames instead") - override var packageName: String? - get() = this.innerResolver.packageName - set(value) { - this.innerResolver.packageName = value - } - - override fun resolvePath(target: Any?, path: String?): Any? { - return this.innerResolver.resolvePath(target, path) - } override fun getContextPath(contextType: String?, targetType: String?): Any? { if (contextType == null) { return null } - for (pn in this.packageNames) { - val packageContextResolutions = - perPackageContextResolutions.getOrPut(pn) { createConcurrentHashMap() } - - val contextTypeResolutions = - packageContextResolutions.getOrPut(contextType) { createConcurrentHashMap() } - - val cached = contextTypeResolutions[targetType] - if (cached != null) { - return cached - } - - val result = this.innerResolver.getContextPath(contextType, targetType) - if (result != null) { - contextTypeResolutions[targetType] = result - return result - } - } - - return null - } + val contextTypeResolutions = + contextResolutions.getOrPut(contextType) { createConcurrentHashMap() } - override fun resolveType(typeName: String?): JavaClass<*>? { - if (typeName == null) { - return null + val cached = contextTypeResolutions[targetType] + if (cached != null) { + return cached } - for (pn in this.packageNames) { - val packageTypeResolutions = - perPackageTypeResolutionsByTypeName.getOrPut(pn) { createConcurrentHashMap() } - - val cached = packageTypeResolutions[typeName] - if (cached != null) { - return cached - } - - val result = this.innerResolver.resolveType(typeName) - if (result != null) { - packageTypeResolutions[typeName] = result - return result - } - } - - return null - } - - override fun resolveType(value: Any?): JavaClass<*>? { - if (value == null) { - return null - } - - val valueClass = value.javaClass - for (pn in this.packageNames) { - val packageTypeResolutions = - perPackageTypeResolutionsByClass.getOrPut(pn) { createConcurrentHashMap() } - - val cached = packageTypeResolutions[valueClass] - if (cached != null) { - return cached - } - - val result = this.innerResolver.resolveType(value) - if (result != null) { - packageTypeResolutions[valueClass] = result - return result - } + val result = this.innerResolver.getContextPath(contextType, targetType) + if (result != null) { + contextTypeResolutions[targetType] = result + return result } return null @@ -97,36 +31,16 @@ open class CachingModelResolverDecorator(val innerResolver: ModelResolver) : Mod return this.innerResolver.createInstance(typeName) } - override fun setValue(target: Any?, path: String?, value: Any?) { - this.innerResolver.setValue(target, path, value) - } - - override fun objectEqual(left: Any?, right: Any?): Boolean? { - return this.innerResolver.objectEqual(left, right) - } - - override fun objectEquivalent(left: Any?, right: Any?): Boolean? { - return this.innerResolver.objectEquivalent(left, right) - } - override fun resolveId(target: Any?): String? { return innerResolver.resolveId(target) } - override fun `is`(value: Any?, type: JavaClass<*>?): Boolean? { - return this.innerResolver.`is`(value, type) - } - - override fun `as`(value: Any?, type: JavaClass<*>?, isStrict: Boolean): Any? { - return this.innerResolver.`as`(value, type, isStrict) + override fun `is`(valueType: String, type: QName): Boolean? { + return this.innerResolver.`is`(valueType, type) } companion object { - private val perPackageContextResolutions = - createConcurrentHashMap>>() - private val perPackageTypeResolutionsByTypeName = - createConcurrentHashMap?>>() - private val perPackageTypeResolutionsByClass = - createConcurrentHashMap, JavaClass<*>?>>() + private val contextResolutions = + createConcurrentHashMap>() } } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/ModelResolver.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/ModelResolver.kt index bc2a92a79..47b74598a 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/ModelResolver.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/model/ModelResolver.kt @@ -1,6 +1,6 @@ package org.opencds.cqf.cql.engine.model -import org.opencds.cqf.cql.engine.util.JavaClass +import org.cqframework.cql.shared.QName /** * A ModelResolver provides support for mapping a logical model (e.g. QDM or FHIR) onto a Java @@ -9,40 +9,6 @@ import org.opencds.cqf.cql.engine.util.JavaClass * also possibly with different property naming schemes, etc. */ interface ModelResolver { - @get:Deprecated("Use getPackageNames() instead") - @set:Deprecated("Use setPackageNames#String instead") - var packageName: String? - - var packageNames: MutableList - /** - * Return the package names of Java objects supported by this model - * - * @return list of Java package names for model objects that support this model. - */ - get() = mutableListOf(this.packageName) - /** - * Set the package names of Java objects supported by this model - * - * @param packageNames list of Java package names for model objects that support this model. - */ - set(packageNames) { - // Intentionally empty. This provides backwards - // compatibility for models that implement a single - // package name and still use the get/setPackageName - // methods. - } - - /** - * Resolve the provided path expression for the provided target. Paths can be things like simple - * dotted property notation (e.g. Patient.id) or more complex things like list indexed property - * expressions (e.g. Patient.name[0].given). The exact details are configured in the model - * definition and passed to the ELM file during CQL to ELM translation. - * - * @return result of the provided expression. Null is expected whenever a path doesn't exist on - * the target. - */ - fun resolvePath(target: Any?, path: String?): Any? - /** * Get the path expression that expresses the relationship between the `targetType` and the * given `contextType`. For example, in a FHIR model, with context type `Patient` and targetType @@ -52,43 +18,14 @@ interface ModelResolver { fun getContextPath(contextType: String?, targetType: String?): Any? /** - * Resolve the Java class that corresponds to the given model type - * - * @param typeName Model type name. In the ELM, model objects are namespaced (e.g. - * FHIR.Patient), but the namespace is removed prior to calling this method, so the input - * would just be Patient. - * @return Class object that represents the specified model type - */ - fun resolveType(typeName: String?): JavaClass<*>? - - /** - * Resolve the Java class that corresponds to the given model object instance. - * - * @param value Object instance - * @return Class object that represents the specified value - */ - fun resolveType(value: Any?): JavaClass<*>? - - /** - * Check whether or not a specified value instance is of the specified type. - * - * @param value - * @param type - * @return true when the value is of the specified type, otherwise false. - */ - fun `is`(value: Any?, type: JavaClass<*>?): Boolean? - - /** - * Cast the specified value to the specified type. When type conversion is not possible, null - * should be returned unless the isStrict flag is set to true wherein an Exception will be - * thrown. + * Check whether or not a specified `valueType` is a subtype of the specified `type`. * - * @param value model object instance - * @param type type to which the value should be case - * @param isStrict flag indicating how to handle invalid type conversion - * @return the result of the value conversion or null if conversion is not possible. + * @param valueType A model type, e.g. `"Patient"` + * @param type E.g. `QName("http://hl7.org/fhir", "DomainResource")` + * @return `true` when `valueType` is a subtype of (or the same type as) the specified `type`, + * otherwise `false`. */ - fun `as`(value: Any?, type: JavaClass<*>?, isStrict: Boolean): Any? + fun `is`(valueType: String, type: QName): Boolean? /** * Create an instance of the model object that corresponds to the specified type. @@ -98,33 +35,6 @@ interface ModelResolver { */ fun createInstance(typeName: String?): Any? - /** - * Set the value of a particular property on the given model object. - * - * @param target model object - * @param path path to the property that will be set - * @param value value to set to the property indicated by the path expression - */ - fun setValue(target: Any?, path: String?, value: Any?) - - /** - * Compare two objects for equality - * - * @param left left hand side of the equality expression - * @param right right hand side of the equality expression - * @return flag indicating whether the objects are equal - */ - fun objectEqual(left: Any?, right: Any?): Boolean? - - /** - * Compare two objects for equivalence - * - * @param left left hand side of the equivalence expression - * @param right right hand side of the equivalence expression - * @return flag indicating whether the objects are equal - */ - fun objectEquivalent(left: Any?, right: Any?): Boolean? - /** * Ensure that for a given object each implementation can introspect that object in its own way * to resolve a String ID. diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlClassInstance.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlClassInstance.kt new file mode 100644 index 000000000..5bbff7247 --- /dev/null +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlClassInstance.kt @@ -0,0 +1,6 @@ +package org.opencds.cqf.cql.engine.runtime + +import org.cqframework.cql.shared.QName + +/** Represents an instance of a named structured type (class). */ +data class CqlClassInstance(val type: QName, val elements: MutableMap) : CqlType diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt new file mode 100644 index 000000000..5ecca2ad1 --- /dev/null +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt @@ -0,0 +1,35 @@ +package org.opencds.cqf.cql.engine.runtime + +import org.cqframework.cql.shared.BigDecimal +import org.cqframework.cql.shared.QName + +/** + * Returns the type as a `QName` for instances of named types. Returns null for intervals, lists, + * and anonymous tuples. + * + * @param value The native CQL value to get the type for. + */ +fun getNamedTypeForCqlValue(value: Any?): QName? { + if (value == null) { + return QName("urn:hl7-org:elm-types:r1", "Any") + } + + return when (value) { + is Boolean -> QName("urn:hl7-org:elm-types:r1", "Boolean") + is Int -> QName("urn:hl7-org:elm-types:r1", "Integer") + is Long -> QName("urn:hl7-org:elm-types:r1", "Long") + is BigDecimal -> QName("urn:hl7-org:elm-types:r1", "Decimal") + is String -> QName("urn:hl7-org:elm-types:r1", "String") + is Date -> QName("urn:hl7-org:elm-types:r1", "Date") + is DateTime -> QName("urn:hl7-org:elm-types:r1", "DateTime") + is Time -> QName("urn:hl7-org:elm-types:r1", "Time") + is Quantity -> QName("urn:hl7-org:elm-types:r1", "Quantity") + is Ratio -> QName("urn:hl7-org:elm-types:r1", "Ratio") + is Code -> QName("urn:hl7-org:elm-types:r1", "Code") + is Concept -> QName("urn:hl7-org:elm-types:r1", "Concept") + is CodeSystem -> QName("urn:hl7-org:elm-types:r1", "CodeSystem") + is ValueSet -> QName("urn:hl7-org:elm-types:r1", "ValueSet") + is CqlClassInstance -> value.type + else -> null + } +} diff --git a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.kt b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.kt index 0f3a26069..ff3dd9147 100644 --- a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.kt +++ b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProviderTest.kt @@ -1,21 +1,9 @@ package org.opencds.cqf.cql.engine.data -import java.time.Month import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import org.opencds.cqf.cql.engine.runtime.Date internal class SystemDataProviderTest { - @Test - fun resolveMissingPropertyReturnsNull() { - val provider = SystemDataProvider() - - val date = Date(2019, Month.JANUARY.value, 1) - - val value = provider.resolvePath(date, "notapath") - Assertions.assertNull(value) - } - @Test fun resolveIdAlwaysReturnsNull() { val provider = SystemDataProvider() diff --git a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt index 91f5fccf5..5c3c57817 100644 --- a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt +++ b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt @@ -31,7 +31,7 @@ internal class FunctionRefEvaluatorTest { ) } Assertions.assertEquals( - "Could not resolve call to operator 'func(java.lang.Integer, java.lang.Integer, java.lang.Integer)' in library 'lib'.", + "Could not resolve call to operator 'func({urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer)' in library 'lib'.", cqlException.message, ) } @@ -79,10 +79,16 @@ internal class FunctionRefEvaluatorTest { var actual = FunctionRefEvaluator.typesToString(state, mutableListOf("a", "b", "c")) - Assertions.assertEquals("java.lang.String, java.lang.String, java.lang.String", actual) + Assertions.assertEquals( + "{urn:hl7-org:elm-types:r1}String, {urn:hl7-org:elm-types:r1}String, {urn:hl7-org:elm-types:r1}String", + actual, + ) actual = FunctionRefEvaluator.typesToString(state, mutableListOf(1, 2, null)) - Assertions.assertEquals("java.lang.Integer, java.lang.Integer, null", actual) + Assertions.assertEquals( + "{urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Any", + actual, + ) actual = FunctionRefEvaluator.typesToString(state, null) Assertions.assertEquals("", actual) diff --git a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlErrorsAndMessagingOperatorsTest.kt b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlErrorsAndMessagingOperatorsTest.kt index 433c3cbe8..45f86d61b 100644 --- a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlErrorsAndMessagingOperatorsTest.kt +++ b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlErrorsAndMessagingOperatorsTest.kt @@ -31,7 +31,7 @@ internal class CqlErrorsAndMessagingOperatorsTest : CqlTestBase() { try { value = engine.expression(library, "TestMessageError") } catch (re: RuntimeException) { - Assertions.assertEquals(re.message, String.format("400: This is an error!\n")) + Assertions.assertEquals(re.message, String.format("400: This is an error!\n4")) } value = engine.expression(library, "TestMessageWithNullSeverity") @@ -91,14 +91,14 @@ internal class CqlErrorsAndMessagingOperatorsTest : CqlTestBase() { value = engine.expression(library, "TestErrorWithNullCode") MatcherAssert.assertThat(value, Matchers.`is`(1)) } catch (re: RuntimeException) { - Assertions.assertEquals(re.message, String.format("This is a message\n")) + Assertions.assertEquals(re.message, String.format("This is a message\n1")) } try { value = engine.expression(library, "TestErrorWithNullMessage") MatcherAssert.assertThat(value, Matchers.`is`(1)) } catch (re: RuntimeException) { - Assertions.assertEquals(re.message, String.format("1: null\n")) + Assertions.assertEquals(re.message, String.format("1: null\n1")) } } diff --git a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlListDistinguishedOverloadsTest.kt b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlListDistinguishedOverloadsTest.kt index 6f2a36aa9..5175129d5 100644 --- a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlListDistinguishedOverloadsTest.kt +++ b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/execution/CqlListDistinguishedOverloadsTest.kt @@ -5,7 +5,6 @@ import org.cqframework.cql.cql2elm.LibraryBuilder import org.hl7.elm.r1.VersionedIdentifier import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test -import org.opencds.cqf.cql.engine.exception.CqlException internal class CqlListDistinguishedOverloadsTest : CqlTestBase() { @Test @@ -14,19 +13,13 @@ internal class CqlListDistinguishedOverloadsTest : CqlTestBase() { val engine1 = getEngine(compilerOptions.withSignatureLevel(LibraryBuilder.SignatureLevel.Overloads)) - val value = engine1.expression(library, "Test") + var value = engine1.expression(library, "Test") Assertions.assertEquals("1, 2, 3, 4, 5", value) val engine2 = getEngine(compilerOptions.withSignatureLevel(LibraryBuilder.SignatureLevel.None)) - val cqlException = - Assertions.assertThrows(CqlException::class.java) { - engine2.expression(library, "Test") - } - Assertions.assertEquals( - "Ambiguous call to operator 'toString(java.util.List)' in library 'CqlListDistinguishedOverloads'.", - cqlException.message, - ) + value = engine2.expression(library, "Test") + Assertions.assertEquals("1, 2, 3, 4, 5", value) } companion object { diff --git a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.kt b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.kt index 1825e7ca8..334fb37ab 100644 --- a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.kt +++ b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/model/CachingModelResolverDecoratorTest.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations @@ -33,10 +32,8 @@ internal class CachingModelResolverDecoratorTest { } @Test - @Suppress("deprecation") fun context_path_resolved_only_once() { val m = Mockito.mock(ModelResolver::class.java) - Mockito.`when`(m.packageName).thenReturn("test.package") Mockito.`when`(m.getContextPath("Patient", "Patient")).thenReturn("id") val cache = CachingModelResolverDecorator(m) @@ -47,24 +44,6 @@ internal class CachingModelResolverDecoratorTest { Mockito.verify(m, Mockito.times(1)).getContextPath("Patient", "Patient") } - @Test - @Suppress("deprecation") - fun type_resolved_only_once() { - val m = Mockito.mock(ModelResolver::class.java) - Mockito.`when`(m.packageName).thenReturn("test.package") - Mockito.`when`(m.resolveType(ArgumentMatchers.isA(Int::class.java))) - .thenReturn(Int::class.java) - Mockito.`when`(m.resolveType(ArgumentMatchers.isA(Class::class.java))) - .thenThrow(RuntimeException("Can't get a class of a class")) - - val cache = CachingModelResolverDecorator(m) - cache.resolveType(5) - val result = cache.resolveType(5) - - Assertions.assertEquals(Int::class.java, result) - Mockito.verify(m, Mockito.times(1)).resolveType(5) - } - @Test fun resolveIdString() { val `object` = "object" From cf5de729ca2e417c78bbc58f36fa83a78288680c Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Thu, 26 Mar 2026 15:35:45 +1300 Subject: [PATCH 02/21] Type compatibility checking --- Src/java/engine/detekt-baseline.xml | 15 +- .../cqf/cql/engine/data/SystemDataProvider.kt | 18 +- .../engine/elm/executing/MessageEvaluator.kt | 5 +- .../cqf/cql/engine/execution/CqlEngine.kt | 3 +- .../cqf/cql/engine/execution/Environment.kt | 206 ++++++++++++++++-- .../cqf/cql/engine/runtime/CqlValue.kt | 49 +++-- 6 files changed, 243 insertions(+), 53 deletions(-) diff --git a/Src/java/engine/detekt-baseline.xml b/Src/java/engine/detekt-baseline.xml index be0af9349..20565142a 100644 --- a/Src/java/engine/detekt-baseline.xml +++ b/Src/java/engine/detekt-baseline.xml @@ -24,7 +24,8 @@ CyclomaticComplexMethod:CqlValue.kt$fun getNamedTypeForCqlValue(value: Any?): QName? CyclomaticComplexMethod:DifferenceBetweenEvaluator.kt$DifferenceBetweenEvaluator$@JvmStatic fun difference(left: Any?, right: Any?, precision: Precision): Any? CyclomaticComplexMethod:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? - CyclomaticComplexMethod:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? + CyclomaticComplexMethod:Environment.kt$Environment$fun `is`(value: Any?, type: TypeSpecifier): Boolean? + CyclomaticComplexMethod:Environment.kt$Environment$fun isCompatible(value: Any?, type: TypeSpecifier): Boolean? CyclomaticComplexMethod:Environment.kt$Environment$fun resolveProperty(target: Any?, property: String): Any? CyclomaticComplexMethod:Environment.kt$Environment$fun setValue(target: Any?, path: String, value: Any?) CyclomaticComplexMethod:EquivalentEvaluator.kt$EquivalentEvaluator$@JvmStatic @JvmOverloads fun equivalent(left: Any?, right: Any?, state: State? = null): Boolean? @@ -70,7 +71,7 @@ ForbiddenComment:SystemExternalFunctionProvider.kt$SystemExternalFunctionProvider$// TODO: Support adding more functions to an existing provider object. ForbiddenComment:TerminologyAwareRetrieveProvider.kt$TerminologyAwareRetrieveProvider$// TODO: Think about how to best handle the decision to expand value sets... Should it be part FunctionNaming:Environment.kt$Environment$fun `as`(operand: Any?, type: TypeSpecifier, isStrict: Boolean): Any? - FunctionNaming:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? + FunctionNaming:Environment.kt$Environment$fun `is`(value: Any?, type: TypeSpecifier): Boolean? FunctionNaming:InEvaluator.kt$InEvaluator$fun `in`(left: Any?, right: Any?, precision: String?, state: State?): Boolean? FunctionNaming:ModelResolver.kt$ModelResolver$fun `is`(valueType: String, type: QName): Boolean? FunctionNaming:TerminologyProvider.kt$TerminologyProvider$fun `in`(code: Code, valueSet: ValueSetInfo): Boolean @@ -110,7 +111,8 @@ LongMethod:DateTimeTest.kt$DateTimeTest$@Test fun offsetPrecisionsTest() LongMethod:DifferenceBetweenEvaluator.kt$DifferenceBetweenEvaluator$@JvmStatic fun difference(left: Any?, right: Any?, precision: Precision): Any? LongMethod:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? - LongMethod:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? + LongMethod:Environment.kt$Environment$fun `is`(value: Any?, type: TypeSpecifier): Boolean? + LongMethod:Environment.kt$Environment$fun isCompatible(value: Any?, type: TypeSpecifier): Boolean? LongMethod:Environment.kt$Environment$fun resolveProperty(target: Any?, property: String): Any? LongMethod:Environment.kt$Environment$fun setValue(target: Any?, path: String, value: Any?) LongMethod:EqualEvaluator.kt$EqualEvaluator$@JvmStatic @JvmOverloads fun equal(left: Any?, right: Any?, state: State? = null): Boolean? @@ -320,7 +322,8 @@ NestedBlockDepth:DebugLibraryMapEntry.kt$DebugLibraryMapEntry$@Suppress("ReturnCount") fun shouldDebug(node: Element?): DebugAction? NestedBlockDepth:DifferenceBetweenEvaluator.kt$DifferenceBetweenEvaluator$@JvmStatic fun difference(left: Any?, right: Any?, precision: Precision): Any? NestedBlockDepth:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? - NestedBlockDepth:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? + NestedBlockDepth:Environment.kt$Environment$fun `is`(value: Any?, type: TypeSpecifier): Boolean? + NestedBlockDepth:Environment.kt$Environment$fun isCompatible(value: Any?, type: TypeSpecifier): Boolean? NestedBlockDepth:FilterEvaluator.kt$FilterEvaluator$@JvmStatic fun filter(elm: Filter?, source: Any?, condition: Any?, state: State?): Any? NestedBlockDepth:FlattenEvaluator.kt$FlattenEvaluator$@JvmStatic fun flatten(operand: Any?): List<Any?>? NestedBlockDepth:InCodeSystemEvaluator.kt$InCodeSystemEvaluator$@JvmStatic fun inCodeSystem(code: Any?, codeSystem: Any?, state: State?): Any? @@ -389,7 +392,8 @@ ReturnCount:DivideEvaluator.kt$DivideEvaluator$@JvmStatic fun divide(left: Any?, right: Any?, state: State?): Any? ReturnCount:DurationBetweenEvaluator.kt$DurationBetweenEvaluator$@JvmStatic fun duration(left: Any?, right: Any?, precision: Precision?): Any? ReturnCount:EndsEvaluator.kt$EndsEvaluator$@JvmStatic fun ends(left: Any?, right: Any?, precision: String?, state: State?): Boolean? - ReturnCount:Environment.kt$Environment$fun `is`(operand: Any?, type: TypeSpecifier): Boolean? + ReturnCount:Environment.kt$Environment$fun `is`(value: Any?, type: TypeSpecifier): Boolean? + ReturnCount:Environment.kt$Environment$fun isCompatible(value: Any?, type: TypeSpecifier): Boolean? ReturnCount:EvaluationVisitor.kt$EvaluationVisitor$override fun visitCase(elm: Case, context: State?): Any? ReturnCount:ExceptEvaluator.kt$ExceptEvaluator$@JvmStatic fun except(left: Any?, right: Any?, state: State?): Any? ReturnCount:ExpandEvaluator.kt$ExpandEvaluator$@JvmStatic fun expand(listOrInterval: Any?, per: Quantity?, state: State?): Any? @@ -561,6 +565,7 @@ TooManyFunctions:TimeJs.kt$OffsetDateTimeJs : TemporalJs TooManyFunctions:TimeJs.kt$org.opencds.cqf.cql.engine.util.TimeJs.kt TooManyFunctions:TimeJvm.kt$org.opencds.cqf.cql.engine.util.TimeJvm.kt + TopLevelPropertyNaming:CqlValue.kt$const val systemModelNamespaceUri = "urn:hl7-org:elm-types:r1" TopLevelPropertyNaming:ReflectionJs.kt$const val unknownPackageName = "unknown" TopLevelPropertyNaming:ReflectionJs.kt$const val unknownSimpleName = "Unknown" UnusedParameter:FunctionRefEvaluator.kt$FunctionRefEvaluator$state: State? diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt index a3ded270a..dfcc57c1a 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/data/SystemDataProvider.kt @@ -21,11 +21,19 @@ open class SystemDataProvider : DataProvider { throw IllegalArgumentException("SystemDataProvider does not support retrieval.") } - override fun `is`(valueType: String, type: QName): Boolean? { - return type.getNamespaceURI() == "urn:hl7-org:elm-types:r1" && - type.getLocalPart() == valueType || - valueType == "Any" || - type == QName("urn:hl7-org:elm-types:r1", "Any") + /** + * Returns true if: + * - `type` is System._valueType_ (exact type name match), or + * - `type` is System.Any (System.Any is a supertype of all types), or + * - value type is System.ValueSet/System.CodeSystem and type is System.Vocabulary (ValueSet and + * CodeSystem are the only types in the system model that don't immediately extend + * System.Any). + */ + override fun `is`(valueType: String, type: QName): Boolean { + return type == QName(systemModelNamespaceUri, valueType) || + type == anyTypeName || + (valueType == valueSetTypeName.getLocalPart() || + valueType == codeSystemTypeName.getLocalPart()) && type == vocabularyTypeName } override fun createInstance(typeName: String?): Any? { diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt index 16eb4a06e..66b8fd2a8 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/MessageEvaluator.kt @@ -10,6 +10,7 @@ import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.runtime.Interval import org.opencds.cqf.cql.engine.runtime.Tuple import org.opencds.cqf.cql.engine.runtime.getNamedTypeForCqlValue +import org.opencds.cqf.cql.engine.runtime.systemModelNamespaceUri object MessageEvaluator { val logger = KotlinLogging.logger("MessageEvaluator") @@ -81,9 +82,7 @@ object MessageEvaluator { is Interval, is Tuple, is Iterable<*> -> - state!! - .environment - .resolveDataProviderByModelUriOrNull("urn:hl7-org:elm-types:r1") + state!!.environment.resolveDataProviderByModelUriOrNull(systemModelNamespaceUri) else -> state!! .environment diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/CqlEngine.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/CqlEngine.kt index cb8922338..6b84b42ef 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/CqlEngine.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/CqlEngine.kt @@ -19,6 +19,7 @@ import org.opencds.cqf.cql.engine.debug.DebugMap import org.opencds.cqf.cql.engine.debug.SourceLocator.Companion.fromNode import org.opencds.cqf.cql.engine.elm.executing.FunctionRefEvaluator.evaluateFunctionDef import org.opencds.cqf.cql.engine.exception.CqlException +import org.opencds.cqf.cql.engine.runtime.systemModelNamespaceUri import org.opencds.cqf.cql.engine.util.ZonedDateTime import org.opencds.cqf.cql.engine.util.zonedDateTimeNow @@ -408,7 +409,7 @@ constructor(val environment: Environment, engineOptions: MutableSet? = if (library.usings != null && !library.usings!!.def.isEmpty()) { for (using in library.usings!!.def) { // Skip system using since the context automatically registers that. - if (using.uri.equals("urn:hl7-org:elm-types:r1")) { + if (using.uri.equals(systemModelNamespaceUri)) { continue } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt index 939c93b1c..3e63988c1 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt @@ -22,7 +22,9 @@ import org.opencds.cqf.cql.engine.runtime.Quantity import org.opencds.cqf.cql.engine.runtime.Ratio import org.opencds.cqf.cql.engine.runtime.Tuple import org.opencds.cqf.cql.engine.runtime.ValueSet +import org.opencds.cqf.cql.engine.runtime.anyTypeName import org.opencds.cqf.cql.engine.runtime.getNamedTypeForCqlValue +import org.opencds.cqf.cql.engine.runtime.systemModelNamespaceUri import org.opencds.cqf.cql.engine.terminology.TerminologyProvider /** @@ -54,8 +56,8 @@ constructor( } } - if (!this.dataProviders.containsKey("urn:hl7-org:elm-types:r1")) { - this.registerDataProvider("urn:hl7-org:elm-types:r1", SystemDataProvider()) + if (!this.dataProviders.containsKey(systemModelNamespaceUri)) { + this.registerDataProvider(systemModelNamespaceUri, SystemDataProvider()) } } @@ -249,60 +251,77 @@ constructor( } } - fun `is`(operand: Any?, type: TypeSpecifier): Boolean? { + /** + * Returns true if the value is of the specified type. Returns null if type relationship cannot + * be determined. This is not the same as type compatibility (see [isCompatible]). + */ + fun `is`(value: Any?, type: TypeSpecifier): Boolean? { // System.Any is a supertype of all types - if (type is NamedTypeSpecifier && type.name == QName("urn:hl7-org:elm-types:r1", "Any")) { + if (type is NamedTypeSpecifier && type.name == anyTypeName) { return true } - if (operand == null) { + if (value == null) { + // `null is X` is true if X is System.Any (handled above) and false otherwise return false } when (type) { is NamedTypeSpecifier -> { - val operandNamedType = getNamedTypeForCqlValue(operand) + val valueNamedType = getNamedTypeForCqlValue(value) - if (operandNamedType == null) { + if (valueNamedType == null) { + // value is not an instance of a named type return false } - val provider = - resolveDataProviderByModelUriOrNull(operandNamedType.getNamespaceURI()) + if (valueNamedType == type.name) { + // Types are the same + return true + } + + val provider = resolveDataProviderByModelUriOrNull(valueNamedType.getNamespaceURI()) if (provider == null) { + // Cannot determine relationship between types return null } - return provider.`is`(operandNamedType.getLocalPart(), type.name!!) + return provider.`is`(valueNamedType.getLocalPart(), type.name!!) } is ListTypeSpecifier -> { - if (operand is Iterable<*>) { - if (operand.any()) { - for (item in operand) { + if (value is Iterable<*>) { + if (value.any()) { + for (item in value) { val result = `is`(item, type.elementType!!) if (result == null) { + // Found an element for which we cannot determine type relationship return null } if (result == false) { + // Found an element that is not of the correct type return false } } return true } - // An empty list has type List - return type.elementType == QName("urn:hl7-org:elm-types:r1", "Any") + + // An empty list has type List. Return true if and only if the + // element type of the type specifier is System.Any. + return type.elementType == NamedTypeSpecifier().withName(anyTypeName) } + + // Must be an Iterable to be of a list type return false } is IntervalTypeSpecifier -> { - if (operand is Interval) { - val lowResult = `is`(operand.low, type.pointType!!) + if (value is Interval) { + val lowResult = `is`(value.low, type.pointType!!) if (lowResult == false) { return false } - val highResult = `is`(operand.high, type.pointType!!) + val highResult = `is`(value.high, type.pointType!!) if (highResult == false) { return false } @@ -313,36 +332,41 @@ constructor( return null } + + // Must be an interval to be of an interval type return false } is TupleTypeSpecifier -> { if ( - operand is Tuple && - operand.elements.keys == type.element.map { it.name!! }.toSet() + value is Tuple && value.elements.keys == type.element.map { it.name!! }.toSet() ) { - for (elementDefinition in type.element) { - val elementValue = operand.elements[elementDefinition.name!!] + val elementValue = value.elements[elementDefinition.name!!] val result = `is`(elementValue, elementDefinition.elementType!!) if (result == null) { + // Found an element for which we cannot determine type relationship return null } if (result == false) { + // Found an element that is not of the correct type return false } } return true } + + // Must be a tuple with the matching element names to be of the specified tuple type return false } is ChoiceTypeSpecifier -> { var foundNull = false for (choice in type.choice) { - val result = `is`(operand, choice) + val result = `is`(value, choice) if (result == null) { foundNull = true } if (result == true) { + // Found a match return true } } @@ -353,8 +377,141 @@ constructor( } } else -> { + throw IllegalArgumentException("Unexpected type specifier: $type.") + } + } + } + + /** + * Returns true if the value's type is compatible with the specified type (so e.g. the value can + * be passed to a function expecting the specified type). Returns null if we cannot determine + * compatibility. This is not the same as is-checking (see [`is`]). + */ + fun isCompatible(value: Any?, type: TypeSpecifier): Boolean? { + // System.Any is a supertype of all types + if (type is NamedTypeSpecifier && type.name == anyTypeName) { + return true + } + + // null is compatible with all types + if (value == null) { + return true + } + + when (type) { + is NamedTypeSpecifier -> { + val valueNamedType = getNamedTypeForCqlValue(value) + + if (valueNamedType == null) { + // value is not an instance of a named type + return false + } + + if (valueNamedType == type.name) { + // Types are the same + return true + } + + val provider = resolveDataProviderByModelUriOrNull(valueNamedType.getNamespaceURI()) + + if (provider == null) { + // Cannot determine compatibility + return null + } + + return provider.`is`(valueNamedType.getLocalPart(), type.name!!) + } + is ListTypeSpecifier -> { + if (value is Iterable<*>) { + if (value.any()) { + for (item in value) { + val result = isCompatible(item, type.elementType!!) + if (result == null) { + // Found an element for which we cannot determine compatibility + return null + } + if (result == false) { + // Found an element that is not compatible + return false + } + } + return true + } + + // An empty list is compatible with all list types + return true + } + + // Must be an Iterable to be compatible with a list type + return false + } + is IntervalTypeSpecifier -> { + if (value is Interval) { + val lowResult = isCompatible(value.low, type.pointType!!) + if (lowResult == false) { + return false + } + + val highResult = isCompatible(value.high, type.pointType!!) + if (highResult == false) { + return false + } + + if (lowResult == true || highResult == true) { + return true + } + + return null + } + + // Must be an interval to be compatible with an interval type + return false + } + is TupleTypeSpecifier -> { + if (value is Tuple) { + for (elementDefinition in type.element) { + if (!value.elements.containsKey(elementDefinition.name!!)) { + // Value is missing an element + return false + } + val elementValue = value.elements[elementDefinition.name!!] + val result = isCompatible(elementValue, elementDefinition.elementType!!) + if (result == null) { + // Found an element for which we cannot determine compatibility + return null + } + if (result == false) { + // Found an element that is not compatible + return false + } + } + return true + } + + // Must be a tuple to be compatible with a tuple type return false } + is ChoiceTypeSpecifier -> { + var foundNull = false + for (choice in type.choice) { + val result = isCompatible(value, choice) + if (result == null) { + foundNull = true + } + if (result == true) { + // Found a type that is compatible + return true + } + } + return if (foundNull) { + null + } else { + false + } + } + else -> { + throw IllegalArgumentException("Unexpected type specifier: $type.") + } } } @@ -395,8 +552,9 @@ constructor( return false } + // Types must not be incompatible return arguments.zip(operands).all { (argument, operand) -> - argument == null || `is`(argument, resolveOperandType(operand)) != false + isCompatible(argument, resolveOperandType(operand)) != false } } diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt index 5ecca2ad1..898ad108c 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/runtime/CqlValue.kt @@ -3,6 +3,25 @@ package org.opencds.cqf.cql.engine.runtime import org.cqframework.cql.shared.BigDecimal import org.cqframework.cql.shared.QName +const val systemModelNamespaceUri = "urn:hl7-org:elm-types:r1" + +val anyTypeName = QName(systemModelNamespaceUri, "Any") +val booleanTypeName = QName(systemModelNamespaceUri, "Boolean") +val integerTypeName = QName(systemModelNamespaceUri, "Integer") +val longTypeName = QName(systemModelNamespaceUri, "Long") +val decimalTypeName = QName(systemModelNamespaceUri, "Decimal") +val stringTypeName = QName(systemModelNamespaceUri, "String") +val dateTypeName = QName(systemModelNamespaceUri, "Date") +val dateTimeTypeName = QName(systemModelNamespaceUri, "DateTime") +val timeTypeName = QName(systemModelNamespaceUri, "Time") +val quantityTypeName = QName(systemModelNamespaceUri, "Quantity") +val ratioTypeName = QName(systemModelNamespaceUri, "Ratio") +val codeTypeName = QName(systemModelNamespaceUri, "Code") +val conceptTypeName = QName(systemModelNamespaceUri, "Concept") +val codeSystemTypeName = QName(systemModelNamespaceUri, "CodeSystem") +val valueSetTypeName = QName(systemModelNamespaceUri, "ValueSet") +val vocabularyTypeName = QName(systemModelNamespaceUri, "Vocabulary") + /** * Returns the type as a `QName` for instances of named types. Returns null for intervals, lists, * and anonymous tuples. @@ -11,24 +30,24 @@ import org.cqframework.cql.shared.QName */ fun getNamedTypeForCqlValue(value: Any?): QName? { if (value == null) { - return QName("urn:hl7-org:elm-types:r1", "Any") + return anyTypeName } return when (value) { - is Boolean -> QName("urn:hl7-org:elm-types:r1", "Boolean") - is Int -> QName("urn:hl7-org:elm-types:r1", "Integer") - is Long -> QName("urn:hl7-org:elm-types:r1", "Long") - is BigDecimal -> QName("urn:hl7-org:elm-types:r1", "Decimal") - is String -> QName("urn:hl7-org:elm-types:r1", "String") - is Date -> QName("urn:hl7-org:elm-types:r1", "Date") - is DateTime -> QName("urn:hl7-org:elm-types:r1", "DateTime") - is Time -> QName("urn:hl7-org:elm-types:r1", "Time") - is Quantity -> QName("urn:hl7-org:elm-types:r1", "Quantity") - is Ratio -> QName("urn:hl7-org:elm-types:r1", "Ratio") - is Code -> QName("urn:hl7-org:elm-types:r1", "Code") - is Concept -> QName("urn:hl7-org:elm-types:r1", "Concept") - is CodeSystem -> QName("urn:hl7-org:elm-types:r1", "CodeSystem") - is ValueSet -> QName("urn:hl7-org:elm-types:r1", "ValueSet") + is Boolean -> booleanTypeName + is Int -> integerTypeName + is Long -> longTypeName + is BigDecimal -> decimalTypeName + is String -> stringTypeName + is Date -> dateTypeName + is DateTime -> dateTimeTypeName + is Time -> timeTypeName + is Quantity -> quantityTypeName + is Ratio -> ratioTypeName + is Code -> codeTypeName + is Concept -> conceptTypeName + is CodeSystem -> codeSystemTypeName + is ValueSet -> valueSetTypeName is CqlClassInstance -> value.type else -> null } From 817453e52817ca3b867624e2def70965d998a40f Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Thu, 26 Mar 2026 15:45:09 +1300 Subject: [PATCH 03/21] Small changes --- .../kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt | 6 +++--- .../engine/elm/executing/FunctionRefEvaluator.kt | 13 ++----------- .../elm/executing/FunctionRefEvaluatorTest.kt | 12 +++--------- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt index ebefb78d3..c9ffe6a4d 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt @@ -245,9 +245,9 @@ class CQLOperationsR4Test : TestFhirPath() { "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent13", "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent17", "r4/tests-fhir-r4/testNotEquivalent/testNotEquivalent21", - "r4/tests-fhir-r4/testObservations/testPolymorphismIsA3", // The engine correctly - // returns false but the - // tests expects null. + "r4/tests-fhir-r4/testObservations/testPolymorphismIsA3", // The engine returns + // false but the test + // expects null. "r4/tests-fhir-r4/testPower/testPower3", "r4/tests-fhir-r4/testPrecedence/testPrecedence3", "r4/tests-fhir-r4/testPrecedence/testPrecedence4", diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt index 421711fe8..ecdaa7935 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluator.kt @@ -10,9 +10,7 @@ import org.opencds.cqf.cql.engine.exception.CqlException import org.opencds.cqf.cql.engine.execution.Libraries import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.execution.Variable -import org.opencds.cqf.cql.engine.runtime.Interval -import org.opencds.cqf.cql.engine.runtime.Tuple -import org.opencds.cqf.cql.engine.runtime.getNamedTypeForCqlValue +import org.opencds.cqf.cql.engine.util.javaClassName object FunctionRefEvaluator { private val logger = KotlinLogging.logger("FunctionRefEvaluator") @@ -181,14 +179,7 @@ object FunctionRefEvaluator { if (arguments != null) { arguments.forEach { a -> argStr.append(if (argStr.isNotEmpty()) ", " else "") - argStr.append( - when (a) { - is Interval -> "Interval" - is Tuple -> "Tuple" - is Iterable<*> -> "List" - else -> getNamedTypeForCqlValue(a) - } - ) + argStr.append(a?.javaClassName ?: "null") } } diff --git a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt index 133bf75b7..593e03797 100644 --- a/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt +++ b/Src/java/engine/src/jvmTest/kotlin/org/opencds/cqf/cql/engine/elm/executing/FunctionRefEvaluatorTest.kt @@ -32,7 +32,7 @@ internal class FunctionRefEvaluatorTest { ) } Assertions.assertEquals( - "Could not resolve call to operator 'func({urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer)' in library 'lib'.", + "Could not resolve call to operator 'func(java.lang.Integer, java.lang.Integer, java.lang.Integer)' in library 'lib'.", cqlException.message, ) } @@ -99,16 +99,10 @@ internal class FunctionRefEvaluatorTest { var actual = FunctionRefEvaluator.typesToString(state, mutableListOf("a", "b", "c")) - Assertions.assertEquals( - "{urn:hl7-org:elm-types:r1}String, {urn:hl7-org:elm-types:r1}String, {urn:hl7-org:elm-types:r1}String", - actual, - ) + Assertions.assertEquals("java.lang.String, java.lang.String, java.lang.String", actual) actual = FunctionRefEvaluator.typesToString(state, mutableListOf(1, 2, null)) - Assertions.assertEquals( - "{urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Any", - actual, - ) + Assertions.assertEquals("java.lang.Integer, java.lang.Integer, null", actual) actual = FunctionRefEvaluator.typesToString(state, null) Assertions.assertEquals("", actual) From b6c5e16aea73d2d7c665cbe646831a7e41166c0b Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Thu, 26 Mar 2026 17:39:17 +1300 Subject: [PATCH 04/21] Clean up tests --- .../hl7/fhirpath/CQLOperationsDstu3Test.kt | 65 ------------------- .../org/hl7/fhirpath/CQLOperationsR4Test.kt | 65 ------------------- .../kotlin/org/hl7/fhirpath/TestFhirPath.kt | 28 ++++++-- 3 files changed, 24 insertions(+), 134 deletions(-) diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt index 704fc26b3..be884696d 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsDstu3Test.kt @@ -9,14 +9,10 @@ import org.junit.jupiter.api.Assumptions import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.opencds.cqf.cql.engine.data.CompositeDataProvider -import org.opencds.cqf.cql.engine.elm.executing.EqualEvaluator -import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.fhir.model.CachedDstu3FhirModelResolver import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver -import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.fhir.retrieve.RestFhirRetrieveProvider import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver -import org.opencds.cqf.cql.engine.runtime.CqlClassInstance class CQLOperationsDstu3Test : TestFhirPath() { @ParameterizedTest(name = "{0}") @@ -27,67 +23,6 @@ class CQLOperationsDstu3Test : TestFhirPath() { runTest(test, "stu3/input/", fhirContext, provider, fhirModelResolver) } - override fun compareResults( - expectedResult: Any?, - actualResult: Any?, - state: State?, - resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, - ): Boolean? { - if (actualResult is CqlClassInstance && actualResult.elements.containsKey("value")) { - return EqualEvaluator.equal(expectedResult, actualResult.elements["value"], state) - } - - return EqualEvaluator.equal(expectedResult, actualResult, state) - - // // Perform FHIR system-defined type conversions - // var actualResult = actualResult - // when (actualResult) { - // is Enumeration<*> -> { - // actualResult = actualResult.valueAsString - // } - // - // is BooleanType -> { - // actualResult = actualResult.value - // } - // - // is IntegerType -> { - // actualResult = actualResult.value - // } - // - // is DecimalType -> { - // actualResult = actualResult.value - // } - // - // is StringType -> { - // actualResult = actualResult.value - // } - // - // is BaseDateTimeType -> { - // actualResult = resolver.toJavaPrimitive(actualResult, actualResult) - // } - // - // is Quantity -> { - // val quantity = actualResult - // actualResult = - // org.opencds.cqf.cql.engine.runtime - // .Quantity() - // .withValue(quantity.getValue()) - // .withUnit(quantity.getUnit()) - // } - // - // is Coding -> { - // val coding = actualResult - // actualResult = - // Code() - // .withCode(coding.getCode()) - // .withDisplay(coding.getDisplay()) - // .withSystem(coding.getSystem()) - // .withVersion(coding.getVersion()) - // } - // } - // return EqualEvaluator.equal(expectedResult, actualResult, state) - } - companion object { private val fhirContext: FhirContext = FhirContext.forCached(FhirVersionEnum.DSTU3) private val fhirModelResolver: Dstu3FhirModelResolver = CachedDstu3FhirModelResolver() diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt index c9ffe6a4d..c93395f05 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/CQLOperationsR4Test.kt @@ -9,14 +9,10 @@ import org.junit.jupiter.api.Assumptions import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.opencds.cqf.cql.engine.data.CompositeDataProvider -import org.opencds.cqf.cql.engine.elm.executing.EqualEvaluator -import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.fhir.model.CachedR4FhirModelResolver -import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver import org.opencds.cqf.cql.engine.fhir.retrieve.RestFhirRetrieveProvider import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver -import org.opencds.cqf.cql.engine.runtime.CqlClassInstance class CQLOperationsR4Test : TestFhirPath() { @ParameterizedTest(name = "{0}") @@ -26,67 +22,6 @@ class CQLOperationsR4Test : TestFhirPath() { runTest(test, "r4/input/", fhirContext, provider, fhirModelResolver) } - override fun compareResults( - expectedResult: Any?, - actualResult: Any?, - state: State?, - resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, - ): Boolean? { - if (actualResult is CqlClassInstance && actualResult.elements.containsKey("value")) { - return EqualEvaluator.equal(expectedResult, actualResult.elements["value"], state) - } - - return EqualEvaluator.equal(expectedResult, actualResult, state) - - // // Perform FHIR system-defined type conversions - // var actualResult = actualResult - // when (actualResult) { - // is Enumeration<*> -> { - // actualResult = actualResult.valueAsString - // } - // - // is BooleanType -> { - // actualResult = actualResult.value - // } - // - // is IntegerType -> { - // actualResult = actualResult.value - // } - // - // is DecimalType -> { - // actualResult = actualResult.value - // } - // - // is StringType -> { - // actualResult = actualResult.value - // } - // - // is BaseDateTimeType -> { - // actualResult = resolver.toJavaPrimitive(actualResult, actualResult) - // } - // - // is Quantity -> { - // val quantity = actualResult - // actualResult = - // org.opencds.cqf.cql.engine.runtime - // .Quantity() - // .withValue(quantity.getValue()) - // .withUnit(quantity.getUnit()) - // } - // - // is Coding -> { - // val coding = actualResult - // actualResult = - // Code() - // .withCode(coding.getCode()) - // .withDisplay(coding.getDisplay()) - // .withSystem(coding.getSystem()) - // .withVersion(coding.getVersion()) - // } - // } - // return EqualEvaluator.equal(expectedResult, actualResult, state) - } - companion object { private val fhirContext: FhirContext = FhirContext.forCached(FhirVersionEnum.R4) private val fhirModelResolver: R4FhirModelResolver = CachedR4FhirModelResolver() diff --git a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt index 736f64ffe..cf51dc662 100644 --- a/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt +++ b/Src/java/engine-fhir/src/test/kotlin/org/hl7/fhirpath/TestFhirPath.kt @@ -3,7 +3,6 @@ package org.hl7.fhirpath import ca.uhn.fhir.context.FhirContext import jakarta.xml.bind.JAXB import java.io.InputStreamReader -import java.lang.Boolean import java.math.BigDecimal import java.time.Instant import java.time.ZoneOffset @@ -17,6 +16,7 @@ import kotlin.String import kotlin.plus import org.cqframework.cql.cql2elm.CqlCompilerException import org.hl7.fhir.instance.model.api.IBaseResource +import org.hl7.fhir.instance.model.api.IPrimitiveType import org.hl7.fhirpath.TranslatorHelper.toElmIdentifier import org.hl7.fhirpath.tests.InvalidType import org.hl7.fhirpath.tests.Output @@ -24,12 +24,14 @@ import org.hl7.fhirpath.tests.OutputType import org.hl7.fhirpath.tests.Test import org.hl7.fhirpath.tests.Tests import org.opencds.cqf.cql.engine.data.CompositeDataProvider +import org.opencds.cqf.cql.engine.elm.executing.EqualEvaluator import org.opencds.cqf.cql.engine.elm.executing.ToQuantityEvaluator import org.opencds.cqf.cql.engine.elm.executing.ToStringEvaluator import org.opencds.cqf.cql.engine.exception.CqlException import org.opencds.cqf.cql.engine.execution.EvaluationResult import org.opencds.cqf.cql.engine.execution.State import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance import org.opencds.cqf.cql.engine.runtime.Date import org.opencds.cqf.cql.engine.runtime.DateTime import org.opencds.cqf.cql.engine.runtime.Time @@ -81,12 +83,30 @@ abstract class TestFhirPath { """ .trimIndent() - abstract fun compareResults( + fun compareResults( expectedResult: Any?, actualResult: Any?, state: State?, resolver: FhirModelResolver<*, *, *, *, *, *, *, *>, - ): kotlin.Boolean? + ): Boolean? { + // Perform FHIR system-defined type conversions + var actualResult = actualResult + + if ( + actualResult is CqlClassInstance && + actualResult.type.namespaceURI == "http://hl7.org/fhir" + ) { + val actualResultClass = resolver.resolveType(actualResult.type.localPart)!! + if ( + actualResultClass.isEnum || + IPrimitiveType::class.java.isAssignableFrom(actualResultClass) + ) { + actualResult = actualResult.elements["value"] + } + } + + return EqualEvaluator.equal(expectedResult, actualResult, state) + } protected fun runTest( test: Test, @@ -180,7 +200,7 @@ abstract class TestFhirPath { val expected = testCase.results[i] val actual: Any? = actualList[i] val comparison = compareResults(expected, actual, engine.state, resolver) - if (Boolean.TRUE != comparison) { + if (true != comparison) { throw failWithContext( "Result mismatch at index %d".format(i), testCase, From 7adc5202d6eb80d56fc7a262ea1f132a1c214edb Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Thu, 26 Mar 2026 18:21:30 +1300 Subject: [PATCH 05/21] detekt fix --- Src/java/engine/detekt-baseline.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/java/engine/detekt-baseline.xml b/Src/java/engine/detekt-baseline.xml index 20565142a..5c14b25b8 100644 --- a/Src/java/engine/detekt-baseline.xml +++ b/Src/java/engine/detekt-baseline.xml @@ -238,7 +238,7 @@ MaxLineLength:FirstEvaluator.kt$FirstEvaluator$/* First(argument List<T>) T The First operator returns the first element in a list. The operator is equivalent to invoking the indexer with an index of 0. If the argument is null, the result is null. */ MaxLineLength:FunctionRefEvaluator.kt$FunctionRefEvaluator$"Ambiguous call to operator '${name}(${typesToString(state, types)})' in library '${state!!.getCurrentLibrary()!!.identifier!!.id}'." MaxLineLength:FunctionRefEvaluator.kt$FunctionRefEvaluator$"Could not resolve call to operator '${name}(${typesToString(state, types)})' in library '${state!!.getCurrentLibrary()!!.identifier!!.id}'." - MaxLineLength:FunctionRefEvaluatorTest.kt$FunctionRefEvaluatorTest$"Could not resolve call to operator 'func({urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer, {urn:hl7-org:elm-types:r1}Integer)' in library 'lib'." + MaxLineLength:FunctionRefEvaluatorTest.kt$FunctionRefEvaluatorTest$"Could not resolve call to operator 'func(java.lang.Integer, java.lang.Integer, java.lang.Integer)' in library 'lib'." MaxLineLength:GreaterEvaluator.kt$GreaterEvaluator$"Greater(Integer, Integer), Greater(Long, Long), Greater(Decimal, Decimal), Greater(Quantity, Quantity), Greater(Date, Date), Greater(DateTime, DateTime), Greater(Time, Time) or Greater(String, String)" MaxLineLength:GreaterEvaluator.kt$GreaterEvaluator$/* >(left Integer, right Integer) Boolean >(left Long, right Long) Boolean >(left Decimal, right Decimal) Boolean >(left Quantity, right Quantity) Boolean >(left Date, right Date) Boolean >(left DateTime, right DateTime) Boolean >(left Time, right Time) Boolean >(left String, right String) Boolean The greater (>) operator returns true if the first argument is greater than the second argument. String comparisons are strictly lexical based on the Unicode value of the individual characters in the string. For comparisons involving quantities, the dimensions of each quantity must be the same, but not necessarily the unit. For example, units of 'cm' and 'm' are comparable, but units of 'cm2' and 'cm' are not. Attempting to operate on quantities with invalid units will result in a null. When a quantity has no units specified, it is treated as a quantity with the default unit ('1'). For date/time values, the comparison is performed by considering each precision in order, beginning with years (or hours for time values). If the values are the same, comparison proceeds to the next precision; if the first value is greater than the second, the result is true; if the first value is less than the second, the result is false; if one input has a value for the precision and the other does not, the comparison stops and the result is null; if neither input has a value for the precision or the last precision has been reached, the comparison stops and the result is false. For example: define DateTimeGreaterIsNull: @2012-01-01 > @2012-01-01T12 If either argument is null, the result is null. */ MaxLineLength:GreaterOrEqualEvaluator.kt$GreaterOrEqualEvaluator$"GreaterOrEqual(Integer, Integer), GreaterOrEqual(Long, Long), GreaterOrEqual(Decimal, Decimal), GreaterOrEqual(Quantity, Quantity), GreaterOrEqual(Date, Date), GreaterOrEqual(DateTime, DateTime), GreaterOrEqual(Time, Time) or GreaterOrEqual(String, String)" From f54c46f3b0f071f5540c07f88017e85fb60ea393 Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Thu, 26 Mar 2026 18:34:34 +1300 Subject: [PATCH 06/21] Cleanup --- .../engine/fhir/model/FhirModelResolver.kt | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt index 0cbed4cdd..df42c1b24 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt @@ -3,7 +3,6 @@ package org.opencds.cqf.cql.engine.fhir.model import ca.uhn.fhir.context.BaseRuntimeChildDefinition import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition import ca.uhn.fhir.context.FhirContext -import ca.uhn.fhir.context.RuntimeChildChoiceDefinition import ca.uhn.fhir.context.RuntimeChildResourceBlockDefinition import ca.uhn.fhir.context.RuntimeChildResourceDefinition import ca.uhn.fhir.model.api.TemporalPrecisionEnum @@ -38,6 +37,7 @@ import org.opencds.cqf.cql.engine.runtime.DateTime import org.opencds.cqf.cql.engine.runtime.Precision import org.opencds.cqf.cql.engine.runtime.TemporalHelper import org.opencds.cqf.cql.engine.runtime.Time +import org.opencds.cqf.cql.engine.runtime.anyTypeName // TODO: Probably quite a bit of redundancy here. Probably only really need the BaseType and the // PrimitiveType @@ -89,7 +89,7 @@ abstract class FhirModelResolver< } override fun resolveId(target: Any?): String? { - if (target is CqlClassInstance && target.type.namespaceURI == "http://hl7.org/fhir") { + if (target is CqlClassInstance && target.type.namespaceURI == fhirModelNamespaceUri) { val clazz = this.resolveType(target.type.localPart) ?: return null if (IBaseResource::class.java.isAssignableFrom(clazz)) { val id = target.elements["id"] as? CqlClassInstance ?: return null @@ -178,11 +178,12 @@ abstract class FhirModelResolver< override fun `is`(valueType: String, type: QName): Boolean? { // System.Any is a supertype of all types - if (type == QName("urn:hl7-org:elm-types:r1", "Any")) { + if (type == anyTypeName) { return true } - if (type.namespaceURI != "http://hl7.org/fhir") { + if (type.namespaceURI != fhirModelNamespaceUri) { + // FHIR model types only extend System.Any or other FHIR model types return false } @@ -328,21 +329,6 @@ abstract class FhirModelResolver< } } - protected fun resolveChoiceProperty( - definition: BaseRuntimeElementCompositeDefinition<*>, - path: String?, - ): BaseRuntimeChildDefinition? { - for (child in definition.children) { - if (child is RuntimeChildChoiceDefinition) { - if (child.elementName.startsWith(path!!)) { - return child - } - } - } - - return null - } - private fun deepSearch(typeName: String): Class<*>? { // Special case for "Codes". This suffix is often removed from the HAPI type. val codelessName = typeName.replace("Codes", "").lowercase() @@ -632,7 +618,7 @@ abstract class FhirModelResolver< elements["value"] = target.valueAsString - return CqlClassInstance(QName("http://hl7.org/fhir", typeName), elements) + return CqlClassInstance(QName(fhirModelNamespaceUri, typeName), elements) } if (target is IPrimitiveType<*>) { @@ -650,7 +636,7 @@ abstract class FhirModelResolver< elements["value"] = toJavaPrimitive(target.value, target) - return CqlClassInstance(QName("http://hl7.org/fhir", elementDefinition.name), elements) + return CqlClassInstance(QName(fhirModelNamespaceUri, elementDefinition.name), elements) } if (target !is IBase) { @@ -675,6 +661,10 @@ abstract class FhirModelResolver< } } - return CqlClassInstance(QName("http://hl7.org/fhir", definition.name), elements) + return CqlClassInstance(QName(fhirModelNamespaceUri, definition.name), elements) + } + + companion object { + const val fhirModelNamespaceUri = "http://hl7.org/fhir" } } From fa15db8b958028e0a9a53692c2bb8367ae8987ff Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Fri, 27 Mar 2026 10:04:56 +1300 Subject: [PATCH 07/21] Argument type check --- .../cqf/cql/engine/fhir/model/FhirModelResolver.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt index df42c1b24..31e73a7a1 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt @@ -580,6 +580,12 @@ abstract class FhirModelResolver< return null } + if (target !is IBase) { + throw IllegalArgumentException( + "Unable to convert an instance of ${target.javaClass.name} to a CQL value. Expected an instance of IBase." + ) + } + val elements = mutableMapOf() if (target is IBaseHasExtensions) { @@ -639,12 +645,6 @@ abstract class FhirModelResolver< return CqlClassInstance(QName(fhirModelNamespaceUri, elementDefinition.name), elements) } - if (target !is IBase) { - throw IllegalArgumentException( - "Unable to convert an instance of ${target.javaClass.name} to a CQL value. Expected an instance of IBase." - ) - } - val definition = resolveRuntimeDefinition(target) for (child in definition.children) { From cf1a57271824636b6d57b0df91cd09333fd768af Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Fri, 27 Mar 2026 10:09:56 +1300 Subject: [PATCH 08/21] KDocs --- .../cqf/cql/engine/fhir/model/FhirModelResolver.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt index 31e73a7a1..6a0b2a80b 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt @@ -571,11 +571,16 @@ abstract class FhirModelResolver< } } - /** Recursively converts a HAPI FHIR construct to a CQL-native equivalent. */ + /** + * Recursively converts a HAPI FHIR construct to a CQL-native equivalent. + * + * @param target The HAPI FHIR object to convert + * @param expandPrimitivesAndEnumerationsWithNoValues Whether to convert a HAPI FHIR primitive/enumeration with no value to a structured type instance with a null `value` child, or to null. + * */ fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, - ): Any? { + ): CqlClassInstance? { if (target == null) { return null } From 5686c6eb05dac90caff0ba9a5792f65d8adacc33 Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Fri, 27 Mar 2026 10:11:47 +1300 Subject: [PATCH 09/21] KDocs --- .../org/opencds/cqf/cql/engine/execution/Environment.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt index 3e63988c1..60bbcc7fa 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt @@ -78,9 +78,12 @@ constructor( } // -- DataProvider "Helpers" + + /** + * Resolves a path on a target object. The path may include qualifiers (`.`) and indexers (`[x]`). + */ fun resolvePath(target: Any?, path: String): Any? { var target = target - // The path attribute may include qualifiers (.) and indexers ([x]) val qualifiersAndIndexers = path.split('.', '[', ']').map { it.trim() }.filter { it.isNotEmpty() } for (qualifierOrIndexer in qualifiersAndIndexers) { @@ -95,6 +98,9 @@ constructor( return target } + /** + * Resolves a property on a target object. + */ fun resolveProperty(target: Any?, property: String): Any? { if (target == null) { return null From e5eda12cc7d38cf6ae74063c1be612a9597b0ab5 Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Fri, 27 Mar 2026 15:54:15 +1300 Subject: [PATCH 10/21] Formatting --- .../org/opencds/cqf/cql/engine/execution/Environment.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt index 60bbcc7fa..43e04de94 100644 --- a/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt +++ b/Src/java/engine/src/commonMain/kotlin/org/opencds/cqf/cql/engine/execution/Environment.kt @@ -80,7 +80,8 @@ constructor( // -- DataProvider "Helpers" /** - * Resolves a path on a target object. The path may include qualifiers (`.`) and indexers (`[x]`). + * Resolves a path on a target object. The path may include qualifiers (`.`) and indexers + * (`[x]`). */ fun resolvePath(target: Any?, path: String): Any? { var target = target @@ -98,9 +99,7 @@ constructor( return target } - /** - * Resolves a property on a target object. - */ + /** Resolves a property on a target object. */ fun resolveProperty(target: Any?, property: String): Any? { if (target == null) { return null From da737a24c27209d2dc379ecf366548782d12f251 Mon Sep 17 00:00:00 2001 From: Anton Vasetenkov Date: Fri, 27 Mar 2026 18:43:44 +1300 Subject: [PATCH 11/21] Tests for `FhirModelResolver.toCqlValue()` --- .../engine-fhir/config/detekt-baseline.xml | 17 +- .../engine/fhir/model/FhirModelResolver.kt | 34 +- .../fhir/model/FhirModelResolverTest.kt | 446 ++++++++++++++++++ 3 files changed, 476 insertions(+), 21 deletions(-) create mode 100644 Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolverTest.kt diff --git a/Src/java/engine-fhir/config/detekt-baseline.xml b/Src/java/engine-fhir/config/detekt-baseline.xml index 2e45e6c8a..87e0ad3be 100644 --- a/Src/java/engine-fhir/config/detekt-baseline.xml +++ b/Src/java/engine-fhir/config/detekt-baseline.xml @@ -10,8 +10,9 @@ CyclomaticComplexMethod:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$override fun toCqlType(value: Any?): Any? CyclomaticComplexMethod:BaseFhirTypeConverter.kt$BaseFhirTypeConverter$override fun toFhirType(value: Any?): IBase? CyclomaticComplexMethod:Dstu3FhirQueryGenerator.kt$Dstu3FhirQueryGenerator$override fun generateFhirQueries( dataRequirement: ICompositeType?, evaluationDateTime: DateTime?, contextValues: MutableMap<String, Any?>?, parameters: MutableMap<String, Any?>?, capabilityStatement: IBaseConformance?, ): MutableList<String> - CyclomaticComplexMethod:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): Any? + CyclomaticComplexMethod:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): CqlClassInstance? CyclomaticComplexMethod:FhirModelResolver.kt$FhirModelResolver$open fun resolveType(typeName: String?): Class<*>? + CyclomaticComplexMethod:FhirModelResolverTest.kt$private fun validateCqlValueAgainstModel( fhirVersion: FhirVersionEnum, expectedFhirTypeName: String, cqlValue: Any?, ) CyclomaticComplexMethod:R4FhirModelResolver.kt$R4FhirModelResolver$override fun resolveType(typeName: String?): Class<*>? CyclomaticComplexMethod:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$override fun generateFhirQueries( dataRequirement: ICompositeType?, evaluationDateTime: DateTime?, contextValues: MutableMap<String, Any?>?, parameters: MutableMap<String, Any?>?, capabilityStatement: IBaseConformance?, ): MutableList<String> CyclomaticComplexMethod:R5FhirModelResolver.kt$R5FhirModelResolver$override fun resolveType(typeName: String?): Class<*>? @@ -31,6 +32,11 @@ ForbiddenComment:FhirHelpersDstu3Test.kt$FhirHelpersDstu3Test$// TODO: Resolve Error: Could not load model information for model FHIR, version ForbiddenComment:FhirModelResolver.kt$FhirModelResolver$/* * // TODO: Find HAPI registry of Primitive Type conversions public Object * fromJavaPrimitive(Object value, Object target) { String simpleName = * target.getClass().getSimpleName(); switch(simpleName) { case "DateTimeType": * case "InstantType": return ((DateTime)value).toJavaDate(); case "DateType": * return ((org.opencds.cqf.cql.engine.runtime.Date)value).toJavaDate(); case * "TimeType": return ((Time) value).toString(); } * * if (value instanceof Time) { return ((Time) value).toString(); } else { * return value; } } */ ForbiddenComment:FhirModelResolver.kt$FhirModelResolver$// TODO: Probably quite a bit of redundancy here. Probably only really need the BaseType and the + ForbiddenComment:FhirModelResolverTest.kt$FhirModelResolverTest$* TODO: Add DSTU2 test. At the moment, * * FhirContext.forDstu2().getElementDefinition(org.hl7.fhir.dstu2.model.Extension().javaClass) * * throws * * HAPI-1690: Unknown profileOf value: class org.hl7.fhir.dstu2.model.StringType in type org.hl7.fhir.dstu2.model.IdType ... + ForbiddenComment:FhirModelResolverTest.kt$FhirModelResolverTest$* TODO: Add DSTU2 test. At the moment, * * FhirContext.forDstu2().getResourceDefinition(org.hl7.fhir.dstu2.model.Patient()) * * throws * * HAPI-1731: This context is for FHIR version "DSTU2" but the class "org.hl7.fhir.dstu2.model.Patient" is for version "DSTU2_HL7ORG" + ForbiddenComment:FhirModelResolverTest.kt$FhirModelResolverTest$* TODO: Add DSTU2 test. At the moment, it throws * * Could not resolve type NameUseEnumFactory. Primary package(s) for this resolver are * ca.uhn.fhir.model.dstu2,org.hl7.fhir.dstu2.model,ca.uhn.fhir.model.primitive + ForbiddenComment:FhirModelResolverTest.kt$FhirModelResolverTest$// TODO: Add DSTU2 test + ForbiddenComment:FhirModelResolverTest.kt$FhirModelResolverTest$// TODO: enable this for DSTU2 and DSTU3 models which use FHIR.ResourceContainer for ForbiddenComment:R4FhirModelResolver.kt$R4FhirModelResolver$// TODO: Might be able to patch some of these by registering custom types in HAPI. ForbiddenComment:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$// TODO: Deal with the case that the value is expressed as an expression extension ForbiddenComment:R4FhirTypeConverter.kt$R4FhirTypeConverter$// TODO: This will construct DateTimeType values in FHIR with the system @@ -66,8 +72,10 @@ LongMethod:EvaluatedResourcesMultiLibLinearDepsTest.kt$EvaluatedResourcesMultiLibLinearDepsTest$@ParameterizedTest @MethodSource("multiLibEnsurePartialCacheAllowsUncachedLibsToBeCompiledParams") fun multiLibEnsurePartialCacheAllowsUncachedLibsToBeCompiled(expressionCaching: Boolean) LongMethod:EvaluatedResourcesMultiLibLinearDepsTest.kt$EvaluatedResourcesMultiLibLinearDepsTest.Companion$@JvmStatic private fun multiLibParams(): List<Arguments> LongMethod:EvaluatedResourcesMultiLibLinearDepsTest.kt$EvaluatedResourcesMultiLibLinearDepsTest.Companion$@JvmStatic private fun singleLibParams(): List<Arguments> - LongMethod:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): Any? + LongMethod:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): CqlClassInstance? LongMethod:FhirModelResolver.kt$FhirModelResolver$protected fun toDateTime( value: BaseDateTimeType, calendarConstant: Int = this.getCalendarConstant(value), ): DateTime + LongMethod:FhirModelResolverTest.kt$FhirModelResolverTest$@Test fun toCqlValueFhirPrimitiveWithValueAndExtension() + LongMethod:FhirModelResolverTest.kt$fun validateCqlValueAgainstModelInner(cqlValue: Any?, modelType: DataType) LongMethod:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$override fun generateFhirQueries( dataRequirement: ICompositeType?, evaluationDateTime: DateTime?, contextValues: MutableMap<String, Any?>?, parameters: MutableMap<String, Any?>?, capabilityStatement: IBaseConformance?, ): MutableList<String> LongMethod:SearchParameterMap.kt$SearchParameterMap$fun toNormalizedQueryString(theCtx: FhirContext): String LongMethod:TestFHIR2Helpers.kt$TestFHIR2Helpers$fun test() @@ -96,6 +104,7 @@ MaxLineLength:FhirModelResolver.kt$FhirModelResolver$"Could not resolve type $typeName. Primary package(s) for this resolver are ${this.packageNames.joinToString(",")}" MaxLineLength:FhirModelResolver.kt$FhirModelResolver$"Unable to convert an instance of ${target.javaClass.name} to a CQL value. Expected an instance of IBase." MaxLineLength:FhirModelResolver.kt$FhirModelResolver$/* * type-to-class and contextPath resolutions are potentially expensive and can be cached * for improved performance. * * See <a href="https://github.com/DBCG/cql-evaluator/blob/master/evaluator.engine/src/main/java/org/opencds/cqf/cql/evaluator/engine/model/CachingModelResolverDecorator.java"/> * for a decorator that adds caching logic for ModelResolvers. */ + MaxLineLength:FhirModelResolverTest.kt$FhirModelResolverTest$* MaxLineLength:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$"Cannot process data requirements with subjects not specified using a code from http://hl7.org/fhir/resource-types" MaxLineLength:R4FhirTerminologyProvider.kt$R4FhirTerminologyProvider$"Could not expand value set ${valueSet.id}; version and code system bindings are not supported at this time." MaxLineLength:TestFhirDataProviderDstu2.kt$TestFhirDataProviderDstu2$// FhirDataProviderDstu2().withEndpoint("http://fhirtest.uhn.ca/baseDstu2").withPackageName("ca.uhn.fhir.model.dstu2.composite"); @@ -112,7 +121,6 @@ MaxLineLength:TestR4FhirQueryGenerator.kt$TestR4FhirQueryGenerator$"Observation?category=http://myterm.com/fhir/CodeSystem/MyValueSet|code4,http://myterm.com/fhir/CodeSystem/MyValueSet|code5,http://myterm.com/fhir/CodeSystem/MyValueSet|code6,http://myterm.com/fhir/CodeSystem/MyValueSet|code7&subject=Patient/{{context.patientId}}" MaxLineLength:TestR4FhirQueryGenerator.kt$TestR4FhirQueryGenerator$"Observation?category=http://myterm.com/fhir/CodeSystem/MyValueSet|code5,http://myterm.com/fhir/CodeSystem/MyValueSet|code6,http://myterm.com/fhir/CodeSystem/MyValueSet|code7,http://myterm.com/fhir/CodeSystem/MyValueSet|code8,http://myterm.com/fhir/CodeSystem/MyValueSet|code9&subject=Patient/{{context.patientId}}" MaxLineLength:TestR4FhirQueryGenerator.kt$TestR4FhirQueryGenerator$"Observation?date=ge${simpleDateFormatter.format(expectedRangeStartDateTime)}&date=le${dateTimeFormatter.format(evaluationDateTimeAsLocal)}&subject=Patient/{{context.patientId}}" - MemberNameEqualsClassName:HapiToCqlValueTest.kt$HapiToCqlValueTest$@Test fun hapiToCqlValueTest() MemberNameEqualsClassName:TestFHIRHelpers.kt$TestFHIRHelpers$@Test fun testFhirHelpers() NestedBlockDepth:CQLOperationsDstu3Test.kt$CQLOperationsDstu3Test.Companion$@JvmStatic fun dataMethod(): Array<Array<Any>> NestedBlockDepth:CQLOperationsR4Test.kt$CQLOperationsR4Test.Companion$@JvmStatic fun dataMethod(): Array<Array<Any>> @@ -146,7 +154,7 @@ ReturnCount:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$override fun toCqlTemporal(value: IPrimitiveType<Date>?): BaseTemporal? ReturnCount:Dstu3FhirTypeConverter.kt$Dstu3FhirTypeConverter$override fun toFhirPeriod(value: Interval?): ICompositeType? ReturnCount:Dstu3TypeConverterTests.kt$Dstu3TypeConverterTests$private fun compareObjects(left: Any?, right: Any?): Boolean - ReturnCount:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): Any? + ReturnCount:FhirModelResolver.kt$FhirModelResolver$fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, ): CqlClassInstance? ReturnCount:FhirModelResolver.kt$FhirModelResolver$open fun resolveType(typeName: String?): Class<*>? ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun `is`(valueType: String, type: QName): Boolean? ReturnCount:FhirModelResolver.kt$FhirModelResolver$override fun getContextPath(contextType: String?, targetType: String?): Any? @@ -177,6 +185,7 @@ SwallowedException:FhirModelResolver.kt$FhirModelResolver$e: InstantiationException SwallowedException:FhirModelResolver.kt$FhirModelResolver$e: InvocationTargetException SwallowedException:FhirModelResolver.kt$FhirModelResolver$e: NoSuchMethodException + SwallowedException:FhirModelResolverTest.kt$e: AssertionError SwallowedException:R4FhirQueryGenerator.kt$R4FhirQueryGenerator$ex: Exception SwallowedException:R4FhirTerminologyProvider.kt$R4FhirTerminologyProvider$rnfe: ResourceNotFoundException SwallowedException:SearchParamFhirRetrieveProvider.kt$SearchParamFhirRetrieveProvider$exception: FhirVersionMisMatchException diff --git a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt index 6a0b2a80b..792225890 100644 --- a/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt +++ b/Src/java/engine-fhir/src/main/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolver.kt @@ -575,8 +575,10 @@ abstract class FhirModelResolver< * Recursively converts a HAPI FHIR construct to a CQL-native equivalent. * * @param target The HAPI FHIR object to convert - * @param expandPrimitivesAndEnumerationsWithNoValues Whether to convert a HAPI FHIR primitive/enumeration with no value to a structured type instance with a null `value` child, or to null. - * */ + * @param expandPrimitivesAndEnumerationsWithNoValues Whether to convert a HAPI FHIR + * primitive/enumeration with no value to a structured type instance with a null `value` + * child, or to null. + */ fun toCqlValue( target: Any?, expandPrimitivesAndEnumerationsWithNoValues: Boolean = false, @@ -595,14 +597,12 @@ abstract class FhirModelResolver< if (target is IBaseHasExtensions) { val extensionsAsCqlValues = target.extension.map { toCqlValue(it) } - elements["extension"] = - if (extensionsAsCqlValues.isEmpty()) null else extensionsAsCqlValues + elements["extension"] = extensionsAsCqlValues.ifEmpty { null } } if (target is IBaseHasModifierExtensions) { val modifierExtensionsAsCqlValues = target.modifierExtension.map { toCqlValue(it) } - elements["extension"] = - if (modifierExtensionsAsCqlValues.isEmpty()) null else modifierExtensionsAsCqlValues + elements["extension"] = modifierExtensionsAsCqlValues.ifEmpty { null } } if (target is IBaseElement) { @@ -653,17 +653,17 @@ abstract class FhirModelResolver< val definition = resolveRuntimeDefinition(target) for (child in definition.children) { - val childName = child.elementName - val hapiValues = child.accessor.getValues(target) - - val hapiValuesAsCqlValues = hapiValues.map { toCqlValue(it) } - - if (child.max == 1) { - elements[childName] = hapiValuesAsCqlValues.firstOrNull() - } else { - elements[childName] = - if (hapiValuesAsCqlValues.isEmpty()) null else hapiValuesAsCqlValues - } + elements[child.elementName] = + child.accessor + .getValues(target) + .map { toCqlValue(it) } + .let { + if (child.max == 1) { + it.firstOrNull() + } else { + it.ifEmpty { null } + } + } } return CqlClassInstance(QName(fhirModelNamespaceUri, definition.name), elements) diff --git a/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolverTest.kt b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolverTest.kt new file mode 100644 index 000000000..614856c83 --- /dev/null +++ b/Src/java/engine-fhir/src/test/kotlin/org/opencds/cqf/cql/engine/fhir/model/FhirModelResolverTest.kt @@ -0,0 +1,446 @@ +package org.opencds.cqf.cql.engine.fhir.model + +import ca.uhn.fhir.context.FhirVersionEnum +import java.math.BigDecimal +import javax.xml.namespace.QName +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.cqframework.cql.cql2elm.ModelManager +import org.hl7.cql.model.ChoiceType +import org.hl7.cql.model.ClassType +import org.hl7.cql.model.DataType +import org.hl7.cql.model.IntervalType +import org.hl7.cql.model.ListType +import org.hl7.cql.model.ModelIdentifier +import org.hl7.cql.model.SimpleType +import org.hl7.cql.model.TupleType +import org.hl7.fhir.dstu2.model.HumanName as Dstu2HumanName +import org.hl7.fhir.dstu2.model.StringType as Dstu2StringType +import org.hl7.fhir.dstu3.model.HumanName as Dstu3HumanName +import org.hl7.fhir.dstu3.model.Patient as Dstu3Patient +import org.hl7.fhir.dstu3.model.StringType as Dstu3StringType +import org.hl7.fhir.r4.model.HumanName as R4HumanName +import org.hl7.fhir.r4.model.Patient as R4Patient +import org.hl7.fhir.r4.model.StringType as R4StringType +import org.hl7.fhir.r5.model.HumanName as R5HumanName +import org.hl7.fhir.r5.model.Patient as R5Patient +import org.hl7.fhir.r5.model.StringType as R5StringType +import org.opencds.cqf.cql.engine.fhir.model.FhirModelResolver.Companion.fhirModelNamespaceUri +import org.opencds.cqf.cql.engine.runtime.Code +import org.opencds.cqf.cql.engine.runtime.CodeSystem +import org.opencds.cqf.cql.engine.runtime.Concept +import org.opencds.cqf.cql.engine.runtime.CqlClassInstance +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.cql.engine.runtime.Quantity +import org.opencds.cqf.cql.engine.runtime.Ratio +import org.opencds.cqf.cql.engine.runtime.Time +import org.opencds.cqf.cql.engine.runtime.Tuple +import org.opencds.cqf.cql.engine.runtime.ValueSet +import org.opencds.cqf.cql.engine.runtime.Vocabulary +import org.opencds.cqf.cql.engine.runtime.anyTypeName + +private val resolvers = + mapOf( + FhirVersionEnum.DSTU2 to CachedDstu2FhirModelResolver(), + FhirVersionEnum.DSTU3 to CachedDstu3FhirModelResolver(), + FhirVersionEnum.R4 to CachedR4FhirModelResolver(), + FhirVersionEnum.R5 to CachedR5FhirModelResolver(), + ) + +/** Collects all elements of a class type, including inherited elements. */ +private fun ClassType.getAllElements(): Map { + return (if (baseType is ClassType) (baseType as ClassType).getAllElements() else emptyMap()) + + elements.associate { it.name to it.type } +} + +/** Validates that a CQL value conforms to the model used by the CQL compiler. */ +private fun validateCqlValueAgainstModel( + fhirVersion: FhirVersionEnum, + expectedFhirTypeName: String, + cqlValue: Any?, +) { + val modelManager = ModelManager() + + fun validateCqlValueAgainstModelInner(cqlValue: Any?, modelType: DataType) { + if (cqlValue == null) { + return + } + + when (modelType) { + is IntervalType -> { + assertIs(cqlValue) + validateCqlValueAgainstModelInner(cqlValue.low, modelType.pointType) + validateCqlValueAgainstModelInner(cqlValue.high, modelType.pointType) + } + is ListType -> { + assertIs>(cqlValue) + for (element in cqlValue) { + validateCqlValueAgainstModelInner(element, modelType.elementType) + } + } + is ChoiceType -> { + for (type in modelType.types) { + try { + validateCqlValueAgainstModelInner(cqlValue, type) + return + } catch (e: AssertionError) { + // Try the next type + } + } + throw AssertionError( + "Value $cqlValue does not match any type in choice type $modelType" + ) + } + is TupleType -> { + assertIs(cqlValue) + assertEquals(modelType.elements.map { it.name }.toSet(), cqlValue.elements.keys) + for (element in modelType.elements) { + validateCqlValueAgainstModelInner(cqlValue.elements[element.name], element.type) + } + } + is ClassType -> { + when (modelType.name) { + "System.Quantity" -> assertIs(cqlValue) + "System.Ratio" -> assertIs(cqlValue) + "System.Code" -> assertIs(cqlValue) + "System.Concept" -> assertIs(cqlValue) + "System.Vocabulary" -> + assertIs(cqlValue) // value may be a ValueSet or CodeSystem + "System.ValueSet" -> assertIs(cqlValue) + "System.CodeSystem" -> assertIs(cqlValue) + else -> { + assertIs(cqlValue) + val cqlValueType = + modelManager + .resolveModelByUri(cqlValue.type.namespaceURI) + .resolveTypeName(cqlValue.type.localPart) as ClassType + assertTrue(cqlValueType.isSubTypeOf(modelType)) + val expectedElements = cqlValueType.getAllElements() + assertEquals(expectedElements.keys, cqlValue.elements.keys) + for ((elementName, elementType) in expectedElements) { + validateCqlValueAgainstModelInner( + cqlValue.elements[elementName], + elementType, + ) + } + } + } + } + is SimpleType -> { + when (modelType.name) { + "System.Boolean" -> assertIs(cqlValue) + "System.Integer" -> assertIs(cqlValue) + "System.Long" -> assertIs(cqlValue) + "System.Decimal" -> assertIs(cqlValue) + "System.String" -> assertIs(cqlValue) + "System.DateTime" -> assertIs(cqlValue) + "System.Date" -> assertIs(cqlValue) + "System.Time" -> assertIs