Skip to content

Commit

Permalink
Allow multiple resources to be set in context (#2000)
Browse files Browse the repository at this point in the history
* allow multiple resources to be set in context

* rename builder api

* address comments

* restore pairing

* simplify logic

* add test case with multiple and address comments
  • Loading branch information
omarismail94 authored May 26, 2023
1 parent 3d6c259 commit b129c66
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,16 @@ class QuestionnaireFragment : Fragment() {
args.add(EXTRA_QUESTIONNAIRE_RESPONSE_JSON_URI to questionnaireResponseUri)
}

fun setQuestionnaireLaunchContext(questionnaireLaunchContext: String) = apply {
args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING to questionnaireLaunchContext)
/**
* The launch context allows information to be passed into questionnaire based on the context in
* which the questionnaire is being evaluated. For example, what patient, what encounter, what
* user, etc. is "in context" at the time the questionnaire response is being completed:
* https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html
*
* @param launchContexts list of serialized resources
*/
fun setQuestionnaireLaunchContexts(launchContexts: List<String>) = apply {
args.add(EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS to launchContexts)
}

/**
Expand Down Expand Up @@ -398,9 +406,9 @@ class QuestionnaireFragment : Fragment() {
*/
internal const val EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING = "questionnaire-response"

/** A JSON encoded string extra for questionnaire context. */
internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING =
"questionnaire-launch-context"
/** A list of JSON encoded strings extra for each questionnaire context. */
internal const val EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS =
"questionnaire-launch-contexts"
/**
* A [URI][android.net.Uri] extra for streaming a JSON encoded questionnaire response.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.datacapture.enablement.EnablementEvaluator
import com.google.android.fhir.datacapture.extensions.EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT
import com.google.android.fhir.datacapture.extensions.EntryMode
import com.google.android.fhir.datacapture.extensions.addNestedItemsToAnswer
import com.google.android.fhir.datacapture.extensions.allItems
Expand All @@ -44,9 +43,10 @@ import com.google.android.fhir.datacapture.extensions.isPaginated
import com.google.android.fhir.datacapture.extensions.isXFhirQuery
import com.google.android.fhir.datacapture.extensions.localizedTextSpanned
import com.google.android.fhir.datacapture.extensions.packRepeatedGroups
import com.google.android.fhir.datacapture.extensions.questionnaireLaunchContexts
import com.google.android.fhir.datacapture.extensions.shouldHaveNestedItemsUnderAnswers
import com.google.android.fhir.datacapture.extensions.unpackRepeatedGroups
import com.google.android.fhir.datacapture.extensions.validateLaunchContext
import com.google.android.fhir.datacapture.extensions.validateLaunchContextExtensions
import com.google.android.fhir.datacapture.extensions.zipByLinkId
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
Expand Down Expand Up @@ -160,25 +160,24 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat

/**
* The launch context allows information to be passed into questionnaire based on the context in
* which he questionnaire is being evaluated. For example, what patient, what encounter, what
* user, etc. is "in context" at the time the questionnaire response is being completed.
* Currently, we support at most one launch context.The supported launch contexts are defined in:
* which the questionnaire is being evaluated. For example, what patient, what encounter, what
* user, etc. is "in context" at the time the questionnaire response is being completed:
* https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html
*/
private val questionnaireLaunchContext: Resource?
private val questionnaireLaunchContextMap: Map<String, Resource>?

init {
questionnaireLaunchContext =
if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING)) {
val questionnaireLaunchContextJson: String =
state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING]!!
questionnaire.extension
.firstOrNull { it.url == EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT }
?.let {
val resource = parser.parseResource(questionnaireLaunchContextJson) as Resource
validateLaunchContext(it, resource.resourceType.name)
resource
}
questionnaireLaunchContextMap =
if (state.contains(QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS)) {

val launchContextJsonStrings: List<String> =
state[QuestionnaireFragment.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS]!!

val launchContexts = launchContextJsonStrings.map { parser.parseResource(it) as Resource }
questionnaire.questionnaireLaunchContexts?.let { launchContextExtensions ->
validateLaunchContextExtensions(launchContextExtensions)
launchContexts.associateBy { it.resourceType.name.lowercase() }
}
} else {
null
}
Expand Down Expand Up @@ -584,7 +583,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat
}

