Skip to content

Commit

Permalink
Suspend validate methods on answerConstraintValidators
Browse files Browse the repository at this point in the history
  • Loading branch information
LZRS committed Feb 27, 2024
1 parent a0746e0 commit 6eeca2c
Show file tree
Hide file tree
Showing 21 changed files with 469 additions and 459 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.withIndex
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Element
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse
Expand Down Expand Up @@ -735,11 +733,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireItem,
questionnaireResponseItem.answer,
this@QuestionnaireViewModel.getApplication(),
) { _, expression ->
) {
expressionEvaluator.evaluateExpressionValue(
questionnaireItem,
questionnaireResponseItem,
expression,
it,
)
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ internal class ExpressionEvaluator(
}

/** Returns the evaluation result of an expression as a [Type] value */
fun evaluateExpressionValue(
suspend fun evaluateExpressionValue(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent?,
expression: Expression,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022-2023 Google LLC
* Copyright 2022-2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,7 +24,6 @@ import org.hl7.fhir.r4.model.ExpressionNode
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.Type
import org.hl7.fhir.r4.utils.FHIRPathEngine

private val fhirPathEngine: FHIRPathEngine =
Expand Down Expand Up @@ -101,9 +100,9 @@ internal fun evaluateToBase(
}

/** Evaluates the given expression and returns list of [Base] */
internal fun evaluateToBase(type: Type, expression: String): List<Base> {
internal fun evaluateToBase(base: Base, expression: String): List<Base> {
return fhirPathEngine.evaluate(
/* base = */ type,
/* base = */ base,
/* path = */ expression,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.google.android.fhir.datacapture.validation

import android.content.Context
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
import org.hl7.fhir.r4.model.Type
Expand All @@ -38,11 +37,11 @@ internal interface AnswerConstraintValidator {
*
* [Learn more](https://www.hl7.org/fhir/questionnaireresponse.html#link).
*/
fun validate(
suspend fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
context: Context,
evaluateExtensionCqfCalculatedValue: (Extension, Expression) -> Type?,
expressionEvaluator: suspend (Expression) -> Type?,
): Result

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import android.content.Context
import com.google.android.fhir.datacapture.extensions.cqfCalculatedValueExpression
import com.google.android.fhir.datacapture.extensions.hasValue
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
import org.hl7.fhir.r4.model.Type
Expand All @@ -40,38 +39,31 @@ internal open class AnswerExtensionConstraintValidator(
val url: String,
val predicate:
(
/*extensionValue*/
/*constraintValue*/
Type,
/*answer*/
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
) -> Boolean,
val messageGenerator: (Type, Context) -> String,
) : AnswerConstraintValidator {
override fun validate(
override suspend fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
context: Context,
evaluateExtensionCqfCalculatedValue: (Extension, Expression) -> Type?,
expressionEvaluator: suspend (Expression) -> Type?,
): AnswerConstraintValidator.Result {
if (questionnaireItem.hasExtension(url)) {
val extension = questionnaireItem.getExtensionByUrl(url)
val extensionValueType =
extension.value.let {
it.cqfCalculatedValueExpression?.let { expression ->
evaluateExtensionCqfCalculatedValue(extension, expression)
}
?: it
}
val extensionCalculatedValue =
extension.value.cqfCalculatedValueExpression?.let { expressionEvaluator(it) }
val extensionValue = extensionCalculatedValue ?: extension.value

// Only checks constraint if both extension and answer have a value
if (
extensionValueType.hasValue() &&
answer.value.hasValue() &&
predicate(extensionValueType, answer)
extensionValue.hasValue() && answer.value.hasValue() && predicate(extensionValue, answer)
) {
return AnswerConstraintValidator.Result(
false,
messageGenerator(extensionValueType, context),
messageGenerator(extensionValue, context),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ internal object MaxDecimalPlacesValidator :
AnswerExtensionConstraintValidator(
url = MAX_DECIMAL_URL,
predicate = {
extensionValue: Type,
constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
val maxDecimalPlaces = (extensionValue as? IntegerType)?.value
val maxDecimalPlaces = (constraintValue as? IntegerType)?.value
answer.hasValueDecimalType() &&
maxDecimalPlaces != null &&
answer.valueDecimalType.valueAsString.substringAfter(".").length > maxDecimalPlaces
},
messageGenerator = { extensionValue: Type, context: Context ->
context.getString(R.string.max_decimal_validation_error_msg, extensionValue.primitiveValue())
messageGenerator = { constraintValue: Type, context: Context ->
context.getString(R.string.max_decimal_validation_error_msg, constraintValue.primitiveValue())
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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.Expression
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
Expand All @@ -31,11 +30,11 @@ import org.hl7.fhir.r4.model.Type
* https://www.hl7.org/fhir/valueset-item-type.html#expansion
*/
internal object MaxLengthValidator : AnswerConstraintValidator {
override fun validate(
override suspend fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
context: Context,
evaluateExtensionCqfCalculatedValue: (Extension, Expression) -> Type?,
expressionEvaluator: suspend (Expression) -> Type?,
): AnswerConstraintValidator.Result {
if (
questionnaireItem.hasMaxLength() &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ internal object MaxValueValidator :
AnswerExtensionConstraintValidator(
url = MAX_VALUE_EXTENSION_URL,
predicate = {
extensionValue: Type,
constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
answer.value > extensionValue
answer.value > constraintValue
},
messageGenerator = { extensionValue: Type, context: Context ->
messageGenerator = { constraintValue: Type, context: Context ->
context.getString(
R.string.max_value_validation_error_msg,
extensionValue.getValueAsString(context),
constraintValue.getValueAsString(context),
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ import org.hl7.fhir.r4.model.Type
internal object MinLengthValidator :
AnswerExtensionConstraintValidator(
url = MIN_LENGTH_EXTENSION_URL,
predicate = { extensionValue, answer ->
predicate = { constraintValue, answer ->
answer.value.isPrimitive &&
(answer.value as PrimitiveType<*>).asStringValue().length <
(extensionValue as IntegerType).value
(constraintValue as IntegerType).value
},
messageGenerator = { extensionValue: Type, context: Context ->
context.getString(R.string.min_length_validation_error_msg, extensionValue.primitiveValue())
messageGenerator = { constraintValue: Type, context: Context ->
context.getString(R.string.min_length_validation_error_msg, constraintValue.primitiveValue())
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ internal object MinValueValidator :
AnswerExtensionConstraintValidator(
url = MIN_VALUE_EXTENSION_URL,
predicate = {
extensionValue: Type,
constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
answer.value < extensionValue
answer.value < constraintValue
},
messageGenerator = { extensionValue: Type, context: Context ->
messageGenerator = { constraintValue: Type, context: Context ->
context.getString(
R.string.min_value_validation_error_msg,
extensionValue.getValueAsString(context),
constraintValue.getValueAsString(context),
)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.google.android.fhir.datacapture.validation
import android.content.Context
import com.google.android.fhir.datacapture.extensions.isHidden
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
import org.hl7.fhir.r4.model.Type
Expand All @@ -44,11 +43,11 @@ internal object QuestionnaireResponseItemValidator {
)

/** Validates [answers] contains valid answer(s) to [questionnaireItem]. */
fun validate(
suspend fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answers: List<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>,
context: Context,
expressionValueEvaluator: (Extension, Expression) -> Type?,
expressionEvaluator: suspend (Expression) -> Type?,
): ValidationResult {
if (questionnaireItem.isHidden) return NotValidated

Expand All @@ -59,9 +58,7 @@ internal object QuestionnaireResponseItemValidator {
val questionnaireResponseItemAnswerConstraintValidationResult =
answerConstraintValidators.flatMap { validator ->
answers.map { answer ->
validator.validate(questionnaireItem, answer, context) { extension, expression ->
expressionValueEvaluator.invoke(extension, expression)
}
validator.validate(questionnaireItem, answer, context, expressionEvaluator)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,11 @@ object QuestionnaireResponseValidator {
questionnaireItem,
questionnaireResponseItem.answer,
context,
) { _, expression ->
) {
expressionEvaluator.evaluateExpressionValue(
questionnaireItem,
questionnaireResponseItem,
expression,
it,
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,22 @@ internal object RegexValidator :
url = REGEX_EXTENSION_URL,
predicate =
predicate@{
extensionValue: Type,
constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
if (!extensionValue.isPrimitive || !answer.value.isPrimitive) {
if (!constraintValue.isPrimitive || !answer.value.isPrimitive) {
return@predicate false
}
try {
val pattern = Pattern.compile((extensionValue as PrimitiveType<*>).asStringValue())
val pattern = Pattern.compile((constraintValue as PrimitiveType<*>).asStringValue())
!pattern.matcher(answer.value.asStringValue()).matches()
} catch (e: PatternSyntaxException) {
Timber.w("Can't parse regex: $extensionValue", e)
Timber.w("Can't parse regex: $constraintValue", e)
false
}
},
messageGenerator = { extensionValue: Type, context: Context ->
context.getString(R.string.regex_validation_error_msg, extensionValue.primitiveValue())
messageGenerator = { constraintValue: Type, context: Context ->
context.getString(R.string.regex_validation_error_msg, constraintValue.primitiveValue())
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.content.Context
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.IntegerType
Expand All @@ -42,51 +43,56 @@ class MaxDecimalPlacesValidatorTest {
}

@Test
fun validate_noExtension_shouldReturnValidResult() {
fun validate_noExtension_shouldReturnValidResult() = runTest {
val questionnaireItem = Questionnaire.QuestionnaireItemComponent()
val validationResult =
MaxDecimalPlacesValidator.validate(
Questionnaire.QuestionnaireItemComponent(),
questionnaireItem,
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
.setValue(DecimalType("1.00")),
context,
) { extension, expression ->
CalculatedValueExpressionEvaluator.evaluate(extension.value, expression)
) {
TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
}

assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
}

@Test
fun validate_validAnswer_shouldReturnValidResult() {
fun validate_validAnswer_shouldReturnValidResult() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2)))
}
val validationResult =
MaxDecimalPlacesValidator.validate(
Questionnaire.QuestionnaireItemComponent().apply {
this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2)))
},
questionnaireItem,
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
.setValue(DecimalType("1.00")),
context,
) { extension, expression ->
CalculatedValueExpressionEvaluator.evaluate(extension.value, expression)
) {
TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
}

assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
}

@Test
fun validate_tooManyDecimalPlaces_shouldReturnInvalidResult() {
fun validate_tooManyDecimalPlaces_shouldReturnInvalidResult() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2)))
}
val validationResult =
MaxDecimalPlacesValidator.validate(
Questionnaire.QuestionnaireItemComponent().apply {
this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2)))
},
questionnaireItem,
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent()
.setValue(DecimalType("1.000")),
context,
) { extension, expression ->
CalculatedValueExpressionEvaluator.evaluate(extension.value, expression)
) {
TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
}

assertThat(validationResult.isValid).isFalse()
Expand Down
Loading

0 comments on commit 6eeca2c

Please sign in to comment.