Skip to content

Commit

Permalink
Add tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
maimoonak committed Sep 2, 2022
1 parent b9f40b6 commit 54a8f11
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ internal fun Questionnaire.QuestionnaireItemComponent.findVariableExpression(
return variableExpressions.find { it.name == variableName }
}

/** Returns Calculated expression, or null */
internal val Questionnaire.QuestionnaireItemComponent.calculatedExpression: Expression?
get() =
this.getExtensionByUrl(EXTENSION_CALCULATED_EXPRESSION_URL)?.let {
it.castToExpression(it.value)
}

/** Returns list of extensions whose value is of type [Expression] */
internal val Questionnaire.QuestionnaireItemComponent.expressionBasedExtensions
get() = this.extension.filter { it.value is Expression }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ object ExpressionEvaluator {
}
}

/** Detects if any item into list is referencing a dependent item in its calculated expression */
internal fun detectExpressionCyclicDependency(
items: List<Questionnaire.QuestionnaireItemComponent>
) {
Expand All @@ -84,6 +85,10 @@ object ExpressionEvaluator {
}
}

/**
* Returns a pair of item and the calculated and evaluated value for all items with calculated
* expression extension, which is dependent on value of updated response
*/
fun evaluateCalculatedExpressions(
updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent,
questionnaire: Questionnaire,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ class MoreQuestionnaireItemComponentsTest {

@Test
fun `calculatedExpression should return expression for valid extension url`() {
val questionnaire =
val item =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
EXTENSION_CALCULATED_EXPRESSION_URL,
Expand All @@ -719,13 +719,13 @@ class MoreQuestionnaireItemComponentsTest {
}
)
}
assertThat(questionnaire.calculatedExpression).isNotNull()
assertThat(questionnaire.calculatedExpression!!.expression).isEqualTo("today()")
assertThat(item.calculatedExpression).isNotNull()
assertThat(item.calculatedExpression!!.expression).isEqualTo("today()")
}

@Test
fun `calculatedExpression should return null for other extension url`() {
val questionnaire =
val item =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(
ITEM_INITIAL_EXPRESSION_URL,
Expand All @@ -735,7 +735,85 @@ class MoreQuestionnaireItemComponentsTest {
}
)
}
assertThat(questionnaire.calculatedExpression).isNull()
assertThat(item.calculatedExpression).isNull()
}

@Test
fun `expressionBasedExtensions should return all extension of type expression`() {
val item =
Questionnaire.QuestionnaireItemComponent().apply {
addExtension(EXTENSION_HIDDEN_URL, BooleanType(true))
addExtension(
EXTENSION_CALCULATED_EXPRESSION_URL,
Expression().apply {
this.expression = "today()"
this.language = "text/fhirpath"
}
)
addExtension(
EXTENSION_ENABLE_WHEN_EXPRESSION_URL,
Expression().apply {
this.expression = "%resource.status == 'draft'"
this.language = "text/fhirpath"
}
)
}

val result = item.expressionBasedExtensions

assertThat(result.count()).isEqualTo(2)
assertThat(result.first().url).isEqualTo(EXTENSION_CALCULATED_EXPRESSION_URL)
assertThat(result.last().url).isEqualTo(EXTENSION_ENABLE_WHEN_EXPRESSION_URL)
}

@Test
fun `isReferencedBy should return true`() {
val item1 =
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "A"
addExtension(
EXTENSION_CALCULATED_EXPRESSION_URL,
Expression().apply {
this.expression = "%resource.item.where(linkId='B')"
this.language = "text/fhirpath"
}
)
}
val item2 = Questionnaire.QuestionnaireItemComponent().apply { linkId = "B" }
assertThat(item2.isReferencedBy(item1)).isTrue()
}

@Test
fun `isReferencedBy should return false`() {
val item1 =
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "A"
addExtension(
EXTENSION_CALCULATED_EXPRESSION_URL,
Expression().apply {
this.expression = "%resource.item.where(answer.value.empty())"
this.language = "text/fhirpath"
}
)
}
val item2 = Questionnaire.QuestionnaireItemComponent().apply { linkId = "B" }
assertThat(item2.isReferencedBy(item1)).isFalse()
}

