Skip to content

Commit

Permalink
R4 Validation -- Move the shared Validation code to Core (#522)
Browse files Browse the repository at this point in the history
* Move Validation classes to Core from shared, using ResourceElement

* Remove dependency on HL7.Model from the validator code

* Use ResourceElement instead of ITypedElement

* Use known constants in place of text literals

* Use generic ToPoco, fix recurse param

* Update to ToResourceElement extension method

* Change KnownFhirPath back to text.div, keep filter for DomainResources

* Change NarrativeValidator.ValidateResource to use ITypedElement
  • Loading branch information
feordin authored Jun 12, 2019
1 parent 740c9d7 commit 9cd68b6
Show file tree
Hide file tree
Showing 25 changed files with 173 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hl7.Fhir.Model;
using Microsoft.Health.Fhir.Core.Models;

namespace Microsoft.Health.Fhir.Core.Features.Conformance
{
public interface IConformanceProvider
{
Task<CapabilityStatement> GetCapabilityStatementAsync(CancellationToken cancellationToken = default(CancellationToken));
Task<ResourceElement> GetCapabilityStatementAsync(CancellationToken cancellationToken = default(CancellationToken));

Task<bool> SatisfiesAsync(IEnumerable<CapabilityQuery> queries, CancellationToken cancellationToken = default(CancellationToken));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
using FluentValidation;
using FluentValidation.Results;
using FluentValidation.Validators;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
using Hl7.Fhir.Validation;
using Microsoft.Health.Fhir.Core.Models;
using Task = System.Threading.Tasks.Task;
using ValidationResult = System.ComponentModel.DataAnnotations.ValidationResult;
Expand All @@ -21,18 +18,26 @@ namespace Microsoft.Health.Fhir.Core.Features.Validation
{
public class AttributeValidator : IPropertyValidator
{
private IModelAttributeValidator _modelAttributeValidator;

public AttributeValidator(IModelAttributeValidator modelAttributeValidator)
{
EnsureArg.IsNotNull(modelAttributeValidator, nameof(modelAttributeValidator));

_modelAttributeValidator = modelAttributeValidator;
}

public PropertyValidatorOptions Options { get; set; } = PropertyValidatorOptions.Empty;

public IEnumerable<ValidationFailure> Validate(PropertyValidatorContext context)
{
EnsureArg.IsNotNull(context, nameof(context));

if (context.PropertyValue is ResourceElement typedElement)
if (context.PropertyValue is ResourceElement resourceElement)
{
var resource = typedElement.Instance.ToPoco<Resource>();
var results = new List<ValidationResult>();

if (!DotNetAttributeValidation.TryValidate(resource, results, recurse: false))
if (!_modelAttributeValidator.TryValidate(resourceElement, results, recurse: false))
{
foreach (var error in results)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// -------------------------------------------------------------------------------------------------
// 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 System.ComponentModel.DataAnnotations;
using Microsoft.Health.Fhir.Core.Models;

namespace Microsoft.Health.Fhir.Core.Features.Validation
{
public interface IModelAttributeValidator
{
bool TryValidate(ResourceElement value, ICollection<ValidationResult> validationResults = null, bool recurse = false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
using EnsureThat;
using FluentValidation.Results;
using FluentValidation.Validators;
using Hl7.Fhir.Model;
using Microsoft.Health.Fhir.Core.Extensions;
using Hl7.Fhir.ElementModel;
using Hl7.FhirPath;
using Microsoft.Health.Fhir.Core.Models;

namespace Microsoft.Health.Fhir.Core.Features.Validation.Narratives
Expand All @@ -31,50 +31,50 @@ public override IEnumerable<ValidationFailure> Validate(PropertyValidatorContext

if (context.PropertyValue is ResourceElement resourceElement)
{
var poco = resourceElement.ToPoco();

if (poco is DomainResource resource)
if (resourceElement.IsDomainResource)
{
foreach (ValidationFailure validationFailure in ValidateResource(resource))
foreach (ValidationFailure validationFailure in ValidateResource(resourceElement.Instance))
{
yield return validationFailure;
}
}

if (poco is Bundle bundle)
else if (resourceElement.InstanceType.Equals(KnownResourceTypes.Bundle, System.StringComparison.OrdinalIgnoreCase))
{
var domainResources = bundle.Entry.Select(x => x.Resource).OfType<DomainResource>();

foreach (ValidationFailure validationFailure in domainResources.SelectMany(ValidateResource))
var bundleEntries = resourceElement.Instance.Select(KnownFhirPaths.BundleEntries);
if (bundleEntries != null)
{
yield return validationFailure;
foreach (ValidationFailure validationFailure in bundleEntries.SelectMany(ValidateResource))
{
yield return validationFailure;
}
}
}
}
}

private IEnumerable<ValidationFailure> ValidateResource(DomainResource domainResource)
private IEnumerable<ValidationFailure> ValidateResource(ITypedElement typedElement)
{
EnsureArg.IsNotNull(domainResource, nameof(domainResource));
EnsureArg.IsNotNull(typedElement, nameof(typedElement));

if (string.IsNullOrEmpty(domainResource.Text?.Div))
var xhtml = typedElement.Scalar(KnownFhirPaths.ResourceNarrative) as string;
if (string.IsNullOrEmpty(xhtml))
{
yield break;
}

var errors = _narrativeHtmlSanitizer.Validate(domainResource.Text.Div);
string propertyName = $"{domainResource.TypeName}.Text.Div";
var errors = _narrativeHtmlSanitizer.Validate(xhtml);
var fullFhirPath = typedElement.InstanceType + "." + KnownFhirPaths.ResourceNarrative;

foreach (var error in errors)
{
yield return new FhirValidationFailure(
propertyName,
fullFhirPath,
error,
new OperationOutcomeIssue(
OperationOutcomeConstants.IssueType.Structure,
OperationOutcomeConstants.IssueSeverity.Error,
error,
location: new[] { propertyName }));
location: new[] { fullFhirPath }));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ namespace Microsoft.Health.Fhir.Core.Features.Validation
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Follows validator naming convention.")]
public class ResourceValidator : AbstractValidator<ResourceElement>
{
public ResourceValidator(INarrativeHtmlSanitizer narrativeHtmlSanitizer)
public ResourceValidator(INarrativeHtmlSanitizer narrativeHtmlSanitizer, IModelAttributeValidator modelAttributeValidator)
{
RuleFor(x => x.Id)
.SetValidator(new IdValidator());
RuleFor(x => x)
.SetValidator(new AttributeValidator());
.SetValidator(new AttributeValidator(modelAttributeValidator));
RuleFor(x => x)
.SetValidator(new NarrativeValidator(narrativeHtmlSanitizer));
}
Expand Down
18 changes: 18 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Models/KnownFhirPaths.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// -------------------------------------------------------------------------------------------------
// 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.Core.Models
{
public static class KnownFhirPaths
{
public const string BundleEntries = "Resource.entry.resource";

public const string BundleNextLink = "Resource.link.where(relation = 'next').url";

public const string BundleSelfLink = "Resource.link.where(relation = 'self').url";

public const string ResourceNarrative = "text.div";
}
}
32 changes: 32 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Models/KnownResourceTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// -------------------------------------------------------------------------------------------------
// 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.Core.Models
{
public static class KnownResourceTypes
{
public const string Bundle = "Bundle";

public const string Device = "Device";

public const string DocumentReference = "DocumentReference";

public const string Encounter = "Encounter";

public const string Immunization = "Immunization";

public const string Observation = "Observation";

public const string OperationOutcome = "OperationOutcome";

public const string Organization = "Organization";

public const string RiskAssessment = "RiskAssessment";

public const string Patient = "Patient";

public const string ValueSet = "ValueSet";
}
}
10 changes: 10 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Models/ResourceElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using EnsureThat;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Serialization;
Expand All @@ -17,6 +19,12 @@ namespace Microsoft.Health.Fhir.Core.Models
public class ResourceElement
{
private readonly Lazy<EvaluationContext> _context;
private List<string> _nonDomainTypes = new List<string>
{
"Bundle",
"Parameters",
"Binary",
};

public ResourceElement(ITypedElement instance)
{
Expand All @@ -43,6 +51,8 @@ internal ResourceElement(ITypedElement instance, object resourceInstance)

public string VersionId => Scalar<string>("Resource.meta.versionId");

public bool IsDomainResource => !_nonDomainTypes.Contains(InstanceType, StringComparer.OrdinalIgnoreCase);

public DateTimeOffset? LastUpdated
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
Expand All @@ -19,6 +20,7 @@
using Microsoft.Health.Fhir.Api.Features.ContentTypes;
using Microsoft.Health.Fhir.Api.Features.Filters;
using Microsoft.Health.Fhir.Core.Exceptions;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features;
using Microsoft.Health.Fhir.Core.Features.Conformance;
using NSubstitute;
Expand All @@ -40,7 +42,7 @@ public ValidateContentTypeFilterAttributeTests()
};

_conformanceProvider = Substitute.For<IConformanceProvider>();
_conformanceProvider.GetCapabilityStatementAsync().Returns(_statement);
_conformanceProvider.GetCapabilityStatementAsync().Returns(_statement.ToTypedElement().ToResourceElement());
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq;
using System.Threading.Tasks;
using EnsureThat;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using Microsoft.AspNetCore.Diagnostics;
Expand All @@ -17,6 +18,7 @@
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Health.Fhir.Api.Features.Formatters;
using Microsoft.Health.Fhir.Core.Exceptions;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features;
using Microsoft.Health.Fhir.Core.Features.Conformance;
using Microsoft.Net.Http.Headers;
Expand Down Expand Up @@ -120,7 +122,8 @@ private async Task<bool> IsFormatSupportedAsync(ResourceFormat resourceFormat)
return isSupported;
}

CapabilityStatement statement = await _conformanceProvider.GetCapabilityStatementAsync();
var typedStatement = await _conformanceProvider.GetCapabilityStatementAsync();
CapabilityStatement statement = typedStatement.ToPoco<CapabilityStatement>();

return _supportedFormats.GetOrAdd(resourceFormat, format =>
{
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Microsoft.Health.Fhir.Core.Features.Conformance;
using Microsoft.Health.Fhir.Core.Features.Context;
using Microsoft.Health.Fhir.Core.Features.Persistence;
using Microsoft.Health.Fhir.Core.Features.Validation;
using Microsoft.Health.Fhir.Core.Features.Validation.Narratives;
using Microsoft.Health.Fhir.Core.Models;

Expand Down Expand Up @@ -165,6 +166,8 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset

services.AddSingleton<INarrativeHtmlSanitizer, NarrativeHtmlSanitizer>();

services.AddSingleton<IModelAttributeValidator, ModelAttributeValidator>();

ModelExtensions.SetModelInfoProvider();
services.Add(_ => ModelInfoProvider.Instance).Singleton().AsSelf().AsImplementedInterfaces();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Net.Http;
using System.Threading;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using MediatR;
Expand Down Expand Up @@ -64,7 +65,7 @@ public ResourceHandlerTests()
patientResource.UpdateCreate = true;
patientResource.Versioning = CapabilityStatement.ResourceVersionPolicy.VersionedUpdate;

_conformanceProvider.GetCapabilityStatementAsync().Returns(_conformanceStatement);
_conformanceProvider.GetCapabilityStatementAsync().Returns(_conformanceStatement.ToTypedElement().ToResourceElement());
var lazyConformanceProvider = new Lazy<IConformanceProvider>(() => _conformanceProvider);

var collection = new ServiceCollection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Resources.Upsert;
using Microsoft.Health.Fhir.Core.Features.Validation;
using Microsoft.Health.Fhir.Core.Features.Validation.Narratives;
using Microsoft.Health.Fhir.Core.Messages.Upsert;
using Microsoft.Health.Fhir.Tests.Common;
Expand All @@ -23,7 +24,7 @@ public class UpsertResourceValidatorTests
[InlineData("00000000000000000000000000000000000000000000000000000000000000065")]
public void GivenAResourceWithoutInvalidId_WhenValidatingUpsert_ThenInvalidShouldBeReturned(string id)
{
var validator = new UpsertResourceValidator(new NarrativeHtmlSanitizer(NullLogger<NarrativeHtmlSanitizer>.Instance));
var validator = new UpsertResourceValidator(new NarrativeHtmlSanitizer(NullLogger<NarrativeHtmlSanitizer>.Instance), new ModelAttributeValidator());
var resource = Samples.GetDefaultObservation()
.UpdateId(id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

using System;
using System.Threading;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
using Microsoft.Health.Fhir.Core.Exceptions;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Conformance;
using Microsoft.Health.Fhir.Core.Features.Validation;
using Microsoft.Health.Fhir.Core.Messages.Delete;
Expand All @@ -28,7 +30,7 @@ public ValidateCapabilityPreProcessorTests()
CapabilityStatementMock.SetupMockResource(statement, ResourceType.Observation, new[] { CapabilityStatement.TypeRestfulInteraction.Read });

_conformanceProvider = Substitute.For<ConformanceProviderBase>();
_conformanceProvider.GetCapabilityStatementAsync().Returns(statement);
_conformanceProvider.GetCapabilityStatementAsync().Returns(statement.ToTypedElement().ToResourceElement());
}

[Fact]
Expand Down
Loading

0 comments on commit 9cd68b6

Please sign in to comment.