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

Add async validation #330

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion firely-validator-api.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- Solution-wide properties for NuGet packaging -->
<PropertyGroup>
<VersionPrefix>2.2.1</VersionPrefix>
<VersionPrefix>2.3.0</VersionPrefix>
<Authors>Firely</Authors>
<Company>Firely (https://fire.ly)</Company>
<Copyright>Copyright 2015-2024 Firely</Copyright>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Firely.Fhir.Validation.Compilation.ElementConversionMode.ContentReference = 2 ->
Firely.Fhir.Validation.Compilation.ElementConversionMode.Full = 0 -> Firely.Fhir.Validation.Compilation.ElementConversionMode
Firely.Fhir.Validation.Compilation.ISchemaBuilder
Firely.Fhir.Validation.Compilation.ISchemaBuilder.Build(Hl7.Fhir.Specification.Navigation.ElementDefinitionNavigator! nav, Firely.Fhir.Validation.Compilation.ElementConversionMode? conversionMode = Firely.Fhir.Validation.Compilation.ElementConversionMode.Full) -> System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.IAssertion!>!
Firely.Fhir.Validation.Compilation.ISchemaBuilder.BuildAsync(Hl7.Fhir.Specification.Navigation.ElementDefinitionNavigator! nav, Firely.Fhir.Validation.Compilation.ElementConversionMode? conversionMode, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Firely.Fhir.Validation.IAssertion![]!>!
Firely.Fhir.Validation.Compilation.SchemaBuilder
Firely.Fhir.Validation.Compilation.SchemaBuilder.Build(Hl7.Fhir.Specification.Navigation.ElementDefinitionNavigator! nav, Firely.Fhir.Validation.Compilation.ElementConversionMode? conversionMode = Firely.Fhir.Validation.Compilation.ElementConversionMode.Full) -> System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.IAssertion!>!
Firely.Fhir.Validation.Compilation.SchemaBuilder.SchemaBuilder(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! source, System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.Compilation.ISchemaBuilder!>? schemaBuilders = null) -> void
Expand Down
22 changes: 22 additions & 0 deletions src/Firely.Fhir.Validation.Compilation.Shared/ISchemaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
using Hl7.Fhir.Specification.Navigation;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Firely.Fhir.Validation.Compilation
{
Expand Down Expand Up @@ -40,5 +43,24 @@ public interface ISchemaBuilder
IEnumerable<IAssertion> Build(
ElementDefinitionNavigator nav,
ElementConversionMode? conversionMode = ElementConversionMode.Full);

/// <summary>
/// Constucts a schema block.
/// </summary>
/// <param name="nav"></param>
/// <param name="conversionMode">The mode indicating the state we are in while constructing the schema block.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[EditorBrowsable(EditorBrowsableState.Never)]
#if NET8_0_OR_GREATER
[System.Diagnostics.CodeAnalysis.Experimental(diagnosticId: "ExperimentalApi")]
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
Task<IAssertion[]> BuildAsync(
ElementDefinitionNavigator nav,
ElementConversionMode? conversionMode,
CancellationToken cancellationToken)
=> Task.FromResult(Build(nav, conversionMode).ToArray());
}
}
290 changes: 289 additions & 1 deletion src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilder.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Hl7.Fhir.Model;
using Hl7.Fhir.Specification.Navigation;
using System.Linq;
using System.Threading.Tasks;

#pragma warning disable CS0618 // Type or member is obsolete

Expand All @@ -29,6 +30,21 @@ internal static class SchemaBuilderExtensions
/// <returns></returns>
public static ElementSchema? BuildSchema(this ISchemaBuilder schemaBuilder, ElementDefinitionNavigator nav)
=> schemaBuilder.Build(nav).SingleOrDefault() is ElementSchema schema ? schema : null;

/// <summary>
/// Converts a <see cref="StructureDefinition"/> to an <see cref="ElementSchema"/>.
/// </summary>
public static Task<ElementSchema?> BuildSchemaAsync(this ISchemaBuilder schemaBuilder, StructureDefinition definition)
=> BuildSchemaAsync(schemaBuilder, ElementDefinitionNavigator.ForSnapshot(definition));

/// <summary>
///
/// </summary>
/// <param name="schemaBuilder"></param>
/// <param name="nav"></param>
/// <returns></returns>
public static async Task<ElementSchema?> BuildSchemaAsync(this ISchemaBuilder schemaBuilder, ElementDefinitionNavigator nav)
=> (await schemaBuilder.BuildAsync(nav, ElementConversionMode.Full, default)).SingleOrDefault() is ElementSchema schema ? schema : null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using Hl7.Fhir.Specification.Source;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

#pragma warning disable CS0618 // Type or member is obsolete

Expand Down Expand Up @@ -42,6 +44,16 @@ public class StandardBuilders(IAsyncResourceResolver source) : ISchemaBuilder
/// <inheritdoc/>
public IEnumerable<IAssertion> Build(ElementDefinitionNavigator nav, ElementConversionMode? conversionMode = ElementConversionMode.Full)
=> _schemaBuilders.SelectMany(ce => ce.Build(nav, conversionMode));
async Task<IAssertion[]> ISchemaBuilder.BuildAsync(ElementDefinitionNavigator nav, ElementConversionMode? conversionMode, CancellationToken cancellationToken)
{
var assertions = new List<IAssertion>();
for (int i = 0; i < _schemaBuilders.Length; i++)
{
assertions.AddRange(await _schemaBuilders[i].BuildAsync(nav, conversionMode, cancellationToken));
}

return assertions.ToArray();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

#pragma warning disable CS0618 // Type or member is obsolete

Expand Down Expand Up @@ -106,6 +108,55 @@ public IEnumerable<IAssertion> Build(ElementDefinitionNavigator nav, ElementConv
};
}

/// <inheritdoc/>
async Task<IAssertion[]> ISchemaBuilder.BuildAsync(ElementDefinitionNavigator nav, ElementConversionMode? conversionMode, CancellationToken cancellationToken)
{
// This constraint is not part of an element refering to a backbone type (see eld-5).
if (conversionMode == ElementConversionMode.ContentReference) return [];

var def = nav.Current;
#if STU3
var hasProfileDetails = def.Type.Any(tr => !string.IsNullOrEmpty(tr.Profile) || !string.IsNullOrEmpty(tr.TargetProfile));
#else
var hasProfileDetails = def.Type.Any(tr => tr.Profile.Any() || tr.TargetProfile.Any());
#endif
if ((!nav.HasChildren || hasProfileDetails) && def.Type.Count > 0)
{
var typeAssertion = await ConvertTypeReferencesAsync(def.Type);
if (typeAssertion is not null)
return [typeAssertion];
}

return [];
}

public async Task<IAssertion?> ConvertTypeReferencesAsync(IEnumerable<ElementDefinition.TypeRefComponent> typeRefs)
{
if (!CommonTypeRefComponent.CanConvert(typeRefs))
throw new IncorrectElementDefinitionException("Encountered an element with typerefs that cannot be converted to a common structure.");

var r4TypeRefs = CommonTypeRefComponent.Convert(typeRefs);

var typeRefList = r4TypeRefs.ToList();
bool hasDuplicateCodes() => typeRefList.Select(t => t.Code).Distinct().Count() != typeRefList.Count;

return typeRefList switch
{
// No type refs -> always successful
{ Count: 0 } => ResultAssertion.SUCCESS,

// In R4, all Codes must be unique (in R3, this was seen as an OR)
_ when hasDuplicateCodes() => throw new IncorrectElementDefinitionException($"Encountered an element with typerefs with non-unique codes."),

// More than one type.Code => build a switch based on the instance's type so we get
// useful error messages, and can continue structural validation once we have determined
// the correct type.
{ Count: > 1 } => await buildSliceAssertionForTypeCasesAsync(r4TypeRefs),

// Just a single typeref, direct conversion.
_ => await ConvertTypeReferenceAsync(r4TypeRefs.Single())
};
}

internal IAssertion? ConvertTypeReference(CommonTypeRefComponent typeRef)
{
Expand Down Expand Up @@ -144,6 +195,43 @@ public IEnumerable<IAssertion> Build(ElementDefinitionNavigator nav, ElementConv
return profileAssertions;
}

internal async Task<IAssertion?> ConvertTypeReferenceAsync(CommonTypeRefComponent typeRef)
{
string code = typeRef.GetCodeFromTypeRef();

var profileAssertions = typeRef.GetTypeProfilesCorrect()?.ToList() switch
{
null => null,
// If there are no explicit profiles, use the schema associated with the declared type code in the typeref.
{ Count: 1 } single => new SchemaReferenceValidator(single.Single()),

// There are one or more profiles, create an "any" slice validating them
var many => ConvertProfilesToSchemaReferences(many, $"Element does not validate against any of the expected profiles ({EXPECTEDPROFILES}).")
};

// Combine the validation against the profiles against some special cases in an "all" schema.
if (ReferencedInstanceValidator.IsSupportedReferenceType(code))
{
// reference types need to start a nested validation of an instance that is referenced by uri against
// the targetProfiles mentioned in the typeref. If there are no target profiles, then the only thing
// we can validate against is the runtime type of the referenced resource.
var targetProfiles = !typeRef.TargetProfile.Any() ? new[] { Canonical.ForCoreType("Resource").ToString() } : typeRef.TargetProfile;
var targetProfileAssertions = await ConvertTargetProfilesToSchemaReferencesAsync(targetProfiles);

var validateReferenceAssertion = buildvalidateInstance(typeRef.AggregationElement, typeRef.Versioning, targetProfileAssertions);
return profileAssertions is not null
? new AllValidator(profileAssertions, validateReferenceAssertion)
: validateReferenceAssertion;
}
else if (!ReferencedInstanceValidator.IsReferenceType(code) && typeRef.TargetProfile.Any())
{
throw new IncorrectElementDefinitionException($"Encountered targetProfiles {string.Join(",", typeRef.TargetProfile)} on an element that is not " +
$"a reference type (canonical or Reference) but a {code}.");
}
else
return profileAssertions;
}



public IAsyncResourceResolver Resolver { get; }
Expand Down Expand Up @@ -171,6 +259,33 @@ SliceValidator.SliceCase buildSliceForTypeCase(CommonTypeRefComponent typeRef)
=> new(typeRef.Code, new FhirTypeLabelValidator(typeRef.Code), ConvertTypeReference(typeRef));
}

/// <summary>
/// Builds a slicing for each typeref with the FhirTypeLabel as the discriminator.
/// </summary>
private async Task<IAssertion> buildSliceAssertionForTypeCasesAsync(IEnumerable<CommonTypeRefComponent> typeRefs)
{
var sliceCases = new List<SliceValidator.SliceCase>();
foreach (var t in typeRefs)
{
sliceCases.Add(await buildSliceForTypeCase(t));
}

// It should be one of the previous choices, otherwise it is an error.
var defaultSlice = buildSliceFailure();

return new SliceValidator(ordered: false, defaultAtEnd: false, @default: defaultSlice, sliceCases);

IAssertion buildSliceFailure()
{
var allowedCodes = string.Join(",", typeRefs.Select(t => $"'{t.Code}'"));
return createFailure(
$"Element is of type '{IssueAssertion.Pattern.INSTANCETYPE}', which is not one of the allowed choice types ({allowedCodes})");
}

async Task<SliceValidator.SliceCase> buildSliceForTypeCase(CommonTypeRefComponent typeRef)
=> new(typeRef.Code, new FhirTypeLabelValidator(typeRef.Code), await ConvertTypeReferenceAsync(typeRef));
}

/// <summary>
/// Builds the validator that fetches a referenced resource from the runtime-supplied reference,
/// and validates it against a targetschema + additional aggregation/versioning rules.
Expand Down Expand Up @@ -223,6 +338,25 @@ StructureDefinition fetchSd(string canonical)
}
}

