Skip to content

Commit

Permalink
After PR review and discussions.
Browse files Browse the repository at this point in the history
* Made FirelyCqlContext a factory class instead of a subclass
* Have left CqlContext open for subclassing
* Added a generic event system for the CQL Engine to use, which replaces both the existing extension mechanism and the OnFunctionCalled event.
* Added an explicit MessageReceived event to the CqlContext (just a forward to the one on Operators)
  • Loading branch information
ewoutkramer committed Sep 5, 2023
1 parent bdaaa7f commit 0c9cbca
Show file tree
Hide file tree
Showing 13 changed files with 74 additions and 129 deletions.
4 changes: 2 additions & 2 deletions Cql/CoreTests/ModelTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ public UnitTestDataSource(IEnumerable<object> data)

public IList<object> Data { get; }

#pragma warning disable CS0067 // The event 'ModelTest.UnitTestDataSource.DataChanged' is never used
#if VNEXT
public event EventHandler DataChanged;
#pragma warning restore CS0067 // The event 'ModelTest.UnitTestDataSource.DataChanged' is never used
#endif

public IEnumerable<T> RetrieveByCodes<T>(IEnumerable<CqlCode> codes = null, PropertyInfo _ = null) where T : class =>
Data.OfType<T>();
Expand Down
18 changes: 2 additions & 16 deletions Cql/Cql.Compiler/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ public ExpressionBuilder(OperatorBinding operatorBinding,
this.options = options ?? new(EmitStackTraces: false);
if (Library.identifier == null)
throw new ArgumentException("Package is missing a library identifier", nameof(elm));

}

/// <summary>
Expand Down Expand Up @@ -2238,19 +2237,6 @@ protected Expression FunctionRef(FunctionRef op, ExpressionBuilderContext ctx)

var funcType = GetFuncType(funcTypeParameters);

var callStackCtor = typeof(FunctionCallEvent).GetConstructor(new[] { typeof(string), typeof(string), typeof(string) })!;
var newCallStack = Expression.New(callStackCtor,
Expression.Constant(op.name, typeof(string)),
Expression.Constant(op.locator, typeof(string)),
Expression.Constant(op.localId, typeof(string)));

