diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
index abb7e7d49d..f5573a33be 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt
@@ -37,11 +37,14 @@ import com.google.android.fhir.datacapture.extensions.filterByCodeInNameExtensio
import com.google.android.fhir.datacapture.extensions.flattened
import com.google.android.fhir.datacapture.extensions.hasDifferentAnswerSet
import com.google.android.fhir.datacapture.extensions.isDisplayItem
-import com.google.android.fhir.datacapture.extensions.isFhirPath
import com.google.android.fhir.datacapture.extensions.isHelpCode
import com.google.android.fhir.datacapture.extensions.isHidden
import com.google.android.fhir.datacapture.extensions.isPaginated
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
+import com.google.android.fhir.datacapture.extensions.maxValue
+import com.google.android.fhir.datacapture.extensions.maxValueCqfCalculatedValueExpression
+import com.google.android.fhir.datacapture.extensions.minValue
+import com.google.android.fhir.datacapture.extensions.minValueCqfCalculatedValueExpression
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts
import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnderAnswers
@@ -69,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
@@ -591,23 +592,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}
- private suspend fun resolveCqfExpression(
- questionnaireItem: QuestionnaireItemComponent,
- questionnaireResponseItem: QuestionnaireResponseItemComponent,
- element: Element,
- ): List {
- val cqfExpression = element.cqfExpression ?: return emptyList()
-
- if (!cqfExpression.isFhirPath) {
- throw UnsupportedOperationException("${cqfExpression.language} not supported yet")
- }
- return expressionEvaluator.evaluateExpression(
- questionnaireItem,
- questionnaireResponseItem,
- cqfExpression,
- )
- }
-
private fun removeDisabledAnswers(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
@@ -749,17 +733,25 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireItem,
questionnaireResponseItem.answer,
this@QuestionnaireViewModel.getApplication(),
- )
+ ) {
+ expressionEvaluator.evaluateExpressionValue(
+ questionnaireItem,
+ questionnaireResponseItem,
+ it,
+ )
+ }
} 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 ->
+ expressionEvaluator
+ .evaluateExpressionValue(questionnaireItem, questionnaireResponseItem, expression)
+ ?.primitiveValue()
+ ?.let { questionnaireResponseItem.text = it }
}
+
val (enabledQuestionnaireAnswerOptions, disabledQuestionnaireResponseAnswers) =
answerOptionsEvaluator.evaluate(
questionnaireItem,
@@ -786,6 +778,24 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
validationResult = validationResult,
answersChangedCallback = answersChangedCallback,
enabledAnswerOptions = enabledQuestionnaireAnswerOptions,
+ minAnswerValue =
+ questionnaireItem.minValueCqfCalculatedValueExpression?.let {
+ expressionEvaluator.evaluateExpressionValue(
+ questionnaireItem,
+ questionnaireResponseItem,
+ it,
+ )
+ }
+ ?: questionnaireItem.minValue,
+ maxAnswerValue =
+ questionnaireItem.maxValueCqfCalculatedValueExpression?.let {
+ expressionEvaluator.evaluateExpressionValue(
+ questionnaireItem,
+ questionnaireResponseItem,
+ it,
+ )
+ }
+ ?: questionnaireItem.maxValue,
draftAnswer = draftAnswerMap[questionnaireResponseItem],
enabledDisplayItems =
questionnaireItem.item.filter {
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
index d4897c94e9..d1b373b1ee 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt
@@ -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
@@ -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"
@@ -294,6 +293,18 @@ val Questionnaire.QuestionnaireItemComponent.sliderStepValue: Int?
return null
}
+internal val Questionnaire.QuestionnaireItemComponent.minValue
+ get() = getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value
+
+internal val Questionnaire.QuestionnaireItemComponent.minValueCqfCalculatedValueExpression
+ get() = getExtensionByUrl(MIN_VALUE_EXTENSION_URL)?.value?.cqfCalculatedValueExpression
+
+internal val Questionnaire.QuestionnaireItemComponent.maxValue
+ get() = getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value
+
+internal val Questionnaire.QuestionnaireItemComponent.maxValueCqfCalculatedValueExpression
+ get() = getExtensionByUrl(MAX_VALUE_EXTENSION_URL)?.value?.cqfCalculatedValueExpression
+
// ********************************************************************************************** //
// //
// Additional display utilities: display item control, localized text spanned, //
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt
index 82ecb6a2e7..540da3578f 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaires.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Google LLC
+ * Copyright 2023-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.
@@ -94,7 +94,7 @@ private fun validateLaunchContextExtension(launchExtension: Extension) {
false
}
- if (nameCoding.system != EXTENSION_LAUNCH_CONTEXT || !isValidResourceType) {
+ if (nameCoding.system != CODE_SYSTEM_LAUNCH_CONTEXT || !isValidResourceType) {
error(
"The extension:name and/or extension:type do not follow the format specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT",
)
@@ -139,7 +139,8 @@ internal const val EXTENSION_ENTRY_MODE_URL: String =
internal const val EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT =
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext"
-internal const val EXTENSION_LAUNCH_CONTEXT = "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext"
+internal const val CODE_SYSTEM_LAUNCH_CONTEXT =
+ "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext"
val Questionnaire.entryMode: EntryMode?
get() {
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt
index 7ac598930c..bf6a2b00bd 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt
@@ -18,7 +18,6 @@ package com.google.android.fhir.datacapture.extensions
import android.content.Context
import com.google.android.fhir.datacapture.R
-import com.google.android.fhir.datacapture.fhirpath.evaluateToBase
import com.google.android.fhir.datacapture.views.factories.localDate
import com.google.android.fhir.datacapture.views.factories.localTime
import com.google.android.fhir.getLocalizedText
@@ -95,13 +94,13 @@ private fun getDisplayString(type: Type, context: Context): String? =
else -> (type as? PrimitiveType<*>)?.valueAsString
}
+/**
+ * Returns the string representation for [PrimitiveType] or [Quantity], otherwise defaults to null
+ */
private fun getValueString(type: Type): String? =
when (type) {
- is DateType,
- is DateTimeType,
- is StringType, -> type.asStringValue()
- is Quantity -> type.value.toString()
- else -> (type as? PrimitiveType<*>)?.valueAsString
+ is Quantity -> type.value?.toString()
+ else -> (type as? PrimitiveType<*>)?.asStringValue()
}
/** Converts StringType to toUriType. */
@@ -132,16 +131,10 @@ internal fun Quantity.toCoding(): Coding {
return Coding(this.system, this.code, this.unit)
}
-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
- evaluateToBase(this, expression).singleOrNull()?.let { it as Type }
- }
- ?: this
- } else {
- this
- }
-}
+internal fun Type.hasValue(): Boolean = !getValueString(this).isNullOrBlank()
+
+internal val Type.cqfCalculatedValueExpression
+ get() = this.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"
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt
index e6a1d9b1a8..b637a6bf08 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt
@@ -141,6 +141,27 @@ internal class ExpressionEvaluator(
)
}
+ /**
+ * Returns single [Type] evaluation value result of an expression, including cqf-expression and
+ * cqf-calculatedValue expressions
+ */
+ suspend fun evaluateExpressionValue(
+ questionnaireItem: QuestionnaireItemComponent,
+ questionnaireResponseItem: QuestionnaireResponseItemComponent?,
+ expression: Expression,
+ ): Type? {
+ if (!expression.isFhirPath) {
+ throw UnsupportedOperationException("${expression.language} not supported yet")
+ }
+ return try {
+ evaluateExpression(questionnaireItem, questionnaireResponseItem, expression).singleOrNull()
+ as? Type
+ } catch (e: Exception) {
+ Timber.w("Could not evaluate expression ${expression.expression} with FHIRPathEngine", e)
+ null
+ }
+ }
+
/**
* Returns a list of pair of item and the calculated and evaluated value for all items with
* calculated expression extension, which is dependent on value of updated response
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt
index 4c7d626913..1e86e4b8fd 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt
@@ -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.
@@ -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 =
@@ -101,9 +100,9 @@ internal fun evaluateToBase(
}
/** Evaluates the given expression and returns list of [Base] */
-internal fun evaluateToBase(type: Type, expression: String): List {
+internal fun evaluateToBase(base: Base, expression: String): List {
return fhirPathEngine.evaluate(
- /* base = */ type,
+ /* base = */ base,
/* path = */ expression,
)
}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt
index ba884217ba..20759607af 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt
@@ -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.
@@ -17,8 +17,10 @@
package com.google.android.fhir.datacapture.validation
import android.content.Context
+import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Type
/**
* Validates [QuestionnaireResponse.QuestionnaireResponseItemComponent] against a particular
@@ -35,10 +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,
+ expressionEvaluator: suspend (Expression) -> Type?,
): Result
/**
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt
index 46928b4435..2d8397b912 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt
@@ -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.
@@ -17,9 +17,12 @@
package com.google.android.fhir.datacapture.validation
import android.content.Context
-import org.hl7.fhir.r4.model.Extension
+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.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Type
/**
* Validates [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent] against a constraint
@@ -35,18 +38,33 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
internal open class AnswerExtensionConstraintValidator(
val url: String,
val predicate:
- (Extension, QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent) -> Boolean,
- val messageGenerator: (Extension, Context) -> String,
+ (
+ /*constraintValue*/
+ Type,
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
+ ) -> Boolean,
+ val messageGenerator: (Type, Context) -> String,
) : AnswerConstraintValidator {
- override fun validate(
+ override suspend fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
context: Context,
+ expressionEvaluator: suspend (Expression) -> Type?,
): AnswerConstraintValidator.Result {
if (questionnaireItem.hasExtension(url)) {
val extension = questionnaireItem.getExtensionByUrl(url)
- if (predicate(extension, answer)) {
- return AnswerConstraintValidator.Result(false, messageGenerator(extension, context))
+ val extensionValue =
+ extension.value.cqfCalculatedValueExpression?.let { expressionEvaluator(it) }
+ ?: extension.value
+
+ // Only checks constraint if both extension and answer have a value
+ if (
+ extensionValue.hasValue() && answer.value.hasValue() && predicate(extensionValue, answer)
+ ) {
+ return AnswerConstraintValidator.Result(
+ false,
+ messageGenerator(extensionValue, context),
+ )
}
}
return AnswerConstraintValidator.Result(true, null)
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt
index ab92e39c6b..fdfc088be6 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Google LLC
+ * Copyright 2023-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.
@@ -25,24 +25,24 @@ package com.google.android.fhir.datacapture.validation
*/
import android.content.Context
import com.google.android.fhir.datacapture.R
-import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Type
internal object MaxDecimalPlacesValidator :
AnswerExtensionConstraintValidator(
url = MAX_DECIMAL_URL,
predicate = {
- extension: Extension,
+ constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
- val maxDecimalPlaces = (extension.value as? IntegerType)?.value
+ val maxDecimalPlaces = (constraintValue as? IntegerType)?.value
answer.hasValueDecimalType() &&
maxDecimalPlaces != null &&
answer.valueDecimalType.valueAsString.substringAfter(".").length > maxDecimalPlaces
},
- messageGenerator = { extension: Extension, context: Context ->
- context.getString(R.string.max_decimal_validation_error_msg, extension.value.primitiveValue())
+ messageGenerator = { constraintValue: Type, context: Context ->
+ context.getString(R.string.max_decimal_validation_error_msg, constraintValue.primitiveValue())
},
)
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt
index e04c0f549d..0d3fb6aa55 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt
@@ -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.
@@ -18,8 +18,10 @@ 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.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Type
/**
* A validator to check if the answer exceeds the maximum number of permitted characters.
@@ -28,10 +30,11 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
* 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,
+ expressionEvaluator: suspend (Expression) -> Type?,
): AnswerConstraintValidator.Result {
if (
questionnaireItem.hasMaxLength() &&
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt
index ba42e702bf..3bd12d4788 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Google LLC
+ * Copyright 2023-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.
@@ -20,9 +20,6 @@ import android.content.Context
import com.google.android.fhir.compareTo
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
@@ -33,22 +30,15 @@ internal object MaxValueValidator :
AnswerExtensionConstraintValidator(
url = MAX_VALUE_EXTENSION_URL,
predicate = {
- extension: Extension,
+ constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
- answer.value > extension.value?.valueOrCalculateValue()!!
+ answer.value > constraintValue
},
- messageGenerator = { extension: Extension, context: Context ->
+ messageGenerator = { constraintValue: Type, context: Context ->
context.getString(
R.string.max_value_validation_error_msg,
- extension.value?.valueOrCalculateValue()?.getValueAsString(context),
+ constraintValue.getValueAsString(context),
)
},
- ) {
-
- fun getMaxValue(questionnaireItemComponent: Questionnaire.QuestionnaireItemComponent): Type? {
- return questionnaireItemComponent.extension
- .firstOrNull { it.url == MAX_VALUE_EXTENSION_URL }
- ?.let { it.value?.valueOrCalculateValue() }
- }
-}
+ )
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt
index 1630cda38c..623554b2f9 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Google LLC
+ * Copyright 2023-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.
@@ -18,9 +18,9 @@ package com.google.android.fhir.datacapture.validation
import android.content.Context
import com.google.android.fhir.datacapture.R
-import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.PrimitiveType
+import org.hl7.fhir.r4.model.Type
/**
* A validator to check if the answer fulfills the minimum number of permitted characters.
@@ -37,13 +37,13 @@ import org.hl7.fhir.r4.model.PrimitiveType
internal object MinLengthValidator :
AnswerExtensionConstraintValidator(
url = MIN_LENGTH_EXTENSION_URL,
- predicate = { extension, answer ->
+ predicate = { constraintValue, answer ->
answer.value.isPrimitive &&
(answer.value as PrimitiveType<*>).asStringValue().length <
- (extension.value as IntegerType).value
+ (constraintValue as IntegerType).value
},
- messageGenerator = { extension: Extension, context: Context ->
- context.getString(R.string.min_length_validation_error_msg, extension.value.primitiveValue())
+ messageGenerator = { constraintValue: Type, context: Context ->
+ context.getString(R.string.min_length_validation_error_msg, constraintValue.primitiveValue())
},
)
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt
index ff44447a59..5cc851e394 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Google LLC
+ * Copyright 2023-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.
@@ -20,9 +20,6 @@ import android.content.Context
import com.google.android.fhir.compareTo
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
@@ -33,24 +30,15 @@ internal object MinValueValidator :
AnswerExtensionConstraintValidator(
url = MIN_VALUE_EXTENSION_URL,
predicate = {
- extension: Extension,
+ constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
- answer.value < extension.value?.valueOrCalculateValue()!!
+ answer.value < constraintValue
},
- messageGenerator = { extension: Extension, context: Context ->
+ messageGenerator = { constraintValue: Type, context: Context ->
context.getString(
R.string.min_value_validation_error_msg,
- extension.value?.valueOrCalculateValue()?.getValueAsString(context),
+ constraintValue.getValueAsString(context),
)
},
- ) {
-
- internal fun getMinValue(
- questionnaireItemComponent: Questionnaire.QuestionnaireItemComponent,
- ): Type? {
- return questionnaireItemComponent.extension
- .firstOrNull { it.url == MIN_VALUE_EXTENSION_URL }
- ?.let { it.value?.valueOrCalculateValue() }
- }
-}
+ )
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt
index 94539af70b..7a386e48b2 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt
@@ -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.
@@ -18,8 +18,10 @@ 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.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Type
internal object QuestionnaireResponseItemValidator {
@@ -41,10 +43,11 @@ internal object QuestionnaireResponseItemValidator {
)
/** Validates [answers] contains valid answer(s) to [questionnaireItem]. */
- fun validate(
+ suspend fun validate(
questionnaireItem: Questionnaire.QuestionnaireItemComponent,
answers: List,
context: Context,
+ expressionEvaluator: suspend (Expression) -> Type?,
): ValidationResult {
if (questionnaireItem.isHidden) return NotValidated
@@ -54,7 +57,9 @@ internal object QuestionnaireResponseItemValidator {
}
val questionnaireResponseItemAnswerConstraintValidationResult =
answerConstraintValidators.flatMap { validator ->
- answers.map { answer -> validator.validate(questionnaireItem, answer, context) }
+ answers.map { answer ->
+ validator.validate(questionnaireItem, answer, context, expressionEvaluator)
+ }
}
return if (
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt
index 9427c38dfa..ec449b36d9 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt
@@ -20,6 +20,7 @@ import android.content.Context
import com.google.android.fhir.datacapture.XFhirQueryResolver
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
+import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Resource
@@ -83,6 +84,12 @@ object QuestionnaireResponseValidator {
launchContextMap,
xFhirQueryResolver,
),
+ ExpressionEvaluator(
+ questionnaire,
+ questionnaireResponse,
+ questionnaireItemParentMap,
+ launchContextMap,
+ ),
linkIdToValidationResultMap,
)
@@ -94,6 +101,7 @@ object QuestionnaireResponseValidator {
questionnaireResponseItemList: List,
context: Context,
enablementEvaluator: EnablementEvaluator,
+ expressionEvaluator: ExpressionEvaluator,
linkIdToValidationResultMap: MutableMap>,
): Map> {
val questionnaireItemListIterator = questionnaireItemList.iterator()
@@ -121,6 +129,7 @@ object QuestionnaireResponseValidator {
questionnaireResponseItem,
context,
enablementEvaluator,
+ expressionEvaluator,
linkIdToValidationResultMap,
)
}
@@ -133,6 +142,7 @@ object QuestionnaireResponseValidator {
questionnaireResponseItem: QuestionnaireResponse.QuestionnaireResponseItemComponent,
context: Context,
enablementEvaluator: EnablementEvaluator,
+ expressionEvaluator: ExpressionEvaluator,
linkIdToValidationResultMap: MutableMap>,
): Map> {
when (checkNotNull(questionnaireItem.type) { "Questionnaire item must have type" }) {
@@ -146,6 +156,7 @@ object QuestionnaireResponseValidator {
questionnaireResponseItem.item,
context,
enablementEvaluator,
+ expressionEvaluator,
linkIdToValidationResultMap,
)
else -> {
@@ -159,6 +170,7 @@ object QuestionnaireResponseValidator {
it.item,
context,
enablementEvaluator,
+ expressionEvaluator,
linkIdToValidationResultMap,
)
}
@@ -169,7 +181,13 @@ object QuestionnaireResponseValidator {
questionnaireItem,
questionnaireResponseItem.answer,
context,
- ),
+ ) {
+ expressionEvaluator.evaluateExpressionValue(
+ questionnaireItem,
+ questionnaireResponseItem,
+ it,
+ )
+ },
)
}
}
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt
index abd7840e4b..97d1a6f56d 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Google LLC
+ * Copyright 2023-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.
@@ -21,9 +21,9 @@ import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.asStringValue
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
-import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.PrimitiveType
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Type
import timber.log.Timber
/**
@@ -37,22 +37,22 @@ internal object RegexValidator :
url = REGEX_EXTENSION_URL,
predicate =
predicate@{
- extension: Extension,
+ constraintValue: Type,
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
->
- if (!extension.value.isPrimitive || !answer.value.isPrimitive) {
+ if (!constraintValue.isPrimitive || !answer.value.isPrimitive) {
return@predicate false
}
try {
- val pattern = Pattern.compile((extension.value 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: " + extension.value, e)
+ Timber.w("Can't parse regex: $constraintValue", e)
false
}
},
- messageGenerator = { extension: Extension, context: Context ->
- context.getString(R.string.regex_validation_error_msg, extension.value.primitiveValue())
+ messageGenerator = { constraintValue: Type, context: Context ->
+ context.getString(R.string.regex_validation_error_msg, constraintValue.primitiveValue())
},
)
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt
index b2ac20c2fd..cd75191115 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt
@@ -23,14 +23,18 @@ import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.displayString
import com.google.android.fhir.datacapture.extensions.isHelpCode
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
+import com.google.android.fhir.datacapture.extensions.maxValue
+import com.google.android.fhir.datacapture.extensions.minValue
import com.google.android.fhir.datacapture.extensions.toSpanned
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
+import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
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.QuestionnaireResponseItemComponent
+import org.hl7.fhir.r4.model.Type
/**
* Data item for [QuestionnaireItemViewHolder] in [RecyclerView].
@@ -55,12 +59,15 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComp
* @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 resolveAnswerValueSet the callback to resolve the answer value set and return the answer
- * @param resolveAnswerExpression the callback to resolve answer options when answer-expression
- * extension exists options
+ * @param enabledAnswerOptions the enabled answer options in [questionnaireItem]
+ * @param minAnswerValue the inclusive lower bound on the range of allowed answer values, that may
+ * be used for widgets that check for bounds and change behavior based on the min allowed answer
+ * value, e.g the Slider widget
+ * @param maxAnswerValue the inclusive upper bound on the range of allowed answer values, that may
+ * be used for widgets that check for bounds and change behavior based on the max allowed answer
+ * value, e.g the Slider widget
* @param draftAnswer the draft input that cannot be stored in the [QuestionnaireResponse].
* @param enabledDisplayItems the enabled display items in the given [questionnaireItem]
- * @param showOptionalText the optional text is being added to the end of the question text
* @param questionViewTextConfiguration configuration to show asterisk, required and optional text
* in the header view.
*/
@@ -77,6 +84,8 @@ data class QuestionnaireViewItem(
) -> Unit,
val enabledAnswerOptions: List =
questionnaireItem.answerOption.ifEmpty { emptyList() },
+ val minAnswerValue: Type? = questionnaireItem.minValue,
+ val maxAnswerValue: Type? = questionnaireItem.maxValue,
val draftAnswer: Any? = null,
val enabledDisplayItems: List = emptyList(),
val questionViewTextConfiguration: QuestionTextConfiguration = QuestionTextConfiguration(),
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt
index d715029ccb..8d65eddea2 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt
@@ -34,8 +34,6 @@ import com.google.android.fhir.datacapture.extensions.getValidationErrorMessage
import com.google.android.fhir.datacapture.extensions.parseDate
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.validation.Invalid
-import com.google.android.fhir.datacapture.validation.MaxValueValidator.getMaxValue
-import com.google.android.fhir.datacapture.validation.MinValueValidator.getMinValue
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.HeaderView
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
@@ -168,8 +166,8 @@ internal object DatePickerViewHolderFactory :
}
private fun getCalenderConstraint(): CalendarConstraints {
- val min = (getMinValue(questionnaireViewItem.questionnaireItem) as? DateType)?.value?.time
- val max = (getMaxValue(questionnaireViewItem.questionnaireItem) as? DateType)?.value?.time
+ val min = (questionnaireViewItem.minAnswerValue as? DateType)?.value?.time
+ val max = (questionnaireViewItem.maxAnswerValue as? DateType)?.value?.time
if (min != null && max != null && min > max) {
throw IllegalArgumentException("minValue cannot be greater than maxValue")
diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt
index 8a50eead3d..eaec4454e4 100644
--- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt
+++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/SliderViewHolderFactory.kt
@@ -24,8 +24,6 @@ import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.extensions.sliderStepValue
import com.google.android.fhir.datacapture.extensions.tryUnwrapContext
import com.google.android.fhir.datacapture.validation.Invalid
-import com.google.android.fhir.datacapture.validation.MaxValueValidator
-import com.google.android.fhir.datacapture.validation.MinValueValidator
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
@@ -34,8 +32,8 @@ import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.material.slider.Slider
import kotlinx.coroutines.launch
import org.hl7.fhir.r4.model.IntegerType
-import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
+import org.hl7.fhir.r4.model.Type
internal object SliderViewHolderFactory : QuestionnaireItemViewHolderFactory(R.layout.slider_view) {
override fun getQuestionnaireItemViewHolderDelegate(): QuestionnaireItemViewHolderDelegate =
@@ -58,8 +56,8 @@ internal object SliderViewHolderFactory : QuestionnaireItemViewHolderFactory(R.l
header.bind(questionnaireViewItem)
header.showRequiredOrOptionalTextInHeaderView(questionnaireViewItem)
val answer = questionnaireViewItem.answers.singleOrNull()
- val minValue = getMinValue(questionnaireViewItem.questionnaireItem)
- val maxValue = getMaxValue(questionnaireViewItem.questionnaireItem)
+ val minValue = getMinValue(questionnaireViewItem.minAnswerValue)
+ val maxValue = getMaxValue(questionnaireViewItem.maxAnswerValue)
if (minValue >= maxValue) {
throw IllegalStateException("minValue $minValue must be smaller than maxValue $maxValue")
}
@@ -106,15 +104,15 @@ private const val SLIDER_DEFAULT_STEP_SIZE = 1
private const val SLIDER_DEFAULT_VALUE_FROM = 0.0F
private const val SLIDER_DEFAULT_VALUE_TO = 100.0F
-private fun getMinValue(questionnaireItem: Questionnaire.QuestionnaireItemComponent) =
- when (val minValue = MinValueValidator.getMinValue(questionnaireItem)) {
+private fun getMinValue(minValue: Type?) =
+ when (minValue) {
is IntegerType -> minValue.value.toFloat()
null -> SLIDER_DEFAULT_VALUE_FROM
else -> throw IllegalArgumentException("Cannot support data type: ${minValue.fhirType()}}")
}
-private fun getMaxValue(questionnaireItem: Questionnaire.QuestionnaireItemComponent) =
- when (val maxValue = MaxValueValidator.getMaxValue(questionnaireItem)) {
+private fun getMaxValue(maxValue: Type?) =
+ when (maxValue) {
is IntegerType -> maxValue.value.toFloat()
null -> SLIDER_DEFAULT_VALUE_TO
else -> throw IllegalArgumentException("Cannot support data type: ${maxValue.fhirType()}}")
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt
index 73e0b496ed..6e1974e3a8 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt
@@ -31,11 +31,13 @@ import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_CANCEL_BUTTON
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_REVIEW_PAGE_FIRST
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_SUBMIT_BUTTON
+import com.google.android.fhir.datacapture.extensions.CODE_SYSTEM_LAUNCH_CONTEXT
import com.google.android.fhir.datacapture.extensions.DisplayItemControlType
import com.google.android.fhir.datacapture.extensions.EXTENSION_ANSWER_OPTION_TOGGLE_EXPRESSION
import com.google.android.fhir.datacapture.extensions.EXTENSION_ANSWER_OPTION_TOGGLE_EXPRESSION_OPTION
import com.google.android.fhir.datacapture.extensions.EXTENSION_ANSWER_OPTION_TOGGLE_EXPRESSION_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_CALCULATED_EXPRESSION_URL
+import com.google.android.fhir.datacapture.extensions.EXTENSION_CQF_CALCULATED_VALUE_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_CQF_EXPRESSION_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_DISPLAY_CATEGORY_INSTRUCTIONS
import com.google.android.fhir.datacapture.extensions.EXTENSION_DISPLAY_CATEGORY_SYSTEM
@@ -45,17 +47,23 @@ import com.google.android.fhir.datacapture.extensions.EXTENSION_ENTRY_MODE_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_HIDDEN_URL
import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_SYSTEM
import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_URL
+import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
import com.google.android.fhir.datacapture.extensions.EXTENSION_VARIABLE_URL
import com.google.android.fhir.datacapture.extensions.EntryMode
import com.google.android.fhir.datacapture.extensions.asStringValue
import com.google.android.fhir.datacapture.extensions.entryMode
import com.google.android.fhir.datacapture.extensions.getNestedQuestionnaireResponseItems
import com.google.android.fhir.datacapture.extensions.logicalId
+import com.google.android.fhir.datacapture.extensions.maxValue
import com.google.android.fhir.datacapture.testing.DataCaptureTestApplication
import com.google.android.fhir.datacapture.validation.Invalid
+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.views.QuestionnaireViewItem
import com.google.common.truth.Truth.assertThat
+import java.time.LocalDate
+import java.time.ZoneId
import java.util.Calendar
import java.util.Date
import java.util.UUID
@@ -4503,13 +4511,13 @@ class QuestionnaireViewModelTest {
extension =
listOf(
Extension(
- "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext",
+ EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT,
)
.apply {
addExtension(
"name",
Coding(
- "http://hl7.org/fhir/uv/sdc/CodeSystem/launchContext",
+ CODE_SYSTEM_LAUNCH_CONTEXT,
"patient",
"Patient",
),
@@ -6004,6 +6012,564 @@ class QuestionnaireViewModelTest {
.isEqualTo("a-birthdate and a-age-years have cyclic dependency in expression based extension")
}
+ // ==================================================================== //
+ // //
+ // cqf-calculatedValue Expression for minValue/maxValue Extension //
+ // //
+ // ==================================================================== //
+
+ @Test
+ fun `should return correct value evaluated for minValue extension with cqf-calculatedValue`() =
+ runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MIN_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today()"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .run {
+ assertThat((this.minAnswerValue as DateType).valueAsString)
+ .isEqualTo(LocalDate.now().toString())
+ }
+ }
+ }
+
+ @Test
+ fun `should return calculated value for minValue extension that has both value and cqf-calculatedValue expression`() =
+ runTest {
+ val lastLocalDate = LocalDate.now().minusMonths(1)
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MIN_VALUE_EXTENSION_URL,
+ DateType().apply {
+ value =
+ Date.from(
+ lastLocalDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(),
+ )
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today()"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .run {
+ assertThat((this.minAnswerValue as DateType).valueAsString)
+ .isEqualTo(LocalDate.now().toString())
+ }
+ }
+ }
+
+ @Test
+ fun `should correctly validate cqf-calculatedValue for minValue extension`() = runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MIN_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today()"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .setAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value = DateType.parseV3("20231010")
+ },
+ )
+
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .run {
+ assertThat(validationResult)
+ .isEqualTo(Invalid(listOf("Minimum value allowed is:${LocalDate.now()}")))
+ }
+ }
+ }
+
+ @Test
+ fun `should return correct evaluated value for maxValue extension with cqf-calculatedValue extension`() =
+ runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MAX_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today()"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .run {
+ assertThat((this.maxAnswerValue as DateType).valueAsString)
+ .isEqualTo(LocalDate.now().toString())
+ }
+ }
+ }
+
+ @Test
+ fun `should return calculated value for maxValue extension that has both value and cqf-calculatedValue expression`() =
+ runTest {
+ val lastLocalDate = LocalDate.now().minusMonths(1)
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MAX_VALUE_EXTENSION_URL,
+ DateType().apply {
+ value =
+ Date.from(
+ lastLocalDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(),
+ )
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today()"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ assertThat(
+ (questionnaire.item.single { it.linkId == "a" }.maxValue as DateType).valueAsString,
+ )
+ .isEqualTo(lastLocalDate.toString())
+
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .run {
+ assertThat((this.maxAnswerValue as DateType).valueAsString)
+ .isEqualTo(LocalDate.now().toString())
+ }
+ }
+ }
+
+ @Test
+ fun `should correctly validate cqf-calculatedValue for maxValue extension`() = runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MAX_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today()"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .setAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value =
+ DateType().apply {
+ val tomorrow = LocalDate.now().plusDays(1)
+ value =
+ Date.from(tomorrow.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())
+ }
+ },
+ )
+
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .run {
+ assertThat(validationResult)
+ .isEqualTo(Invalid(listOf("Maximum value allowed is:${LocalDate.now()}")))
+ }
+ }
+ }
+
+ @Test
+ fun `should evaluate cqf-calculatedValue with expression dependent on other question`() =
+ runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select minimum date"
+ },
+ )
+
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "b"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MIN_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression =
+ "%resource.repeat(item).where(linkId='a' and answer.empty().not()).select(answer.value)"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ // Checks dependent answer is null at first
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "b" }
+ .run { assertThat((this.minAnswerValue as? DateType)?.valueAsString).isNull() }
+
+ // Answers the first question
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .setAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value = DateType.parseV3("20231014")
+ },
+ )
+
+ // Checks dependent answer has min value set correctly
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "b" }
+ .run {
+ assertThat((this.minAnswerValue as DateType).valueAsString).isEqualTo("2023-10-14")
+ }
+ }
+ }
+
+ @Test
+ fun `should evaluate cqf-calculatedValue with expression dependent on a variable expression`() =
+ runTest {
+ val questionnaire =
+ Questionnaire().apply {
+ addExtension(
+ Extension(EXTENSION_VARIABLE_URL).apply {
+ setValue(
+ Expression().apply {
+ name = "dateToday"
+ expression = "today()"
+ language = "text/fhirpath"
+ },
+ )
+ },
+ )
+
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MIN_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "%dateToday"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single()
+ .run {
+ assertThat((this.minAnswerValue as DateType).valueAsString)
+ .isEqualTo(LocalDate.now().toString())
+ }
+ }
+ }
+
+ @Test
+ fun `should correctly evaluate cqf-calculatedValue with expression dependent on x-fhir-query launchContext`() =
+ runTest {
+ val testDate = LocalDate.now().minusYears(20)
+ val questionnaire =
+ Questionnaire().apply {
+ addExtension(
+ Extension(EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT).apply {
+ addExtension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "patient", "Patient"))
+ addExtension("type", CodeType("Patient"))
+ },
+ )
+
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MIN_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "%patient.birthDate"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val patient0 =
+ Patient().apply {
+ birthDate = Date.from(testDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())
+ }
+ state[EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP] =
+ mapOf("patient" to printer.encodeResourceToString(patient0))
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single()
+ .run {
+ assertThat((this.minAnswerValue as DateType).valueAsString)
+ .isEqualTo(testDate.toString())
+ }
+ }
+ }
+
+ @Test
+ fun `should correctly validate cqf-calculatedValue with expression dependent on x-fhir-query launchContext`() =
+ runTest {
+ val testDate = LocalDate.now().minusYears(20)
+
+ val questionnaire =
+ Questionnaire().apply {
+ addExtension(
+ Extension(EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT).apply {
+ addExtension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "patient", "Patient"))
+ addExtension("type", CodeType("Patient"))
+ },
+ )
+
+ addItem(
+ QuestionnaireItemComponent().apply {
+ linkId = "a"
+ type = Questionnaire.QuestionnaireItemType.DATE
+ text = "Select a date"
+ addExtension(
+ Extension(
+ MIN_VALUE_EXTENSION_URL,
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "%patient.birthDate"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ ),
+ )
+ },
+ )
+ }
+
+ val patient0 =
+ Patient().apply {
+ birthDate = Date.from(testDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())
+ }
+ state[EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_MAP] =
+ mapOf("patient" to printer.encodeResourceToString(patient0))
+ val viewModel = createQuestionnaireViewModel(questionnaire)
+
+ viewModel.runViewModelBlocking {
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .setAnswer(
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value =
+ DateType().apply {
+ value = patient0.birthDate
+ add(Calendar.MONTH, -1)
+ }
+ },
+ )
+
+ viewModel
+ .getQuestionnaireItemViewItemList()
+ .map { it.asQuestion() }
+ .single { it.questionnaireItem.linkId == "a" }
+ .run {
+ assertThat(validationResult)
+ .isEqualTo(Invalid(listOf("Minimum value allowed is:$testDate")))
+ }
+ }
+ }
+
// ==================================================================== //
// //
// Display Category //
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreTypesTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreTypesTest.kt
index 64d845aa5b..3f0e7b28cc 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreTypesTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreTypesTest.kt
@@ -22,7 +22,6 @@ import androidx.test.core.app.ApplicationProvider
import ca.uhn.fhir.model.api.TemporalPrecisionEnum
import com.google.common.truth.Truth.assertThat
import java.time.Instant
-import java.time.LocalDate
import java.time.ZoneId
import java.util.Date
import java.util.TimeZone
@@ -32,10 +31,7 @@ import org.hl7.fhir.r4.model.CanonicalType
import org.hl7.fhir.r4.model.CodeType
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateTimeType
-import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
-import org.hl7.fhir.r4.model.Expression
-import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.InstantType
import org.hl7.fhir.r4.model.IntegerType
@@ -274,46 +270,14 @@ class MoreTypesTest {
}
@Test
- fun `should return calculated value for cqf expression`() {
- val today = LocalDate.now().toString()
- val type =
- DateType().apply {
- extension =
- listOf(
- Extension(
- EXTENSION_CQF_CALCULATED_VALUE_URL,
- Expression().apply {
- language = "text/fhirpath"
- expression = "today()"
- },
- ),
- )
- }
- assertThat((type.valueOrCalculateValue() as DateType).valueAsString).isEqualTo(today)
- }
-
- @Test
- fun `should return calculated value for a non-cqf extension`() {
- LocalDate.now().toString()
- val type =
- DateType().apply {
- extension =
- listOf(
- Extension(
- "http://hl7.org/fhir/StructureDefinition/my-own-expression",
- Expression().apply {
- language = "text/fhirpath"
- expression = "today()"
- },
- ),
- )
- }
- assertThat((type.valueOrCalculateValue() as DateType).valueAsString).isEqualTo(null)
+ fun `getValueAsString should return 'not answered' for an empty Quantity`() {
+ val quantity = Quantity()
+ assertThat(quantity.getValueAsString(context)).isEqualTo("Not Answered")
}
@Test
- fun `should return entered value when no cqf expression is defined`() {
- val type = IntegerType().apply { value = 500 }
- assertThat((type.valueOrCalculateValue() as IntegerType).value).isEqualTo(500)
+ fun `getValueAsString should return correct value for a Quantity`() {
+ val quantity = Quantity(20L)
+ assertThat(quantity.getValueAsString(context)).isEqualTo("20")
}
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt
index df84802581..b8bb271973 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt
@@ -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.
@@ -22,7 +22,7 @@ import androidx.test.core.app.ApplicationProvider
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
-import com.google.android.fhir.datacapture.extensions.EXTENSION_LAUNCH_CONTEXT
+import com.google.android.fhir.datacapture.extensions.CODE_SYSTEM_LAUNCH_CONTEXT
import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
import com.google.android.fhir.datacapture.extensions.ITEM_INITIAL_EXPRESSION_URL
import com.google.android.fhir.datacapture.views.factories.localDate
@@ -1519,7 +1519,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")),
Extension("type", CodeType("Patient")),
)
}
@@ -1527,7 +1527,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "mother", "Mother")),
Extension("type", CodeType("Patient")),
)
}
@@ -1538,7 +1538,7 @@ class ResourceMapperTest {
Extension(
"name",
Coding(
- EXTENSION_LAUNCH_CONTEXT,
+ CODE_SYSTEM_LAUNCH_CONTEXT,
"registration-encounter",
"Registration Encounter",
),
@@ -1643,7 +1643,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")),
Extension("type", CodeType("Patient")),
)
}
@@ -1718,7 +1718,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")),
Extension("type", CodeType("Patient")),
)
}
@@ -1758,7 +1758,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")),
Extension("type", CodeType("Patient")),
)
}
@@ -1798,7 +1798,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")),
Extension("type", CodeType("Patient")),
)
}
@@ -1845,7 +1845,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "patient", "Patient")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "patient", "Patient")),
Extension("type", CodeType("Patient")),
)
}
@@ -1882,7 +1882,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")),
Extension("type", CodeType("Patient")),
)
}
@@ -1922,7 +1922,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "mother", "Mother")),
Extension("type", CodeType("Patient")),
)
}
@@ -1976,7 +1976,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "mother", "Mother")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "mother", "Mother")),
Extension("type", CodeType("Patient")),
)
}
@@ -2928,7 +2928,7 @@ class ResourceMapperTest {
url = EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
extension =
listOf(
- Extension("name", Coding(EXTENSION_LAUNCH_CONTEXT, "father", "Father")),
+ Extension("name", Coding(CODE_SYSTEM_LAUNCH_CONTEXT, "father", "Father")),
Extension("type", CodeType("Patient")),
)
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt
index 524aa9cbd5..52b3f9f1ab 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt
@@ -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.
@@ -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
@@ -42,46 +43,57 @@ 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,
- )
+ ) {
+ 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,
- )
+ ) {
+ 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,
- )
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult.isValid).isFalse()
assertThat(validationResult.errorMessage)
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt
index 73c49047e6..cb9edda933 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt
@@ -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.
@@ -22,6 +22,7 @@ import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import java.net.URI
import java.text.SimpleDateFormat
+import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
@@ -51,86 +52,89 @@ class MaxLengthValidatorTest {
}
@Test
- fun boolean_answerOverMaxLength_shouldReturnInvalidResult() {
+ fun boolean_answerOverMaxLength_shouldReturnInvalidResult() = runTest {
checkAnswerOverMaxLength(maxLength = 4, value = BooleanType(false))
}
@Test
- fun boolean_answerUnderMaxLength_shouldReturnValidResult() {
+ fun boolean_answerUnderMaxLength_shouldReturnValidResult() = runTest {
checkAnswerUnderMaxLength(maxLength = 6, value = BooleanType(false))
}
@Test
- fun decimal_answerOverMaxLength_shouldReturnInvalidResult() {
+ fun decimal_answerOverMaxLength_shouldReturnInvalidResult() = runTest {
checkAnswerOverMaxLength(maxLength = 10, value = DecimalType(3.1415926535))
}
@Test
- fun decimal_answerUnderMaxLength_shouldReturnValidResult() {
+ fun decimal_answerUnderMaxLength_shouldReturnValidResult() = runTest {
checkAnswerUnderMaxLength(maxLength = 16, value = DecimalType(3.1415926535))
}
@Test
- fun int_answerOverMaxLength_shouldReturnInvalidResult() {
+ fun int_answerOverMaxLength_shouldReturnInvalidResult() = runTest {
checkAnswerOverMaxLength(maxLength = 5, value = IntegerType(1234567890))
}
@Test
- fun int_answerUnderMaxLength_shouldReturnValidResult() {
+ fun int_answerUnderMaxLength_shouldReturnValidResult() = runTest {
checkAnswerUnderMaxLength(maxLength = 10, value = IntegerType(1234567890))
}
@Test
- fun dateType_answerOverMaxLength_shouldReturnInvalidResult() {
+ fun dateType_answerOverMaxLength_shouldReturnInvalidResult() = runTest {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
checkAnswerOverMaxLength(maxLength = 5, value = DateType(dateFormat.parse("2021-06-01")))
}
@Test
- fun date_answerUnderMaxLength_shouldReturnValidResult() {
+ fun date_answerUnderMaxLength_shouldReturnValidResult() = runTest {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
checkAnswerUnderMaxLength(maxLength = 11, value = DateType(dateFormat.parse("2021-06-01")))
}
@Test
- fun time_answerOverMaxLength_shouldReturnInvalidResult() {
+ fun time_answerOverMaxLength_shouldReturnInvalidResult() = runTest {
checkAnswerOverMaxLength(maxLength = 5, value = TimeType("18:00:59"))
}
@Test
- fun time_answerUnderMaxLength_shouldReturnValidResult() {
+ fun time_answerUnderMaxLength_shouldReturnValidResult() = runTest {
checkAnswerUnderMaxLength(maxLength = 9, value = TimeType("18:00:59"))
}
@Test
- fun string_answerOverMaxLength_shouldReturnInvalidResult() {
+ fun string_answerOverMaxLength_shouldReturnInvalidResult() = runTest {
checkAnswerOverMaxLength(maxLength = 5, value = StringType("Hello World"))
}
@Test
- fun string_answerUnderMaxLength_shouldReturnValidResult() {
+ fun string_answerUnderMaxLength_shouldReturnValidResult() = runTest {
checkAnswerUnderMaxLength(maxLength = 11, value = StringType("Hello World"))
}
@Test
- fun uri_answerOverMaxLength_shouldReturnInvalidResult() {
+ fun uri_answerOverMaxLength_shouldReturnInvalidResult() = runTest {
checkAnswerOverMaxLength(maxLength = 5, value = UriType(URI.create("https://www.hl7.org/")))
}
@Test
- fun uri_answerUnderMaxLength_shouldReturnValidResult() {
+ fun uri_answerUnderMaxLength_shouldReturnValidResult() = runTest {
checkAnswerUnderMaxLength(maxLength = 20, value = UriType(URI.create("https://www.hl7.org/")))
}
@Test
- fun nonPrimitiveOverMaxLength_shouldReturnValidResult() {
+ fun nonPrimitiveOverMaxLength_shouldReturnValidResult() = runTest {
val requirement = Questionnaire.QuestionnaireItemComponent().apply { maxLength = 5 }
val answer =
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
this.value = Quantity(1234567.89)
}
- val validationResult = MaxLengthValidator.validate(requirement, answer, context)
+ val validationResult =
+ MaxLengthValidator.validate(requirement, answer, context) {
+ TestExpressionValueEvaluator.evaluate(requirement, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
@@ -141,11 +145,17 @@ class MaxLengthValidatorTest {
var context: Context = ApplicationProvider.getApplicationContext()
@JvmStatic
- fun checkAnswerOverMaxLength(maxLength: Int, value: PrimitiveType<*>) {
+ suspend fun checkAnswerOverMaxLength(maxLength: Int, value: PrimitiveType<*>) {
val testComponent = createMaxLengthQuestionnaireTestItem(maxLength, value)
val validationResult =
- MaxLengthValidator.validate(testComponent.requirement, testComponent.answer, context)
+ MaxLengthValidator.validate(
+ testComponent.requirement,
+ testComponent.answer,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(testComponent.requirement, it)
+ }
assertThat(validationResult.isValid).isFalse()
assertThat(validationResult.errorMessage)
@@ -155,11 +165,17 @@ class MaxLengthValidatorTest {
}
@JvmStatic
- fun checkAnswerUnderMaxLength(maxLength: Int, value: PrimitiveType<*>) {
+ suspend fun checkAnswerUnderMaxLength(maxLength: Int, value: PrimitiveType<*>) {
val testComponent = createMaxLengthQuestionnaireTestItem(maxLength, value)
val validationResult =
- MaxLengthValidator.validate(testComponent.requirement, testComponent.answer, context)
+ MaxLengthValidator.validate(
+ testComponent.requirement,
+ testComponent.answer,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(testComponent.requirement, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt
index 9f3d738c4a..2c3b1346bd 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt
@@ -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.
@@ -19,10 +19,13 @@ package com.google.android.fhir.datacapture.validation
import android.content.Context
import android.os.Build
import androidx.test.core.app.ApplicationProvider
+import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.fhir.datacapture.extensions.EXTENSION_CQF_CALCULATED_VALUE_URL
import com.google.common.truth.Truth.assertThat
-import java.text.SimpleDateFormat
import java.time.LocalDate
+import java.time.ZoneId
+import java.util.Date
+import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Extension
@@ -47,7 +50,7 @@ class MaxValueValidatorTest {
}
@Test
- fun `should return invalid result when entered value is greater than maxValue`() {
+ fun `should return invalid result when entered value is greater than maxValue`() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
@@ -62,14 +65,17 @@ class MaxValueValidatorTest {
value = IntegerType(200001)
}
- val validationResult = MaxValueValidator.validate(questionnaireItem, answer, context)
+ val validationResult =
+ MaxValueValidator.validate(questionnaireItem, answer, context) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult.isValid).isFalse()
assertThat(validationResult.errorMessage).isEqualTo("Maximum value allowed is:200000")
}
@Test
- fun `should return valid result when entered value is less than maxValue`() {
+ fun `should return valid result when entered value is less than maxValue`() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
@@ -84,36 +90,101 @@ class MaxValueValidatorTest {
value = IntegerType(501)
}
- val validationResult = MaxValueValidator.validate(questionnaireItem, answer, context)
+ val validationResult =
+ MaxValueValidator.validate(questionnaireItem, answer, context) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
}
@Test
- fun `should return maxValue date`() {
- val dateType = DateType(SimpleDateFormat("yyyy-MM-dd").parse("2023-06-01"))
- val questionItem =
- listOf(
+ fun `should return invalid result with correct max allowed value if contains only cqf-calculatedValue`() =
+ runTest {
+ val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
Extension().apply {
- url = MAX_VALUE_EXTENSION_URL
- this.setValue(dateType)
+ this.url = MAX_VALUE_EXTENSION_URL
+ this.setValue(
+ DateType().apply {
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today() - 7 'days'"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ )
},
)
- },
- )
+ }
+ val answer =
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value = DateType().apply { value = Date() }
+ }
- assertThat((MaxValueValidator.getMaxValue(questionItem.first()) as? DateType)?.value)
- .isEqualTo(dateType.value)
- }
+ val validationResult =
+ MaxValueValidator.validate(questionnaireItem, answer, context) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
+
+ assertThat(validationResult.isValid).isFalse()
+ assertThat(validationResult.errorMessage)
+ .isEqualTo("Maximum value allowed is:${LocalDate.now().minusDays(7)}")
+ }
@Test
- fun `should return today's date when expression evaluates to today`() {
- val today = LocalDate.now().toString()
- val questionItem =
- listOf(
+ fun `should return invalid result with correct max allowed value if contains both value and cqf-calculatedValue`() =
+ runTest {
+ val tenDaysAgo = LocalDate.now().minusDays(10)
+
+ val questionnaireItem =
+ Questionnaire.QuestionnaireItemComponent().apply {
+ addExtension(
+ Extension().apply {
+ this.url = MAX_VALUE_EXTENSION_URL
+ this.setValue(
+ DateType().apply {
+ value =
+ Date.from(tenDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())
+ addExtension(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ expression = "today() - 7 'days'"
+ language = "text/fhirpath"
+ },
+ ),
+ )
+ },
+ )
+ },
+ )
+ }
+ val answer =
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value = DateType().apply { value = Date() }
+ }
+
+ val validationResult =
+ MaxValueValidator.validate(questionnaireItem, answer, context) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
+
+ assertThat(validationResult.isValid).isFalse()
+ assertThat(validationResult.errorMessage)
+ .isEqualTo("Maximum value allowed is:${LocalDate.now().minusDays(7)}")
+ }
+
+ @Test
+ fun `should return valid result and removes constraint for an answer value when maxValue cqf-calculatedValue evaluates to empty`() =
+ runTest {
+ val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
Extension().apply {
@@ -126,7 +197,7 @@ class MaxValueValidatorTest {
EXTENSION_CQF_CALCULATED_VALUE_URL,
Expression().apply {
language = "text/fhirpath"
- expression = "today()"
+ expression = "yesterday()" // invalid FHIRPath expression
},
),
)
@@ -134,18 +205,30 @@ class MaxValueValidatorTest {
)
},
)
- },
- )
+ }
- assertThat((MaxValueValidator.getMaxValue(questionItem.first()) as? DateType)?.valueAsString)
- .isEqualTo(today)
- }
+ val answer =
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value = DateType(Date())
+ }
+
+ val validationResult =
+ MaxValueValidator.validate(
+ questionnaireItem,
+ answer,
+ InstrumentationRegistry.getInstrumentation().context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
+
+ assertThat(validationResult.isValid).isTrue()
+ assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
+ }
@Test
- fun `should return future's date when expression evaluates`() {
- val fiveDaysAhead = LocalDate.now().plusDays(5).toString()
- val questionItem =
- listOf(
+ fun `should return valid result and removes constraint for an answer with an empty value`() =
+ runTest {
+ val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
Extension().apply {
@@ -158,7 +241,7 @@ class MaxValueValidatorTest {
EXTENSION_CQF_CALCULATED_VALUE_URL,
Expression().apply {
language = "text/fhirpath"
- expression = "today() + 5 'days' "
+ expression = "today()"
},
),
)
@@ -166,10 +249,23 @@ class MaxValueValidatorTest {
)
},
)
- },
- )
+ }
- assertThat((MaxValueValidator.getMaxValue(questionItem.first()) as? DateType)?.valueAsString)
- .isEqualTo(fiveDaysAhead)
- }
+ val answer =
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value = DateType()
+ }
+
+ val validationResult =
+ MaxValueValidator.validate(
+ questionnaireItem,
+ answer,
+ InstrumentationRegistry.getInstrumentation().context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
+
+ assertThat(validationResult.isValid).isTrue()
+ assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
+ }
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt
index 3c14cee33b..c41ffbe49e 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt
@@ -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.
@@ -22,6 +22,7 @@ import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import java.net.URI
import java.text.SimpleDateFormat
+import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
@@ -52,79 +53,79 @@ class MinLengthValidatorTest {
}
@Test
- fun boolean_answerUnderMinLength_shouldReturnInvalidResult() {
+ fun boolean_answerUnderMinLength_shouldReturnInvalidResult() = runTest {
checkAnswerUnderMinLength(minLength = 10, value = BooleanType(false))
}
@Test
- fun boolean_answerOverMinLength_shouldReturnValidResult() {
+ fun boolean_answerOverMinLength_shouldReturnValidResult() = runTest {
checkAnswerOverMinLength(minLength = 5, value = BooleanType(false))
}
@Test
- fun decimal_answerUnderMinLength_shouldReturnInvalidResult() {
+ fun decimal_answerUnderMinLength_shouldReturnInvalidResult() = runTest {
checkAnswerUnderMinLength(minLength = 15, value = DecimalType(3.1415926535))
}
@Test
- fun decimal_answerOverMinLength_shouldReturnValidResult() {
+ fun decimal_answerOverMinLength_shouldReturnValidResult() = runTest {
checkAnswerOverMinLength(minLength = 10, value = DecimalType(3.1415926535))
}
@Test
- fun int_answerUnderMinLength_shouldReturnInvalidResult() {
+ fun int_answerUnderMinLength_shouldReturnInvalidResult() = runTest {
checkAnswerUnderMinLength(minLength = 5, value = IntegerType(123))
}
@Test
- fun int_answerOverMinLength_shouldReturnValidResult() {
+ fun int_answerOverMinLength_shouldReturnValidResult() = runTest {
checkAnswerOverMinLength(minLength = 10, value = IntegerType(1234567890))
}
@Test
- fun dateType_answerUnderMinLength_shouldReturnInvalidResult() {
+ fun dateType_answerUnderMinLength_shouldReturnInvalidResult() = runTest {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
checkAnswerUnderMinLength(minLength = 11, value = DateType(dateFormat.parse("2021-06-01")))
}
@Test
- fun date_answerOverMinLength_shouldReturnValidResult() {
+ fun date_answerOverMinLength_shouldReturnValidResult() = runTest {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
checkAnswerOverMinLength(minLength = 10, value = DateType(dateFormat.parse("2021-06-01")))
}
@Test
- fun time_answerUnderMinLength_shouldReturnInvalidResult() {
+ fun time_answerUnderMinLength_shouldReturnInvalidResult() = runTest {
checkAnswerUnderMinLength(minLength = 10, value = TimeType("18:00:59"))
}
@Test
- fun time_answerOverMinLength_shouldReturnValidResult() {
+ fun time_answerOverMinLength_shouldReturnValidResult() = runTest {
checkAnswerOverMinLength(minLength = 5, value = TimeType("18:00:59"))
}
@Test
- fun string_answerUnderMinLength_shouldReturnInvalidResult() {
+ fun string_answerUnderMinLength_shouldReturnInvalidResult() = runTest {
checkAnswerUnderMinLength(minLength = 12, value = StringType("Hello World"))
}
@Test
- fun string_answerOverMinLength_shouldReturnValidResult() {
+ fun string_answerOverMinLength_shouldReturnValidResult() = runTest {
checkAnswerOverMinLength(minLength = 5, value = StringType("Hello World"))
}
@Test
- fun uri_answerUnderMinLength_shouldReturnInvalidResult() {
+ fun uri_answerUnderMinLength_shouldReturnInvalidResult() = runTest {
checkAnswerUnderMinLength(minLength = 21, value = UriType(URI.create("https://www.hl7.org/")))
}
@Test
- fun uri_answerOverMinLength_shouldReturnValidResult() {
+ fun uri_answerOverMinLength_shouldReturnValidResult() = runTest {
checkAnswerOverMinLength(minLength = 5, value = UriType(URI.create("https://www.hl7.org/")))
}
@Test
- fun nonPrimitiveUnderMinLength_shouldReturnValidResult() {
+ fun nonPrimitiveUnderMinLength_shouldReturnValidResult() = runTest {
val requirement =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
@@ -139,7 +140,10 @@ class MinLengthValidatorTest {
this.value = Quantity(1234567.89)
}
- val validationResult = MaxLengthValidator.validate(requirement, answer, context)
+ val validationResult =
+ MaxLengthValidator.validate(requirement, answer, context) {
+ TestExpressionValueEvaluator.evaluate(requirement, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
@@ -150,22 +154,34 @@ class MinLengthValidatorTest {
var context: Context = ApplicationProvider.getApplicationContext()
@JvmStatic
- fun checkAnswerOverMinLength(minLength: Int, value: PrimitiveType<*>) {
+ suspend fun checkAnswerOverMinLength(minLength: Int, value: PrimitiveType<*>) {
val testComponent = createMaxLengthQuestionnaireTestItem(minLength, value)
val validationResult =
- MinLengthValidator.validate(testComponent.requirement, testComponent.answer, context)
+ MinLengthValidator.validate(
+ testComponent.requirement,
+ testComponent.answer,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(testComponent.requirement, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
}
@JvmStatic
- fun checkAnswerUnderMinLength(minLength: Int, value: PrimitiveType<*>) {
+ suspend fun checkAnswerUnderMinLength(minLength: Int, value: PrimitiveType<*>) {
val testComponent = createMaxLengthQuestionnaireTestItem(minLength, value)
val validationResult =
- MinLengthValidator.validate(testComponent.requirement, testComponent.answer, context)
+ MinLengthValidator.validate(
+ testComponent.requirement,
+ testComponent.answer,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(testComponent.requirement, it)
+ }
assertThat(validationResult.isValid).isFalse()
assertThat(validationResult.errorMessage)
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt
index b12ecbf2af..8622e7289e 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt
@@ -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.
@@ -22,16 +22,16 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.fhir.datacapture.extensions.EXTENSION_CQF_CALCULATED_VALUE_URL
import com.google.common.truth.Truth.assertThat
-import java.text.SimpleDateFormat
import java.time.LocalDate
-import java.util.Calendar
+import java.time.ZoneId
import java.util.Date
-import org.hl7.fhir.r4.model.DateTimeType
+import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.DateType
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.Questionnaire
+import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent
import org.junit.Before
import org.junit.Test
@@ -51,7 +51,7 @@ class MinValueValidatorTest {
}
@Test
- fun `should return invalid result when entered value is less than minValue`() {
+ fun `should return invalid result when entered value is less than minValue`() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
@@ -68,14 +68,16 @@ class MinValueValidatorTest {
questionnaireItem,
answer,
InstrumentationRegistry.getInstrumentation().context,
- )
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult.isValid).isFalse()
assertThat(validationResult.errorMessage).isEqualTo("Minimum value allowed is:10")
}
@Test
- fun `should return valid result when entered value is greater than minValue`() {
+ fun `should return valid result when entered value is greater than minValue`() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
@@ -92,106 +94,109 @@ class MinValueValidatorTest {
questionnaireItem,
answer,
InstrumentationRegistry.getInstrumentation().context,
- )
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
}
@Test
- fun `should return invalid result when entered value is less than minValue for cqf calculated expression`() {
- val questionnaireItem =
- Questionnaire.QuestionnaireItemComponent().apply {
- addExtension(
- Extension().apply {
- url = MIN_VALUE_EXTENSION_URL
- this.setValue(
- DateType().apply {
- extension =
- listOf(
- Extension(
- EXTENSION_CQF_CALCULATED_VALUE_URL,
- Expression().apply {
- language = "text/fhirpath"
- expression = "today() - 1 'days'"
- },
- ),
- )
- },
- )
- },
- )
- }
- val answerDate =
- DateType(
- SimpleDateFormat("yyyy-MM-dd")
- .parse(
- (DateTimeType.today()
- .apply {
- add(Calendar.YEAR, -1)
- add(Calendar.DAY_OF_MONTH, -1)
- }
- .valueAsString),
- ),
- )
- val answer = QuestionnaireResponseItemAnswerComponent().apply { value = answerDate }
+ fun `should return valid result when entered value is greater than minValue for cqf calculated expression`() =
+ runTest {
+ val questionnaireItem =
+ Questionnaire.QuestionnaireItemComponent().apply {
+ addExtension(
+ Extension().apply {
+ url = MIN_VALUE_EXTENSION_URL
+ this.setValue(
+ DateType().apply {
+ extension =
+ listOf(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ language = "text/fhirpath"
+ expression = "today() - 1 'days'"
+ },
+ ),
+ )
+ },
+ )
+ },
+ )
+ }
- val validationResult =
- MinValueValidator.validate(
- questionnaireItem,
- answer,
- InstrumentationRegistry.getInstrumentation().context,
- )
- val expectedDateRange =
- (MinValueValidator.getMinValue(questionnaireItem) as? DateType)?.valueAsString
- assertThat(validationResult.isValid).isFalse()
- assertThat(validationResult.errorMessage)
- .isEqualTo("Minimum value allowed is:$expectedDateRange")
- }
+ val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType(Date()) }
+
+ val validationResult =
+ MinValueValidator.validate(
+ questionnaireItem,
+ answer,
+ InstrumentationRegistry.getInstrumentation().context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
+
+ assertThat(validationResult.isValid).isTrue()
+ assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
+ }
@Test
- fun `should return valid result when entered value is greater than minValue for cqf calculated expression`() {
- val questionnaireItem =
- Questionnaire.QuestionnaireItemComponent().apply {
- addExtension(
- Extension().apply {
- url = MIN_VALUE_EXTENSION_URL
- this.setValue(
- DateType().apply {
- extension =
- listOf(
+ fun `should return invalid result with correct min allowed value if contains both value and cqf-calculatedValue`() =
+ runTest {
+ val sevenDaysAgo = LocalDate.now().minusDays(7)
+
+ val questionnaireItem =
+ Questionnaire.QuestionnaireItemComponent().apply {
+ addExtension(
+ Extension().apply {
+ this.url = MIN_VALUE_EXTENSION_URL
+ this.setValue(
+ DateType().apply {
+ value =
+ Date.from(
+ sevenDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(),
+ )
+ addExtension(
Extension(
EXTENSION_CQF_CALCULATED_VALUE_URL,
Expression().apply {
+ expression = "today() - 3 'days'"
language = "text/fhirpath"
- expression = "today() - 1 'days'"
},
),
)
- },
- )
- },
- )
- }
-
- val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType(Date()) }
+ },
+ )
+ },
+ )
+ }
+ val answer =
+ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
+ value =
+ DateType().apply {
+ val fiveDaysAgo = LocalDate.now().minusDays(5)
+ value =
+ Date.from(fiveDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())
+ }
+ }
- val validationResult =
- MinValueValidator.validate(
- questionnaireItem,
- answer,
- InstrumentationRegistry.getInstrumentation().context,
- )
+ val validationResult =
+ MinValueValidator.validate(questionnaireItem, answer, context) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
- assertThat(validationResult.isValid).isTrue()
- assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
- }
+ assertThat(validationResult.isValid).isFalse()
+ assertThat(validationResult.errorMessage)
+ .isEqualTo("Minimum value allowed is:${LocalDate.now().minusDays(3)}")
+ }
@Test
- fun `should return today's date when expression evaluates to today`() {
- val today = LocalDate.now().toString()
- val questionItem =
- listOf(
+ fun `should return valid result and removes constraint for an answer value when minValue cqf-calculatedValue evaluates to empty`() =
+ runTest {
+ val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
Extension().apply {
@@ -204,7 +209,7 @@ class MinValueValidatorTest {
EXTENSION_CQF_CALCULATED_VALUE_URL,
Expression().apply {
language = "text/fhirpath"
- expression = "today()"
+ expression = "yesterday()" // invalid FHIRPath expression
},
),
)
@@ -212,28 +217,65 @@ class MinValueValidatorTest {
)
},
)
- },
- )
- assertThat((MinValueValidator.getMinValue(questionItem.first()) as? DateType)?.valueAsString)
- .isEqualTo(today)
- }
+ }
+
+ val twoDaysAgo =
+ Date.from(
+ LocalDate.now().minusDays(2).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(),
+ )
+ val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType(twoDaysAgo) }
+
+ val validationResult =
+ MinValueValidator.validate(
+ questionnaireItem,
+ answer,
+ InstrumentationRegistry.getInstrumentation().context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
+
+ assertThat(validationResult.isValid).isTrue()
+ assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
+ }
@Test
- fun `should return minValue date`() {
- val dateType = DateType(SimpleDateFormat("yyyy-MM-dd").parse("2021-06-01"))
- val questionItem =
- listOf(
+ fun `should return valid result and removes constraint for an answer with an empty value`() =
+ runTest {
+ val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
Extension().apply {
url = MIN_VALUE_EXTENSION_URL
- this.setValue(dateType)
+ this.setValue(
+ DateType().apply {
+ extension =
+ listOf(
+ Extension(
+ EXTENSION_CQF_CALCULATED_VALUE_URL,
+ Expression().apply {
+ language = "text/fhirpath"
+ expression = "today()"
+ },
+ ),
+ )
+ },
+ )
},
)
- },
- )
+ }
- assertThat((MinValueValidator.getMinValue(questionItem.first()) as? DateType)?.value)
- .isEqualTo(dateType.value)
- }
+ val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType() }
+
+ val validationResult =
+ MinValueValidator.validate(
+ questionnaireItem,
+ answer,
+ InstrumentationRegistry.getInstrumentation().context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
+
+ assertThat(validationResult.isValid).isTrue()
+ assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
+ }
}
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt
index dbfff75c12..a0219d3f27 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt
@@ -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.
@@ -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.Extension
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.Questionnaire
@@ -43,7 +44,7 @@ class QuestionnaireResponseItemValidatorTest {
}
@Test
- fun `should return valid result`() {
+ fun `should return valid result`() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
@@ -73,13 +74,19 @@ class QuestionnaireResponseItemValidatorTest {
)
val validationResult =
- QuestionnaireResponseItemValidator.validate(questionnaireItem, answers, context)
+ QuestionnaireResponseItemValidator.validate(
+ questionnaireItem,
+ answers,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult).isEqualTo(Valid)
}
@Test
- fun `should validate individual answers and combine results`() {
+ fun `should validate individual answers and combine results`() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "a-question"
@@ -111,7 +118,13 @@ class QuestionnaireResponseItemValidatorTest {
)
val validationResult =
- QuestionnaireResponseItemValidator.validate(questionnaireItem, answers, context)
+ QuestionnaireResponseItemValidator.validate(
+ questionnaireItem,
+ answers,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult).isInstanceOf(Invalid::class.java)
val invalidValidationResult = validationResult as Invalid
@@ -120,7 +133,7 @@ class QuestionnaireResponseItemValidatorTest {
}
@Test
- fun `should validate all answers`() {
+ fun `should validate all answers`() = runTest {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.INTEGER
@@ -129,7 +142,13 @@ class QuestionnaireResponseItemValidatorTest {
val answers = listOf()
val validationResult =
- QuestionnaireResponseItemValidator.validate(questionnaireItem, answers, context)
+ QuestionnaireResponseItemValidator.validate(
+ questionnaireItem,
+ answers,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(questionnaireItem, it)
+ }
assertThat(validationResult).isInstanceOf(Invalid::class.java)
val invalidValidationResult = validationResult as Invalid
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt
index feaa9f63ca..6baf6f5b98 100644
--- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt
@@ -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.
@@ -22,6 +22,7 @@ import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import java.net.URI
import java.text.SimpleDateFormat
+import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
@@ -52,37 +53,37 @@ class RegexValidatorTest {
}
@Test
- fun boolean_notMatchingRegex_shouldReturnInvalidResult() {
+ fun boolean_notMatchingRegex_shouldReturnInvalidResult() = runTest {
checkAnswerNotMatchingRegex(regex = "true", value = BooleanType(false))
}
@Test
- fun boolean_matchingRegex_shouldReturnValidResult() {
+ fun boolean_matchingRegex_shouldReturnValidResult() = runTest {
checkAnswerMatchingRegex(regex = "true", value = BooleanType(true))
}
@Test
- fun decimal_notMatchingRegex_shouldReturnInvalidResult() {
+ fun decimal_notMatchingRegex_shouldReturnInvalidResult() = runTest {
checkAnswerNotMatchingRegex(regex = "[0-9]+\\.[0-9]+", value = DecimalType(31234))
}
@Test
- fun decimal_matchingRegex_shouldReturnValidResult() {
+ fun decimal_matchingRegex_shouldReturnValidResult() = runTest {
checkAnswerMatchingRegex(regex = "[0-9]+\\.[0-9]+", value = DecimalType(3.1415926535))
}
@Test
- fun int_notMatchingRegex_shouldReturnInvalidResult() {
+ fun int_notMatchingRegex_shouldReturnInvalidResult() = runTest {
checkAnswerNotMatchingRegex(regex = "[0-9]+", value = IntegerType(-1))
}
@Test
- fun int_matchingRegex_shouldReturnValidResult() {
+ fun int_matchingRegex_shouldReturnValidResult() = runTest {
checkAnswerMatchingRegex(regex = "[0-9]+", value = IntegerType(1234567890))
}
@Test
- fun dateType_notMatchingRegex_shouldReturnInvalidResult() {
+ fun dateType_notMatchingRegex_shouldReturnInvalidResult() = runTest {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
checkAnswerNotMatchingRegex(
regex = "[0-9]{2}-[0-9]{2}-[0-9]{2}",
@@ -91,7 +92,7 @@ class RegexValidatorTest {
}
@Test
- fun date_matchingRegex_shouldReturnValidResult() {
+ fun date_matchingRegex_shouldReturnValidResult() = runTest {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
checkAnswerMatchingRegex(
regex = "[0-9]{4}-[0-9]{2}-[0-9]{2}",
@@ -100,17 +101,17 @@ class RegexValidatorTest {
}
@Test
- fun time_matchingRegex_shouldReturnInvalidResult() {
+ fun time_matchingRegex_shouldReturnInvalidResult() = runTest {
checkAnswerNotMatchingRegex(regex = "[0-9]{2}:[0-9]{2}", value = TimeType("18:00:59"))
}
@Test
- fun time_notMatchingRegex_shouldReturnValidResult() {
+ fun time_notMatchingRegex_shouldReturnValidResult() = runTest {
checkAnswerMatchingRegex(regex = "[0-9]{2}:[0-9]{2}:[0-9]{2}", value = TimeType("18:00:59"))
}
@Test
- fun string_notMatchingRegex_shouldReturnInvalidResult() {
+ fun string_notMatchingRegex_shouldReturnInvalidResult() = runTest {
checkAnswerNotMatchingRegex(
regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]",
value = StringType("www.hl7.org"),
@@ -118,7 +119,7 @@ class RegexValidatorTest {
}
@Test
- fun string_matchingRegex_shouldReturnValidResult() {
+ fun string_matchingRegex_shouldReturnValidResult() = runTest {
checkAnswerMatchingRegex(
regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]",
value = StringType("https://www.hl7.org/"),
@@ -126,7 +127,7 @@ class RegexValidatorTest {
}
@Test
- fun uri_notMatchingRegex_shouldReturnInvalidResult() {
+ fun uri_notMatchingRegex_shouldReturnInvalidResult() = runTest {
checkAnswerNotMatchingRegex(
regex = "[a-z]+",
value = UriType(URI.create("https://www.hl7.org/")),
@@ -134,7 +135,7 @@ class RegexValidatorTest {
}
@Test
- fun uri_matchingRegex_shouldReturnValidResult() {
+ fun uri_matchingRegex_shouldReturnValidResult() = runTest {
checkAnswerMatchingRegex(
regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]",
value = UriType(URI.create("https://www.hl7.org/")),
@@ -142,12 +143,12 @@ class RegexValidatorTest {
}
@Test
- fun invalidRegex_shouldReturnValidResult() {
+ fun invalidRegex_shouldReturnValidResult() = runTest {
checkAnswerMatchingRegex("[.*", StringType("http://www.google.com"))
}
@Test
- fun nonPrimitive_notMatchingRegex_shouldReturnValidResult() {
+ fun nonPrimitive_notMatchingRegex_shouldReturnValidResult() = runTest {
val requirement =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
@@ -160,7 +161,10 @@ class RegexValidatorTest {
val response =
QuestionnaireResponseItemAnswerComponent().apply { this.value = Quantity(1234567.89) }
- val validationResult = RegexValidator.validate(requirement, response, context)
+ val validationResult =
+ RegexValidator.validate(requirement, response, context) {
+ TestExpressionValueEvaluator.evaluate(requirement, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
@@ -171,22 +175,34 @@ class RegexValidatorTest {
var context: Context = ApplicationProvider.getApplicationContext()
@JvmStatic
- fun checkAnswerMatchingRegex(regex: String, value: PrimitiveType<*>) {
+ suspend fun checkAnswerMatchingRegex(regex: String, value: PrimitiveType<*>) {
val testComponent = createRegexQuestionnaireTestItem(regex, value)
val validationResult =
- RegexValidator.validate(testComponent.requirement, testComponent.answer, context)
+ RegexValidator.validate(
+ testComponent.requirement,
+ testComponent.answer,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(testComponent.requirement, it)
+ }
assertThat(validationResult.isValid).isTrue()
assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue()
}
@JvmStatic
- fun checkAnswerNotMatchingRegex(regex: String, value: PrimitiveType<*>) {
+ suspend fun checkAnswerNotMatchingRegex(regex: String, value: PrimitiveType<*>) {
val testComponent = createRegexQuestionnaireTestItem(regex, value)
val validationResult =
- RegexValidator.validate(testComponent.requirement, testComponent.answer, context)
+ RegexValidator.validate(
+ testComponent.requirement,
+ testComponent.answer,
+ context,
+ ) {
+ TestExpressionValueEvaluator.evaluate(testComponent.requirement, it)
+ }
assertThat(validationResult.isValid).isFalse()
assertThat(validationResult.errorMessage)
diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt
new file mode 100644
index 0000000000..5cb104fb75
--- /dev/null
+++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 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.
+ * 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.validation
+
+import com.google.android.fhir.datacapture.fhirpath.evaluateToBase
+import org.hl7.fhir.r4.model.Base
+import org.hl7.fhir.r4.model.Expression
+import org.hl7.fhir.r4.model.Type
+
+object TestExpressionValueEvaluator {
+ /**
+ * Doesn't handle expressions containing FHIRPath supplements
+ * https://build.fhir.org/ig/HL7/sdc/expressions.html#fhirpath-supplements
+ */
+ fun evaluate(base: Base, expression: Expression): Type? =
+ try {
+ evaluateToBase(base, expression.expression).singleOrNull() as? Type
+ } catch (_: Exception) {
+ null
+ }
+}