Skip to content

Commit

Permalink
♻️ Adapt the function that parses FhirPathFunction to the changes
Browse files Browse the repository at this point in the history
  • Loading branch information
camemre49 committed Jan 23, 2025
1 parent df25fab commit a49f23b
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.onfhir.api.FHIR_DATA_TYPES
import io.onfhir.path.grammar.FhirPathExprParser.ExpressionContext

import java.lang.reflect.InvocationTargetException
import io.onfhir.path.annotation.FhirPathFunction
import io.onfhir.path.annotation.{FhirPathFunction, FhirPathFunctionDocumentation, FhirPathFunctionParameter, FhirPathFunctionReturn}

import scala.reflect.runtime.currentMirror
import scala.reflect.runtime.universe._
Expand All @@ -19,13 +19,43 @@ abstract class AbstractFhirPathFunctionLibrary {
*/
def getFunctionSignatures():Seq[(String, Int)] = getClass.getMethods.filterNot(_.getName.startsWith("$anonfun$")).map(m => m.getName -> m.getParameterCount).toSeq

/**
* Method that will be used to call a FHIR Path function from the function library
* Function library should define the function handlers as public methods with the name of the function that gets 0 or more ExpressionContext as parameters and return Seq of FhirPathResult
*
* @param fname Name of the function
* @param params Supplied parameters
* @return
*/
@throws[FhirPathException]
def callFhirPathFunction(fname:String, params:Seq[ExpressionContext]):Seq[FhirPathResult] = {
try {
val method = getClass.getMethod(fname, params.map(_ => classOf[ExpressionContext]): _*)
val result = method.invoke(this, params:_*)
val fhirPathResult = result.asInstanceOf[Seq[FhirPathResult]]
fhirPathResult
} catch {
case n:NoSuchMethodException =>
throw new FhirPathException(s"Invalid FHIR Path function call, function $fname does not exist or take ${params.length} arguments !!!")
case ite:InvocationTargetException =>
ite.getTargetException match {
case fpe:FhirPathException => throw fpe
case e:Throwable => throw FhirPathException.apply(s"Invalid FHIR Path function call $fname!", e)
}
}
}

/**
* Returns documentations of functions in this library. It searches for the methods having FhirPathFunction annotation
* and returns them.
*
* @return a list of FhirPathFunction representing the documentation of functions
* */
def getFunctionDocumentation():Seq[FhirPathFunction] = currentMirror.classSymbol(Class.forName(getClass.getName))
def getFunctionDocumentation():Seq[FhirPathFunction] = {
var document: FhirPathFunctionDocumentation = null

// Using reflection to inspect the methods annotated with @FhirPathFunction
currentMirror.classSymbol(Class.forName(getClass.getName))
.toType
.decls
// filter the methods having FhirPathFunction annotation
Expand All @@ -38,8 +68,13 @@ abstract class AbstractFhirPathFunctionLibrary {
.find(_.tree.tpe =:= typeOf[FhirPathFunction]).head
.tree.children.tail
.collect({
// Extract documentation details from the annotation
case field if field.tpe.toString.contains("FhirPathFunctionDocumentation") =>
document = getFhirPathDocumentation(field);

// matches 'String' fields
case Literal(Constant(s: String)) => s

// matches 'Seq[String]' fields
case Apply(_: Tree, args: List[Tree]) =>
args.collect({
Expand All @@ -52,39 +87,14 @@ abstract class AbstractFhirPathFunctionLibrary {
})
})
// create an instance of FhirPathFunction
new FhirPathFunction(documentation = annotationFields.headOption.get.toString,
new FhirPathFunction(documentation = document,
insertText = annotationFields.lift(1).get.toString,
detail = annotationFields.lift(2).get.toString,
label = annotationFields.lift(3).get.toString,
kind = annotationFields.lift(4).get.toString,
returnType = annotationFields.lift(5).get.asInstanceOf[Seq[String]],
inputType = annotationFields.lift(6).get.asInstanceOf[Seq[String]])
}).toSeq

/**
* Method that will be used to call a FHIR Path function from the function library
* Function library should define the function handlers as public methods with the name of the function that gets 0 or more ExpressionContext as parameters and return Seq of FhirPathResult
*
* @param fname Name of the function
* @param params Supplied parameters
* @return
*/
@throws[FhirPathException]
def callFhirPathFunction(fname:String, params:Seq[ExpressionContext]):Seq[FhirPathResult] = {
try {
val method = getClass.getMethod(fname, params.map(_ => classOf[ExpressionContext]): _*)
val result = method.invoke(this, params:_*)
val fhirPathResult = result.asInstanceOf[Seq[FhirPathResult]]
fhirPathResult
} catch {
case n:NoSuchMethodException =>
throw new FhirPathException(s"Invalid FHIR Path function call, function $fname does not exist or take ${params.length} arguments !!!")
case ite:InvocationTargetException =>
ite.getTargetException match {
case fpe:FhirPathException => throw fpe
case e:Throwable => throw FhirPathException.apply(s"Invalid FHIR Path function call $fname!", e)
}
}
}

/**
Expand Down Expand Up @@ -112,4 +122,139 @@ abstract class AbstractFhirPathFunctionLibrary {
None // Return None if the input string does not match the pattern
}
}

/**
* Parses the annotation syntax tree to extract the documentation for a FHIR path function.
* This method processes the fields of the annotation to populate the details, warnings, parameters,
* return value, and examples for the function's documentation.
*
* @param annotationSyntaxTree The syntax tree of the annotation from which the documentation is extracted.
* @return An instance of FhirPathFunctionDocumentation populated with the extracted fields.
*/
def getFhirPathDocumentation(annotationSyntaxTree: Tree): FhirPathFunctionDocumentation = {
// Initializing variables to store extracted information
var detail: String = ""
var warnings: Option[Seq[String]] = None
var parameters: Option[Seq[FhirPathFunctionParameter]] = None
var returnValue: FhirPathFunctionReturn = FhirPathFunctionReturn(None, Seq())
var examples: Seq[String] = Seq()

annotationSyntaxTree match {
case Apply(_: Tree, args: List[Tree]) =>
// Extract the 'detail' field
args.lift(0).foreach {
case Literal(Constant(s: String)) => detail = s
}

// Extract the 'warnings' field
args.lift(1).foreach {
case Apply(_, warningsArgs: List[Tree]) =>
val extractedWarnings = warningsArgs.head.collect {
case Literal(Constant(warning: String)) => warning
}
warnings = if (extractedWarnings.nonEmpty) Some(extractedWarnings) else None
case _ => // Do nothing
}

// Extract the 'parameters' field
args.lift(2).foreach {
case Apply(_: Tree, parametersArgs: List[Tree]) =>
parametersArgs.foreach({
case Apply(_: Tree, args: List[Tree]) =>
parameters = readFhirPathFunctionParameter(args)
case _ => // Do nothing
})
case _ => // Do nothing
}

// Extract the 'returnValue' field
args.lift(3).foreach {
case Apply(_: Tree, returnValueArgs: List[Tree]) =>
returnValue = readFhirPathFunctionReturn(returnValueArgs)
case _ => // Do nothing
}

// Extract 'examples' field
args.lift(4).foreach {
case Apply(_: Tree, examplesArgs: List[Tree]) =>
examples = examplesArgs.collect {
case Literal(Constant(example: String)) => example
}
}

case _ => // Do nothing
}

// Return the populated FhirPathFunctionDocumentation object
FhirPathFunctionDocumentation(
detail = detail,
usageWarnings = warnings,
parameters = parameters,
returnValue = returnValue,
examples = examples
)
}

/**
* Parses a list of trees to extract parameters for a FHIR path function.
* This method processes each field in the list, extracting the name, detail, and examples for each parameter.
*
* @param parameterSyntaxTreeList A list of trees representing the fields of the parameter annotation.
* @return An `Option` containing a sequence of `FhirPathFunctionParameter` objects if parameters are found,
* otherwise `None`.
*/
private def readFhirPathFunctionParameter(parameterSyntaxTreeList: List[Tree]): Option[Seq[FhirPathFunctionParameter]] = {
// Process each field in the list
val parameters = parameterSyntaxTreeList.collect {
case Apply(_: Tree, args: List[Tree]) =>
// Extract values from `args`
val name = args.headOption.collect {
case Literal(Constant(value: String)) => value
}.getOrElse("")

val detail = args.lift(1).collect {
case Literal(Constant(value: String)) => value
}.getOrElse("")

val examples = args.lift(2) match {
case Some(Apply(_: Tree, exampleArgs: List[Tree])) =>
exampleArgs.collect {
case Literal(Constant(value: String)) => value
}
case _ => Seq.empty
}

// Construct the parameter object
FhirPathFunctionParameter(name, detail, if (examples.nonEmpty) Some(examples) else None)
}

// Return the collected parameters wrapped in an Option
if (parameters.nonEmpty) Some(parameters) else None
}

/**
* Extracts the return value details for a FHIR path function from a list of trees.
* This method processes the list of fields, extracting the `detail` and `examples` for the return value.
* If no valid details are found, it defaults to an empty return value.
*
* @param returnValueSyntaxTreeList A list of trees representing the fields of the return annotation.
* @return An instance of `FhirPathFunctionReturn` containing the extracted `detail` and `examples`.
*/
private def readFhirPathFunctionReturn(returnValueSyntaxTreeList: List[Tree]): FhirPathFunctionReturn = {
// Extract the `detail` and `examples`
val detail = returnValueSyntaxTreeList.headOption match {
case Some(Literal(Constant(detail: String))) => Some(detail)
case _ => None
}

val examples = returnValueSyntaxTreeList.lift(1) match {
case Some(Apply(_: Tree, exampleArgs: List[Tree])) =>
exampleArgs.collect {
case Literal(Constant(example: String)) => example
}
case _ => Seq.empty
}

FhirPathFunctionReturn(detail, examples)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class FhirPathFunction(val documentation: FhirPathFunctionDocumentation,
* To add a JSON as an example, use the following syntax:
* - Wrap your JSON in triple quotation marks with the "json" language identifier for proper formatting:
* """<JSON>[{"reference":"Observation/123"},{"reference":"Observation/456"}]"""
* - Note: JSON formatting is supported only for parameters and return value examples. <JSON> tag will not affect other fields.
*
* @param detail A detailed explanation of the function, its behavior, or its purpose.
* @param usageWarnings A collection of warnings or notes regarding the function's usage. These might include
Expand Down

0 comments on commit a49f23b

Please sign in to comment.