Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support evaluation of variables, launchContext and other FHIR contexts for cqf-calculatedValue expressions #2326

Merged
merged 16 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ import com.google.android.fhir.datacapture.expressions.EnabledAnswerOptionsEvalu
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 @@ -67,13 +69,14 @@ 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
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 @@ -562,21 +565,21 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}

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

if (!cqfExpression.isFhirPath) {
throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
expression: Expression,
): Base? {
if (!expression.isFhirPath) {
throw UnsupportedOperationException("${expression.language} not supported yet")
}
return expressionEvaluator.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
cqfExpression,
)
return expressionEvaluator
.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
expression,
)
.singleOrNull()
}

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

private fun resolveCqfCalculatedValueExpressions(
LZRS marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -707,6 +731,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

restoreFromDisabledQuestionnaireItemAnswersCache(questionnaireResponseItem)

// Evaluate cqf-calculatedValues
resolveCqfCalculatedValueExpressions(questionnaireItem, questionnaireResponseItem)

// Determine the validation result, which will be displayed on the item itself
val validationResult =
if (
Expand All @@ -717,18 +744,19 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
QuestionnaireResponseItemValidator.validate(
questionnaireItem,
questionnaireResponseItem.answer,
this@QuestionnaireViewModel.getApplication(),
context = this@QuestionnaireViewModel.getApplication(),
LZRS marked this conversation as resolved.
Show resolved Hide resolved
)
} else {
NotValidated
}

// 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 @@ -82,9 +84,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 Expand Up @@ -293,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 @@ -97,10 +99,8 @@ private fun getDisplayString(type: Type, context: Context): String? =

private fun getValueString(type: Type): String? =
jingtang10 marked this conversation as resolved.
Show resolved Hide resolved
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,15 +125,29 @@ internal fun Coding.toCodeType(): CodeType {
}

fun Type.valueOrCalculateValue(): Type {
return if (this.hasExtension()) {
this.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 }
return if (getValueString(this) != null) {
LZRS marked this conversation as resolved.
Show resolved Hide resolved
this
} else {
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
} else {
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
LZRS marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -35,7 +35,10 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
internal open class AnswerExtensionConstraintValidator(
val url: String,
val predicate:
(Extension, QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent) -> Boolean,
(
Extension,
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
) -> Boolean,
val messageGenerator: (Extension, Context) -> String,
) : AnswerConstraintValidator {
override fun validate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.getValueAsString
import com.google.android.fhir.datacapture.extensions.valueOrCalculateValue
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Type

internal const val MAX_VALUE_EXTENSION_URL = "http://hl7.org/fhir/StructureDefinition/maxValue"

Expand All @@ -44,11 +42,4 @@ internal object MaxValueValidator :
extension.value?.valueOrCalculateValue()?.getValueAsString(context),
)
},
) {

fun getMaxValue(questionnaireItemComponent: Questionnaire.QuestionnaireItemComponent): Type? {
return questionnaireItemComponent.extension
.firstOrNull { it.url == MAX_VALUE_EXTENSION_URL }
?.let { it.value?.valueOrCalculateValue() }
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.getValueAsString
import com.google.android.fhir.datacapture.extensions.valueOrCalculateValue
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Type

internal const val MIN_VALUE_EXTENSION_URL = "http://hl7.org/fhir/StructureDefinition/minValue"

Expand All @@ -44,13 +42,4 @@ internal object MinValueValidator :
extension.value?.valueOrCalculateValue()?.getValueAsString(context),
)
},
) {

internal fun getMinValue(
questionnaireItemComponent: Questionnaire.QuestionnaireItemComponent,
): Type? {
return questionnaireItemComponent.extension
.firstOrNull { it.url == MIN_VALUE_EXTENSION_URL }
?.let { it.value?.valueOrCalculateValue() }
}
}
)
Loading
Loading