Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions datacapture/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ dependencies {

coreLibraryDesugaring(Dependencies.desugarJdkLibs)

debugImplementation(project(":engine"))

implementation(Dependencies.androidFhirCommon)
implementation(Dependencies.Androidx.appCompat)
implementation(Dependencies.Androidx.constraintLayout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.app.Application
import com.google.android.fhir.datacapture.DataCaptureConfig.Provider
import org.hl7.fhir.r4.context.SimpleWorkerContext
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.StructureMap
import org.hl7.fhir.utilities.npm.NpmPackage

Expand All @@ -45,7 +46,10 @@ data class DataCaptureConfig(
* should try to include the smallest [NpmPackage] possible that contains only the resources
* needed by [StructureMap]s used by the client app.
*/
var npmPackage: NpmPackage? = null
var npmPackage: NpmPackage? = null,

/** A [XFhirQueryResolver] may be set by the client to resolve xFhir queries for the library.*/
var xFhirQueryResolver: XFhirQueryResolver? = null
) {

internal val simpleWorkerContext: SimpleWorkerContext by lazy {
Expand Down Expand Up @@ -75,3 +79,14 @@ data class DataCaptureConfig(
interface ExternalAnswerValueSetResolver {
suspend fun resolve(uri: String): List<Coding>
}

/**
* Resolves resources based on the provided xFhir query. This allows the library to resolve
* x-fhir-query answer expressions.
*
* NOTE: The result of the resolution may be cached to improve performance. In other words, the
* resolver may be called only once after which the Resources may be used multiple times in the UI.
*/
fun interface XFhirQueryResolver {
suspend fun resolve(xFhirQuery: String): List<Resource>
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import androidx.core.text.HtmlCompat
import com.google.android.fhir.datacapture.common.datatype.asStringValue
import com.google.android.fhir.datacapture.utilities.evaluateToDisplay
import com.google.android.fhir.getLocalizedText
import com.google.android.fhir.logicalId
import org.hl7.fhir.r4.model.Base
import org.hl7.fhir.r4.model.BooleanType
import org.hl7.fhir.r4.model.CodeType
Expand Down Expand Up @@ -545,3 +544,8 @@ fun List<Questionnaire.QuestionnaireItemComponent>.flattened():
*/
fun Questionnaire.QuestionnaireItemComponent.getNestedQuestionnaireResponseItems() =
item.map { it.createQuestionnaireResponseItem() }

val Resource.logicalId: String
get() {
return this.idElement?.idPart.orEmpty()
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import androidx.lifecycle.viewModelScope
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions
Expand All @@ -38,7 +37,6 @@ import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValid
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.validation.ValidationResult
import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem
import com.google.android.fhir.search.search
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -57,7 +55,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
AndroidViewModel(application) {

private val parser: IParser by lazy { FhirContext.forCached(FhirVersionEnum.R4).newJsonParser() }
private val fhirEngine by lazy { FhirEngineProvider.getInstance(application) }
private val xFhirQueryResolver: XFhirQueryResolver? by lazy {
DataCapture.getConfiguration(application).xFhirQueryResolver
}

/** The current questionnaire as questions are being answered. */
internal val questionnaire: Questionnaire
Expand Down Expand Up @@ -499,13 +499,18 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
expression: Expression,
): List<Questionnaire.QuestionnaireItemAnswerOptionComponent> {
val data =
if (expression.isXFhirQuery) fhirEngine.search(expression.expression)
else if (expression.isFhirPath)
if (expression.isXFhirQuery) {
checkNotNull(xFhirQueryResolver) {
"XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig"
}
xFhirQueryResolver!!.resolve(expression.expression)
} else if (expression.isFhirPath) {
fhirPathEngine.evaluate(questionnaireResponse, expression.expression)
else
} else {
throw UnsupportedOperationException(
"${expression.language} not supported for answer-expression yet"
)
}

return item.extractAnswerOptions(data)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2894,13 +2894,13 @@ class QuestionnaireViewModelTest {
fun `resolveAnswerExpression() should return questionnaire item answer options for answer expression and choice column`() =
runBlocking {
val practitioner =
Practitioner()
.apply {
id = UUID.randomUUID().toString()
active = true
addName(HumanName().apply { this.family = "John" })
}
.also { fhirEngine.create(it) }
Practitioner().apply {
id = UUID.randomUUID().toString()
active = true
addName(HumanName().apply { this.family = "John" })
}
ApplicationProvider.getApplicationContext<DataCaptureTestApplication>()
.dataCaptureConfiguration = DataCaptureConfig(xFhirQueryResolver = { listOf(practitioner) })

val questionnaire =
Questionnaire().apply {
Expand Down Expand Up @@ -2939,6 +2939,47 @@ class QuestionnaireViewModelTest {
.isEqualTo("Practitioner/${practitioner.logicalId}")
}

@Test
fun `resolveAnswerExpression() should throw exception when XFhirQueryResolver is not provided`() {
val questionnaire =
Questionnaire().apply {
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "a"
text = "answer expression question text"
type = Questionnaire.QuestionnaireItemType.REFERENCE
extension =
listOf(
Extension(
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression",
Expression().apply {
this.expression = "Practitioner?active=true"
this.language = Expression.ExpressionLanguage.APPLICATION_XFHIRQUERY.toCode()
}
),
Extension(
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-choiceColumn"
)
.apply {
this.addExtension(Extension("path", StringType("id")))
this.addExtension(Extension("label", StringType("name")))
this.addExtension(Extension("forDisplay", BooleanType(true)))
}
)
}
)
}
state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire))
val viewModel = QuestionnaireViewModel(context, state)
val exception =
Assert.assertThrows(null, IllegalStateException::class.java) {
runBlocking { viewModel.resolveAnswerExpression(questionnaire.itemFirstRep) }
}
assertThat(exception.message)
.isEqualTo(
"XFhirQueryResolver cannot be null. Please provide the XFhirQueryResolver via DataCaptureConfig"
)
}
// Test cases for submit button

@Test
Expand Down