Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Constraint support (local and error case only) #2199

Merged
merged 20 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2636e77
Create a base interface for validator
FikriMilano Mar 15, 2024
83ab824
Extend base interface to other validator
FikriMilano Mar 15, 2024
c4273ac
Make validate suspend for QuestionnaireResponseItemConstraintValidator
FikriMilano Mar 15, 2024
48ebf1f
Add constraint-extension urls and severity types
FikriMilano Mar 15, 2024
8a35e2f
Create ConstraintExtensionValidator
FikriMilano Mar 15, 2024
4bf0df9
Put QR and expressionEvaluator in the validator constructor
FikriMilano Mar 15, 2024
886ec24
Change parameter from answers to responseItem
FikriMilano Mar 15, 2024
aa4fc8c
Move the high order func for cqfCalculated value to the validator itself
FikriMilano Mar 15, 2024
2e54b13
Add constraint sample to catalog
FikriMilano Mar 15, 2024
6f7694b
Update tests
FikriMilano Mar 15, 2024
bef8419
Merge branch 'master' into 2003-questionnaire-constraint
FikriMilano Mar 19, 2024
e0bbe87
Merge branch 'master' into 2003-questionnaire-constraint
FikriMilano Mar 22, 2024
95241b0
Merge branch 'master' into 2003-questionnaire-constraint
FikriMilano Mar 25, 2024
126535f
Update TODO comment
FikriMilano Mar 26, 2024
46083ad
Use FHIRPathUtil convertToBoolean
FikriMilano Mar 26, 2024
136b6d7
spotlessApply
FikriMilano Mar 26, 2024
d2678c7
Rename ConstraintExtensionValidator to ConstraintItemExtensionValidator
FikriMilano Mar 26, 2024
a1cc5d0
Fix test
FikriMilano Mar 26, 2024
9f83835
Merge branch 'master' of github.com:google/android-fhir into 2003-que…
FikriMilano Mar 26, 2024
36ac091
Merge branch '2003-questionnaire-constraint' of github.com:opensrp/an…
FikriMilano Mar 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions catalog/src/main/assets/behavior_questionnaire_constraint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"text": "Password",
"type": "string",
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "1.1",
"text": "Fill the password first",
"type": "display"
}
]
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-constraint",
"extension": [
{
"url": "key",
"valueId": "constraint-1"
},
{
"url": "requirements",
"valueString": "Confirm password field must have the same value as password field"
},
{
"url": "severity",
"valueCode": "error"
},
{
"url": "expression",
"valueString": "%context.answer.value = %resource.descendants().where(linkId='1').answer.value"
},
{
"url": "human",
"valueString": "Password does not match"
},
{
"url": "location",
"valueString": "1"
}
]
}
],
"linkId": "2",
"text": "Confirm password",
"type": "string",
"item": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "2.1",
"text": "Show error message if confirm password does not match with password",
"type": "display"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ class BehaviorListViewModel(application: Application) : AndroidViewModel(applica
R.string.behavior_name_dynamic_question_text,
"behavior_dynamic_question_text.json",
),
QUESTIONNAIRE_CONSTRAINT(
R.drawable.ic_rule,
R.string.behavior_name_questionnaire_constraint,
"behavior_questionnaire_constraint.json",
),
}

fun isBehavior(context: Context, title: String) =
Expand Down
12 changes: 12 additions & 0 deletions catalog/src/main/res/drawable/ic_rule.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
>
<path
android:fillColor="#1A73E8"
android:pathData="M576,800L534,758L645,647L534,536L576,494L687,605L798,494L840,536L729,647L840,758L798,800L687,689L576,800ZM659,426L517,284L559,242L659,341L838,162L880,205L659,426ZM80,670L80,610L440,610L440,670L80,670ZM80,350L80,290L440,290L440,350L80,350Z"
/>
</vector>
1 change: 1 addition & 0 deletions catalog/src/main/res/layout/behavior_list_fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/component_horizontal_margin"
android:layout_marginBottom="@dimen/bottom_navigation_view_height"
/>

</FrameLayout>
3 changes: 3 additions & 0 deletions catalog/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
<string
name="behavior_name_dynamic_question_text"
>Dynamic question text</string>
<string
name="behavior_name_questionnaire_constraint"
>Questionnaire constraint</string>
<string name="component_name_initial_value">Initial Value</string>
<string
name="behavior_name_calculated_expression_info"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
externalValueSetResolver,
)

private val questionnaireResponseItemValidator: QuestionnaireResponseItemValidator =
QuestionnaireResponseItemValidator(expressionEvaluator)

/**
* Adds empty [QuestionnaireResponseItemComponent]s to `responseItems` so that each
* [QuestionnaireItemComponent] in `questionnaireItems` has at least one corresponding
Expand Down Expand Up @@ -729,17 +732,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
forceValidation ||
isInReviewModeFlow.value
) {
QuestionnaireResponseItemValidator.validate(
questionnaireResponseItemValidator.validate(
questionnaireItem,
questionnaireResponseItem.answer,
questionnaireResponseItem,
[email protected](),
) {
expressionEvaluator.evaluateExpressionValue(
questionnaireItem,
questionnaireResponseItem,
it,
)
}
)
} else {
NotValidated
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.IntegerType
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.Reference
import org.hl7.fhir.r4.model.Resource
Expand Down Expand Up @@ -113,6 +114,27 @@ internal const val EXTENSION_MAX_SIZE = "http://hl7.org/fhir/StructureDefinition

internal const val EXTENSION_MIME_TYPE = "http://hl7.org/fhir/StructureDefinition/mimeType"

/**
* Extension for questionnaire and its items, representing a rule that must be satisfied before
* [QuestionnaireResponse] can be considered valid.
*
* See https://hl7.org/fhir/extensions/StructureDefinition-questionnaire-constraint.html.
*/
internal const val EXTENSION_QUESTIONNAIRE_CONSTRAINT_URL =
FikriMilano marked this conversation as resolved.
Show resolved Hide resolved
"http://hl7.org/fhir/StructureDefinition/questionnaire-constraint"