public async Task<IAssertion> ConvertTargetProfilesToSchemaReferencesAsync(IEnumerable<string> targetProfiles)
{
var p = new List<TypeChoice>();
foreach (var tp in targetProfiles)
{
p.Add(new TypeChoice((await fetchSd(tp)).Type, tp));
}

var typecases = p.GroupBy(pp => pp.TypeLabel).ToList();

return buildLabelledChoice(typecases);

async Task<StructureDefinition> fetchSd(string canonical)
{
var sd = await Resolver.FindStructureDefinitionAsync(canonical);
return sd ?? throw new InvalidOperationException($"Compiler needs access to profile '{canonical}', but it cannot be resolved.");
}
}

private static string replacep(string pattern, IEnumerable<string>? profiles) => pattern.Replace(EXPECTEDPROFILES, string.Join(", ", profiles ?? Enumerable.Empty<string>()));

// This method creates a slicing on the instance type, where each case will then try to validate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;

#pragma warning disable CS0618 // Type or member is obsolete

Expand Down Expand Up @@ -130,6 +131,29 @@ internal StructureDefinitionToElementSchemaResolver(IAsyncResourceResolver sourc
schemaUri, e);
}
}

/// <summary>
/// Use the <see cref="Source"/> to retrieve a StructureDefinition and turn it into an
/// <see cref="ElementSchema"/>.
/// </summary>
/// <param name="schemaUri">The canonical url of the StructureDefinition.</param>
/// <returns>The schema, or <c>null</c> if the schema uri could not be resolved as a
/// StructureDefinition canonical.</returns>
async ValueTask<ElementSchema?> IElementSchemaResolver.GetSchemaAsync(Canonical schemaUri)
{
try
{
return await (Source.FindStructureDefinitionAsync((string)schemaUri)) is StructureDefinition sd
? await _schemaBuilder.BuildSchemaAsync(sd)
: null;
}
catch (Exception e)
{
throw new SchemaResolutionFailedException(
$"Encountered an error while loading schema '{schemaUri}': {e.Message}",
schemaUri, e);
}
}
}
}
#pragma warning restore CS0618 // Type or member is obsolete
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Firely.Fhir.Validation.Compilation.ElementConversionMode.ContentReference = 2 ->
Firely.Fhir.Validation.Compilation.ElementConversionMode.Full = 0 -> Firely.Fhir.Validation.Compilation.ElementConversionMode
Firely.Fhir.Validation.Compilation.ISchemaBuilder
Firely.Fhir.Validation.Compilation.ISchemaBuilder.Build(Hl7.Fhir.Specification.Navigation.ElementDefinitionNavigator! nav, Firely.Fhir.Validation.Compilation.ElementConversionMode? conversionMode = Firely.Fhir.Validation.Compilation.ElementConversionMode.Full) -> System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.IAssertion!>!
Firely.Fhir.Validation.Compilation.ISchemaBuilder.BuildAsync(Hl7.Fhir.Specification.Navigation.ElementDefinitionNavigator! nav, Firely.Fhir.Validation.Compilation.ElementConversionMode? conversionMode, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Firely.Fhir.Validation.IAssertion![]!>!
Firely.Fhir.Validation.Compilation.SchemaBuilder
Firely.Fhir.Validation.Compilation.SchemaBuilder.Build(Hl7.Fhir.Specification.Navigation.ElementDefinitionNavigator! nav, Firely.Fhir.Validation.Compilation.ElementConversionMode? conversionMode = Firely.Fhir.Validation.Compilation.ElementConversionMode.Full) -> System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.IAssertion!>!
Firely.Fhir.Validation.Compilation.SchemaBuilder.SchemaBuilder(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! source, System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.Compilation.ISchemaBuilder!>? schemaBuilders = null) -> void
Expand Down
2 changes: 2 additions & 0 deletions src/Firely.Fhir.Validation.R4/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ Firely.Fhir.Validation.ValidationSettingsExtensions
Firely.Fhir.Validation.Validator
Firely.Fhir.Validation.Validator.Validate(Hl7.Fhir.ElementModel.ElementNode! instance, string? profile = null) -> Hl7.Fhir.Model.OperationOutcome!
Firely.Fhir.Validation.Validator.Validate(Hl7.Fhir.Model.Resource! instance, string? profile = null) -> Hl7.Fhir.Model.OperationOutcome!
Firely.Fhir.Validation.Validator.ValidateAsync(Hl7.Fhir.ElementModel.ElementNode! instance, string? profile, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Hl7.Fhir.Model.OperationOutcome!>!
Firely.Fhir.Validation.Validator.ValidateAsync(Hl7.Fhir.Model.Resource! instance, string? profile, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Hl7.Fhir.Model.OperationOutcome!>!
Firely.Fhir.Validation.Validator.Validator(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! resourceResolver, Hl7.Fhir.Specification.Terminology.ICodeValidationTerminologyService! terminologyService, Firely.Fhir.Validation.IExternalReferenceResolver? referenceResolver = null, Firely.Fhir.Validation.ValidationSettings? settings = null) -> void
static Firely.Fhir.Validation.ValidationSettingsExtensions.SetSkipConstraintValidation(this Firely.Fhir.Validation.ValidationSettings! vc, bool skip) -> void
2 changes: 2 additions & 0 deletions src/Firely.Fhir.Validation.R4B/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ Firely.Fhir.Validation.ValidationSettingsExtensions
Firely.Fhir.Validation.Validator
Firely.Fhir.Validation.Validator.Validate(Hl7.Fhir.ElementModel.ElementNode! instance, string? profile = null) -> Hl7.Fhir.Model.OperationOutcome!
Firely.Fhir.Validation.Validator.Validate(Hl7.Fhir.Model.Resource! instance, string? profile = null) -> Hl7.Fhir.Model.OperationOutcome!
Firely.Fhir.Validation.Validator.ValidateAsync(Hl7.Fhir.ElementModel.ElementNode! instance, string? profile, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Hl7.Fhir.Model.OperationOutcome!>!
Firely.Fhir.Validation.Validator.ValidateAsync(Hl7.Fhir.Model.Resource! instance, string? profile, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Hl7.Fhir.Model.OperationOutcome!>!
Firely.Fhir.Validation.Validator.Validator(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! resourceResolver, Hl7.Fhir.Specification.Terminology.ICodeValidationTerminologyService! terminologyService, Firely.Fhir.Validation.IExternalReferenceResolver? referenceResolver = null, Firely.Fhir.Validation.ValidationSettings? settings = null) -> void
static Firely.Fhir.Validation.ValidationSettingsExtensions.SetSkipConstraintValidation(this Firely.Fhir.Validation.ValidationSettings! vc, bool skip) -> void
Loading