Expression deeper = options.EmitStackTraces
? Expression.Call(
ctx.RuntimeContextParameter,
typeof(CqlContext).GetMethod(nameof(CqlContext.OnFunctionCalled), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!,
newCallStack)
: ctx.RuntimeContextParameter;

// FHIRHelpers has special handling in CQL-to-ELM and does not translate correctly - specifically,
// it interprets ToString(value string) oddly. Normally when string is used in CQL it is resolved to the elm type.
// In FHIRHelpers, this string gets treated as a FHIR string, which is normally mapped to a StringElement abstraction.
Expand All @@ -2264,7 +2250,7 @@ protected Expression FunctionRef(FunctionRef op, ExpressionBuilderContext ctx)
}
else
{
var bind = OperatorBinding.Bind(CqlOperator.Convert, deeper,
var bind = OperatorBinding.Bind(CqlOperator.Convert, ctx.RuntimeContextParameter,

new[] { operands[0], Expression.Constant(typeof(string), typeof(Type)) });
return bind;
Expand All @@ -2273,7 +2259,7 @@ protected Expression FunctionRef(FunctionRef op, ExpressionBuilderContext ctx)
}
// all functions still take the bundle and context parameters, plus whatver the operands
// to the actual function are.
operands = operands.Prepend(deeper).ToArray();
operands = operands.Prepend(ctx.RuntimeContextParameter).ToArray();

var invoke = InvokeDefinedFunctionThroughRuntimeContext(op.name!, op.libraryName!, funcType, operands, ctx);
return invoke;
Expand Down
4 changes: 2 additions & 2 deletions Cql/Cql.Firely/BundleDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ public BundleDataSource(Bundle bundle,
private readonly ICqlComparer _codeComparer;
private readonly ICqlComparer _systemComparer;

#if VNEXT
/// <inheritdoc/>
/// <remarks>Since it is not possible to monitor changes in a FHIR POCO, this source will not trigger when
/// external changes are made to the Bundle.</remarks>
#pragma warning disable CS0067 // The event 'BundleDataSource.DataChanged' is never used
public event EventHandler? DataChanged;
#pragma warning restore CS0067 // The event 'BundleDataSource.DataChanged' is never used
#endif

/// <inheritdoc/>
public IEnumerable<T> RetrieveByCodes<T>(IEnumerable<CqlCode?>? allowedCodes = null, PropertyInfo? codeProperty = null) where T : class
Expand Down
20 changes: 8 additions & 12 deletions Cql/Cql.Firely/FhirCqlContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,23 @@ namespace Hl7.Cql.Fhir
/// Factory methods to initialize an <see cref="CqlContext"/> that uses the SDK POCO model
/// as binding for the Cql engine, supplying data using POCO instances.
/// </summary>
public class FhirCqlContext : CqlContext
public static class FhirCqlContext
{
internal FhirCqlContext(IDataSource? dataSource = null,
internal static CqlContext createContext(IDataSource? dataSource = null,
IDictionary<string, object>? parameters = null,
IValueSetDictionary? valueSets = null,
DateTimeOffset? now = null,
DefinitionDictionary<Delegate>? delegates = null,
FhirModelBindingOptions? options = null) :
base(
FhirModelBindingOptions? options = null) =>
new CqlContext(
new FhirModelBindingSetup(dataSource, valueSets, now, options).Operators,
parameters,
delegates,
extensionState: null)
{
// Nothing
}
delegates);

/// <summary>
/// Factory method for creating a setup of the engine with the given <see cref="Bundle"/>.
/// </summary>
public static FhirCqlContext ForBundle(Bundle? bundle = null,
public static CqlContext ForBundle(Bundle? bundle = null,
IDictionary<string, object>? parameters = null,
IValueSetDictionary? valueSets = null,
DateTimeOffset? now = null,
Expand All @@ -54,14 +50,14 @@ public static FhirCqlContext ForBundle(Bundle? bundle = null,
/// <summary>
/// Factory method for creating a setup of the engine with the given <see cref="IDataSource"/>.
/// </summary>
public static FhirCqlContext WithDataSource(IDataSource? source = null,
public static CqlContext WithDataSource(IDataSource? source = null,
IDictionary<string, object>? parameters = null,
IValueSetDictionary? valueSets = null,
DateTimeOffset? now = null,
DefinitionDictionary<Delegate>? delegates = null,
FhirModelBindingOptions? options = null)
{
return new(source, parameters, valueSets, now, delegates, options);
return createContext(source, parameters, valueSets, now, delegates, options);
}
}
}
4 changes: 2 additions & 2 deletions Cql/Cql.Firely/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Hl7.Cql.Fhir.FhirModelBindingOptions.FhirModelBindingOptions() -> void
Hl7.Cql.Fhir.FhirModelBindingOptions.ResourceIdComparer.get -> System.StringComparer?
Hl7.Cql.Fhir.FhirModelBindingOptions.ResourceIdComparer.init -> void
Hl7.Cql.Fhir.ValueSetExtensions
static Hl7.Cql.Fhir.FhirCqlContext.ForBundle(Hl7.Fhir.Model.Bundle? bundle = null, System.Collections.Generic.IDictionary<string!, object!>? parameters = null, Hl7.Cql.ValueSets.IValueSetDictionary? valueSets = null, System.DateTimeOffset? now = null, Hl7.Cql.Runtime.DefinitionDictionary<System.Delegate!>? delegates = null, Hl7.Cql.Fhir.FhirModelBindingOptions? options = null) -> Hl7.Cql.Fhir.FhirCqlContext!
static Hl7.Cql.Fhir.FhirCqlContext.WithDataSource(Hl7.Cql.Operators.IDataSource? source = null, System.Collections.Generic.IDictionary<string!, object!>? parameters = null, Hl7.Cql.ValueSets.IValueSetDictionary? valueSets = null, System.DateTimeOffset? now = null, Hl7.Cql.Runtime.DefinitionDictionary<System.Delegate!>? delegates = null, Hl7.Cql.Fhir.FhirModelBindingOptions? options = null) -> Hl7.Cql.Fhir.FhirCqlContext!
static Hl7.Cql.Fhir.FhirCqlContext.ForBundle(Hl7.Fhir.Model.Bundle? bundle = null, System.Collections.Generic.IDictionary<string!, object!>? parameters = null, Hl7.Cql.ValueSets.IValueSetDictionary? valueSets = null, System.DateTimeOffset? now = null, Hl7.Cql.Runtime.DefinitionDictionary<System.Delegate!>? delegates = null, Hl7.Cql.Fhir.FhirModelBindingOptions? options = null) -> Hl7.Cql.Runtime.CqlContext!
static Hl7.Cql.Fhir.FhirCqlContext.WithDataSource(Hl7.Cql.Operators.IDataSource? source = null, System.Collections.Generic.IDictionary<string!, object!>? parameters = null, Hl7.Cql.ValueSets.IValueSetDictionary? valueSets = null, System.DateTimeOffset? now = null, Hl7.Cql.Runtime.DefinitionDictionary<System.Delegate!>? delegates = null, Hl7.Cql.Fhir.FhirModelBindingOptions? options = null) -> Hl7.Cql.Runtime.CqlContext!
static Hl7.Cql.Fhir.ValueSetExtensions.ToValueSetDictionary(this System.Collections.Generic.IEnumerable<Hl7.Fhir.Model.ValueSet!>! values, bool activeOnly = true, System.Func<Hl7.Fhir.Model.ValueSet!, bool>? onInvalidValueSet = null) -> Hl7.Cql.ValueSets.IValueSetDictionary!
static readonly Hl7.Cql.Fhir.FhirModelBindingOptions.Default -> Hl7.Cql.Fhir.FhirModelBindingOptions!
2 changes: 1 addition & 1 deletion Cql/Cql.Logging/LoggingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static class LoggingExtensions
/// </summary>
public static CqlContext AddMessageLogging(this CqlContext ctx, ILogger logger)
{
ctx.Operators.MessageReceived += (sender, args) =>
ctx.MessageReceived += (sender, args) =>
{
var level = Level(args);
logger.Log(level, new EventId(default, args.Code), args.Message, args.Source);
Expand Down
5 changes: 4 additions & 1 deletion Cql/Cql.Operators/CompositeDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ public CompositeDataSource(params IDataSource[] sources)
DataSources = sources ?? throw new ArgumentNullException(nameof(sources));
if (sources.Any(r => r is null))
throw new ArgumentNullException(nameof(sources), "At least one data source supplied is null.");

#if VNEXT
foreach (var r in sources)
r.DataChanged += triggerDataChanged;
#endif
}

#if VNEXT
/// <inheritdoc/>
public event EventHandler? DataChanged;

private void triggerDataChanged(object? sender, EventArgs e)
{
DataChanged?.Invoke(sender, e);
}
#endif

/// <summary>
/// The data sources whose data is composed.
Expand Down
3 changes: 2 additions & 1 deletion Cql/Cql.Operators/IDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

using Hl7.Cql.Abstractions;
using Hl7.Cql.Primitives;
using System;
using System.Collections.Generic;
using System.Reflection;

Expand Down Expand Up @@ -57,10 +56,12 @@ public interface IDataSource
/// <returns>Resources of type <typeparamref name="T"/> matching the parameter criteria.</returns>
IEnumerable<T> RetrieveByValueSet<T>(CqlValueSet? valueSet = null, PropertyInfo? codeProperty = null) where T : class;

#if VNEXT
/// <summary>
/// Will be triggered when the data in the source has changed, e.g. because of updates or because different
/// data was loaded altogether.
/// </summary>
event EventHandler? DataChanged;
#endif
}
}
2 changes: 0 additions & 2 deletions Cql/Cql.Operators/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#nullable enable
Hl7.Cql.Operators.CompositeDataSource
Hl7.Cql.Operators.CompositeDataSource.CompositeDataSource(params Hl7.Cql.Operators.IDataSource![]! sources) -> void
Hl7.Cql.Operators.CompositeDataSource.DataChanged -> System.EventHandler?
Hl7.Cql.Operators.CompositeDataSource.DataSources.get -> Hl7.Cql.Operators.IDataSource![]!
Hl7.Cql.Operators.CompositeDataSource.RetrieveByCodes<T>(System.Collections.Generic.IEnumerable<Hl7.Cql.Primitives.CqlCode?>? codes = null, System.Reflection.PropertyInfo? codeProperty = null) -> System.Collections.Generic.IEnumerable<T!>!
Hl7.Cql.Operators.CompositeDataSource.RetrieveByValueSet<T>(Hl7.Cql.Primitives.CqlValueSet? valueSet = null, System.Reflection.PropertyInfo? codeProperty = null) -> System.Collections.Generic.IEnumerable<T!>!
Expand Down Expand Up @@ -504,7 +503,6 @@ Hl7.Cql.Operators.ICqlOperators.Width(Hl7.Cql.Primitives.CqlInterval<long?>? thi
Hl7.Cql.Operators.ICqlOperators.Xor(bool? left, bool? right) -> bool?
Hl7.Cql.Operators.ICqlOperators.Xor(System.Lazy<bool?>! left, System.Lazy<bool?>! right) -> bool?
Hl7.Cql.Operators.IDataSource
Hl7.Cql.Operators.IDataSource.DataChanged -> System.EventHandler?
Hl7.Cql.Operators.IDataSource.RetrieveByCodes<T>(System.Collections.Generic.IEnumerable<Hl7.Cql.Primitives.CqlCode?>? codes = null, System.Reflection.PropertyInfo? codeProperty = null) -> System.Collections.Generic.IEnumerable<T!>!
Hl7.Cql.Operators.IDataSource.RetrieveByValueSet<T>(Hl7.Cql.Primitives.CqlValueSet? valueSet = null, System.Reflection.PropertyInfo? codeProperty = null) -> System.Collections.Generic.IEnumerable<T!>!
Hl7.Cql.Operators.MessageEventArgs
Expand Down
54 changes: 15 additions & 39 deletions Cql/Cql.Runtime/CqlContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@

using Hl7.Cql.Operators;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Hl7.Cql.Runtime
{

/// <summary>
/// Contains information required to execute CQL.
/// </summary>
Expand All @@ -29,16 +27,6 @@ public class CqlContext
/// </summary>
public ICqlOperators Operators { get; }

/// <summary>
/// An external dictionary that contains the runtime state for extensions.
/// </summary>
/// <remarks>
/// Runtime extensions can provide functionality like logging and timing by altering how the translation
/// between ELM and .NET expressions is done. For implementations that need to hold state, they can use
/// keys in this dictionary to store any kind of state they need.
/// </remarks>
internal ConcurrentDictionary<string, object> Extensions { get; }

/// <summary>
/// Gets the values of library parameters for this execution.
/// </summary>
Expand All @@ -55,31 +43,36 @@ public class CqlContext
/// </summary>
/// <param name="operators">The <see cref="ICqlOperators"/> implementation to use.</param>
/// <param name="parameters">The input parameters, or <see langword="null"/>. </param>
/// <param name="extensionState">A dictionary that will keep state used by extensions.</param>
/// <param name="delegates">The delegates, or <see langword="null"/>. If <see langword="null"/>, runtime errors will occur when CQL expressions attempt to reference other definitions.</param>
protected internal CqlContext(ICqlOperators operators,
IDictionary<string, object>? parameters = null,
DefinitionDictionary<Delegate>? delegates = null,
ConcurrentDictionary<string, object>? extensionState = null)
DefinitionDictionary<Delegate>? delegates = null)
{
Operators = operators;
Definitions = delegates ?? new DefinitionDictionary<Delegate>();
Extensions = extensionState ?? new();
Parameters = parameters ?? new Dictionary<string, object>();
}

/// <summary>
/// Notifies subscribers when a function is called.
/// Notifies subscribers when a CQL Message has been dispatched.
/// </summary>
public event EventHandler<FunctionCallEvent>? FunctionCalled;
public event EventHandler<MessageEventArgs>? MessageReceived
{
add => Operators.MessageReceived += value;
remove => Operators.MessageReceived -= value;
}

/// <summary>
/// Raise the <see cref="FunctionCalled"/> event.
/// Notifies subscribers when a generic event is raised in the engine.
/// </summary>
/// <param name="eventData"></param>
public CqlContext OnFunctionCalled(FunctionCallEvent eventData)
public event EventHandler<ContextEventArgs>? ContextEvent;

/// <summary>
/// Raise the <see cref="ContextEvent"/> event.
/// </summary>
public CqlContext RaiseContextEvent(ContextEventArgs eventData)
{
FunctionCalled?.Invoke(this, eventData);
ContextEvent?.Invoke(this, eventData);
return this;
}

Expand All @@ -106,22 +99,5 @@ public CqlContext OnFunctionCalled(FunctionCallEvent eventData)

return defaultValue;
}

/// <summary>
/// Gets the value of the extension in <see cref="Extensions"/> given key.
/// </summary>
/// <param name="key">The extension key.</param>
/// <param name="defaultValue">The default value to use if the extension isn't found.</param>
/// <returns>The value of the parameter or <paramref name="defaultValue"/> if not defined.</returns>
public object? ResolveExtension(string key, object? defaultValue)
{
if (!Extensions.TryGetValue(key, out var value))
{
Extensions[key] = defaultValue;
return defaultValue;
}

return value;
}
}
}
42 changes: 0 additions & 42 deletions Cql/Cql.Runtime/FunctionCallEvent.cs

This file was deleted.

29 changes: 29 additions & 0 deletions Cql/Cql.Runtime/FunctionCallEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2023, NCQA and contributors
* See the file CONTRIBUTORS for details.
*
* This file is licensed under the BSD 3-Clause license
* available at https://raw.githubusercontent.com/FirelyTeam/cql-sdk/main/LICENSE
*/

namespace Hl7.Cql.Runtime
{
/// <summary>
/// Data for a context event, which is a generic event raised by the CQL engine or one of its custom extension components.
/// </summary>
public class ContextEventArgs
{
/// <summary>
/// Creates an instance.
/// </summary>
public ContextEventArgs(object? state = null)
{
State = state;
}

/// <summary>
/// Generic state for the event.
/// </summary>
public object? State { get; }
}
}
Loading

0 comments on commit 0c9cbca

Please sign in to comment.