diff --git a/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs b/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs index 35884e22f5..5087720033 100644 --- a/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs +++ b/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs @@ -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 - } - - /// - /// Create an EvaluationContext with the given value for %resource. - /// - /// The data that will be represented by %resource - public EvaluationContext(ITypedElement resource) : this(resource, null) { } - - /// - /// Create an EvaluationContext with the given value for %resource and %rootResource. - /// - /// The data that will be represented by %resource. - /// The data that will be represented by %rootResource. - public EvaluationContext(ITypedElement resource, ITypedElement rootResource) - { - Resource = resource; - RootResource = rootResource ?? resource; - } - - public EvaluationContext(ITypedElement resource, ITypedElement rootResource, IDictionary> environment) : this(resource, rootResource) - { - Environment = environment; - } - - /// - /// The data represented by %rootResource. - /// - public ITypedElement RootResource { get; set; } - - /// - /// The data represented by %resource. - /// - public ITypedElement Resource { get; set; } + // no defaults yet + } + + /// + /// Create an EvaluationContext with the given value for %resource. + /// + /// The data that will be represented by %resource + public EvaluationContext(ITypedElement? resource) : this(resource, null) { } + + /// + /// Create an EvaluationContext with the given value for %resource and %rootResource. + /// + /// The data that will be represented by %resource. + /// The data that will be represented by %rootResource. + public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource) + { + Resource = resource; + RootResource = rootResource ?? resource; + } - /// - /// The environment variables that are available to the FHIRPath expressions. - /// - public IDictionary> Environment { get; set; } - - /// - /// A delegate that handles the output for the trace() function. - /// - public Action> Tracer { get; set; } + public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, IDictionary> environment) : this(resource, rootResource) + { + Environment = environment; } + + /// + /// The data represented by %rootResource. + /// + public ITypedElement? RootResource { get; set; } + + /// + /// The data represented by %resource. + /// + public ITypedElement? Resource { get; set; } + + /// + /// The environment variables that are available to the FHIRPath expressions. + /// + public IDictionary> Environment { get; set; } = new Dictionary>(); + + /// + /// A delegate that handles the output for the trace() function. + /// + public Action>? Tracer { get; set; } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs index b88580d522..1ab9a5727a 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs @@ -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 { @@ -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 }); diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/EvaluatorVisitor.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/EvaluatorVisitor.cs index 19a675ca85..4c4fa60ab8 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/EvaluatorVisitor.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/EvaluatorVisitor.cs @@ -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 chainResolves(Closure context, IEnumerable invokees) { - return Cvre.Resolve; + return context.ResolveValue(expression.Name) ?? resolve(Symbols, expression.Name, Enumerable.Empty())(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()); } - + private static Invokee resolve(SymbolTable scope, string name, IEnumerable argumentTypes) { // For now, we don't have the types or the parameters statically, so we just match on name diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs index 98b9718dc9..fd84567cdc 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/ExpressionNode.cs @@ -329,14 +329,6 @@ public override int GetHashCode() return base.GetHashCode() ^ Name.GetHashCode(); } } - - public class ContextVariableRefExpression(string name) : VariableRefExpression(name) - { - internal IEnumerable Resolve(Closure context, IEnumerable _) - { - return context.EvaluationContext.Environment[Name] ?? throw Error.InvalidOperation($"Variable {Name} not found in environment"); - } - } public class AxisExpression : VariableRefExpression { diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs index 688d4d2f78..a89c57d534 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs @@ -206,7 +206,8 @@ public static Invokee Invoke(string functionName, IEnumerable arguments { try { - return invokee(ctx, arguments); + var wrappedArguments = arguments.Skip(1).Select(wrapWithNextContext); + return invokee(ctx, [arguments.First(),.. wrappedArguments]); } catch (Exception e) { @@ -214,6 +215,14 @@ public static Invokee Invoke(string functionName, IEnumerable arguments $"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) { diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs index eb05411612..5ba897213e 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs @@ -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), typeof(object), typeof(Invokee)), runRepeat); t.Add(new CallSignature("trace", typeof(IEnumerable), typeof(string), typeof(object), typeof(Invokee)), Trace); + t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string)), DefineVariable); + t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string), typeof(Invokee)), DefineVariable); t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee)), runAggregate); t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee), typeof(Invokee)), runAggregate); @@ -298,6 +300,29 @@ private static IEnumerable Trace(Closure ctx, IEnumerable DefineVariable(Closure ctx, IEnumerable 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 runIif(Closure ctx, IEnumerable arguments) { diff --git a/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs b/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs index 74b96e72cd..d8ed5b71cd 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Parser/Grammar.cs @@ -60,7 +60,6 @@ internal class Grammar // : invocation #invocationTerm // | literal #literalTerm // | externalConstant #externalConstantTerm - // | externalVariable #externalVariableTerm // | '(' expression ')' #parenthesizedTerm // | '{' '}' #nullLiteral // ; @@ -102,7 +101,6 @@ public static Parser FunctionInvocation(Expression focus) public static readonly Parser 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) diff --git a/src/Hl7.Fhir.Base/FhirPath/Parser/Lexer.cs b/src/Hl7.Fhir.Base/FhirPath/Parser/Lexer.cs index b412267b4b..11a2874d1c 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Parser/Lexer.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Parser/Lexer.cs @@ -78,13 +78,6 @@ from closeQ in Parse.Char(delimiter) // ; public static readonly Parser Identifier = Id.XOr(DelimitedIdentifier); - - // externalVariable - // : '%%' identifier - // ; - public static readonly Parser ExternalVariable = - Parse.String("%%").Then(_ => Identifier.XOr(String)) - .Named("external variable"); // externalConstant // : '%' identifier diff --git a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs index 541135c0da..31254c4006 100644 --- a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs +++ b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTest.cs @@ -9,6 +9,8 @@ // To introduce the DSTU2 FHIR specification // extern alias dstu2; +using FluentAssertions; +using Hl7.Fhir.ElementModel; using Hl7.Fhir.FhirPath; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; @@ -17,11 +19,9 @@ using Hl7.FhirPath.Tests; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using System.Xml.Linq; namespace Hl7.FhirPath.R4.Tests @@ -29,6 +29,7 @@ namespace Hl7.FhirPath.R4.Tests public class PatientFixture : IDisposable { public Patient TestInput; + public Patient PatientExample; public Questionnaire Questionnaire; public StructureDefinition UuidProfile; public int Counter = 0; @@ -41,6 +42,9 @@ public PatientFixture() TestInput = parser.Parse(tpXml); + var epXml = TestData.ReadTextFile("patient-example.xml"); + PatientExample = parser.Parse(epXml); + tpXml = TestData.ReadTextFile("questionnaire-example.xml"); Questionnaire = parser.Parse(tpXml); @@ -366,66 +370,313 @@ public void TestStringOps() // var pat = (new FhirXmlParser()).Parse(patXml); // var patNav = new PocoNavigator(pat); - // var result = PathExpression.Select("name.given | name.family", new[] { patNav }); + // var result = PathExpression.Select("name.given | name.family", new[] { patNav } // Assert.Equal(5, result.Count()); //} [TestMethod] public void CompilationIsCached() { - //setup, use reflection to access cache. - var cacheDictionary = getCache(); + // If the test failed, try again, we might have been + // bugged by temporary slowness of the CI build. + if (!test()) + { + Assert.IsTrue(test()); + } - fixture.TestInput.Select($"Patient.name[0]"); - Assert.IsTrue(checkIfPresentInCache(cacheDictionary, $"Patient.name[0]")); + static bool test() + { + var uncached = run(null, out var last); + var cached = run(last, out var _); + Console.WriteLine("Uncached: {0}, cached: {1}".FormatWith(uncached, cached)); - Assert.IsFalse(checkIfPresentInCache(cacheDictionary, $"Patient.name[1]")); + return cached < uncached / 2; + } + + static long run(string fixd, out string lastExpression) + { + lastExpression = null; + var sw = new Stopwatch(); + sw.Start(); + + var random = new Random(); + + // something that has not been compiled before + for (int i = 0; i < 1000; i++) + { + var next = random.Next(0, 10000); + lastExpression = fixd ?? $"Patient.name[{next}]"; + fixture.TestInput.Select(lastExpression); + } + sw.Stop(); + + return sw.ElapsedMilliseconds; + } + } - fixture.TestInput.Select($"Patient.name[1]"); - Assert.IsTrue(checkIfPresentInCache(cacheDictionary, $"Patient.name[1]")); + // Verifies https://github.com/FirelyTeam/firely-net-sdk/issues/1140 + [TestMethod] + public void TestELD13Bug() + { + var emptyPat = new Patient(); + // Test how the engine treats primitives with no values in operations that + // do not propagate null values.... + Assert.AreEqual("", emptyPat.Scalar("name & gender")); + } + } + + [TestClass] + public class FhirPathDefineVariableTests + { + static PatientFixture fixture; + + [ClassInitialize] + public static void Initialize(TestContext ctx) + { + fixture = new PatientFixture(); } [TestMethod] - public void TestDateTimeArithmetic() + public void SimplestVariable() { - fixture.IsTrue(@"(Patient.birthDate + 100 years) > @2000"); - fixture.IsTrue(@"(Patient.birthDate - 100 years) < @2000"); - fixture.IsTrue(@"(now() - 100 seconds) < now()"); - fixture.IsTrue(@"(now() + 100 seconds) > now()"); + var expr = "defineVariable('v1', 'value1').select(%v1)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(1, r.Count()); + Assert.AreEqual("value1", r.First().ToString()); } - private ConcurrentDictionary> getCache() + [TestMethod] + public void SimpleUseOfAVariable() { - var cache = typeof(FhirPathExtensions) - .GetField("CACHE", BindingFlags.NonPublic | BindingFlags.Static) - .GetValue(null); + var expr = "defineVariable('n1', name.first()).select(%n1.given)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(2, r.Count()); + Assert.AreEqual("Peter", r.First().ToString()); + Assert.AreEqual("James", r.Skip(1).First().ToString()); + // .toStrictEqual(["Peter", "James"]); + } + [TestMethod] + public void simple_use_of_a_variable_2_selects() + { + var expr = "defineVariable('n1', name.first()).select(%n1.given).first()"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(1, r.Count()); + Assert.AreEqual("Peter", r.First().ToString()); + } - var cachedExpressions = typeof(FhirPathCompilerCache) - .GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance) - .GetValue(cache) as Cache; + [TestMethod] + public void use_of_a_variable_in_separate_contexts() + { + // this example defines the same variable name in 2 different contexts + // this shouldn't report an issue where the variable is being redefined (as it's not in the same context) + var expr = "defineVariable('n1', name.first()).select(%n1.given) | defineVariable('n1', name.skip(1).first()).select(%n1.given)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(3, r.Count()); + Assert.AreEqual("Peter", r.First().ToString()); + Assert.AreEqual("James", r.Skip(1).First().ToString()); + Assert.AreEqual("Jim", r.Skip(2).First().ToString()); + // .toStrictEqual(["Peter", "James", "Jim"]); + } + [TestMethod] + public void use_of_a_variable_in_separate_contexts_defined_in_2_but_used_in_1() + { + // this example defines the same variable name in 2 different contexts, + // but only uses it in the second. This ensures that the first context doesn't remain when using it in another context + var expr = "defineVariable('n1', name.first()).where(active.not()) | defineVariable('n1', name.skip(1).first()).select(%n1.given)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(1, r.Count()); + Assert.AreEqual("Jim", r.First().ToString()); + // .toStrictEqual(["Jim"]); + } - return typeof(Cache) - .GetField("_cached", BindingFlags.NonPublic | BindingFlags.Instance) - .GetValue(cachedExpressions) as ConcurrentDictionary>; + [TestMethod] + public void use_of_different_variables_in_different_contexts() + { + var expr = "defineVariable('n1', name.first()).select(id & '-' & %n1.given.join('|')) | defineVariable('n2', name.skip(1).first()).select(%n2.given)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(2, r.Count()); + Assert.AreEqual("example-Peter|James", r.First().ToString()); + Assert.AreEqual("Jim", r.Skip(1).First().ToString()); + // .toStrictEqual(["example-Peter|James", "Jim"]); } - private static bool checkIfPresentInCache(ConcurrentDictionary> cache, string expression) + [TestMethod] + public void Two_vars_one_unused() { - return cache.TryGetValue(expression, out var result); + var expr = "defineVariable('n1', name.first()).active | defineVariable('n2', name.skip(1).first()).select(%n2.given)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(2, r.Count()); + Assert.AreEqual(true, ((FhirBoolean)r.First()).Value); + Assert.AreEqual("Jim", r.Skip(1).First().ToString()); + // .toStrictEqual([true, "Jim"]); } - // Verifies https://github.com/FirelyTeam/firely-net-sdk/issues/1140 [TestMethod] - public void TestELD13Bug() + public void composite_variable_use() { - var emptyPat = new Patient(); + var expr = "defineVariable('v1', 'value1').select(%v1).trace('data').defineVariable('v2', 'value2').select($this & ':' & %v1 & '-' & %v2) | defineVariable('v3', 'value3').select(%v3)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(2, r.Count()); + Assert.AreEqual("value1:value1-value2", r.First().ToString()); + Assert.AreEqual("value3", r.Skip(1).First().ToString()); + //.toStrictEqual(["value1:value1-value2", "value3"]); + } - // Test how the engine treats primitives with no values in operations that - // do not propagate null values.... - Assert.AreEqual("", emptyPat.Scalar("name & gender")); + + + [TestMethod] + public void use_of_a_variable_outside_context_throws_error() + { + // test with a variable that is not in the context that should throw an error + var expr = "defineVariable('n1', name.first()).active | defineVariable('n2', name.skip(1).first()).select(%n1.given)"; + try + { + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.Fail("Should have thrown an exception"); + } + catch(InvalidOperationException ex) + { + ex.Message.Should().Contain("Unknown symbol 'n1'"); + } + } + + [TestMethod] + public void use_undefined_variable_throws_error() + { + // test with a variable that is not in the context that should throw an error + var expr = "select(%fam.given)"; + try + { + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.Fail("Should have thrown an exception"); + } + catch(InvalidOperationException ex) + { + ex.Message.Should().Contain("Unknown symbol 'fam'"); + } + } + + [TestMethod] + public void redefining_variable_throws_error() + { + var expr = "defineVariable('v1').defineVariable('v1').select(%v1)"; + Assert.ThrowsException(() => fixture.PatientExample.Select(expr).ToList()); + } + + + [TestMethod] + public void sequence_of_variable_definitions_tweak() + { + var expr = "Patient.name.defineVariable('n2', skip(1).first()).defineVariable('res', %n2.given+%n2.given).select(%res)"; + var r = fixture.PatientExample.Select(expr).ToList(); + foreach (var item in r) { Console.WriteLine(item.ToXml()); } + Assert.AreEqual(2, r.Count()); + Assert.AreEqual("JimJim", r.First().ToString()); + Assert.AreEqual("JimJim", r.Skip(1).First().ToString()); + // .toStrictEqual(["JimJim", "JimJim", "JimJim"]); + } + + [TestMethod] + public void sequence_of_variable_definitions_original() + { + // A variable defined based on another variable + var expr = "Patient.name.defineVariable('n1', first()).exists(%n1) | Patient.name.defineVariable('n2', skip(1).first()).defineVariable('res', %n2.given+%n2.given).select(%res)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(2, r.Count()); + Assert.AreEqual(true, ((FhirBoolean)r.First()).Value); + Assert.AreEqual("JimJim", r.Skip(1).First().ToString()); + // the duplicate JimJim values are removed due to the | operator + // .toStrictEqual([true, "JimJim"]); + } + + + [TestMethod] + public void multi_tree_vars_valid() + { + var expr = "defineVariable('root', 'r1-').select(defineVariable('v1', 'v1').defineVariable('v2', 'v2').select(%v1 | %v2)).select(%root & $this)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(2, r.Count()); + Assert.AreEqual("r1-v1", r.First().ToString()); + Assert.AreEqual("r1-v2", r.Skip(1).First().ToString()); + // .toStrictEqual(["r1-v1", "r1-v2"]); + } + + [TestMethod] + public void defineVariable_with_compile_success() + { + var expr = "defineVariable('root', 'r1-').select(defineVariable('v1', 'v1').defineVariable('v2', 'v2').select(%v1 | %v2)).select(%root & $this)"; + var compiler = new FhirPathCompiler(); + var exprCompiled = compiler.Compile(expr); + var r = exprCompiled(fixture.PatientExample.ToTypedElement(), FhirEvaluationContext.CreateDefault()); + Assert.AreEqual(2, r.Count()); + Assert.AreEqual("r1-v1", r.First().ToString()); + Assert.AreEqual("r1-v2", r.Skip(1).First().ToString()); + // .toStrictEqual(["r1-v1", "r1-v2"]); + } + /* + [TestMethod] + public void defineVariable_with_compile_error() + { + var expr = "defineVariable('root', 'r1-').select(defineVariable('v1', 'v1').defineVariable('v2', 'v2').select(%v1 | %v2)).select(%root & $this & %v1)"; + var f = fhirpath.compile(expr, r4_model); + expect(() => { f(input.patientExample); }) + .toThrowError("Attempting to access an undefined environment variable: v1"); + } + + [TestMethod] + public void defineVariable_cant_overwrite_an_environment_var() + { + var expr = "defineVariable('context', 'oops')"; + var f = fhirpath.compile(expr, r4_model); + expect(() => { f(input.patientExample); }) + .toThrowError("Environment Variable %context already defined"); } + + [TestMethod] + public void realistic_example_with_conceptmap() + { + var expr = """ + group.select( + defineVariable('grp') + .element + .select( + defineVariable('ele') + .target + .select(% grp.source & '|' & % ele.code & ' ' & equivalence & ' ' & % grp.target & '|' & code) + ) + ) + .trace('all') + .isDistinct() + """; + expect(fhirpath.evaluate(input.conceptMapExample, expr, r4_model) + ).toStrictEqual([ + false + ]); + } + */ + + [TestMethod] + public void defineVariable_in_function_parameters1() + { + var expr = "defineVariable(defineVariable('param','ppp').select(%param), defineVariable('param','value').select(%param)).select(%ppp)"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(1, r.Count()); + Assert.AreEqual("value", r.First().ToString()); + // .toStrictEqual(["value"]); + } + + [TestMethod] + public void defineVariable_in_function_parameters2() + { + var expr = "'aaa'.replace(defineVariable('param', 'aaa').select(%param), defineVariable('param','bbb').select(%param))"; + var r = fixture.PatientExample.Select(expr).ToList(); + Assert.AreEqual(1, r.Count()); + Assert.AreEqual("bbb", r.First().ToString()); + // .toStrictEqual(["bbb"]); + } + } -} +} \ No newline at end of file diff --git a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTestFromSpec.cs b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTestFromSpec.cs index bd3416d1f8..e3c2110692 100644 --- a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTestFromSpec.cs +++ b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathEvaluatorTestFromSpec.cs @@ -177,7 +177,7 @@ private void runTests(string pathToTest, IEnumerable ignoreTestcases) bool invalid = expressionNode.Attribute("invalid")?.Value == "true"; if (mode?.Value == "strict" || invalid) continue; // don't do 'strict' or invlaid tests yet - string basepath = Path.Combine(TestData.GetTestDataBasePath(), @"fhirpath\input"); + string basepath = Path.Combine(TestData.GetTestDataBasePath(), @"fhirpath/input"); if (!_cache.ContainsKey(inputfile)) { diff --git a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathScaleTest.cs b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathScaleTest.cs index dd493de137..d887f88d3c 100644 --- a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathScaleTest.cs +++ b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathScaleTest.cs @@ -19,7 +19,7 @@ public class FhirPathPerformanceTests [TestMethod] public void QuestionnaireResponseFhirpathPocoTest() { - var xml = File.ReadAllText(@"TestData\Large-QuestionnaireResponse.xml"); + var xml = File.ReadAllText(@"TestData/Large-QuestionnaireResponse.xml"); var qr = (new FhirXmlParser()).Parse(xml); diff --git a/src/Hl7.FhirPath.Tests/Tests/EnviromentTests.cs b/src/Hl7.FhirPath.Tests/Tests/EnviromentTests.cs index 8901e1e387..03f2e4a8b5 100644 --- a/src/Hl7.FhirPath.Tests/Tests/EnviromentTests.cs +++ b/src/Hl7.FhirPath.Tests/Tests/EnviromentTests.cs @@ -12,7 +12,7 @@ public class EnviromentTests public void TestEnvironment() { var compiler = new FhirPathCompiler(); - var expr = compiler.Compile("%%var = 1"); + var expr = compiler.Compile("%var = 1"); expr.IsTrue(null, new EvaluationContext(null, null, new Dictionary> { { "var", new [] { ElementNode.ForPrimitive(1) } } })); expr.IsBoolean(false, null, new EvaluationContext(null, null, new Dictionary> { { "var", new[] { ElementNode.ForPrimitive(2) } } })); diff --git a/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs b/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs index 4b3405ef32..f6bd6aa46f 100644 --- a/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs +++ b/src/Hl7.FhirPath.Tests/Tests/FhirPathGrammarTest.cs @@ -75,7 +75,6 @@ public void FhirPath_Gramm_Term() AssertParser.SucceedsMatch(parser, "doSomething('hi', 3.14)", new FunctionCallExpression(AxisExpression.This, "doSomething", TypeSpecifier.Any, new ConstantExpression("hi"), new ConstantExpression(3.14m))); AssertParser.SucceedsMatch(parser, "%external", new VariableRefExpression("external")); - AssertParser.SucceedsMatch(parser, "%%contextvar", new ContextVariableRefExpression("contextvar")); AssertParser.SucceedsMatch(parser, "@2013-12", new ConstantExpression(P.Date.Parse("2013-12"))); AssertParser.SucceedsMatch(parser, "@2013-12T", new ConstantExpression(P.DateTime.Parse("2013-12"))); AssertParser.SucceedsMatch(parser, "3", new ConstantExpression(3));