val xFhirExpressionString =
ExpressionEvaluator.createXFhirQueryFromExpression(expression, questionnaireLaunchContext)
ExpressionEvaluator.createXFhirQueryFromExpression(
expression,
questionnaireLaunchContextMap
)
xFhirQueryResolver!!.resolve(xFhirExpressionString)
} else if (expression.isFhirPath) {
fhirPathEngine.evaluate(questionnaireResponse, expression.expression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.android.fhir.datacapture.extensions

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.Expression
import org.hl7.fhir.r4.model.Extension
Expand All @@ -38,6 +39,16 @@ internal val Questionnaire.variableExpressions: List<Expression>
get() =
this.extension.filter { it.url == EXTENSION_VARIABLE_URL }.map { it.castToExpression(it.value) }

/**
* A list of extensions that define the resources that provide context for form processing logic:
* https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html
*/
internal val Questionnaire.questionnaireLaunchContexts: List<Extension>?
get() =
this.extension
.filter { it.url == EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT }
.takeIf { it.isNotEmpty() }

/**
* Finds the specific variable name [String] at questionnaire [Questionnaire] level
*
Expand All @@ -48,38 +59,57 @@ internal fun Questionnaire.findVariableExpression(variableName: String): Express
variableExpressions.find { it.name == variableName }

/**
* Validates the questionnaire launch context extension, if it exists, and well formed, and
* validates if the resource type is applicable as a launch context.
* Validates each questionnaire launch context extension matches:
* https://build.fhir.org/ig/HL7/sdc/StructureDefinition-sdc-questionnaire-launchContext.html
*/
internal fun validateLaunchContext(extension: Extension, resourceType: String) {
val nameExtension =
extension.extension
.firstOrNull { it.url == "name" }
?.value.takeIf { type ->
type is Coding &&
QuestionnaireLaunchContextSet.values().any {
it.code == type.code && it.display == type.display && it.system == type.system
}
internal fun validateLaunchContextExtensions(launchContextExtensions: List<Extension>) =
launchContextExtensions.forEach { launchExtension ->
validateLaunchContextExtension(
Extension().apply {
addExtension(launchExtension.extension.firstOrNull { it.url == "name" })
addExtension(launchExtension.extension.firstOrNull { it.url == "type" })
}

val typeExtension =
extension.extension
.firstOrNull { it.url == "type" }
?.takeIf { it.valueAsPrimitive.valueAsString == resourceType }

if (nameExtension == null) {
error(
"The value of the extension:name field in " +
"$EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT is not one of the ones defined in " +
"$EXTENSION_LAUNCH_CONTEXT."
)
}

if (typeExtension == null) {
/**
* Checks that the extension:name extension exists and its value contains a valid code from
* [QuestionnaireLaunchContextSet]
*/
private fun validateLaunchContextExtension(launchExtension: Extension) {
check(launchExtension.extension.size == 2) {
"The extension:name or extension:type extension is missing in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT"
}

val isValidExtension =
QuestionnaireLaunchContextSet.values().any {
launchExtension.equalsDeep(
Extension().apply {
addExtension(
Extension().apply {
url = "name"
setValue(
Coding().apply {
code = it.code
display = it.display
system = it.system
}
)
}
)
addExtension(
Extension().apply {
url = "type"
setValue(CodeType().setValue(it.resourceType))
}
)
}
)
}
if (!isValidExtension) {
error(
"The resource type set in the extension:type field in " +
"$EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT does not match the resource type of the " +
"context passed in: $resourceType."
"The extension:name extension and/or extension:type extension do not follow the format " +
"specified in $EXTENSION_SDC_QUESTIONNAIRE_LAUNCH_CONTEXT"
)
}
}
Expand All @@ -91,12 +121,16 @@ private enum class QuestionnaireLaunchContextSet(
val code: String,
val display: String,
val system: String,
val resourceType: String,
) {
PATIENT("patient", "Patient", EXTENSION_LAUNCH_CONTEXT),
ENCOUNTER("encounter", "Encounter", EXTENSION_LAUNCH_CONTEXT),
LOCATION("location", "Location", EXTENSION_LAUNCH_CONTEXT),
USER("user", "User", EXTENSION_LAUNCH_CONTEXT),
STUDY("study", "ResearchStudy", EXTENSION_LAUNCH_CONTEXT),
PATIENT("patient", "Patient", EXTENSION_LAUNCH_CONTEXT, "Patient"),
ENCOUNTER("encounter", "Encounter", EXTENSION_LAUNCH_CONTEXT, "Encounter"),
LOCATION("location", "Location", EXTENSION_LAUNCH_CONTEXT, "Location"),
USER_AS_PATIENT("user", "User", EXTENSION_LAUNCH_CONTEXT, "Patient"),
USER_AS_PRACTITIONER("user", "User", EXTENSION_LAUNCH_CONTEXT, "Practitioner"),
USER_AS_PRACTITIONER_ROLE("user", "User", EXTENSION_LAUNCH_CONTEXT, "PractitionerRole"),
USER_AS_RELATED_PERSON("user", "User", EXTENSION_LAUNCH_CONTEXT, "RelatedPerson"),
STUDY("study", "ResearchStudy", EXTENSION_LAUNCH_CONTEXT, "ResearchStudy"),
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,16 +282,16 @@ object ExpressionEvaluator {
* Creates an x-fhir-query string for evaluation
*
* @param expression x-fhir-query expression
* @param launchContext if passed, the launch context to evaluate the expression against
* @param launchContextMap if passed, the launch context to evaluate the expression against
*/
internal fun createXFhirQueryFromExpression(
expression: Expression,
launchContext: Resource?
launchContextMap: Map<String, Resource>?
): String {
if (launchContext == null) {
if (launchContextMap == null) {
return expression.expression
}
return evaluateXFhirEnhancement(expression, launchContext).fold(expression.expression) {
return evaluateXFhirEnhancement(expression, launchContextMap).fold(expression.expression) {
acc: String,
pair: Pair<String, String> ->
acc.replace(pair.first, pair.second)
Expand All @@ -305,11 +305,11 @@ object ExpressionEvaluator {
*
* @param expression x-fhir-query expression containing a FHIRpath, e.g.
* Practitioner?active=true&{{Practitioner.name.family}}
* @param resource the launch context to evaluate the expression against
* @param launchContextMap the launch context to evaluate the expression against
*/
private fun evaluateXFhirEnhancement(
expression: Expression,
resource: Resource
launchContextMap: Map<String, Resource>
): Sequence<Pair<String, String>> =
xFhirQueryEnhancementRegex
.findAll(expression.expression)
Expand All @@ -318,12 +318,15 @@ object ExpressionEvaluator {
// TODO(omarismail94): See if FHIRPathEngine.check() can be used to distinguish invalid
// expression vs an expression that is valid, but does not return one resource only.
val expressionNode = fhirPathEngine.parse(fhirPath)
val resourceType =
expressionNode.constant?.primitiveValue()?.substring(1)
?: expressionNode.name?.lowercase()
val evaluatedResult =
fhirPathEngine.evaluateToString(
mapOf(resource.resourceType.name.lowercase() to resource),
launchContextMap,
null,
null,
resource,
launchContextMap[resourceType],
expressionNode
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import com.google.android.fhir.FhirEngine
import com.google.android.fhir.FhirEngineProvider
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_ENABLE_REVIEW_PAGE
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_QUESTIONNAIRE_RESPONSE_JSON_STRING
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_READ_ONLY
import com.google.android.fhir.datacapture.QuestionnaireFragment.Companion.EXTRA_SHOW_REVIEW_PAGE_FIRST
Expand Down Expand Up @@ -86,7 +86,6 @@ import org.hl7.fhir.r4.model.Practitioner
import org.hl7.fhir.r4.model.Quantity
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent
import org.hl7.fhir.r4.model.StringType
import org.hl7.fhir.r4.model.ValueSet
import org.hl7.fhir.r4.utils.ToolingExtensions
Expand Down Expand Up @@ -3921,13 +3920,7 @@ class QuestionnaireViewModelTest {
)

val patientId = "123"
val patient =
Patient().apply {
id = patientId
active = true
gender = Enumerations.AdministrativeGender.MALE
addName(HumanName().apply { this.family = "Johnny" })
}
val patient = Patient().apply { id = patientId }

val questionnaire =
Questionnaire().apply {
Expand Down Expand Up @@ -3968,8 +3961,8 @@ class QuestionnaireViewModelTest {
}
state.set(EXTRA_QUESTIONNAIRE_JSON_STRING, printer.encodeResourceToString(questionnaire))
state.set(
EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRING,
printer.encodeResourceToString(patient)
EXTRA_QUESTIONNAIRE_LAUNCH_CONTEXT_JSON_STRINGS,
listOf(printer.encodeResourceToString(patient))
)

val viewModel = QuestionnaireViewModel(context, state)
Expand Down
Loading

0 comments on commit b129c66

Please sign in to comment.