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

Handle primitive extensions and implement memberOf function #82

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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