Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Personal/rogordon/validation aggregration updates #179

Merged
merged 3 commits into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,28 @@ namespace Microsoft.Health.Fhir.Ingest.Validation.Extensions
{
public static class IResultExtensions
{
public static void CaptureError(this IResult validationResult, string message, ErrorLevel errorLevel)
public static void CaptureError(this IResult validationResult, string message, ErrorLevel errorLevel, ValidationCategory category)
{
EnsureArg.IsNotNull(validationResult, nameof(validationResult));
EnsureArg.IsNotNullOrWhiteSpace(message, nameof(message));

validationResult.Exceptions.Add(new ValidationError(message, errorLevel));
validationResult.Exceptions.Add(new ValidationError(message, category, errorLevel));
}

public static void CaptureException(this IResult validationResult, Exception exception)
public static void CaptureException(this IResult validationResult, Exception exception, ValidationCategory category)
{
EnsureArg.IsNotNull(validationResult, nameof(validationResult));
EnsureArg.IsNotNull(exception, nameof(exception));

validationResult.Exceptions.Add(new ValidationError(exception.Message));
validationResult.Exceptions.Add(new ValidationError(exception.Message, category));
}

public static void CaptureWarning(this IResult validationResult, string warning)
public static void CaptureWarning(this IResult validationResult, string warning, ValidationCategory category)
{
EnsureArg.IsNotNull(validationResult, nameof(validationResult));
EnsureArg.IsNotNullOrWhiteSpace(warning, nameof(warning));

validationResult.Exceptions.Add(new ValidationError(warning, ErrorLevel.WARN));
validationResult.Exceptions.Add(new ValidationError(warning, category, ErrorLevel.WARN));
}

public static IEnumerable<ValidationError> GetErrors(this IResult validationResult, ErrorLevel errorLevel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public interface IMappingValidator
/// <param name="deviceEvents">A collection of DeviceEvents. Optional</param>
/// <param name="deviceMappingContent">A device mapping template. Optional</param>
/// <param name="fhirMappingContent">A fhir mapping template. Optional</param>
/// <param name="aggregateDeviceEvents">Indicates if DeviceResults should be aggregated</param>
/// <returns>A ValidationResult object</returns>
ValidationResult PerformValidation(IEnumerable<JToken> deviceEvents, string deviceMappingContent, string fhirMappingContent);
ValidationResult PerformValidation(IEnumerable<JToken> deviceEvents, string deviceMappingContent, string fhirMappingContent, bool aggregateDeviceEvents = false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ public ValidationResult PerformValidation(
string deviceMappingContent,
string fhirMappingContent)
{
return PerformValidation(new List<JToken>() { deviceEvent }, deviceMappingContent, fhirMappingContent);
return PerformValidation(new List<JToken>() { deviceEvent }, deviceMappingContent, fhirMappingContent, false);
}

public ValidationResult PerformValidation(
IEnumerable<JToken> deviceEvents,
string deviceMappingContent,
string fhirMappingContent)
string fhirMappingContent,
bool aggregateDeviceEvents = false)
{
if (string.IsNullOrWhiteSpace(deviceMappingContent) && string.IsNullOrWhiteSpace(fhirMappingContent))
{
Expand Down Expand Up @@ -77,27 +78,80 @@ public ValidationResult PerformValidation(
return validationResult;
}

ValidateDeviceEvents(deviceEvents, contentTemplate, fhirTemplate, validationResult, aggregateDeviceEvents);

return validationResult;
}

/// <summary>
/// Validates device events. This method then enriches the passed in ValidationResult object with DeviceResults.
/// </summary>
/// <param name="deviceEvents">The device events to validate</param>
/// <param name="contentTemplate">The device mapping template</param>
/// <param name="fhirTemplate">The fhir mapping template</param>
/// <param name="validationResult">The ValidationResult</param>
/// <param name="aggregateDeviceEvents">Indicates if DeviceResults should be aggregated</param>
protected virtual void ValidateDeviceEvents(
IEnumerable<JToken> deviceEvents,
IContentTemplate contentTemplate,
ILookupTemplate<IFhirTemplate> fhirTemplate,
ValidationResult validationResult,
bool aggregateDeviceEvents)
{
var aggregatedDeviceResults = new Dictionary<string, DeviceResult>();

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);
ProcessNormalizedEvent(m, fhirTemplate, deviceResult);
}
}

if (aggregateDeviceEvents)
{
/*
* During aggregation we group DeviceEvents by the exceptions that they produce.
* This allows us to return a DeviceResult with a sample Device Event payload,
* the running count grouped DeviceEvents and the exception that they are grouped by.
*/
foreach (var exception in deviceResult.Exceptions)
{
if (aggregatedDeviceResults.TryGetValue(exception.Message, out DeviceResult result))
{
// If we've already seen this error message before, simply increment the running total
result.AggregatedCount++;
}
else
{
// Create a new DeviceResult to hold details about this new exception.
var aggregatedDeviceResult = new DeviceResult()
{
DeviceEvent = deviceResult.DeviceEvent, // A sample device event which exhibits the error
AggregatedCount = 1,
Exceptions = new List<ValidationError>() { exception },
};

aggregatedDeviceResults[exception.Message] = aggregatedDeviceResult;
validationResult.DeviceResults.Add(aggregatedDeviceResult);
}
}
}
else
{
validationResult.DeviceResults.Add(deviceResult);
}
}
}

