diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/GeneralFiltersTests.cs b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/GeneralFiltersTests.cs index 66a2a1702..940eda3b3 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/GeneralFiltersTests.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/GeneralFiltersTests.cs @@ -58,6 +58,57 @@ public void GetPropertyTest() Assert.Null(Filters.GetProperty(context, "M", string.Empty, "code")); } + [Fact] + public void GetProperty_WhenCodeMappingIsAppended_ReturnsMappedValue() + { + // empty context + var context = new Context(CultureInfo.InvariantCulture); + Assert.Null(Filters.GetProperty(context, null, null, null)); + + // context with null CodeMapping + context = new Context( + environments: new List(), + outerScope: new Hash(), + registers: new Hash(), + errorsOutputMode: ErrorsOutputMode.Rethrow, + maxIterations: 0, + formatProvider: CultureInfo.InvariantCulture, + cancellationToken: CancellationToken.None); + context["CodeMapping"] = null; + + // First CodeMapping from CodeSystem + var firstMapping = new CodeMapping(new Dictionary>> + { + { + "CodeSystem/Gender", new Dictionary> + { + { "M", new Dictionary { { "code", "male" } } }, + } + }, + }); + + // Additional CodeMapping from ValueSet + var additionalMapping = new CodeMapping(new Dictionary>> + { + { + "ValueSet/Gender", new Dictionary> + { + { "F", new Dictionary { { "code", "female" } } }, + { "O", new Dictionary { { "code", "other" } } }, + } + }, + }); + + // Append ValueSet to CodeMapping + firstMapping.Append(additionalMapping); + context["CodeMapping"] = firstMapping; + + // Assert both CodeSystem/Gender and ValueSet/Gender + Assert.Equal("male", Filters.GetProperty(context, "M", "CodeSystem/Gender", "code")); + Assert.Equal("female", Filters.GetProperty(context, "F", "ValueSet/Gender", "code")); + Assert.Equal("other", Filters.GetProperty(context, "O", "ValueSet/Gender", "code")); + } + [Fact] public void EvaluateTest() { diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Models/CodeMapping.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Models/CodeMapping.cs index 6b6a11147..9af07745c 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Models/CodeMapping.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Models/CodeMapping.cs @@ -3,6 +3,7 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- +using System; using System.Collections.Generic; using DotLiquid; @@ -16,5 +17,52 @@ public CodeMapping(Dictionary>> Mapping { get; set; } + + /// + /// Appends mappings from another CodeMapping instance. + /// Throws InvalidOperationException if any key path already exists with a different value. + /// + /// The CodeMapping to append. + /// Thrown if is null. + /// Thrown if any key path already exists with a different value. + public void Append(CodeMapping additionalMapping) + { + ArgumentNullException.ThrowIfNull(additionalMapping, nameof(additionalMapping)); + + foreach (var level1 in additionalMapping.Mapping) + { + if (!Mapping.TryGetValue(level1.Key, out var level2Dict)) + { + Mapping[level1.Key] = new Dictionary>(level1.Value); + continue; + } + + foreach (var level2 in level1.Value) + { + if (!level2Dict.TryGetValue(level2.Key, out var level3Dict)) + { + level2Dict[level2.Key] = new Dictionary(level2.Value); + continue; + } + + foreach (var level3 in level2.Value) + { + if (level3Dict.TryGetValue(level3.Key, out var existingValue)) + { + if (!string.Equals(existingValue, level3.Value, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"Conflict at path [{level1.Key}][{level2.Key}][{level3.Key}]: " + + $"existing='{existingValue}', new='{level3.Value}'"); + } + } + else + { + level3Dict[level3.Key] = level3.Value; + } + } + } + } + } } } diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/BaseProcessor.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/BaseProcessor.cs index 806113469..9858419ca 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/BaseProcessor.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/BaseProcessor.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Threading; using DotLiquid; using EnsureThat; @@ -63,7 +64,23 @@ public string Convert(string data, string rootTemplate, ITemplateProvider templa protected abstract string InternalConvert(string data, string rootTemplate, ITemplateProvider templateProvider, TraceInfo traceInfo = null); - protected virtual Context CreateContext(ITemplateProvider templateProvider, IDictionary data, string rootTemplate) + protected Context CreateContext(ITemplateProvider templateProvider, IDictionary data, string rootTemplate) + { + Context context = CreateBaseContext(templateProvider, data); + + // Load filters + context.AddFilters(typeof(Filters)); + + // Add root template's parent path to context. + AddRootTemplatePathScope(context, templateProvider, rootTemplate); + + // Inject of codemapping into context. + InjectCodeMappingIntoContext(context, templateProvider); + + return context; + } + + protected virtual Context CreateBaseContext(ITemplateProvider templateProvider, IDictionary data) { // Load data and templates var cancellationToken = Settings.TimeOut > 0 ? new CancellationTokenSource(Settings.TimeOut).Token : CancellationToken.None; @@ -76,12 +93,6 @@ protected virtual Context CreateContext(ITemplateProvider templateProvider, IDic formatProvider: CultureInfo.InvariantCulture, cancellationToken: cancellationToken); - // Load filters - context.AddFilters(typeof(Filters)); - - // Add root template's parent path to context. - AddRootTemplatePathScope(context, templateProvider, rootTemplate); - return context; } @@ -182,5 +193,38 @@ protected void LogTelemetry(string telemetryName, double duration) Logger.LogInformation("{Metric}: {Duration} milliseconds.", telemetryName, duration); } } + + protected void InjectCodeMappingIntoContext(Context context, ITemplateProvider templateProvider) + { + var rootTemplateParentPath = context[TemplateUtility.RootTemplateParentPathScope]?.ToString(); + List codeMappings = new List { "ValueSet/ValueSet", "CodeSystem/CodeSystem", }; + var allPaths = codeMappings + .Select(name => TemplateUtility.GetFormattedTemplatePath(name, rootTemplateParentPath)) + .ToList(); + + CodeMapping combinedMapping = null; + + foreach (var path in allPaths) + { + var template = templateProvider.GetTemplate(path); + var node = template?.Root?.NodeList?.FirstOrDefault() as CodeMapping; + if (node != null) + { + if (combinedMapping == null) + { + combinedMapping = node; + } + else + { + combinedMapping.Append(node); + } + } + } + + if (combinedMapping != null) + { + context["CodeMapping"] = combinedMapping; + } + } } } diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/CcdaProcessor.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/CcdaProcessor.cs index d759955e2..08b4cad9a 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/CcdaProcessor.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/CcdaProcessor.cs @@ -36,25 +36,5 @@ protected override string InternalConvert(string data, string rootTemplate, ITem return InternalConvertFromObject(ccdaData, rootTemplate, templateProvider, traceInfo); } - - protected override Context CreateContext(ITemplateProvider templateProvider, IDictionary data, string rootTemplate) - { - // Load value set mapping - var context = base.CreateContext(templateProvider, data, rootTemplate); - var codeMapping = templateProvider.GetTemplate(GetCodeMappingTemplatePath(context)); - if (codeMapping?.Root?.NodeList?.First() != null) - { - context["CodeMapping"] = codeMapping.Root.NodeList.First(); - } - - return context; - } - - private string GetCodeMappingTemplatePath(Context context) - { - var rootTemplateParentPath = context[TemplateUtility.RootTemplateParentPathScope]?.ToString(); - var codeSystemTemplateName = "ValueSet/ValueSet"; - return TemplateUtility.GetFormattedTemplatePath(codeSystemTemplateName, rootTemplateParentPath); - } } } diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/FhirToHl7v2Processor.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/FhirToHl7v2Processor.cs index 54fbf3865..68e446e9b 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/FhirToHl7v2Processor.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/FhirToHl7v2Processor.cs @@ -225,11 +225,11 @@ public string ConvertHl7MessageToString(Hl7v2Data message) return sb.ToString(); } - protected override Context CreateContext(ITemplateProvider templateProvider, IDictionary data, string rootTemplate) + protected override Context CreateBaseContext(ITemplateProvider templateProvider, IDictionary data) { // Load data and templates var cancellationToken = Settings.TimeOut > 0 ? new CancellationTokenSource(Settings.TimeOut).Token : CancellationToken.None; - var context = new JSchemaContext( + return new JSchemaContext( environments: new List { Hash.FromDictionary(data) }, outerScope: new Hash(), registers: Hash.FromDictionary(new Dictionary { { "file_system", templateProvider.GetTemplateFileSystem() } }), @@ -240,11 +240,6 @@ protected override Context CreateContext(ITemplateProvider templateProvider, IDi { ValidateSchemas = new List(), }; - - // Load filters - context.AddFilters(typeof(Filters)); - - return context; } protected override void CreateTraceInfo(object data, Context context, TraceInfo traceInfo) diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/Hl7v2Processor.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/Hl7v2Processor.cs index 1be2a9015..c7625be4a 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/Hl7v2Processor.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/Hl7v2Processor.cs @@ -40,19 +40,6 @@ protected override string InternalConvert(string data, string rootTemplate, ITem return InternalConvertFromObject(hl7v2Data, rootTemplate, templateProvider, traceInfo); } - protected override Context CreateContext(ITemplateProvider templateProvider, IDictionary data, string rootTemplate) - { - // Load code system mapping - var context = base.CreateContext(templateProvider, data, rootTemplate); - var codeMapping = templateProvider.GetTemplate(GetCodeMappingTemplatePath(context)); - if (codeMapping?.Root?.NodeList?.First() != null) - { - context["CodeMapping"] = codeMapping.Root.NodeList.First(); - } - - return context; - } - protected override void CreateTraceInfo(object data, Context context, TraceInfo traceInfo) { if (traceInfo is Hl7v2TraceInfo hl7v2TraceInfo) @@ -60,12 +47,5 @@ protected override void CreateTraceInfo(object data, Context context, TraceInfo hl7v2TraceInfo.UnusedSegments = Hl7v2TraceInfo.CreateTraceInfo(data as Hl7v2Data).UnusedSegments; } } - - private string GetCodeMappingTemplatePath(Context context) - { - var rootTemplateParentPath = context[TemplateUtility.RootTemplateParentPathScope]?.ToString(); - var codeSystemTemplateName = "CodeSystem/CodeSystem"; - return TemplateUtility.GetFormattedTemplatePath(codeSystemTemplateName, rootTemplateParentPath); - } } } diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/JsonProcessor.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/JsonProcessor.cs index 2229e1a62..af7571297 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/JsonProcessor.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Processors/JsonProcessor.cs @@ -54,11 +54,11 @@ public string Convert(JObject data, string rootTemplate, ITemplateProvider templ return InternalConvertFromObject(jsonData, rootTemplate, templateProvider, traceInfo); } - protected override Context CreateContext(ITemplateProvider templateProvider, IDictionary data, string rootTemplate) + protected override Context CreateBaseContext(ITemplateProvider templateProvider, IDictionary data) { // Load data and templates var cancellationToken = Settings.TimeOut > 0 ? new CancellationTokenSource(Settings.TimeOut).Token : CancellationToken.None; - var context = new JSchemaContext( + return new JSchemaContext( environments: new List { Hash.FromDictionary(data) }, outerScope: new Hash(), registers: Hash.FromDictionary(new Dictionary { { "file_system", templateProvider.GetTemplateFileSystem() } }), @@ -69,14 +69,6 @@ protected override Context CreateContext(ITemplateProvider templateProvider, IDi { ValidateSchemas = new List(), }; - - // Load filters - context.AddFilters(typeof(Filters)); - - // Add root template's parent path to context. - AddRootTemplatePathScope(context, templateProvider, rootTemplate); - - return context; } protected override void CreateTraceInfo(object data, Context context, TraceInfo traceInfo)