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

X-FHIR-Query support for variable extension #2076

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1960713
Add X-FHIR-Query support for variable extension
FikriMilano Jul 17, 2023
b7cd327
Add unit tests
FikriMilano Jul 17, 2023
2c6edf4
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Jul 17, 2023
a132746
Fix dynamic answerExpression based on #2066
FikriMilano Jul 18, 2023
6c6ea81
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Jul 18, 2023
628ebe7
Address review
FikriMilano Jul 19, 2023
648ef72
Makes sure initial value got emitted to the questionnaireStateFlow
FikriMilano Jul 20, 2023
9d33869
Notify to not delay the update of UI
FikriMilano Aug 8, 2023
e10c8c8
WIP
FikriMilano Aug 14, 2023
3b2448e
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Aug 24, 2023
adb82de
Adjust code after major merge conflict
FikriMilano Aug 24, 2023
919e7c1
spotlessApply
FikriMilano Aug 24, 2023
bef102a
Fix test
FikriMilano Aug 24, 2023
62b4653
Fix test
FikriMilano Aug 24, 2023
aeb3186
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Sep 15, 2023
51635e2
Integrate suspend functions
FikriMilano Sep 15, 2023
c85a83a
spotlessApply
FikriMilano Sep 15, 2023
7f86476
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Sep 15, 2023
0432ffa
Add missing xFhirQueryResolver param
FikriMilano Sep 16, 2023
c3b01f6
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Sep 16, 2023
02cab42
Address review
FikriMilano Oct 12, 2023
ad986f8
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Oct 15, 2023
3b62f41
Address review and refactor to use suspend function
FikriMilano Oct 16, 2023
784e2bf
Run suspend for Barcode
FikriMilano Oct 16, 2023
2525cd1
Address review
FikriMilano Nov 23, 2023
a277d01
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Nov 23, 2023
d6939d6
Remove hasMissingParamValue
FikriMilano Dec 3, 2023
c6e3cb6
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Dec 3, 2023
830f7d5
Fix test
FikriMilano Dec 3, 2023
425add0
Update QuestionnaireViewItemTest
FikriMilano Feb 25, 2024
a49f27e
Use runTest in QuestionnaireUiEspressoTest
FikriMilano Feb 25, 2024
6e506fb
Merge branch 'master' of github.com:google/android-fhir into 2075-x-f…
FikriMilano Feb 25, 2024
7b0bda3
Add missing import to access child of ChipGroup
FikriMilano Feb 25, 2024
d611c93
spotlessApply
FikriMilano Feb 25, 2024
5d49131
Fix test
FikriMilano Feb 26, 2024
acfc230
Add missing coroutine launch
FikriMilano Feb 26, 2024
fe8785e
spotlessApply
FikriMilano Feb 26, 2024
4cb2321
Merge branch 'master' into 2075-x-fhir-query-support-for-variable
FikriMilano Feb 27, 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
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,14 @@ import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.QuestionTextConfiguration
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Element
Expand Down Expand Up @@ -183,6 +185,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}
}

private val questionnaireVariablesMap: MutableMap<String, Base?> = mutableMapOf()