return validationResult;
}

private IContentTemplate LoadDeviceTemplate(string deviceMappingContent, TemplateResult validationResult)
Expand All @@ -110,7 +164,7 @@ private IContentTemplate LoadDeviceTemplate(string deviceMappingContent, Templat
}
catch (Exception e)
{
validationResult.CaptureException(e);
validationResult.CaptureException(e, ValidationCategory.NORMALIZATION);
}

return null;
Expand All @@ -126,7 +180,7 @@ private ILookupTemplate<IFhirTemplate> LoadFhirTemplate(string fhirMappingConten
}
catch (Exception e)
{
validationResult.CaptureException(e);
validationResult.CaptureException(e, ValidationCategory.FHIRTRANSFORMATION);
}

return null;
Expand Down Expand Up @@ -168,23 +222,27 @@ private void CheckForTemplateCompatibility(IContentTemplate contentTemplate, ILo
{
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.");
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.",
ValidationCategory.FHIRTRANSFORMATION);
}
}
}
}
catch (TemplateNotFoundException)
{
validationResult.CaptureWarning($"No matching Fhir Template exists for Device Mapping [{extractor.Template.TypeName}]. Ensure case matches. Available Fhir Templates: [{availableFhirTemplates}].");
validationResult.CaptureWarning(
$"No matching Fhir Template exists for Device Mapping [{extractor.Template.TypeName}]. Ensure case matches. Available Fhir Templates: [{availableFhirTemplates}].",
ValidationCategory.FHIRTRANSFORMATION);
}
catch (Exception e)
{
validationResult.CaptureException(e);
validationResult.CaptureException(e, ValidationCategory.FHIRTRANSFORMATION);
}
}
}

private void ProcessDeviceEvent(JToken deviceEvent, IContentTemplate contentTemplate, DeviceResult validationResult)
protected virtual void ProcessDeviceEvent(JToken deviceEvent, IContentTemplate contentTemplate, DeviceResult validationResult)
kenziedolish marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
Expand All @@ -195,16 +253,16 @@ private void ProcessDeviceEvent(JToken deviceEvent, IContentTemplate contentTemp

if (validationResult.Measurements.Count == 0)
{
validationResult.CaptureWarning("No measurements were produced for the given device data.");
validationResult.CaptureWarning("No measurements were produced for the given device data.", ValidationCategory.NORMALIZATION);
}
}
catch (Exception e)
{
validationResult.CaptureException(e);
validationResult.CaptureException(e, ValidationCategory.NORMALIZATION);
}
}

private void ProcessNormalizedeEvent(Measurement normalizedEvent, ILookupTemplate<IFhirTemplate> fhirTemplate, DeviceResult validationResult)
protected virtual void ProcessNormalizedEvent(Measurement normalizedEvent, ILookupTemplate<IFhirTemplate> fhirTemplate, DeviceResult validationResult)
kenziedolish marked this conversation as resolved.
Show resolved Hide resolved
{
var measurementGroup = new MeasurementGroup
{
Expand All @@ -228,11 +286,12 @@ private void ProcessNormalizedeEvent(Measurement normalizedEvent, ILookupTemplat
{
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);
ErrorLevel.ERROR,
ValidationCategory.FHIRTRANSFORMATION);
}
catch (Exception e)
{
validationResult.CaptureException(e);
validationResult.CaptureException(e, ValidationCategory.FHIRTRANSFORMATION);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ public class DeviceResult : IResult
public IList<Model.Observation> Observations { get; set; } = new List<Model.Observation>();

public IList<ValidationError> Exceptions { get; set; } = new List<ValidationError>();

/// <summary>
/// Indicates how many Device Events produced the associated Exception. This value is only set when aggregating DeviceEvent results. Otherwise
/// it will be zero.
/// </summary>
public int AggregatedCount { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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 ValidationCategory
{
/// <summary>
/// Indicates that the area of validation is related to Normalization.
/// </summary>
NORMALIZATION,

/// <summary>
/// Indicates that the area of validation is related to Fhir Transformation.
/// </summary>
FHIRTRANSFORMATION,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ namespace Microsoft.Health.Fhir.Ingest.Validation.Models
{
public class ValidationError
{
public ValidationError(string message, ErrorLevel errorLevel = ErrorLevel.ERROR)
public ValidationError(string message, ValidationCategory category, ErrorLevel errorLevel = ErrorLevel.ERROR)
{
Message = EnsureArg.IsNotNullOrWhiteSpace(message, nameof(message));
Level = errorLevel;
Category = category;
}

public string Message { get; }

public ErrorLevel Level { get; }

public ValidationCategory Category { get; }
}
}
Loading