Skip to content

Commit

Permalink
Merge pull request #82 from srdc/handle-primitive-extensions
Browse files Browse the repository at this point in the history
Handle primitive extensions and implement memberOf function
  • Loading branch information
tnamli authored Nov 27, 2024
2 parents de968ff + 97c561a commit 1bba161
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.onfhir.path

import io.onfhir.api.service.{IFhirIdentityService, IFhirTerminologyService}
import io.onfhir.api.validation.IReferenceResolver
import io.onfhir.api.validation.{IFhirTerminologyValidator, IReferenceResolver}

import scala.collection.mutable
import scala.util.matching.Regex
Expand All @@ -23,6 +23,7 @@ case class FhirPathEnvironment(
val functionLibraries:Map[String, IFhirPathFunctionLibraryFactory] = Map.empty,
val terminologyService:Option[IFhirTerminologyService] = None,
val identityService:Option[IFhirIdentityService] = None,
val terminologyValidator: Option[IFhirTerminologyValidator] = None,
val _index:Int = 0,
val _total:Option[FhirPathResult] = None,
val isContentFhir:Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.io.ByteArrayInputStream
import java.nio.charset.StandardCharsets
import java.time.{LocalTime, ZoneId}
import java.time.temporal.Temporal
import io.onfhir.api.validation.IReferenceResolver
import io.onfhir.api.validation.{IFhirTerminologyValidator, IReferenceResolver}
import io.onfhir.path.grammar.{FhirPathExprLexer, FhirPathExprParser}
import org.antlr.v4.runtime.{CharStreams, CommonTokenStream}
import org.json4s.JsonAST.{JArray, JBool, JValue}
Expand All @@ -25,6 +25,7 @@ case class FhirPathEvaluator (
functionLibraries:Map[String, IFhirPathFunctionLibraryFactory] = Map.empty,
terminologyService:Option[IFhirTerminologyService] = None,
identityService:Option[IFhirIdentityService] = None,
terminologyValidator: Option[IFhirTerminologyValidator] = None,
isContentFhir:Boolean = true
) {
private val logger: Logger = LoggerFactory.getLogger(this.getClass)
Expand Down Expand Up @@ -103,7 +104,8 @@ case class FhirPathEvaluator (
functionLibraries,
terminologyService,
identityService,
isContentFhir = isContentFhir
isContentFhir = isContentFhir,
terminologyValidator = terminologyValidator
)
val evaluator = new FhirPathExpressionEvaluator(environment, resource)
evaluator.visit(expr)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,28 @@ class FhirPathExpressionEvaluator(context:FhirPathEnvironment, current:Seq[FhirP
* */
override def visitMemberInvocation(ctx: FhirPathExprParser.MemberInvocationContext):Seq[FhirPathResult] = {
//Element path
val pathName = FhirPathLiteralEvaluator.parseIdentifier(ctx.identifier().getText) + targetType.getOrElse("") //if there is target type add it e.g. Observation.value as Quantity --> search for valueQuantity
var pathName = FhirPathLiteralEvaluator.parseIdentifier(ctx.identifier().getText) + targetType.getOrElse("") //if there is target type add it e.g. Observation.value as Quantity --> search for valueQuantity

//Execute the path and return
current
.filter(_.isInstanceOf[FhirPathComplex]) //Only get the complex objects
.flatMap(r => {
// Check if the current field is a complex type
val jsonValue = r.asInstanceOf[FhirPathComplex].json \ pathName
val isComplex: Boolean = jsonValue match {
case _: JObject => true // Complex type
case _: JArray => true // Consider arrays as complex types
case _ => false // Any primitive type (e.g., JString, JNumber, JBool)
}
if (!isComplex) {
// If the current field is not complex (i.e., it's a primitive type), check if the next token is an "extension"
if (isNextTokenExtension(ctx)) {
// If the next token is "extension", modify the path name to access the corresponding "_<field>" element
// This is necessary because FHIR stores extensions for primitive fields under a separate path prefixed with "_"
pathName = s"_$pathName"
}
}

FhirPathValueTransformer.transform(r.asInstanceOf[FhirPathComplex].json \ pathName, context.isContentFhir) match { //Execute JSON path for each element
//The field can be a multi valued so we should check if there is a field starting with the path
case Nil if targetType.isEmpty && context.isContentFhir =>
Expand Down Expand Up @@ -503,4 +519,37 @@ class FhirPathExpressionEvaluator(context:FhirPathEnvironment, current:Seq[FhirP
}
}

/**
* Checks if the next member in the FHIRPath expression chain after the given context
* is the "extension" function.
*
* This function traverses up the parse tree to locate the parent context that contains
* the entire expression and checks if the token following the current context is ".extension".
*
* @param ctx The current `MemberInvocationContext` representing the current member.
* @return `true` if the next token in the chain is "extension", `false` otherwise.
*/
private def isNextTokenExtension(ctx: FhirPathExprParser.MemberInvocationContext): Boolean = {
// Cache the current context's text to avoid duplicate calls
val currentText = ctx.getText
// Start with the parent context to traverse the tree
var parent = ctx.getParent
while (parent != null) {
val parentText = parent.getText
// Check if the current context's text differs from the parent's text
if (!parentText.contentEquals(currentText)) {
// Extract the substring after the current context in the parent's text
val nextToken = parentText.substring(parentText.indexOf(currentText) + currentText.length)
// Check if the next token starts with ".extension"
if (nextToken.startsWith(".extension")) {
return true
}
// Return false if the next token is not ".extension"
return false
}
// Move to the next parent in the parse tree
parent = parent.getParent
}
false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,43 @@ class FhirPathFunctionEvaluator(context: FhirPathEnvironment, current: Seq[FhirP
result
}

/**
* Validates if the current element belongs to a specified FHIR value set.
*
* This function checks whether the `code` from the current context is a member of
* the value set specified by the provided `urlExp`.
*
* @param urlExp The URL expression representing the FHIR value set to validate against.
* @return A sequence containing a single `FhirPathBoolean` result:
* - `true` if the code is a member of the specified value set.
* - `false` otherwise.
* @throws FhirPathException if `urlExp` does not return a valid URL.
*/
@FhirPathFunction(
documentation = "\uD83D\uDCDC Returns whether the current element is a member of a specific value set.\n\n\uD83D\uDCDD <span style=\"color:#ff0000;\">_@param_</span> **`urlExp`** \nThe URL of the FHIR value set to validate against.\n\n\uD83D\uDD19 <span style=\"color:#ff0000;\">_@return_</span> \nA boolean indicating if the code is valid within the specified value set:\n```json\ntrue | false\n```\n\n\uD83D\uDCA1 **E.g.** \n`code.memberOf(\"http://example.org/fhir/ValueSet/my-value-set\")`",
insertText = "memberOf(<urlExp>)", detail = "Validate if the current element belongs to a FHIR value set.", label = "memberOf", kind = "Method", returnType = Seq("boolean"), inputType = Seq("string")
)
def memberOf(urlExp: ExpressionContext): Seq[FhirPathResult] = {
// Evaluate the URL expression and ensure it resolves to a single valid URL string
val url = new FhirPathExpressionEvaluator(context, current).visit(urlExp)
if (url.length != 1 || !url.head.isInstanceOf[FhirPathString]) {
throw new FhirPathException(
s"Invalid function call 'memberOf': expression ${urlExp.getText} does not return a valid URL!"
)
}

// Retrieve the terminology validator and validate the code against the specified value set
val isValid = context.terminologyValidator.get
.validateCodeAgainstValueSet(
vsUrl = url.head.asInstanceOf[FhirPathString].s, // Value set URL
code = current.head.asInstanceOf[FhirPathString].s, // Current code to validate
version = None,
codeSystem = None
)

// Return the validation result as a FhirPathBoolean
Seq(FhirPathBoolean(isValid))
}

/**
* Type functions, for these basic casting or type checking are done before calling the function on the left expression
Expand Down
105 changes: 105 additions & 0 deletions onfhir-path/src/test/resources/patient2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"_gender": {
"extension": [
{
"url": "http://fhir.de/StructureDefinition/gender-amtlich-de",
"valueCoding": {
"code": "D",
"display": "divers",
"system": "http://fhir.de/CodeSystem/gender-amtlich-de"
}
}
]
},
"address": [
{
"city": "Köln",
"country": "DE",
"line": [
"Teststraße 2"
],
"postalCode": "50823",
"type": "both",
"extension": [
{
"url": "http://example.org/fhir/StructureDefinition/address-description",
"valueString": "This is the primary residence address."
},
{
"url": "http://example.org/fhir/StructureDefinition/address-verified",
"valueBoolean": true
}
]
}
],
"birthDate": "1998-09-19",
"gender": "other",
"id": "ExamplePatientPatientMinimal",
"identifier": [
{
"assigner": {
"display": "Charité – Universitätsmedizin Berlin",
"identifier": {
"system": "http://fhir.de/NamingSystem/arge-ik/iknr",
"value": "261101015",
"extension": [
{
"url": "http://example.org/fhir/StructureDefinition/identifier-verified",
"valueBoolean": true
}
]
}
},
"system": "https://www.example.org/fhir/sid/patienten",
"type": {
"coding": [
{
"code": "MR",
"system": "http://terminology.hl7.org/CodeSystem/v2-0203"
}
]
},
"use": "usual",
"value": "42285243"
},
{
"assigner": {
"identifier": {
"system": "http://fhir.de/sid/arge-ik/iknr",
"use": "official",
"value": "260326822"
}
},
"system": "http://fhir.de/sid/gkv/kvid-10",
"type": {
"coding": [
{
"code": "KVZ10",
"system": "http://fhir.de/CodeSystem/identifier-type-de-basis"
}
]
},
"use": "official",
"value": "A999999999"
}
],
"managingOrganization": {
"reference": "Organization/Charite-Universitaetsmedizin-Berlin"
},
"meta": {
"profile": [
"https://www.medizininformatik-initiative.de/fhir/core/modul-person/StructureDefinition/Patient"
]
},
"name": [
{
"family": "Van-der-Dussen",
"given": [
"Julia",
"Maja"
],
"use": "official"
}
],
"resourceType": "Patient"
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class FhirPathEvaluatorTest extends Specification {

val encounter = Source.fromInputStream(getClass.getResourceAsStream("/encounter.json")).mkString.parseJson

val patient = Source.fromInputStream(getClass.getResourceAsStream("/patient2.json")).mkString.parseJson

val emptyBundle = Source.fromInputStream(getClass.getResourceAsStream("/emptybundle.json")).mkString.parseJson

val medicationAdministration = Source.fromInputStream(getClass.getResourceAsStream("/med-adm.json")).mkString.parseJson
Expand Down Expand Up @@ -661,6 +663,19 @@ class FhirPathEvaluatorTest extends Specification {
results2 mustEqual Seq(10)
}

"evaluate primitive extensions" in {
val evaluator = FhirPathEvaluator().withDefaultFunctionLibraries()

val result = evaluator.evaluateBoolean("gender.extension('http://fhir.de/StructureDefinition/gender-amtlich-de').exists()", patient).head
result mustEqual true
val result2 = evaluator.evaluateBoolean("birthDate.extension('http://fhir.de/StructureDefinition/birthdate').exists()", patient).head
result2 mustEqual false
val result3 = evaluator.evaluateBoolean("address[0].extension('http://example.org/fhir/StructureDefinition/address-verified').exists()", patient).head
result3 mustEqual true
val result4 = evaluator.evaluateBoolean("identifier[0].assigner.identifier.extension('http://example.org/fhir/StructureDefinition/identifier-verified').exists()", patient).head
result4 mustEqual true
}

"evaluate new constraints in FHIR 4.0.1" in {
var result = FhirPathEvaluator().satisfies("empty() or ($this = '*') or (toInteger() >= 0)", JString("*"))
result mustEqual true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import org.json4s.JsonAST.JValue
*/
case class ConstraintsRestriction(fhirConstraints: Seq[FhirConstraint]) extends FhirRestriction {
override def evaluate(value: JValue, fhirContentValidator: AbstractFhirContentValidator): Seq[ConstraintFailure] = {
val fhirPathEvaluator = FhirPathEvaluator.apply(fhirContentValidator.referenceResolver)
val fhirPathEvaluator = FhirPathEvaluator.apply(referenceResolver = fhirContentValidator.referenceResolver, terminologyValidator = Some(fhirContentValidator.terminologyValidator))
fhirConstraints.flatMap(_.evaluate(value, fhirPathEvaluator))
}
}
Expand Down

0 comments on commit 1bba161

Please sign in to comment.