Skip to content

Commit

Permalink
Merge pull request #2841 from FirelyTeam/feature/FP-extensions-defaul…
Browse files Browse the repository at this point in the history
…t-context

%resource and %rootResource in default context
  • Loading branch information
ewoutkramer authored Sep 4, 2024
2 parents b7c15eb + eddf26d commit 4c38d9a
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 26 deletions.
6 changes: 6 additions & 0 deletions src/Hl7.Fhir.Base/ElementModel/ScopedNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private ScopedNode(ScopedNode parentNode, ScopedNode? parentResource, ITypedElem
Current = wrapped;
ExceptionHandler = parentNode.ExceptionHandler;
ParentResource = parentNode.AtResource ? parentNode : parentResource;
Parent = parentNode;

_fullUrl = fullUrl;

Expand All @@ -67,6 +68,11 @@ private ScopedNode(ScopedNode parentNode, ScopedNode? parentResource, ITypedElem
/// </remarks>
public readonly ScopedNode? ParentResource;

/// <summary>
/// The resource or element which is the direct parent of this node.
/// </summary>
public readonly ScopedNode? Parent;

/// <summary>
/// Returns the location of the current element within its most direct parent resource or datatype.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ namespace Hl7.FhirPath;

public class EvaluationContext
{
[Obsolete("This method does not initialize any members and will be removed in a future version. Use the empty constructor instead.")]
public static EvaluationContext CreateDefault() => new();


public EvaluationContext()
{
// no defaults yet
Expand All @@ -19,24 +21,33 @@ public EvaluationContext()
/// Create an EvaluationContext with the given value for <c>%resource</c>.
/// </summary>
/// <param name="resource">The data that will be represented by %resource</param>
[Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the EvaluationContext.WithResourceOverrides() method.")]
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>
[Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the EvaluationContext.WithResourceOverrides() method.")]
public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource)
{
Resource = resource;
RootResource = rootResource ?? resource;
}

[Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the EvaluationContext.WithResourceOverrides() method. Environment can be set explicitly after construction of the base context")]
public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, IDictionary<string, IEnumerable<ITypedElement>> environment) : this(resource, rootResource)
{
Environment = environment;
}

/// <summary>
/// Explicitly override the values of %resource and %rootResource in the evaluation context.
/// </summary>
public static EvaluationContext WithResourceOverrides(ITypedElement? resource, ITypedElement? rootResource = null) =>
new EvaluationContext { Resource = resource, RootResource = rootResource ?? resource };

/// <summary>
/// The data represented by <c>%rootResource</c>.
/// </summary>
Expand Down
44 changes: 34 additions & 10 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,40 @@ public Closure()

public static Closure Root(ITypedElement root, EvaluationContext ctx = null)
{
var newContext = new Closure() { EvaluationContext = ctx ?? EvaluationContext.CreateDefault() };
var newContext = ctx ?? new EvaluationContext();

var node = root as ScopedNode;

newContext.Resource ??= node != null // if the value has been manually set, we do nothing. Otherwise, if the root is a scoped node:
? getResourceFromNode(node) // we infer the resource from the scoped node
: (root?.Definition?.IsResource is true // if we do not have a scoped node, we see if this is even a resource to begin with
? root // if it is, we use the root as the resource
: null // if not, this breaks the spec in every way (but we will still continue, hopefully we do not need %resource or %rootResource)
);

// Same thing, but we copy the resource into the root resource if we cannot infer it from the node.
newContext.RootResource ??= node != null
? getRootResourceFromNode(node)
: newContext.Resource;

var newClosure = new Closure() { EvaluationContext = ctx ?? new EvaluationContext() };

var input = new[] { root };

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

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

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

return newContext;
return newClosure;
}

private Dictionary<string, IEnumerable<ITypedElement>> _namedValues = new Dictionary<string, IEnumerable<ITypedElement>>();
Expand Down Expand Up @@ -82,5 +98,13 @@ public virtual IEnumerable<ITypedElement> ResolveValue(string name)

return null;
}

private static ScopedNode getResourceFromNode(ScopedNode node) => node.AtResource ? node : node.ParentResource;

private static ScopedNode getRootResourceFromNode(ScopedNode node)
{
var resource = getResourceFromNode(node);
return resource?.Name is "contained" ? resource.ParentResource : resource;
}
}
}
24 changes: 19 additions & 5 deletions src/Hl7.Fhir.Base/FhirPath/FhirEvaluationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,30 @@
using System;
using System.Collections.Generic;

#nullable enable

namespace Hl7.Fhir.FhirPath
{
public class FhirEvaluationContext : EvaluationContext
{
/// <summary>Creates a new <see cref="FhirEvaluationContext"/> instance with default property values.</summary>
[Obsolete("This method does not initialize any members and will be removed in a future version. Use the empty constructor instead.")]
public static new FhirEvaluationContext CreateDefault() => new();

/// <summary>Default constructor. Creates a new <see cref="FhirEvaluationContext"/> instance with default property values.</summary>
public FhirEvaluationContext() : base()
public FhirEvaluationContext()
{
}

/// <inheritdoc cref="EvaluationContext(ITypedElement)"/>
[Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the FhirEvaluationContext.WithResourceOverrides() method.")]
public FhirEvaluationContext(ITypedElement resource) : base(resource)
{
}

/// <inheritdoc cref="EvaluationContext(ITypedElement, ITypedElement)"/>
public FhirEvaluationContext(ITypedElement resource, ITypedElement rootResource) : base(resource, rootResource)
[Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the FhirEvaluationContext.WithResourceOverrides() method.")]
public FhirEvaluationContext(ITypedElement? resource, ITypedElement? rootResource) : base(resource, rootResource)
{
}

Expand All @@ -39,6 +44,7 @@ public FhirEvaluationContext(ITypedElement resource, ITypedElement rootResource)
/// </summary>
/// <param name="resource"></param>
/// <param name="environment"></param>
[Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the FhirEvaluationContext.WithResourceOverrides() method.")]
public FhirEvaluationContext(ITypedElement resource, IDictionary<string, IEnumerable<ITypedElement>> environment) : base(resource, null, environment)
{
}
Expand All @@ -47,13 +53,19 @@ public FhirEvaluationContext(ITypedElement resource, IDictionary<string, IEnumer
/// Create a FhirEvaluationContext and also set the variables <c>%resource</c> and <c>%rootResource</c> to their correct values.
/// </summary>
/// <param name="node">input for determining the variables <c>%resource</c> and <c>%rootResource</c></param>
[Obsolete("%resource and %rootResource are inferred from scoped nodes by the evaluator. If you do not have access to a scoped node, or if you wish to explicitly override this behaviour, use the FhirEvaluationContext.WithResourceOverrides() method.")]
public FhirEvaluationContext(ScopedNode node)
: this(toNearestResource(node))
{
RootResource = Resource is ScopedNode sn ? sn.ResourceContext : node;
}

public ITerminologyService TerminologyService { get; set; }
/// <summary>
/// Explicitly override the values of %resource and %rootResource in the evaluation context.
/// </summary>
public static new FhirEvaluationContext WithResourceOverrides(ITypedElement? resource, ITypedElement? rootResource = null) =>
(FhirEvaluationContext)EvaluationContext.WithResourceOverrides(resource, rootResource);
public ITerminologyService? TerminologyService { get; set; }

private static ITypedElement toNearestResource(ScopedNode node)
{
Expand All @@ -67,12 +79,14 @@ private static ITypedElement toNearestResource(ScopedNode node)
return scan;
}

private Func<string, ITypedElement> _elementResolver;
private Func<string, ITypedElement>? _elementResolver;

public Func<string, ITypedElement> ElementResolver
public Func<string, ITypedElement>? ElementResolver
{
get { return _elementResolver; }
set { _elementResolver = value; }
}
}
}

#nullable restore
10 changes: 5 additions & 5 deletions src/Hl7.Fhir.Base/FhirPath/FhirPathCompilerCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public IEnumerable<ITypedElement> Select(ITypedElement input, string expression,
{
input = input.ToScopedNode();
var evaluator = GetCompiledExpression(expression);
return evaluator(input, ctx ?? EvaluationContext.CreateDefault());
return evaluator(input, ctx ?? new EvaluationContext());
}

/// <summary>
Expand All @@ -76,7 +76,7 @@ public IEnumerable<ITypedElement> Select(ITypedElement input, string expression,
{
input = input.ToScopedNode();
var evaluator = GetCompiledExpression(expression);
return evaluator.Scalar(input, ctx ?? EvaluationContext.CreateDefault());
return evaluator.Scalar(input, ctx ?? new EvaluationContext());
}

/// <summary>
Expand All @@ -90,7 +90,7 @@ public bool Predicate(ITypedElement input, string expression, EvaluationContext?
{
input = input.ToScopedNode();
var evaluator = GetCompiledExpression(expression);
return evaluator.Predicate(input, ctx ?? EvaluationContext.CreateDefault());
return evaluator.Predicate(input, ctx ?? new EvaluationContext());
}

/// <summary>
Expand All @@ -104,7 +104,7 @@ public bool IsTrue(ITypedElement input, string expression, EvaluationContext? ct
{
input = input.ToScopedNode();
var evaluator = GetCompiledExpression(expression);
return evaluator.IsTrue(input, ctx ?? EvaluationContext.CreateDefault());
return evaluator.IsTrue(input, ctx ?? new EvaluationContext());
}


Expand All @@ -121,7 +121,7 @@ public bool IsBoolean(ITypedElement input, string expression, bool value, Evalua
input = input.ToScopedNode();

var evaluator = GetCompiledExpression(expression);
return evaluator.IsBoolean(value, input, ctx ?? EvaluationContext.CreateDefault());
return evaluator.IsBoolean(value, input, ctx ?? new EvaluationContext());
}

}
Expand Down
10 changes: 5 additions & 5 deletions src/Hl7.Fhir.Shims.STU3AndUp/FhirPath/FhirPathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,23 @@ public static class FhirPathExtensions

/// <inheritdoc cref="FhirPathCompilerCache.Select(ITypedElement, string, EvaluationContext?)"/>
public static IEnumerable<Base?> Select(this Base input, string expression, FhirEvaluationContext? ctx = null)
=> CACHE.Select(input.ToTypedElement(), expression, ctx ?? FhirEvaluationContext.CreateDefault()).ToFhirValues();
=> CACHE.Select(input.ToTypedElement().ToScopedNode(), expression, ctx ?? new FhirEvaluationContext()).ToFhirValues();

/// <inheritdoc cref="FhirPathCompilerCache.Scalar(ITypedElement, string, EvaluationContext?)"/>
public static object? Scalar(this Base input, string expression, FhirEvaluationContext? ctx = null)
=> CACHE.Scalar(input.ToTypedElement(), expression, ctx ?? FhirEvaluationContext.CreateDefault());
=> CACHE.Scalar(input.ToTypedElement().ToScopedNode(), expression, ctx ?? new FhirEvaluationContext());

/// <inheritdoc cref="FhirPathCompilerCache.Predicate(ITypedElement, string, EvaluationContext?)"/>
public static bool Predicate(this Base input, string expression, FhirEvaluationContext? ctx = null)
=> CACHE.Predicate(input.ToTypedElement(), expression, ctx ?? FhirEvaluationContext.CreateDefault());
=> CACHE.Predicate(input.ToTypedElement().ToScopedNode(), expression, ctx ?? new FhirEvaluationContext());

/// <inheritdoc cref="FhirPathCompilerCache.IsTrue(ITypedElement, string, EvaluationContext?)"/>
public static bool IsTrue(this Base input, string expression, FhirEvaluationContext? ctx = null)
=> CACHE.IsTrue(input.ToTypedElement(), expression, ctx ?? FhirEvaluationContext.CreateDefault());
=> CACHE.IsTrue(input.ToTypedElement().ToScopedNode(), expression, ctx ?? new FhirEvaluationContext());

/// <inheritdoc cref="FhirPathCompilerCache.IsBoolean(ITypedElement, string, bool, EvaluationContext?) "/>
public static bool IsBoolean(this Base input, string expression, bool value, FhirEvaluationContext? ctx = null)
=> CACHE.IsBoolean(input.ToTypedElement(), expression, value, ctx ?? FhirEvaluationContext.CreateDefault());
=> CACHE.IsBoolean(input.ToTypedElement().ToScopedNode(), expression, value, ctx ?? new FhirEvaluationContext());
}
}

Expand Down
39 changes: 39 additions & 0 deletions src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathContextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using FluentAssertions;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.FhirPath;
using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;
using System.Linq;

namespace Hl7.FhirPath.R4.Tests.PocoTests;

[TestClass]
public class FhirPathContextTests
{
ScopedNode _bundle;

[TestInitialize]
public void SetupSource()
{
var bundleXml = File.ReadAllText(Path.Combine("TestData", "bundle-contained-references.xml"));

_bundle = ((new FhirXmlParser()).Parse<Bundle>(bundleXml)).ToTypedElement().ToScopedNode();
}

[TestMethod]
public void TestFhirEvaluationContext()
{
_bundle.IsTrue("entry[2].resource.contained[0].select(%resource) = %resource"); // should stay the same
_bundle.IsTrue("%rootResource = Bundle.entry[2].resource.contained[0].select(%rootResource)"); // should stay the same

var elemInContainedResource = _bundle.Children("entry").Skip(2).First().Children("resource").First().Children("contained").First().Children("id").First().ToScopedNode();
elemInContainedResource.IsTrue("%rootResource != %resource"); // should be true
elemInContainedResource.Select("%rootResource").Should().BeEquivalentTo(_bundle.Select("entry[2].resource"));

var elemInDomainResource = _bundle.Children("entry").Skip(2).First().Children("resource").First().Children("id").First().ToScopedNode();
elemInDomainResource.IsTrue("%rootResource = %resource");
elemInDomainResource.Select("%rootResource").Should().BeEquivalentTo(_bundle.Select("entry[2].resource"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,5 @@ public void defineVariable_in_function_parameters2()
Assert.AreEqual("bbb", r.First().ToString());
// .toStrictEqual(["bbb"]);
}

}
}
1 change: 1 addition & 0 deletions src/Hl7.FhirPath.Tests/Tests/EnviromentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Hl7.FhirPath;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Text.Json;

namespace HL7.FhirPath.Tests.Tests;

Expand Down

0 comments on commit 4c38d9a

Please sign in to comment.