Skip to content

Commit

Permalink
Refactor evaluation of cqf-calculatedValue expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
LZRS committed Nov 22, 2023
1 parent c91dc3f commit 3e9a620
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,17 @@ 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
import com.google.android.fhir.datacapture.extensions.cqfCalculatedValueExpression
import com.google.android.fhir.datacapture.extensions.cqfExpression
import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem
import com.google.android.fhir.datacapture.extensions.entryMode
import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtension
import com.google.android.fhir.datacapture.extensions.flattened
import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet
import com.google.android.fhir.datacapture.extensions.isCqfCalculatedValue
import com.google.android.fhir.datacapture.extensions.isDisplayItem
import com.google.android.fhir.datacapture.extensions.isFhirPath
import com.google.android.fhir.datacapture.extensions.isHidden
Expand Down Expand Up @@ -68,7 +69,6 @@ import kotlinx.coroutines.flow.stateIn
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
Expand Down Expand Up @@ -565,36 +565,21 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}

private fun resolveCqfExpression(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
element: Element,
): List<Base> {
val cqfExpression = element.cqfExpression ?: return emptyList()

if (!cqfExpression.isFhirPath) {
throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
}
return expressionEvaluator.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
cqfExpression,
)
}

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

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

private fun removeDisabledAnswers(
Expand Down Expand Up @@ -687,6 +672,27 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
)
}

private fun resolveCqfCalculatedValueExpressions(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
) {
questionnaireItem.extension
.filter { it.hasValue() && it.value.isCqfCalculatedValue }
.forEach { extension ->
val currentExtensionValue = extension.value
val currentExtensionValueExtensions = currentExtensionValue.extension
val evaluatedCqfCalculatedValue =
currentExtensionValue.cqfCalculatedValueExpression?.let {
resolveExpression(questionnaireItem, questionnaireResponseItem, it) as? Type
}
// add previous extensions to the evaluated value
evaluatedCqfCalculatedValue?.setExtension(currentExtensionValueExtensions)
if (evaluatedCqfCalculatedValue != null) {
extension.setValue(evaluatedCqfCalculatedValue)
}
}
}

/**
* Returns the list of [QuestionnaireViewItem]s generated for the questionnaire items and
* questionnaire response items.
Expand Down Expand Up @@ -726,21 +732,7 @@ 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)
}
}
resolveCqfCalculatedValueExpressions(questionnaireItem, questionnaireResponseItem)

// Determine the validation result, which will be displayed on the item itself
val validationResult =
Expand All @@ -759,11 +751,12 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}

// Set question text dynamically from CQL expression
questionnaireResponseItem.apply {
resolveCqfExpression(questionnaireItem, this, questionnaireItem.textElement)
.firstOrNull()
?.let { text = it.primitiveValue() }
questionnaireItem.textElement.cqfExpression?.let { expression ->
resolveExpression(questionnaireItem, questionnaireResponseItem, expression)
?.primitiveValue()
?.let { questionnaireResponseItem.text = it }
}

val (enabledQuestionnaireAnswerOptions, disabledQuestionnaireResponseAnswers) =
answerOptionsEvaluator.evaluate(
questionnaireItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import ca.uhn.fhir.util.UrlUtil
import com.google.android.fhir.datacapture.DataCapture
import com.google.android.fhir.datacapture.QuestionnaireViewHolderType
import com.google.android.fhir.datacapture.fhirpath.evaluateToDisplay
import com.google.android.fhir.datacapture.validation.MAX_VALUE_EXTENSION_URL
import com.google.android.fhir.datacapture.validation.MIN_VALUE_EXTENSION_URL
import com.google.android.fhir.getLocalizedText
import java.math.BigDecimal
import java.time.LocalDate
Expand Down Expand Up @@ -290,6 +292,14 @@ val Questionnaire.QuestionnaireItemComponent.sliderStepValue: Int?
return null
}

/** The inclusive lower bound on the range of allowed answer values. */
internal val Questionnaire.QuestionnaireItemComponent.minValue
get() = getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value?.valueOrCalculateValue()

/** The inclusive upper bound on the range of allowed answer values. */
internal val Questionnaire.QuestionnaireItemComponent.maxValue
get() = getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value?.valueOrCalculateValue()

// ********************************************************************************************** //
// //
// Additional display utilities: display item control, localized text spanned, //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.google.android.fhir.datacapture.fhirpath.fhirPathEngine
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.exceptions.FHIRException
import org.hl7.fhir.r4.model.Attachment
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.CodeType
Expand All @@ -38,6 +39,7 @@ import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.StringType
import org.hl7.fhir.r4.model.Type
import org.hl7.fhir.r4.model.UriType
import timber.log.Timber