internal const val EXTENSION_QUESTIONNAIRE_CONSTRAINT_KEY = "key"

internal const val EXTENSION_QUESTIONNAIRE_CONSTRAINT_REQUIREMENTS = "requirements"

internal const val EXTENSION_QUESTIONNAIRE_CONSTRAINT_SEVERITY = "severity"

internal const val EXTENSION_QUESTIONNAIRE_CONSTRAINT_EXPRESSION = "expression"

internal const val EXTENSION_QUESTIONNAIRE_CONSTRAINT_HUMAN = "human"

internal const val EXTENSION_QUESTIONNAIRE_CONSTRAINT_LOCATION = "location"

/**
* Extension for questionnaire items of integer and decimal types including a single unit to be
* displayed.
Expand Down Expand Up @@ -775,6 +797,14 @@ internal fun Questionnaire.QuestionnaireItemComponent.extractAnswerOptions(
}.map { Questionnaire.QuestionnaireItemAnswerOptionComponent(it) }
}

/** See http://hl7.org/fhir/constraint-severity */
enum class ConstraintSeverityTypes(
val code: String,
) {
ERROR("error"),
WARNING("warning"),
}

// ********************************************************************************************** //
// //
// Utilities: zip with questionnaire response item list, nested items, create response items, //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Type

/**
* Validates [QuestionnaireResponse.QuestionnaireResponseItemComponent] against a particular
* Validates [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent] against a particular
* constraint.
*/
internal interface AnswerConstraintValidator {
internal interface AnswerConstraintValidator : ConstraintValidator {
/**
* Validates whether the [answer] satisfies any constraints of the [questionnaireItem] according
* to the [Structured Data Capture Implementation Guide]
Expand All @@ -42,11 +42,5 @@ internal interface AnswerConstraintValidator {
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
context: Context,
expressionEvaluator: suspend (Expression) -> Type?,
): Result

/**
* The validation result containing whether the answer is valid and any error message if it is not
* valid.
*/
data class Result(val isValid: Boolean, val errorMessage: String?)
): ConstraintValidator.Result
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal open class AnswerExtensionConstraintValidator(
answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent,
context: Context,
expressionEvaluator: suspend (Expression) -> Type?,
): AnswerConstraintValidator.Result {
): ConstraintValidator.Result {
if (questionnaireItem.hasExtension(url)) {
val extension = questionnaireItem.getExtensionByUrl(url)
val extensionValue =
Expand All @@ -61,12 +61,12 @@ internal open class AnswerExtensionConstraintValidator(
if (
extensionValue.hasValue() && answer.value.hasValue() && predicate(extensionValue, answer)
) {
return AnswerConstraintValidator.Result(
return ConstraintValidator.Result(
false,
messageGenerator(extensionValue, context),
)
}
}
return AnswerConstraintValidator.Result(true, null)
return ConstraintValidator.Result(true, null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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.
* 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 android.content.Context
import com.google.android.fhir.datacapture.extensions.ConstraintSeverityTypes
import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_EXPRESSION
import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_HUMAN
import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_SEVERITY
import com.google.android.fhir.datacapture.extensions.EXTENSION_QUESTIONNAIRE_CONSTRAINT_URL
import com.google.android.fhir.datacapture.extensions.asStringValue
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import com.google.android.fhir.datacapture.fhirpath.convertToBoolean
import org.hl7.fhir.r4.model.CodeType
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent

/**
* TODO: Add constraint support for global case, create a separate validator,
* https://github.com/google/android-fhir/issues/2479
*/
internal class ConstraintItemExtensionValidator(
private val expressionEvaluator: ExpressionEvaluator,
) : QuestionnaireResponseItemConstraintValidator {
override suspend fun validate(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
context: Context,
): List<ConstraintValidator.Result> {
return questionnaireItem.extension
.filter { extension ->
/**
* TODO: Add constraint support for warning case, update the [ConstraintValidator.Result]
* data class to also include warning state,
* https://github.com/google/android-fhir/issues/2480
*/
extension.url == EXTENSION_QUESTIONNAIRE_CONSTRAINT_URL &&
(extension.getExtensionByUrl(EXTENSION_QUESTIONNAIRE_CONSTRAINT_SEVERITY).value
as CodeType)
.valueAsString == ConstraintSeverityTypes.ERROR.code
}
.map { extension ->
val expression =
Expression().apply {
language = "text/fhirpath"
expression =
extension
.getExtensionByUrl(EXTENSION_QUESTIONNAIRE_CONSTRAINT_EXPRESSION)
.value
.asStringValue()
}
val isValid =
expressionEvaluator
.evaluateExpression(
questionnaireItem,
questionnaireResponseItem,
expression,
)
.let { convertToBoolean(it) }
if (isValid) {
ConstraintValidator.Result(true, null)
} else {
val errorMessage =
extension
.getExtensionByUrl(EXTENSION_QUESTIONNAIRE_CONSTRAINT_HUMAN)
.value
.asStringValue()
ConstraintValidator.Result(false, errorMessage)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.
* 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

/** Validator base interface. */
internal interface ConstraintValidator {

/**
* The validation result containing whether the response item is valid and any error message if it
* is not valid.
*/
data class Result(val isValid: Boolean, val errorMessage: String?)
}
Loading
Loading