From 64d7be5259cf899354aaab5cdcb487d248882e6a Mon Sep 17 00:00:00 2001 From: rogordon01 <71304426+rogordon01@users.noreply.github.com> Date: Mon, 20 Dec 2021 11:45:35 -0800 Subject: [PATCH] Personal/rogrdon/create common validation logic (#152) * Initial commit * Allowing a batch of events to be validated * Adding documentation * Changes due to code review * Changed due to code review * Ensure multiple deviceEvents get validated * Updating test --- Microsoft.Health.Fhir.Ingest.sln | 33 ++- .../CollectionContentTemplate.cs | 2 + .../FhirLookupTemplate.cs | 3 + .../Extensions/IResultExtensions.cs | 46 +++ .../IMappingValidator.cs | 40 +++ .../MappingValidator.cs | 258 ++++++++++++++++ ...osoft.Health.Fhir.Ingest.Validation.csproj | 48 +++ .../Models/DeviceResult.cs | 23 ++ .../Models/ErrorLevel.cs | 20 ++ .../Models/IResult.cs | 14 + .../Models/TemplateResult.cs | 14 + .../Models/ValidationError.cs | 22 ++ .../Models/ValidationResult.cs | 16 + .../MappingValidatorTests.cs | 280 ++++++++++++++++++ ...th.Fhir.Ingest.Validation.UnitTests.csproj | 57 ++++ ...alueFhirTemplateInvalid_MissingFields.json | 18 ++ ...ontentTemplateHrAndBloodPressureValid.json | 43 +++ ...data_CollectionContentTemplateInvalid.json | 32 ++ ...FhirTemplateIncorrectValueNameInvalid.json | 76 +++++ ...lectionFhirTemplateMissingTypeInvalid.json | 76 +++++ .../data_CollectionFhirTemplateValid.json | 76 +++++ 21 files changed, 1186 insertions(+), 11 deletions(-) create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Extensions/IResultExtensions.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/IMappingValidator.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/MappingValidator.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Microsoft.Health.Fhir.Ingest.Validation.csproj create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/DeviceResult.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ErrorLevel.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/IResult.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/TemplateResult.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationError.cs create mode 100644 src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationResult.cs create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/MappingValidatorTests.cs create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/Microsoft.Health.Fhir.Ingest.Validation.UnitTests.csproj create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CodeValueFhirTemplateInvalid_MissingFields.json create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateInvalid.json create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateIncorrectValueNameInvalid.json create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateMissingTypeInvalid.json create mode 100644 test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateValid.json diff --git a/Microsoft.Health.Fhir.Ingest.sln b/Microsoft.Health.Fhir.Ingest.sln index 620e995b..b234fcf7 100644 --- a/Microsoft.Health.Fhir.Ingest.sln +++ b/Microsoft.Health.Fhir.Ingest.sln @@ -3,10 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29009.5 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Fhir.Ingest", "src\lib\Microsoft.Health.Fhir.Ingest\Microsoft.Health.Fhir.Ingest.csproj", "{269436ED-1F69-4A83-A2EB-FBE82233472F}" - ProjectSection(ProjectDependencies) = postProject - {11032625-0B2B-4468-B1F1-431B5682DB7A} = {11032625-0B2B-4468-B1F1-431B5682DB7A} - EndProjectSection +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Fhir.Ingest", "src\lib\Microsoft.Health.Fhir.Ingest\Microsoft.Health.Fhir.Ingest.csproj", "{269436ED-1F69-4A83-A2EB-FBE82233472F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{513D67B4-80E1-476D-955F-E7E7C79D144A}" EndProject @@ -16,14 +13,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{FAF8B402-8 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Fhir.Ingest.Host", "src\func\Microsoft.Health.Fhir.Ingest.Host\Microsoft.Health.Fhir.Ingest.Host.csproj", "{11F6C10F-483A-4DD7-99A3-14FDD5CCA3F1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Fhir.Ingest.UnitTests", "test\Microsoft.Health.Fhir.Ingest.UnitTests\Microsoft.Health.Fhir.Ingest.UnitTests.csproj", "{F6422A39-DC02-436B-AC1F-82CC9CF75E73}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Fhir.Ingest.UnitTests", "test\Microsoft.Health.Fhir.Ingest.UnitTests\Microsoft.Health.Fhir.Ingest.UnitTests.csproj", "{F6422A39-DC02-436B-AC1F-82CC9CF75E73}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Fhir.R4.Ingest", "src\lib\Microsoft.Health.Fhir.R4.Ingest\Microsoft.Health.Fhir.R4.Ingest.csproj", "{98D001A4-3B1B-4E60-9148-71881A0D457D}" ProjectSection(ProjectDependencies) = postProject {11032625-0B2B-4468-B1F1-431B5682DB7A} = {11032625-0B2B-4468-B1F1-431B5682DB7A} EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Fhir.R4.Ingest.UnitTests", "test\Microsoft.Health.Fhir.R4.Ingest.UnitTests\Microsoft.Health.Fhir.R4.Ingest.UnitTests.csproj", "{10E71A88-CC24-4DFA-92F3-18E47D42E985}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Fhir.R4.Ingest.UnitTests", "test\Microsoft.Health.Fhir.R4.Ingest.UnitTests\Microsoft.Health.Fhir.R4.Ingest.UnitTests.csproj", "{10E71A88-CC24-4DFA-92F3-18E47D42E985}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deploy", "deploy", "{AC515DEF-E7DA-4D6C-8EBC-0F17DE83E400}" EndProject @@ -54,13 +51,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Extensions EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Common", "src\lib\Microsoft.Health.Common\Microsoft.Health.Common.csproj", "{B5BDAF20-DA88-47AF-94C2-33789479DF99}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Tests.Common", "test\Microsoft.Health.Tests.Common\Microsoft.Health.Tests.Common.csproj", "{9779F9F5-1F23-48B5-B271-0497D9ECE956}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Tests.Common", "test\Microsoft.Health.Tests.Common\Microsoft.Health.Tests.Common.csproj", "{9779F9F5-1F23-48B5-B271-0497D9ECE956}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Common.UnitTests", "test\Microsoft.Health.Common.UnitTests\Microsoft.Health.Common.UnitTests.csproj", "{F8F17DE0-1A3D-4E08-98C0-7EF9AD1E98B0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Common.UnitTests", "test\Microsoft.Health.Common.UnitTests\Microsoft.Health.Common.UnitTests.csproj", "{F8F17DE0-1A3D-4E08-98C0-7EF9AD1E98B0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Extensions.Fhir.UnitTests", "test\Microsoft.Health.Extensions.Fhir.UnitTests\Microsoft.Health.Extensions.Fhir.UnitTests.csproj", "{73C97786-1CC7-4CF9-A420-8AB66D0BD731}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Extensions.Fhir.UnitTests", "test\Microsoft.Health.Extensions.Fhir.UnitTests\Microsoft.Health.Extensions.Fhir.UnitTests.csproj", "{73C97786-1CC7-4CF9-A420-8AB66D0BD731}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Extensions.Fhir.R4.UnitTests", "test\Microsoft.Health.Extensions.Fhir.R4.UnitTests\Microsoft.Health.Extensions.Fhir.R4.UnitTests.csproj", "{90402958-43C6-43E8-A22A-1B1469A4FCC5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Extensions.Fhir.R4.UnitTests", "test\Microsoft.Health.Extensions.Fhir.R4.UnitTests\Microsoft.Health.Extensions.Fhir.R4.UnitTests.csproj", "{90402958-43C6-43E8-A22A-1B1469A4FCC5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Extensions.Host", "src\lib\Microsoft.Health.Extensions.Host\Microsoft.Health.Extensions.Host.csproj", "{9110663E-BFA0-4082-B1BE-F85D91DEFCA2}" EndProject @@ -76,7 +73,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Fhir.Inges EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Fhir.Ingest.Template.UnitTests", "test\Microsoft.Health.Fhir.Ingest.Template.UnitTests\Microsoft.Health.Fhir.Ingest.Template.UnitTests.csproj", "{EE072537-807D-4FE2-BFEB-424B64DCD7F9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Tests.Common.R4", "test\Microsoft.Health.Tests.Common.R4\Microsoft.Health.Tests.Common.R4.csproj", "{0B0F4BCE-E439-47AB-A67A-8B3778407339}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Tests.Common.R4", "test\Microsoft.Health.Tests.Common.R4\Microsoft.Health.Tests.Common.R4.csproj", "{0B0F4BCE-E439-47AB-A67A-8B3778407339}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Events", "src\lib\Microsoft.Health.Events\Microsoft.Health.Events.csproj", "{22275DE3-859D-40F0-9547-7711568164C0}" EndProject @@ -103,6 +100,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Expression EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Expressions.UnitTests", "test\Microsoft.Health.Expressions.UnitTests\Microsoft.Health.Expressions.UnitTests.csproj", "{9BF918FE-E09B-4072-8CC5-2BE479230719}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Fhir.Ingest.Validation", "src\lib\Microsoft.Health.Fhir.Ingest.Validation\Microsoft.Health.Fhir.Ingest.Validation.csproj", "{7F507901-30BD-41CD-99B8-FD8FA103D70A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Fhir.Ingest.Validation.UnitTests", "test\Microsoft.Health.Fhir.Ingest.Validation.UnitTests\Microsoft.Health.Fhir.Ingest.Validation.UnitTests.csproj", "{34B7EABB-DC53-47AC-86E0-35596FF18312}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,6 +202,14 @@ Global {9BF918FE-E09B-4072-8CC5-2BE479230719}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BF918FE-E09B-4072-8CC5-2BE479230719}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BF918FE-E09B-4072-8CC5-2BE479230719}.Release|Any CPU.Build.0 = Release|Any CPU + {7F507901-30BD-41CD-99B8-FD8FA103D70A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F507901-30BD-41CD-99B8-FD8FA103D70A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F507901-30BD-41CD-99B8-FD8FA103D70A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F507901-30BD-41CD-99B8-FD8FA103D70A}.Release|Any CPU.Build.0 = Release|Any CPU + {34B7EABB-DC53-47AC-86E0-35596FF18312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34B7EABB-DC53-47AC-86E0-35596FF18312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34B7EABB-DC53-47AC-86E0-35596FF18312}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34B7EABB-DC53-47AC-86E0-35596FF18312}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -235,6 +244,8 @@ Global {657FB6EC-2F58-43B6-AD14-5EDE9D73C1E5} = {75D08B93-4CE1-4967-B0C3-DAA792F1D19A} {0BC152B5-DF88-4750-A7CC-1B7429D879F6} = {513D67B4-80E1-476D-955F-E7E7C79D144A} {9BF918FE-E09B-4072-8CC5-2BE479230719} = {FAF8B402-892E-4EA2-B4CF-69B0C70BA762} + {7F507901-30BD-41CD-99B8-FD8FA103D70A} = {513D67B4-80E1-476D-955F-E7E7C79D144A} + {34B7EABB-DC53-47AC-86E0-35596FF18312} = {FAF8B402-892E-4EA2-B4CF-69B0C70BA762} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A358924D-F948-4AE8-8CD0-A0F56225CE0C} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Template/CollectionContentTemplate.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Template/CollectionContentTemplate.cs index ee805d65..a3aa282e 100644 --- a/src/lib/Microsoft.Health.Fhir.Ingest.Template/CollectionContentTemplate.cs +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Template/CollectionContentTemplate.cs @@ -15,6 +15,8 @@ public class CollectionContentTemplate : IContentTemplate { private readonly IList _templates = new List(10); + public IReadOnlyList Templates => (_templates as List).AsReadOnly(); + public CollectionContentTemplate RegisterTemplate(IContentTemplate contentTemplate) { EnsureArg.IsNotNull(contentTemplate, nameof(contentTemplate)); diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Template/FhirLookupTemplate.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Template/FhirLookupTemplate.cs index 780b4275..fe681510 100644 --- a/src/lib/Microsoft.Health.Fhir.Ingest.Template/FhirLookupTemplate.cs +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Template/FhirLookupTemplate.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using EnsureThat; namespace Microsoft.Health.Fhir.Ingest.Template @@ -13,6 +14,8 @@ public class FhirLookupTemplate : ILookupTemplate { private readonly IDictionary _templates = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + public IReadOnlyList Templates => _templates.Values.ToList().AsReadOnly(); + public FhirLookupTemplate RegisterTemplate(IFhirTemplate fhirTemplate) { EnsureArg.IsNotNull(fhirTemplate, nameof(fhirTemplate)); diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Extensions/IResultExtensions.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Extensions/IResultExtensions.cs new file mode 100644 index 00000000..655c4f9f --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Extensions/IResultExtensions.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using EnsureThat; +using Microsoft.Health.Fhir.Ingest.Validation.Models; + +namespace Microsoft.Health.Fhir.Ingest.Validation.Extensions +{ + public static class IResultExtensions + { + public static void CaptureError(this IResult validationResult, string message, ErrorLevel errorLevel) + { + EnsureArg.IsNotNull(validationResult, nameof(validationResult)); + EnsureArg.IsNotNullOrWhiteSpace(message, nameof(message)); + + validationResult.Exceptions.Add(new ValidationError(message, errorLevel)); + } + + public static void CaptureException(this IResult validationResult, Exception exception) + { + EnsureArg.IsNotNull(validationResult, nameof(validationResult)); + EnsureArg.IsNotNull(exception, nameof(exception)); + + validationResult.Exceptions.Add(new ValidationError(exception.Message)); + } + + public static void CaptureWarning(this IResult validationResult, string warning) + { + EnsureArg.IsNotNull(validationResult, nameof(validationResult)); + EnsureArg.IsNotNullOrWhiteSpace(warning, nameof(warning)); + + validationResult.Exceptions.Add(new ValidationError(warning, ErrorLevel.WARN)); + } + + public static IEnumerable GetErrors(this IResult validationResult, ErrorLevel errorLevel) + { + EnsureArg.IsNotNull(validationResult, nameof(validationResult)); + return validationResult.Exceptions.Where(error => errorLevel.Equals(error.Level)); + } + } +} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/IMappingValidator.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/IMappingValidator.cs new file mode 100644 index 00000000..f1aa7f04 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/IMappingValidator.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Health.Fhir.Ingest.Validation.Models; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Health.Fhir.Ingest.Validation +{ + public interface IMappingValidator + { + /// + /// Performs validation of Device and Fhir Mapping templates. The templates will be first be validated individually and then validated for compatibility + /// with each other. Finally, if a device event is supplied Measurements and Fhir Observations will attempt to be built from it. Any errors/warnings + /// found will be captured in the resulting ValidationResult object. + /// + /// At least one of deviceMappingContent or fhirMappingContent must be provided. + /// + /// A sample DeviceEvent. Optional + /// A device mapping template. Optional + /// A fhir mapping template. Optional + /// A ValidationResult object + ValidationResult PerformValidation(JToken deviceEvent, string deviceMappingContent, string fhirMappingContent); + + /// + /// Performs validation of Device and Fhir Mapping templates. The templates will be first be validated individually and then validated for compatibility + /// with each other. Finally, if device events are supplied Measurements and Fhir Observations will attempt to be built from them. Any errors/warnings + /// found will be captured in the resulting ValidationResult object. + /// + /// At least one of deviceMappingContent or fhirMappingContent must be provided. + /// + /// A collection of DeviceEvents. Optional + /// A device mapping template. Optional + /// A fhir mapping template. Optional + /// A ValidationResult object + ValidationResult PerformValidation(IEnumerable deviceEvents, string deviceMappingContent, string fhirMappingContent); + } +} \ No newline at end of file diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/MappingValidator.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/MappingValidator.cs new file mode 100644 index 00000000..b6760f39 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/MappingValidator.cs @@ -0,0 +1,258 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using EnsureThat; +using Microsoft.Health.Fhir.Ingest.Data; +using Microsoft.Health.Fhir.Ingest.Template; +using Microsoft.Health.Fhir.Ingest.Validation.Extensions; +using Microsoft.Health.Fhir.Ingest.Validation.Models; +using Newtonsoft.Json.Linq; +using Model = Hl7.Fhir.Model; + +namespace Microsoft.Health.Fhir.Ingest.Validation +{ + public class MappingValidator : IMappingValidator + { + private readonly IFhirTemplateProcessor, Model.Observation> _fhirTemplateProcessor; + + private readonly CollectionTemplateFactory _collectionTemplateFactory; + private readonly ITemplateFactory>> _fhirTemplateFactory; + + public MappingValidator( + CollectionTemplateFactory collectionTemplateFactory, + ITemplateFactory>> fhirTemplateFactory, + IFhirTemplateProcessor, Model.Observation> fhirTemplateProcessor) + { + _collectionTemplateFactory = EnsureArg.IsNotNull(collectionTemplateFactory, nameof(collectionTemplateFactory)); + _fhirTemplateFactory = EnsureArg.IsNotNull(fhirTemplateFactory, nameof(fhirTemplateFactory)); + _fhirTemplateProcessor = EnsureArg.IsNotNull(fhirTemplateProcessor, nameof(fhirTemplateProcessor)); + } + + public ValidationResult PerformValidation( + JToken deviceEvent, + string deviceMappingContent, + string fhirMappingContent) + { + return PerformValidation(new List() { deviceEvent }, deviceMappingContent, fhirMappingContent); + } + + public ValidationResult PerformValidation( + IEnumerable deviceEvents, + string deviceMappingContent, + string fhirMappingContent) + { + if (string.IsNullOrWhiteSpace(deviceMappingContent) && string.IsNullOrWhiteSpace(fhirMappingContent)) + { + throw new ArgumentException($"At least one of [{nameof(deviceMappingContent)}] or [{nameof(fhirMappingContent)}] must be provided"); + } + + var validationResult = new ValidationResult(); + + IContentTemplate contentTemplate = null; + ILookupTemplate fhirTemplate = null; + + if (!string.IsNullOrEmpty(deviceMappingContent)) + { + contentTemplate = LoadDeviceTemplate(deviceMappingContent, validationResult.TemplateResult); + } + + if (!string.IsNullOrEmpty(fhirMappingContent)) + { + fhirTemplate = LoadFhirTemplate(fhirMappingContent, validationResult.TemplateResult); + } + + if (contentTemplate != null && fhirTemplate != null) + { + CheckForTemplateCompatibility(contentTemplate, fhirTemplate, validationResult.TemplateResult); + } + + if (validationResult.TemplateResult.GetErrors(ErrorLevel.ERROR).Count() > 0) + { + // Fail early since there are errors with the template. + return validationResult; + } + + foreach (var payload in deviceEvents) + { + if (payload != null && contentTemplate != null) + { + var deviceResult = new DeviceResult(); + deviceResult.DeviceEvent = payload; + validationResult.DeviceResults.Add(deviceResult); + + ProcessDeviceEvent(payload, contentTemplate, deviceResult); + + if (fhirTemplate != null) + { + foreach (var m in deviceResult.Measurements) + { + ProcessNormalizedeEvent(m, fhirTemplate, deviceResult); + } + } + } + } + + return validationResult; + } + + private IContentTemplate LoadDeviceTemplate(string deviceMappingContent, TemplateResult validationResult) + { + try + { + var templateContext = _collectionTemplateFactory.Create(deviceMappingContent); + templateContext.EnsureValid(); + return templateContext.Template; + } + catch (Exception e) + { + validationResult.CaptureException(e); + } + + return null; + } + + private ILookupTemplate LoadFhirTemplate(string fhirMappingContent, TemplateResult validationResult) + { + try + { + var fhirTemplateContext = _fhirTemplateFactory.Create(fhirMappingContent); + fhirTemplateContext.EnsureValid(); + return fhirTemplateContext.Template; + } + catch (Exception e) + { + validationResult.CaptureException(e); + } + + return null; + } + + private void CheckForTemplateCompatibility(IContentTemplate contentTemplate, ILookupTemplate fhirTemplate, TemplateResult validationResult) + { + var deviceTemplates = new List(); + var fhirTemplates = new List(); + var availableFhirTemplates = string.Empty; + + // TODO: Confirm that outer template factories are always collections for both Device and Fhir Mappings. This implies that + // customers must always wrap their templates inside of a CollectionXXX Template. + + if (contentTemplate is CollectionContentTemplate collectionContentTemplate) + { + deviceTemplates.AddRange(collectionContentTemplate.Templates.Select(t => t as MeasurementExtractor)); + } + + if (fhirTemplate is FhirLookupTemplate fhirLookupTemplate) + { + fhirTemplates.AddRange(fhirLookupTemplate.Templates.Select(t => t as CodeValueFhirTemplate)); + availableFhirTemplates = string.Join(" ,", fhirTemplates.Select(t => t.TypeName)); + } + + foreach (var extractor in deviceTemplates) + { + try + { + var innerTemplate = extractor.Template; + var matchingFhirTemplate = fhirTemplate.GetTemplate(innerTemplate.TypeName) as CodeValueFhirTemplate; + var availableFhirValueNames = GetFhirValues(matchingFhirTemplate).Select(v => v.ValueName).ToHashSet(); + var availableFhirValueNamesDisplay = string.Join(" ,", availableFhirValueNames); + + // Ensure all values are present + if (extractor.Template.Values != null) + { + foreach (var v in extractor.Template.Values) + { + if (!availableFhirValueNames.Contains(v.ValueName)) + { + validationResult.CaptureWarning($"The value [{v.ValueName}] in Device Mapping [{extractor.Template.TypeName}] is not represented within the Fhir Template of type [{innerTemplate.TypeName}]. Available values are: [{availableFhirValueNamesDisplay}]. No value will appear inside of Observations."); + } + } + } + } + catch (TemplateNotFoundException) + { + validationResult.CaptureWarning($"No matching Fhir Template exists for Device Mapping [{extractor.Template.TypeName}]. Ensure case matches. Available Fhir Templates: [{availableFhirTemplates}]."); + } + catch (Exception e) + { + validationResult.CaptureException(e); + } + } + } + + private void ProcessDeviceEvent(JToken deviceEvent, IContentTemplate contentTemplate, DeviceResult validationResult) + { + try + { + foreach (var m in contentTemplate.GetMeasurements(deviceEvent)) + { + validationResult.Measurements.Add(m); + } + + if (validationResult.Measurements.Count == 0) + { + validationResult.CaptureWarning("No measurements were produced for the given device data."); + } + } + catch (Exception e) + { + validationResult.CaptureException(e); + } + } + + private void ProcessNormalizedeEvent(Measurement normalizedEvent, ILookupTemplate fhirTemplate, DeviceResult validationResult) + { + var measurementGroup = new MeasurementGroup + { + MeasureType = normalizedEvent.Type, + CorrelationId = normalizedEvent.CorrelationId, + DeviceId = normalizedEvent.DeviceId, + EncounterId = normalizedEvent.EncounterId, + PatientId = normalizedEvent.PatientId, + Data = new List() { normalizedEvent }, + }; + + try + { + // Convert Measurement to Observation Group + var observationGroup = _fhirTemplateProcessor.CreateObservationGroups(fhirTemplate, measurementGroup).First(); + + // Build HL7 Observation + validationResult.Observations.Add(_fhirTemplateProcessor.CreateObservation(fhirTemplate, observationGroup)); + } + catch (TemplateNotFoundException e) + { + validationResult.CaptureError( + $"No Fhir Template exists with the type name [{e.Message}]. Ensure that all Fhir Template type names match Device Mapping type names (including casing)", + ErrorLevel.ERROR); + } + catch (Exception e) + { + validationResult.CaptureException(e); + } + } + + private static ISet GetFhirValues(CodeValueFhirTemplate codeValueFhirTemplate) + { + var fhirTemplateValues = new HashSet(); + + if (codeValueFhirTemplate.Components != null) + { + foreach (var c in codeValueFhirTemplate.Components) + { + fhirTemplateValues.Add(c.Value); + } + } + else + { + fhirTemplateValues.Add(codeValueFhirTemplate.Value); + } + + return fhirTemplateValues; + } + } +} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Microsoft.Health.Fhir.Ingest.Validation.csproj b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Microsoft.Health.Fhir.Ingest.Validation.csproj new file mode 100644 index 00000000..b41f05a7 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Microsoft.Health.Fhir.Ingest.Validation.csproj @@ -0,0 +1,48 @@ + + + netcoreapp3.1 + ..\..\..\CustomAnalysisRules.ruleset + true + Microsoft.Health.Fhir.Ingest.Validation + Microsoft.Health.Fhir.Ingest.Validation + 8.0 + Release;Debug + + + true + + + true + + + + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/DeviceResult.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/DeviceResult.cs new file mode 100644 index 00000000..475d5fd7 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/DeviceResult.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using Microsoft.Health.Fhir.Ingest.Data; +using Newtonsoft.Json.Linq; +using Model = Hl7.Fhir.Model; + +namespace Microsoft.Health.Fhir.Ingest.Validation.Models +{ + public class DeviceResult : IResult + { + public JToken DeviceEvent { get; set; } + + public IList Measurements { get; set; } = new List(); + + public IList Observations { get; set; } = new List(); + + public IList Exceptions { get; set; } = new List(); + } +} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ErrorLevel.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ErrorLevel.cs new file mode 100644 index 00000000..975c1ffc --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ErrorLevel.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +namespace Microsoft.Health.Fhir.Ingest.Validation.Models +{ + public enum ErrorLevel + { + /// + /// Indicates an error that will prevent the mapping process from succeeding. + /// + ERROR, + + /// + /// Indicates an issue with a mapping operation that may result in an unexpected outcome for the user + /// + WARN, + } +} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/IResult.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/IResult.cs new file mode 100644 index 00000000..d3215162 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/IResult.cs @@ -0,0 +1,14 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Health.Fhir.Ingest.Validation.Models +{ + public interface IResult + { + IList Exceptions { get; set; } + } +} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/TemplateResult.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/TemplateResult.cs new file mode 100644 index 00000000..0af13ec7 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/TemplateResult.cs @@ -0,0 +1,14 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Health.Fhir.Ingest.Validation.Models +{ + public class TemplateResult : IResult + { + public IList Exceptions { get; set; } = new List(); + } +} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationError.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationError.cs new file mode 100644 index 00000000..7439b5f2 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationError.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using EnsureThat; + +namespace Microsoft.Health.Fhir.Ingest.Validation.Models +{ + public class ValidationError + { + public ValidationError(string message, ErrorLevel errorLevel = ErrorLevel.ERROR) + { + Message = EnsureArg.IsNotNullOrWhiteSpace(message, nameof(message)); + Level = errorLevel; + } + + public string Message { get; } + + public ErrorLevel Level { get; } + } +} diff --git a/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationResult.cs b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationResult.cs new file mode 100644 index 00000000..8bb54e28 --- /dev/null +++ b/src/lib/Microsoft.Health.Fhir.Ingest.Validation/Models/ValidationResult.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Health.Fhir.Ingest.Validation.Models +{ + public class ValidationResult + { + public TemplateResult TemplateResult { get; set; } = new TemplateResult(); + + public IList DeviceResults { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/MappingValidatorTests.cs b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/MappingValidatorTests.cs new file mode 100644 index 00000000..8b83ea47 --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/MappingValidatorTests.cs @@ -0,0 +1,280 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Microsoft.Health.Fhir.Ingest.Template; +using Microsoft.Health.Fhir.Ingest.Validation.Extensions; +using Microsoft.Health.Tests.Common; +using Newtonsoft.Json.Linq; +using Xunit; +using Model = Hl7.Fhir.Model; + +namespace Microsoft.Health.Fhir.Ingest.Validation.UnitTests +{ + public class MappingValidatorTests + { + private CollectionTemplateFactory _collectionTemplateFactory; + private ITemplateFactory>> _fhirTemplateFactory; + private MappingValidator _iotConnectorValidator; + + public MappingValidatorTests() + { + _fhirTemplateFactory = CollectionFhirTemplateFactory.Default; + _collectionTemplateFactory = new CollectionContentTemplateFactory( + new JsonPathContentTemplateFactory(), + new IotJsonPathContentTemplateFactory(), + new IotCentralJsonPathContentTemplateFactory()); + + _iotConnectorValidator = new MappingValidator( + _collectionTemplateFactory, + _fhirTemplateFactory, + new R4FhirLookupTemplateProcessor()); + } + + [Fact] + public void When_No_MappingFilesAreProvided_Exception_Is_Thrown() + { + Assert.Throws(() => _iotConnectorValidator.PerformValidation(null, null, null)); + } + + [Theory] + [FileData(@"TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json", @"TestInput/data_CollectionFhirTemplateValid.json")] + public void Given_ValidMappingFiles_And_No_DeviceMapping_No_Exceptions_Or_Warnings_Found(string deviceMapping, string fhirMapping) + { + var result = _iotConnectorValidator.PerformValidation(null, deviceMapping, fhirMapping); + Assert.Empty(result.TemplateResult.Exceptions); + Assert.Empty(result.DeviceResults); + } + + [Theory] + [FileData(@"TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json", @"TestInput/data_CollectionFhirTemplateValid.json")] + public void Given_ValidMappingFiles_And_Valid_DeviceMapping_No_Exceptions_And_MeasurementsAreCreated(string deviceMapping, string fhirMapping) + { + var time = DateTime.UtcNow; + var token = JToken.FromObject(new + { + systolic = "60", + diastolic = "80", + device = "abc", + date = time, + session = "abcdefg", + patient = "patient123", + }); + + var result = _iotConnectorValidator.PerformValidation(token, deviceMapping, fhirMapping); + Assert.Empty(result.TemplateResult.Exceptions); + Assert.Collection(result.DeviceResults, d => + { + Assert.Collection(d.Measurements, m => + { + Assert.Equal("bloodpressure", m.Type); + Assert.Equal(time, m.OccurrenceTimeUtc); + Assert.Equal("abc", m.DeviceId); + Assert.Equal("patient123", m.PatientId); + Assert.Null(m.CorrelationId); + Assert.Null(m.EncounterId); + Assert.Collection( + m.Properties, + p => + { + Assert.Equal("systolic", p.Name); + Assert.Equal("60", p.Value); + }, + p => + { + Assert.Equal("diastolic", p.Name); + Assert.Equal("80", p.Value); + }); + }); + }); + + Assert.Collection(result.DeviceResults, d => + { + Assert.Collection(d.Observations, o => + { + Assert.Equal("bloodpressure", o.Code.Text); + Assert.Collection( + o.Component, + c => + { + Assert.Equal("diastolic", c.Code.Text); + Assert.Contains("80", (c.Value as Model.SampledData).Data); + }, + c => + { + Assert.Equal("systolic", c.Code.Text); + Assert.Contains("60", (c.Value as Model.SampledData).Data); + }); + }); + }); + } + + [Theory] + [FileData(@"TestInput/data_CollectionContentTemplateInvalid.json")] + public void When_Only_InvalidDeviceMapping_Provided_Only_DeviceMapping_Exceptions_Logged(string deviceMapping) + { + var result = _iotConnectorValidator.PerformValidation(null, deviceMapping, null); + Assert.Collection( + result.TemplateResult.GetErrors(Models.ErrorLevel.ERROR), + (error) => error.Message.Contains("Required property 'DeviceIdExpression' not found in JSON")); + Assert.Empty(result.DeviceResults); + } + + [Theory] + [FileData(@"TestInput/data_CodeValueFhirTemplateInvalid_MissingFields.json")] + public void When_Only_InvalidFhirMapping_Provided_Only_FhirMapping_Exceptions_Logged(string fhirMapping) + { + var result = _iotConnectorValidator.PerformValidation(null, null, fhirMapping); + Assert.Collection( + result.TemplateResult.GetErrors(Models.ErrorLevel.ERROR), + (error) => error.Message.Contains("Expected TemplateType value CollectionFhirTemplate, actual CodeValueFhir")); + Assert.Empty(result.DeviceResults); + } + + [Theory] + [FileData(@"TestInput/data_CollectionContentTemplateInvalid.json", @"TestInput/data_CodeValueFhirTemplateInvalid_MissingFields.json")] + public void Given_InvalidMappingFiles_Exceptions_Found(string deviceMapping, string fhirMapping) + { + // [0]"Validation errors:\nFailed to deserialize the JsonPathContentTemplate content: \n Required property 'DeviceIdExpression' not found in JSON. \n Required property 'TimestampExpression' not found in JSON. \nFailed to deserialize the IotJsonPathContentTemplate content: \n Required property 'TypeMatchExpression' not found in JSON. " string + // "Validation errors:\nExpected TemplateType value CollectionFhirTemplate, actual CodeValueFhir." + var result = _iotConnectorValidator.PerformValidation(null, deviceMapping, fhirMapping); + Assert.Collection( + result.TemplateResult.GetErrors(Models.ErrorLevel.ERROR), + (error) => error.Message.Contains("Required property 'DeviceIdExpression' not found in JSON"), + (error) => error.Message.Contains("Expected TemplateType value CollectionFhirTemplate, actual CodeValueFhir")); + Assert.Empty(result.DeviceResults); + } + + [Theory] + [FileData(@"TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json", @"TestInput/data_CollectionFhirTemplateMissingTypeInvalid.json")] + public void Given_ValidDeviceMapping_And_FhirMapping_WithMissingTypeName_Warnings_Found(string deviceMapping, string fhirMapping) + { + var time = DateTime.UtcNow; + var token = JToken.FromObject(new + { + systolic = "60", + diastolic = "80", + device = "abc", + date = time, + session = "abcdefg", + patient = "patient123", + }); + var result = _iotConnectorValidator.PerformValidation(token, deviceMapping, fhirMapping); + Assert.Empty(result.TemplateResult.GetErrors(Models.ErrorLevel.ERROR)); + Assert.Collection( + result.TemplateResult.GetErrors(Models.ErrorLevel.WARN), + (error) => error.Message.Contains("No matching Fhir Template exists for Device Mapping [bloodpressure]")); + Assert.Collection(result.DeviceResults, d => + { + Assert.Collection( + d.GetErrors(Models.ErrorLevel.ERROR), + e => e.Message.StartsWith("No Fhir Template exists with the type name [bloodpressure]")); + Assert.Single(d.Measurements); + Assert.NotNull(d.DeviceEvent); + Assert.Empty(d.Observations); + Assert.Empty(d.GetErrors(Models.ErrorLevel.WARN)); + }); + } + + [Theory] + [FileData(@"TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json", @"TestInput/data_CollectionFhirTemplateIncorrectValueNameInvalid.json")] + public void Given_ValidDeviceMapping_And_FhirMapping_WithIncorrectValueName_Warnings_Found(string deviceMapping, string fhirMapping) + { + var time = DateTime.UtcNow; + var token = JToken.FromObject(new + { + systolic = "60", + diastolic = "80", + device = "abc", + date = time, + session = "abcdefg", + patient = "patient123", + }); + var token2 = JToken.FromObject(new + { + systolic = "60", + diastolic = "180", + device = "abc", + date = time, + session = "abcdefg", + patient = "patient123", + }); + + var result = _iotConnectorValidator.PerformValidation(new List() { token, token2 }, deviceMapping, fhirMapping); + + Assert.Empty(result.TemplateResult.GetErrors(Models.ErrorLevel.ERROR)); + Assert.Collection( + result.TemplateResult.GetErrors(Models.ErrorLevel.WARN), + (error) => error.Message.StartsWith("The value [systolic] in Device Mapping [bloodpressure] is not represented within the Fhir Template")); + + Assert.Collection( + result.DeviceResults, + d => + { + Assert.Single(d.Measurements); + Assert.NotNull(d.DeviceEvent); + Assert.Empty(d.GetErrors(Models.ErrorLevel.WARN)); + Assert.Empty(d.Exceptions); + Assert.Collection(d.Observations, o => + { + Assert.Equal("bloodpressure", o.Code.Text); + Assert.Collection( + o.Component, + c => + { + Assert.Equal("diastolic", c.Code.Text); + Assert.Contains("80", (c.Value as Model.SampledData).Data); + }); + }); + }, + d => + { + Assert.Single(d.Measurements); + Assert.NotNull(d.DeviceEvent); + Assert.Empty(d.GetErrors(Models.ErrorLevel.WARN)); + Assert.Empty(d.Exceptions); + Assert.Collection(d.Observations, o => + { + Assert.Equal("bloodpressure", o.Code.Text); + Assert.Collection( + o.Component, + c => + { + Assert.Equal("diastolic", c.Code.Text); + Assert.Contains("180", (c.Value as Model.SampledData).Data); + }); + }); + }); + } + + [Theory] + [FileData(@"TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json", @"TestInput/data_CollectionFhirTemplateValid.json")] + public void Given_ValidMappingFiles_And_Valid_DeviceMapping_NoMeasurementsAreCreated_With_Bad_DeviceData(string deviceMapping, string fhirMapping) + { + var time = DateTime.UtcNow; + var token = JToken.FromObject(new + { + device = "abc", + date = time, + session = "abcdefg", + patient = "patient123", + }); + + var result = _iotConnectorValidator.PerformValidation(token, deviceMapping, fhirMapping); + Assert.Empty(result.TemplateResult.Exceptions); + Assert.Collection(result.DeviceResults, d => + { + Assert.Collection( + d.GetErrors(Models.ErrorLevel.WARN), + (error) => error.Message.StartsWith("No measurements were produced")); + Assert.Empty(d.Measurements); + Assert.Empty(d.Observations); + Assert.NotNull(d.DeviceEvent); + Assert.Empty(d.GetErrors(Models.ErrorLevel.ERROR)); + }); + } + } +} diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/Microsoft.Health.Fhir.Ingest.Validation.UnitTests.csproj b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/Microsoft.Health.Fhir.Ingest.Validation.UnitTests.csproj new file mode 100644 index 00000000..06493275 --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/Microsoft.Health.Fhir.Ingest.Validation.UnitTests.csproj @@ -0,0 +1,57 @@ + + + + netcoreapp3.1 + ..\..\CustomAnalysisRules.Test.ruleset + true + Off + Microsoft.Health.Fhir.Ingest.Validation.UnitTests + Microsoft.Health.Fhir.Ingest.Validation.UnitTests + 8.0 + Release;Debug + + + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + Always + + + Always + + + + + + diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CodeValueFhirTemplateInvalid_MissingFields.json b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CodeValueFhirTemplateInvalid_MissingFields.json new file mode 100644 index 00000000..4c211a07 --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CodeValueFhirTemplateInvalid_MissingFields.json @@ -0,0 +1,18 @@ +{ + "templateType": "CodeValueFhir", + "template": { + "codes": [ + { + "code": "8867-4", + "system": "http://loinc.org" + } + ], + "periodInterval": 60, + "value": { + "defaultPeriod": 5000, + "unit": "bpm", + "valueName": "hr", + "valueType": "sampledData" + } + } +} \ No newline at end of file diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json new file mode 100644 index 00000000..775e3220 --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateHrAndBloodPressureValid.json @@ -0,0 +1,43 @@ +{ + "templateType": "CollectionContent", + "template": [ + { + "templateType": "JsonPathContent", + "template": { + "typeName": "heartrate", + "typeMatchExpression": "$..[?(@heartrate)]", + "deviceIdExpression": "$.device", + "timestampExpression": "$.date", + "values": [ + { + "required": "true", + "valueExpression": "$.heartrate", + "valueName": "hr" + } + ] + } + }, + { + "templateType": "JsonPathContent", + "template": { + "typeName": "bloodpressure", + "typeMatchExpression": "$..[?(@systolic && @diastolic)]", + "deviceIdExpression": "$.device", + "patientIdExpression": "$.patient", + "timestampExpression": "$.date", + "values": [ + { + "required": "true", + "valueExpression": "$.systolic", + "valueName": "systolic" + }, + { + "required": "true", + "valueExpression": "$.diastolic", + "valueName": "diastolic" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateInvalid.json b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateInvalid.json new file mode 100644 index 00000000..5315a1c5 --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionContentTemplateInvalid.json @@ -0,0 +1,32 @@ +{ + "templateType": "CollectionContent", + "template": [ + { + "templateType": "JsonPathContent", + "template": { + "typeName": "heartrate", + "typeMatchExpression": "$..[?(@heartRate && @endDate)]", + "values": [ + { + "required": "true", + "valueExpression": "$.heartRate", + "valueName": "hr" + } + ] + } + }, + { + "templateType": "IotJsonPathContent", + "template": { + "typeName": "bootTime", + "values": [ + { + "required": "false", + "valueExpression": "$.Body.patientActiveTime", + "valueName": "activeTime" + } + ] + } + } + ] +} diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateIncorrectValueNameInvalid.json b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateIncorrectValueNameInvalid.json new file mode 100644 index 00000000..b6c42785 --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateIncorrectValueNameInvalid.json @@ -0,0 +1,76 @@ +{ + "templateType": "CollectionFhir", + "template": [ + { + "templateType": "CodeValueFhir", + "template": { + "codes": [ + { + "code": "8867-4", + "system": "http://loinc.org", + "display": "Heart rate" + } + ], + "periodInterval": 60, + "typeName": "heartrate", + "value": { + "defaultPeriod": 1000, + "unit": "count/min", + "valueName": "hr", + "valueType": "SampledData" + } + } + }, + { + "templateType": "CodeValueFhir", + "template": { + "codes": [ + { + "code": "85354-9", + "display": "Blood pressure panel", + "system": "http://loinc.org" + } + ], + "periodInterval": 60, + "typeName": "bloodpressure", + "components": [ + { + "codes": [ + { + "code": "8867-4", + "display": "Diastolic blood pressure", + "system": "http://loinc.org" + } + ], + "value": { + "defaultPeriod": 1000, + "unit": "mmHg", + "valueName": "diastolic", + "valueType": "SampledData" + } + }, + { + "codes": [ + { + "code": "8480-6", + "display": "Systolic blood pressure", + "system": "http://loinc.org" + }, + { + "code": "271649006", + "display": "Systolic blood pressure", + "system": "http://snomed.info/sct" + } + ], + "value": { + "defaultPeriod": 1000, + "unit": "mmHg", + "valueName": "systolicBadValueName", + "valueType": "SampledData" + } + } + ] + } + } + ] +} diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateMissingTypeInvalid.json b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateMissingTypeInvalid.json new file mode 100644 index 00000000..b092e6db --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateMissingTypeInvalid.json @@ -0,0 +1,76 @@ +{ + "templateType": "CollectionFhir", + "template": [ + { + "templateType": "CodeValueFhir", + "template": { + "codes": [ + { + "code": "8867-4", + "system": "http://loinc.org", + "display": "Heart rate" + } + ], + "periodInterval": 60, + "typeName": "heartrate", + "value": { + "defaultPeriod": 1000, + "unit": "count/min", + "valueName": "hr", + "valueType": "SampledData" + } + } + }, + { + "templateType": "CodeValueFhir", + "template": { + "codes": [ + { + "code": "85354-9", + "display": "Blood pressure panel", + "system": "http://loinc.org" + } + ], + "periodInterval": 60, + "typeName": "bloodpressureBadTypeName", + "components": [ + { + "codes": [ + { + "code": "8867-4", + "display": "Diastolic blood pressure", + "system": "http://loinc.org" + } + ], + "value": { + "defaultPeriod": 1000, + "unit": "mmHg", + "valueName": "diastolic", + "valueType": "SampledData" + } + }, + { + "codes": [ + { + "code": "8480-6", + "display": "Systolic blood pressure", + "system": "http://loinc.org" + }, + { + "code": "271649006", + "display": "Systolic blood pressure", + "system": "http://snomed.info/sct" + } + ], + "value": { + "defaultPeriod": 1000, + "unit": "mmHg", + "valueName": "systolic", + "valueType": "SampledData" + } + } + ] + } + } + ] +} diff --git a/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateValid.json b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateValid.json new file mode 100644 index 00000000..6498ecba --- /dev/null +++ b/test/Microsoft.Health.Fhir.Ingest.Validation.UnitTests/TestInput/data_CollectionFhirTemplateValid.json @@ -0,0 +1,76 @@ +{ + "templateType": "CollectionFhir", + "template": [ + { + "templateType": "CodeValueFhir", + "template": { + "codes": [ + { + "code": "8867-4", + "system": "http://loinc.org", + "display": "Heart rate" + } + ], + "periodInterval": 60, + "typeName": "heartrate", + "value": { + "defaultPeriod": 1000, + "unit": "count/min", + "valueName": "hr", + "valueType": "SampledData" + } + } + }, + { + "templateType": "CodeValueFhir", + "template": { + "codes": [ + { + "code": "85354-9", + "display": "Blood pressure panel", + "system": "http://loinc.org" + } + ], + "periodInterval": 60, + "typeName": "bloodpressure", + "components": [ + { + "codes": [ + { + "code": "8867-4", + "display": "Diastolic blood pressure", + "system": "http://loinc.org" + } + ], + "value": { + "defaultPeriod": 1000, + "unit": "mmHg", + "valueName": "diastolic", + "valueType": "SampledData" + } + }, + { + "codes": [ + { + "code": "8480-6", + "display": "Systolic blood pressure", + "system": "http://loinc.org" + }, + { + "code": "271649006", + "display": "Systolic blood pressure", + "system": "http://snomed.info/sct" + } + ], + "value": { + "defaultPeriod": 1000, + "unit": "mmHg", + "valueName": "systolic", + "valueType": "SampledData" + } + } + ] + } + } + ] +}