/**
* Returns the string representation of a [PrimitiveType].
Expand Down Expand Up @@ -126,16 +128,26 @@ 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.cqfCalculatedValueExpression?.let { expression ->
try {
fhirPathEngine.evaluate(this, expression.expression).singleOrNull() as? Type
} catch (e: FHIRException) {
Timber.w("Could not evaluate expression with FHIRPathEngine", e)
null
}
}
?: this
}
}

internal val Type.isCqfCalculatedValue
get() = this.hasExtension(EXTENSION_CQF_CALCULATED_VALUE_URL)

internal val Type.cqfCalculatedValueExpression
get() =
this.takeIf { isCqfCalculatedValue }
?.getExtensionByUrl(EXTENSION_CQF_CALCULATED_VALUE_URL)
?.value as? Expression

internal const val EXTENSION_CQF_CALCULATED_VALUE_URL: String =
"http://hl7.org/fhir/StructureDefinition/cqf-calculatedValue"
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ package com.google.android.fhir.datacapture.validation

import android.content.Context
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.extensions.EXTENSION_CQF_CALCULATED_VALUE_URL
import com.google.android.fhir.datacapture.extensions.cqfCalculatedValueExpression
import com.google.android.fhir.datacapture.extensions.isCqfCalculatedValue
import com.google.android.fhir.datacapture.extensions.isFhirPath
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
Expand Down Expand Up @@ -144,21 +145,19 @@ object QuestionnaireResponseValidator {
if (enabled) {
// Evaluate cqf-calculatedValues
questionnaireItem.extension
.filter { it.hasValue() && it.value.hasExtension(EXTENSION_CQF_CALCULATED_VALUE_URL) }
.filter { it.hasValue() && it.value.isCqfCalculatedValue }
.forEach { extension ->
val expression =
extension.value.getExtensionByUrl(EXTENSION_CQF_CALCULATED_VALUE_URL).value
as Expression
expressionEvaluator
.resolveCqfCalculatedValue(
questionnaireItem,
questionnaireResponseItem,
expression,
)
?.let {
it.apply { setExtension(extension.value.extension) }
extension.setValue(it)
}
extension.value.cqfCalculatedValueExpression?.let { expression ->
val previousValueExtensions = extension.value.extension
val evaluatedValue =
expressionEvaluator.resolveCqfCalculatedValue(
questionnaireItem,
questionnaireResponseItem,
expression,
)
evaluatedValue?.setExtension(previousValueExtensions)
if (evaluatedValue != null) extension.setValue(evaluatedValue)
}
}

validateQuestionnaireResponseItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.displayString
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.extensions.toSpanned
import com.google.android.fhir.datacapture.validation.MAX_VALUE_EXTENSION_URL
import com.google.android.fhir.datacapture.validation.MIN_VALUE_EXTENSION_URL
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
Expand Down Expand Up @@ -54,7 +52,7 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
* @param validationResult the [ValidationResult] of the answer(s) against the `questionnaireItem`
* @param answersChangedCallback the callback to notify the view model that the answers have been
* changed for the [QuestionnaireResponse.QuestionnaireResponseItemComponent]
* @param enabledAnswerOptions the enabled answer options in [questionnaireItem]]
* @param enabledAnswerOptions the enabled answer options in [questionnaireItem]
* @param draftAnswer the draft input that cannot be stored in the [QuestionnaireResponse].
* @param enabledDisplayItems the enabled display items in the given [questionnaireItem]
* @param questionViewTextConfiguration configuration to show asterisk, required and optional text
Expand Down Expand Up @@ -93,10 +91,6 @@ data class QuestionnaireViewItem(
val answers: List<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent> =
questionnaireResponseItem.answer.map { it.copy() }

val minValue by lazy { questionnaireItem.getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value }

val maxValue by lazy { questionnaireItem.getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value }

/** Updates the answers. This will override any existing answers and removes the draft answer. */
fun setAnswer(
vararg questionnaireResponseItemAnswerComponent:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import com.google.android.fhir.datacapture.extensions.format
import com.google.android.fhir.datacapture.extensions.getDateSeparator
import com.google.android.fhir.datacapture.extensions.getRequiredOrOptionalText
import com.google.android.fhir.datacapture.extensions.getValidationErrorMessage
import com.google.android.fhir.datacapture.extensions.maxValue
import com.google.android.fhir.datacapture.extensions.minValue
import com.google.android.fhir.datacapture.extensions.parseDate
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.validation.Invalid
Expand Down Expand Up @@ -161,8 +163,8 @@ internal object DatePickerViewHolderFactory :
}

private fun getCalenderConstraint(): CalendarConstraints {
val min = (questionnaireViewItem.minValue as? DateType)?.value?.time
val max = (questionnaireViewItem.maxValue as? DateType)?.value?.time
val min = (questionnaireViewItem.questionnaireItem.minValue as? DateType)?.value?.time
val max = (questionnaireViewItem.questionnaireItem.maxValue as? DateType)?.value?.time

if (min != null && max != null && min > max) {
throw IllegalArgumentException("minValue cannot be greater than maxValue")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package com.google.android.fhir.datacapture.views.factories
import android.view.View
import android.widget.TextView
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.maxValue
import com.google.android.fhir.datacapture.extensions.minValue
import com.google.android.fhir.datacapture.extensions.sliderStepValue
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.NotValidated
Expand Down Expand Up @@ -50,8 +52,8 @@ internal object SliderViewHolderFactory : QuestionnaireItemViewHolderFactory(R.l
header.bind(questionnaireViewItem)
header.showRequiredOrOptionalTextInHeaderView(questionnaireViewItem)
val answer = questionnaireViewItem.answers.singleOrNull()
val minValue = getMinValue(questionnaireViewItem.minValue)
val maxValue = getMaxValue(questionnaireViewItem.maxValue)
val minValue = getMinValue(questionnaireViewItem.questionnaireItem.minValue)
val maxValue = getMaxValue(questionnaireViewItem.questionnaireItem.maxValue)
if (minValue >= maxValue) {
throw IllegalStateException("minValue $minValue must be smaller than maxValue $maxValue")
}
Expand Down
Loading

0 comments on commit 3e9a620

Please sign in to comment.