@Test
fun `flattened should return linear list`() {
val items =
listOf(
Questionnaire.QuestionnaireItemComponent().apply { linkId = "A" },
Questionnaire.QuestionnaireItemComponent()
.apply { linkId = "B" }
.addItem(
Questionnaire.QuestionnaireItemComponent()
.apply { linkId = "C" }
.addItem(Questionnaire.QuestionnaireItemComponent().apply { linkId = "D" })
)
)
assertThat(items.flattened().map { it.linkId }).containsExactly("A", "B", "C", "D")
}

@Test
Expand Down Expand Up @@ -933,7 +1011,7 @@ class MoreQuestionnaireItemComponentsTest {
}

@Test
fun `createQuestionResponse should not set answer for quantity type with missing value `() {
fun `createQuestionResponse should not set answer for quantity type with missing value`() {
val question =
Questionnaire.QuestionnaireItemComponent(
StringType("age"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.google.android.fhir.datacapture.EXTENSION_CALCULATED_EXPRESSION_URL
import com.google.android.fhir.datacapture.EXTENSION_VARIABLE_URL
import com.google.android.fhir.datacapture.common.datatype.asStringValue
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.detectExpressionCyclicDependency
import com.google.android.fhir.datacapture.fhirpath.ExpressionEvaluator.evaluateCalculatedExpressions
import com.google.android.fhir.datacapture.variableExpressions
import com.google.common.truth.Truth.assertThat
import java.util.Calendar
Expand All @@ -28,6 +29,7 @@ import kotlinx.coroutines.runBlocking
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.IntegerType
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.Type
Expand Down Expand Up @@ -477,6 +479,136 @@ class ExpressionEvaluatorTest {
assertThat((result as Type).asStringValue()).isEqualTo("2")
}

@Test
fun `evaluateCalculatedExpressions should return list of calculated values`() = runBlocking {
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "a-birthdate"
type = Questionnaire.QuestionnaireItemType.DATE
addExtension().apply {
url = EXTENSION_CALCULATED_EXPRESSION_URL
setValue(
Expression().apply {
this.language = "text/fhirpath"
this.expression =
"%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)"
}
)
}
}
)
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "a-age-years"
type = Questionnaire.QuestionnaireItemType.QUANTITY
}
)
}

val questionnaireResponse =
QuestionnaireResponse().apply {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-birthdate"
}
)
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-age-years"
answer =
listOf(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
this.value = Quantity(1).apply { unit = "year" }
}
)
}
)
}

val result =
evaluateCalculatedExpressions(
questionnaire.item.elementAt(1),
questionnaire,
questionnaireResponse,
emptySet(),
emptyMap()
)

assertThat(result.first().second.first().asStringValue())
.isEqualTo(DateType(Date()).apply { add(Calendar.YEAR, -1) }.asStringValue())
}

@Test
fun `evaluateCalculatedExpressions should not include item in list when item has been modified`() =
runBlocking {
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "a-birthdate"
type = Questionnaire.QuestionnaireItemType.DATE
addExtension().apply {
url = EXTENSION_CALCULATED_EXPRESSION_URL
setValue(
Expression().apply {
this.language = "text/fhirpath"
this.expression =
"%resource.repeat(item).where(linkId='a-age-years' and answer.empty().not()).select(today() - answer.value)"
}
)
}
}
)
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
linkId = "a-age-years"
type = Questionnaire.QuestionnaireItemType.QUANTITY
}
)
}

val questionnaireResponse =
QuestionnaireResponse().apply {
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-birthdate"
answer =
listOf(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = DateType(Date())
}
)
}
)
addItem(
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
linkId = "a-age-years"
answer =
listOf(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
this.value = Quantity(1).apply { unit = "year" }
}
)
}
)
}

val result =
evaluateCalculatedExpressions(
questionnaire.item.elementAt(1),
questionnaire,
questionnaireResponse,
setOf(questionnaireResponse.itemFirstRep),
emptyMap()
)

assertThat(result).isEmpty()
}

@Test
fun `detectExpressionCyclicDependency() should throw illegal argument exception when item with calculated expression have cyclic dependency`() {
val questionnaire =
Expand Down

0 comments on commit 54a8f11

Please sign in to comment.