Skip to content

Commit

Permalink
Refactor evaluation of cqf-calculatedValue in QuestionnaireViewModel
Browse files Browse the repository at this point in the history
  • Loading branch information
LZRS committed Nov 8, 2023
1 parent 6ae9c5e commit b6d50ed
Show file tree
Hide file tree
Showing 26 changed files with 373 additions and 192 deletions.
51 changes: 51 additions & 0 deletions datacapture/sampledata/test_date_cql-calculatedvalue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"text": "Enter a date",
"type": "date",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/entryFormat",
"valueString": "yyyy-mm-dd"
},
{
"url": "http://hl7.org/fhir/StructureDefinition/minValue",
"valueDate": "2023-12-14",
"_valueDate": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue",
"valueExpression": {
"language": "text/fhirpath",
"expression": "today() - 7days"
}
}
]
}
}
],
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "1-most-recent",
"text": "Use keyboard entry or date picker",
"type": "display"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture

import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Type
import org.junit.Test

class JustTest {

@Test
fun just_testing() {
val jsonString = readFileFromAssets("/test_date_cql-calculatedvalue.json")
val parser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser()
val questionnaire = parser.parseResource(jsonString) as Questionnaire
val minValExtension =
questionnaire.item.first().extension.first {
it.url == "http://hl7.org/fhir/StructureDefinition/minValue"
}
println(minValExtension.value)
println(
minValExtension.value.hasExtension(
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue",
),
)

val expression =
minValExtension.value
.getExtensionByUrl("http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue")
.value as Expression
val evaluatedValue =
fhirPathEngine.evaluate(minValExtension.value, expression.expression).singleOrNull()?.let {
it as Type
}
evaluatedValue?.apply { extension = minValExtension.value.extension }
minValExtension.setValue(evaluatedValue)

println(minValExtension.value)
println(
minValExtension.value.hasExtension(
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue",
),
)
}

private fun readFileFromAssets(filename: String) =
javaClass.getResourceAsStream(filename)!!.bufferedReader().use { it.readText() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.expressions.EnabledAnswerOptionsEvaluator
import com.google.android.fhir.datacapture.extensions.EXTENSION_CQF_CALCULATED_VALUE_URL
import com.google.android.fhir.datacapture.extensions.EntryMode
import com.google.android.fhir.datacapture.extensions.addNestedItemsToAnswer
import com.google.android.fhir.datacapture.extensions.allItems
Expand Down Expand Up @@ -68,12 +69,14 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.withIndex
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Element
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.Type
import timber.log.Timber

internal class QuestionnaireViewModel(application: Application, state: SavedStateHandle) :
Expand Down Expand Up @@ -579,6 +582,21 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)
}

private fun resolveCqfCalculatedValueExpression(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
expression: Expression,
): Type? {
if (!expression.isFhirPath) {
throw UnsupportedOperationException("${expression.language} not supported yet")
}

return expressionEvaluator
.evaluateExpression(questionnaireItem, questionnaireResponseItem, expression)
.singleOrNull()
?.let { it as Type }
}

