Skip to content

Commit

Permalink
Merge pull request #2768 from FirelyTeam/feature/fhirpath-definevariable
Browse files Browse the repository at this point in the history
Added defineVariable to FhirPath
  • Loading branch information
ewoutkramer authored Apr 11, 2024
2 parents c259241 + 730f973 commit dc32002
Show file tree
Hide file tree
Showing 13 changed files with 388 additions and 112 deletions.
99 changes: 50 additions & 49 deletions src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,58 @@
using System;
using System.Collections.Generic;

namespace Hl7.FhirPath
#nullable enable

namespace Hl7.FhirPath;

public class EvaluationContext
{
public class EvaluationContext
public static EvaluationContext CreateDefault() => new();

public EvaluationContext()
{
public static EvaluationContext CreateDefault() => new();

public EvaluationContext()
{
// no defaults yet
}

/// <summary>
/// Create an EvaluationContext with the given value for <c>%resource</c>.
/// </summary>
/// <param name="resource">The data that will be represented by %resource</param>
public EvaluationContext(ITypedElement resource) : this(resource, null) { }

/// <summary>
/// Create an EvaluationContext with the given value for <c>%resource</c> and <c>%rootResource</c>.
/// </summary>
/// <param name="resource">The data that will be represented by <c>%resource</c>.</param>
/// <param name="rootResource">The data that will be represented by <c>%rootResource</c>.</param>
public EvaluationContext(ITypedElement resource, ITypedElement rootResource)
{
Resource = resource;
RootResource = rootResource ?? resource;
}

public EvaluationContext(ITypedElement resource, ITypedElement rootResource, IDictionary<string, IEnumerable<ITypedElement>> environment) : this(resource, rootResource)
{
Environment = environment;
}

/// <summary>
/// The data represented by <c>%rootResource</c>.
/// </summary>
public ITypedElement RootResource { get; set; }

/// <summary>
/// The data represented by <c>%resource</c>.
/// </summary>
public ITypedElement Resource { get; set; }
// no defaults yet
}

/// <summary>
/// Create an EvaluationContext with the given value for <c>%resource</c>.
/// </summary>
/// <param name="resource">The data that will be represented by %resource</param>
public EvaluationContext(ITypedElement? resource) : this(resource, null) { }

/// <summary>
/// Create an EvaluationContext with the given value for <c>%resource</c> and <c>%rootResource</c>.
/// </summary>
/// <param name="resource">The data that will be represented by <c>%resource</c>.</param>
/// <param name="rootResource">The data that will be represented by <c>%rootResource</c>.</param>
public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource)
{
Resource = resource;
RootResource = rootResource ?? resource;
}

/// <summary>
/// The environment variables that are available to the FHIRPath expressions.
/// </summary>
public IDictionary<string, IEnumerable<ITypedElement>> Environment { get; set; }

/// <summary>
/// A delegate that handles the output for the <c>trace()</c> function.
/// </summary>
public Action<string, IEnumerable<ITypedElement>> Tracer { get; set; }
public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, IDictionary<string, IEnumerable<ITypedElement>> environment) : this(resource, rootResource)
{
Environment = environment;
}

/// <summary>
/// The data represented by <c>%rootResource</c>.
/// </summary>
public ITypedElement? RootResource { get; set; }

/// <summary>
/// The data represented by <c>%resource</c>.
/// </summary>
public ITypedElement? Resource { get; set; }

/// <summary>
/// The environment variables that are available to the FHIRPath expressions.
/// </summary>
public IDictionary<string, IEnumerable<ITypedElement>> Environment { get; set; } = new Dictionary<string, IEnumerable<ITypedElement>>();

/// <summary>
/// A delegate that handles the output for the <c>trace()</c> function.
/// </summary>
public Action<string, IEnumerable<ITypedElement>>? Tracer { get; set; }
}
10 changes: 10 additions & 0 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

using Hl7.Fhir.ElementModel;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using System.Linq;

namespace Hl7.FhirPath.Expressions
{
Expand All @@ -25,10 +28,17 @@ public static Closure Root(ITypedElement root, EvaluationContext ctx = null)
var newContext = new Closure() { EvaluationContext = ctx ?? EvaluationContext.CreateDefault() };

var input = new[] { root };

foreach (var assignment in newContext.EvaluationContext.Environment)
{
newContext.SetValue(assignment.Key, assignment.Value);
}

newContext.SetThis(input);
newContext.SetThat(input);
newContext.SetIndex(ElementNode.CreateList(0));
newContext.SetOriginalContext(input);

if (ctx.Resource != null) newContext.SetResource(new[] { ctx.Resource });
if (ctx.RootResource != null) newContext.SetRootResource(new[] { ctx.RootResource });

Expand Down
12 changes: 5 additions & 7 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/EvaluatorVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,14 @@ public override Invokee VisitVariableRef(FP.VariableRefExpression expression)
if (expression.Name == "rootResource")
return InvokeeFactory.GetRootResource;

if (expression is ContextVariableRefExpression Cvre)
return chainResolves;

IEnumerable<ITypedElement> chainResolves(Closure context, IEnumerable<Invokee> invokees)
{
return Cvre.Resolve;
return context.ResolveValue(expression.Name) ?? resolve(Symbols, expression.Name, Enumerable.Empty<Type>())(context, []);
}

// Variables are still functions without arguments. For now variables are treated separately here,
//Functions are handled elsewhere.
return resolve(Symbols, expression.Name, Enumerable.Empty<Type>());
}

