diff --git a/onfhir-path/src/main/scala/io/onfhir/path/AbstractFhirPathFunctionLibrary.scala b/onfhir-path/src/main/scala/io/onfhir/path/AbstractFhirPathFunctionLibrary.scala index 0bbb2b7..37a0748 100644 --- a/onfhir-path/src/main/scala/io/onfhir/path/AbstractFhirPathFunctionLibrary.scala +++ b/onfhir-path/src/main/scala/io/onfhir/path/AbstractFhirPathFunctionLibrary.scala @@ -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._ @@ -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 @@ -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({ @@ -52,7 +87,7 @@ 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, @@ -60,31 +95,6 @@ abstract class AbstractFhirPathFunctionLibrary { 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) - } - } } /** @@ -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) + } } diff --git a/onfhir-path/src/main/scala/io/onfhir/path/annotation/FhirPathFunction.scala b/onfhir-path/src/main/scala/io/onfhir/path/annotation/FhirPathFunction.scala index 9b33039..7e00b7f 100644 --- a/onfhir-path/src/main/scala/io/onfhir/path/annotation/FhirPathFunction.scala +++ b/onfhir-path/src/main/scala/io/onfhir/path/annotation/FhirPathFunction.scala @@ -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: * """[{"reference":"Observation/123"},{"reference":"Observation/456"}]""" + * - Note: JSON formatting is supported only for parameters and return value examples. 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