private fun removeDisabledAnswers(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
Expand Down Expand Up @@ -707,6 +725,23 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

restoreFromDisabledQuestionnaireItemAnswersCache(questionnaireResponseItem)

// Evaluate cqf-calculatedValues
questionnaireItem.extension
.filter { it.hasValue() && it.value.hasExtension(EXTENSION_CQF_CALCULATED_VALUE_URL) }
.forEach { extension ->
val expression =
extension.value.getExtensionByUrl(EXTENSION_CQF_CALCULATED_VALUE_URL).value as Expression
resolveCqfCalculatedValueExpression(
questionnaireItem,
questionnaireResponseItem,
expression,
)
?.let {
it.apply { setExtension(extension.value.extension) }
extension.setValue(it)
}
}

// Determine the validation result, which will be displayed on the item itself
val validationResult =
if (
Expand All @@ -717,9 +752,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
QuestionnaireResponseItemValidator.validate(
questionnaireItem,
questionnaireResponseItem.answer,
expressionEvaluator = {
expressionEvaluator.evaluateExpression(questionnaireItem, questionnaireResponseItem, it)
},
context = this@QuestionnaireViewModel.getApplication(),
)
} else {
Expand Down Expand Up @@ -764,13 +796,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireResponseItem,
)
},
expressionEvaluator = {
expressionEvaluator.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
it,
)
},
questionViewTextConfiguration =
QuestionTextConfiguration(
showAsterisk = showAsterisk,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,6 @@ internal const val EXTENSION_CHOICE_ORIENTATION_URL =
internal const val EXTENSION_CHOICE_COLUMN_URL: String =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn"

internal const val EXTENSION_CQF_CALCULATED_VALUE_URL: String =
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue"

internal const val EXTENSION_DISPLAY_CATEGORY_URL =
"http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import com.google.android.fhir.datacapture.views.factories.localDate
import com.google.android.fhir.datacapture.views.factories.localTime
import com.google.android.fhir.getLocalizedText
import org.hl7.fhir.r4.model.Attachment
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.CodeType
import org.hl7.fhir.r4.model.Coding
Expand All @@ -40,6 +39,9 @@ import org.hl7.fhir.r4.model.StringType
import org.hl7.fhir.r4.model.Type
import org.hl7.fhir.r4.model.UriType

internal const val EXTENSION_CQF_CALCULATED_VALUE_URL: String =
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue"

/**
* Returns the string representation of a [PrimitiveType].
*
Expand Down Expand Up @@ -98,10 +100,8 @@ private fun getDisplayString(type: Type, context: Context): String? =

private fun getValueString(type: Type): String? =
when (type) {
is DateType,
is DateTimeType,
is StringType, -> type.asStringValue()
is Quantity -> type.value.toString()
is StringType -> type.getLocalizedText() ?: type.valueAsString
is Quantity -> type.takeIf { it.hasValue() }?.value?.toString()
else -> (type as? PrimitiveType<*>)?.valueAsString
}

Expand All @@ -125,19 +125,15 @@ internal fun Coding.toCodeType(): CodeType {
return CodeType(code)
}

fun Type.valueOrCalculateValue(
expressionEvaluator: (Expression) -> List<Base> = {
fhirPathEngine.evaluate(this, it.expression)
},
): Type {
return if (this.hasExtension()) {
this.extension
.firstOrNull { it.url == EXTENSION_CQF_CALCULATED_VALUE_URL }
?.let { extension ->
expressionEvaluator.invoke(extension.value as Expression).singleOrNull()?.let { it as Type }
}
?: this
} else {
fun Type.valueOrCalculateValue(): Type {
return if (getValueString(this) != null){
this
}else{
this.takeIf { hasExtension(EXTENSION_CQF_CALCULATED_VALUE_URL) }?.extension
?.firstOrNull { it.url == EXTENSION_CQF_CALCULATED_VALUE_URL }
?.let { extension ->
val expression = (extension.value as Expression).expression
fhirPathEngine.evaluate(this, expression).singleOrNull()?.let { it as Type }
} ?: this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package com.google.android.fhir.datacapture.validation

import android.content.Context
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse

Expand All @@ -40,7 +38,6 @@ internal interface AnswerConstraintValidator {
fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
expressionEvaluator: (Expression) -> List<Base>,
context: Context,
): Result

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package com.google.android.fhir.datacapture.validation

import android.content.Context
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
Expand All @@ -40,19 +38,17 @@ internal open class AnswerExtensionConstraintValidator(
(
Extension,
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
expressionEvaluator: (Expression) -> List<Base>,
) -> Boolean,
val messageGenerator: (Extension, Context) -> String,
) : AnswerConstraintValidator {
override fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
expressionEvaluator: (Expression) -> List<Base>,
context: Context,
): AnswerConstraintValidator.Result {
if (questionnaireItem.hasExtension(url)) {
val extension = questionnaireItem.getExtensionByUrl(url)
if (predicate(extension, answer, expressionEvaluator)) {
if (predicate(extension, answer)) {
return AnswerConstraintValidator.Result(false, messageGenerator(extension, context))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ package com.google.android.fhir.datacapture.validation
*/
import android.content.Context
import com.google.android.fhir.datacapture.R
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.QuestionnaireResponse
Expand All @@ -37,7 +35,6 @@ internal object MaxDecimalPlacesValidator :
predicate = {
extension: Extension,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
_: (Expression) -> List<Base>,
->
val maxDecimalPlaces = (extension.value as? IntegerType)?.value
answer.hasValueDecimalType() &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package com.google.android.fhir.datacapture.validation

import android.content.Context
import com.google.android.fhir.datacapture.extensions.asStringValue
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse

Expand All @@ -33,7 +31,6 @@ internal object MaxLengthValidator : AnswerConstraintValidator {
override fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
expressionEvaluator: (Expression) -> List<Base>,
context: Context,
): AnswerConstraintValidator.Result {
if (
Expand Down
Loading

0 comments on commit b6d50ed

Please sign in to comment.