/** The map from each item in the [Questionnaire] to its parent. */
private var questionnaireItemParentMap:
Map<QuestionnaireItemComponent, QuestionnaireItemComponent>
Expand Down Expand Up @@ -465,32 +469,37 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
updatedQuestionnaireItem: QuestionnaireItemComponent,
updatedQuestionnaireResponseItem: QuestionnaireResponseItemComponent?,
) {
evaluateCalculatedExpressions(
updatedQuestionnaireItem,
updatedQuestionnaireResponseItem,
questionnaire,
questionnaireResponse,
questionnaireItemParentMap
)
.forEach { (questionnaireItem, calculatedAnswers) ->
// update all response item with updated values
questionnaireResponse.allItems
// Item answer should not be modified and touched by user;
// https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html
.filter {
it.linkId == questionnaireItem.linkId &&
!modifiedQuestionnaireResponseItemSet.contains(it)
}
.forEach { questionnaireResponseItem ->
// update and notify only if new answer has changed to prevent any event loop
if (questionnaireResponseItem.answer.hasDifferentAnswerSet(calculatedAnswers)) {
questionnaireResponseItem.answer =
calculatedAnswers.map {
QuestionnaireResponseItemAnswerComponent().apply { value = it }
}
runBlocking(Dispatchers.IO) {
evaluateCalculatedExpressions(
updatedQuestionnaireItem,
updatedQuestionnaireResponseItem,
questionnaire,
questionnaireResponse,
questionnaireItemParentMap,
questionnaireVariablesMap,
questionnaireLaunchContextMap,
xFhirQueryResolver
)
.forEach { (questionnaireItem, calculatedAnswers) ->
// update all response item with updated values
questionnaireResponse.allItems
// Item answer should not be modified and touched by user;
// https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-calculatedExpression.html
.filter {
it.linkId == questionnaireItem.linkId &&
!modifiedQuestionnaireResponseItemSet.contains(it)
}
}
}
.forEach { questionnaireResponseItem ->
// update and notify only if new answer has changed to prevent any event loop
if (questionnaireResponseItem.answer.hasDifferentAnswerSet(calculatedAnswers)) {
questionnaireResponseItem.answer =
calculatedAnswers.map {
QuestionnaireResponseItemAnswerComponent().apply { value = it }
}
}
}
}
}
}

internal suspend fun resolveAnswerValueSet(
Expand Down Expand Up @@ -540,19 +549,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
): List<Questionnaire.QuestionnaireItemAnswerOptionComponent> {
// Check cache first for database queries
val answerExpression = item.answerExpression ?: return emptyList()
if (answerExpression.isXFhirQuery && answerExpressionMap.contains(answerExpression.expression)
) {
return answerExpressionMap[answerExpression.expression]!!
}

val options = loadAnswerExpressionOptions(item, answerExpression)

if (answerExpression.isXFhirQuery) answerExpressionMap[answerExpression.expression] = options

return options
return loadAnswerExpressionOptions(item, answerExpression)
}

private fun resolveCqfExpression(
private suspend fun resolveCqfExpression(
questionnaireItem: QuestionnaireItemComponent,
questionnaireResponseItem: QuestionnaireResponseItemComponent,
element: Element,
Expand All @@ -568,25 +569,47 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
questionnaireItem,
questionnaireResponseItem,
cqfExpression,
questionnaireItemParentMap
questionnaireItemParentMap,
questionnaireVariablesMap,
questionnaireLaunchContextMap,
xFhirQueryResolver
)
}

private suspend fun loadAnswerExpressionOptions(
item: QuestionnaireItemComponent,
expression: Expression,
): List<Questionnaire.QuestionnaireItemAnswerOptionComponent> {
var xFhirExpressionString = ""
val data =
if (expression.isXFhirQuery) {
checkNotNull(xFhirQueryResolver) {
"XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig."
}

val xFhirExpressionString =
val copyVariablesMap =
mutableMapOf<String, Base?>().apply { putAll(questionnaireVariablesMap) }
ExpressionEvaluator.extractDependentVariables(
expression,
questionnaire,
questionnaireResponse,
questionnaireItemParentMap,
item,
copyVariablesMap,
questionnaireLaunchContextMap,
xFhirQueryResolver
)

xFhirExpressionString =
ExpressionEvaluator.createXFhirQueryFromExpression(
expression,
questionnaireLaunchContextMap
questionnaireLaunchContextMap,
copyVariablesMap
)

if (answerExpressionMap.contains(xFhirExpressionString)) {
return answerExpressionMap[xFhirExpressionString]!!
}
xFhirQueryResolver!!.resolve(xFhirExpressionString)
} else if (expression.isFhirPath) {
fhirPathEngine.evaluate(questionnaireResponse, expression.expression)
Expand All @@ -595,8 +618,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
"${expression.language} not supported for answer-expression yet"
)
}

return item.extractAnswerOptions(data)
val options = item.extractAnswerOptions(data)
if (expression.isXFhirQuery) answerExpressionMap[xFhirExpressionString] = options
return options
}

/**
Expand Down Expand Up @@ -717,11 +741,13 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
NotValidated
}

// Set question text dynamically from CQL expression
questionnaireResponseItem.apply {
resolveCqfExpression(questionnaireItem, this, questionnaireItem.textElement)
.firstOrNull()
?.let { text = it.primitiveValue() }
runBlocking(Dispatchers.IO) {
// Set question text dynamically from CQL expression
questionnaireResponseItem.apply {
resolveCqfExpression(questionnaireItem, this, questionnaireItem.textElement)
.firstOrNull()
?.let { text = it.primitiveValue() }
}
}

val items = buildList {
Expand Down
Loading