private static Invokee resolve(SymbolTable scope, string name, IEnumerable<Type> argumentTypes)
{
// For now, we don't have the types or the parameters statically, so we just match on name
Expand Down
8 changes: 0 additions & 8 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,6 @@ public override int GetHashCode()
return base.GetHashCode() ^ Name.GetHashCode();
}
}

public class ContextVariableRefExpression(string name) : VariableRefExpression(name)
{
internal IEnumerable<ITypedElement> Resolve(Closure context, IEnumerable<Invokee> _)
{
return context.EvaluationContext.Environment[Name] ?? throw Error.InvalidOperation($"Variable {Name} not found in environment");
}
}

public class AxisExpression : VariableRefExpression
{
Expand Down
11 changes: 10 additions & 1 deletion src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,23 @@ public static Invokee Invoke(string functionName, IEnumerable<Invokee> arguments
{
try
{
return invokee(ctx, arguments);
var wrappedArguments = arguments.Skip(1).Select(wrapWithNextContext);
return invokee(ctx, [arguments.First(),.. wrappedArguments]);
}
catch (Exception e)
{
throw new InvalidOperationException(
$"Invocation of {formatFunctionName(functionName)} failed: {e.Message}");
}
};

Invokee wrapWithNextContext(Invokee unwrappedArgument)
{
return (ctx, args) =>
{
return unwrappedArgument(ctx.Nest(ctx.GetThis()), args);
};
}

string formatFunctionName(string name)
{
Expand Down
25 changes: 25 additions & 0 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ public static SymbolTable AddStandardFP(this SymbolTable t)
t.Add(new CallSignature("exists", typeof(bool), typeof(object), typeof(Invokee)), runAny);
t.Add(new CallSignature("repeat", typeof(IEnumerable<ITypedElement>), typeof(object), typeof(Invokee)), runRepeat);
t.Add(new CallSignature("trace", typeof(IEnumerable<ITypedElement>), typeof(string), typeof(object), typeof(Invokee)), Trace);
t.Add(new CallSignature("defineVariable", typeof(IEnumerable<ITypedElement>), typeof(object), typeof(string)), DefineVariable);
t.Add(new CallSignature("defineVariable", typeof(IEnumerable<ITypedElement>), typeof(object), typeof(string), typeof(Invokee)), DefineVariable);

t.Add(new CallSignature("aggregate", typeof(IEnumerable<ITypedElement>), typeof(Invokee), typeof(Invokee)), runAggregate);
t.Add(new CallSignature("aggregate", typeof(IEnumerable<ITypedElement>), typeof(Invokee), typeof(Invokee), typeof(Invokee)), runAggregate);
Expand Down Expand Up @@ -298,6 +300,29 @@ private static IEnumerable<ITypedElement> Trace(Closure ctx, IEnumerable<Invokee

return focus;
}

private static IEnumerable<ITypedElement> DefineVariable(Closure ctx, IEnumerable<Invokee> arguments)
{
Invokee[] enumerable = arguments as Invokee[] ?? arguments.ToArray();
var focus = enumerable[0](ctx, InvokeeFactory.EmptyArgs);
string name = enumerable[1](ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string;

if(ctx.ResolveValue(name) is not null) throw new InvalidOperationException($"Variable {name} is already defined in this scope");

if (enumerable.Length == 2)
{
ctx.SetValue(name, focus);
}
else
{
var newContext = ctx.Nest(focus);
newContext.SetThis(focus);
var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs);
ctx.SetValue(name, result);
}

return focus;
}

private static IEnumerable<ITypedElement> runIif(Closure ctx, IEnumerable<Invokee> arguments)
{
Expand Down
2 changes: 0 additions & 2 deletions src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ internal class Grammar
// : invocation #invocationTerm
// | literal #literalTerm
// | externalConstant #externalConstantTerm
// | externalVariable #externalVariableTerm
// | '(' expression ')' #parenthesizedTerm
// | '{' '}' #nullLiteral
// ;
Expand Down Expand Up @@ -102,7 +101,6 @@ public static Parser<Expression> FunctionInvocation(Expression focus)
public static readonly Parser<Expression> Term =
Literal
.Or(FunctionInvocation(AxisExpression.That))
.XOr(Lexer.ExternalVariable.Select(n => new ContextVariableRefExpression(n)))
.Or(Lexer.ExternalConstant.Select(n => BuildVariableRefExpression(n))) //Was .XOr(Lexer.ExternalConstant.Select(v => Eval.ExternalConstant(v)))
.XOr(BracketExpr)
.XOr(EmptyList)
Expand Down
7 changes: 0 additions & 7 deletions src/Hl7.Fhir.Base/FhirPath/Parser/Lexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,6 @@ from closeQ in Parse.Char(delimiter)
// ;
public static readonly Parser<string> Identifier =
Id.XOr(DelimitedIdentifier);

// externalVariable
// : '%%' identifier
// ;
public static readonly Parser<string> ExternalVariable =
Parse.String("%%").Then(_ => Identifier.XOr(String))
.Named("external variable");

// externalConstant
// : '%' identifier
Expand Down
Loading

0 comments on commit dc32002

Please sign in to comment.