Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Firely.Fhir.Validation.Compilation.SchemaBuilder.SchemaBuilder(Hl7.Fhir.Specific
Firely.Fhir.Validation.Compilation.StandardBuilders
Firely.Fhir.Validation.Compilation.StandardBuilders.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.StandardBuilders.StandardBuilders(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! source) -> void
Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionExtensions
Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionsResolver
Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionsResolver.Nested.get -> Hl7.Fhir.Specification.Source.IAsyncResourceResolver!
Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionsResolver.ResolveByCanonicalUri(string! uri) -> Hl7.Fhir.Model.Resource?
Expand All @@ -23,6 +24,10 @@ Firely.Fhir.Validation.Compilation.StructureDefinitionToElementSchemaResolver.Ge
Firely.Fhir.Validation.Compilation.StructureDefinitionToElementSchemaResolver.GetSchema(Hl7.Fhir.Specification.Navigation.ElementDefinitionNavigator! nav) -> Firely.Fhir.Validation.IValidatable!
Firely.Fhir.Validation.Compilation.StructureDefinitionToElementSchemaResolver.Source.get -> Hl7.Fhir.Specification.Source.IAsyncResourceResolver!
readonly Firely.Fhir.Validation.Compilation.SchemaBuilder.Source -> Hl7.Fhir.Specification.Source.IAsyncResourceResolver!
static Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionExtensions.Correct(this Hl7.Fhir.Model.Resource? resource) -> void
static Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionExtensions.CorrectDifferential(this Hl7.Fhir.Model.StructureDefinition? sd, System.Collections.Generic.ICollection<Hl7.Fhir.Model.ElementDefinition!>? elements) -> void
static Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionExtensions.CorrectSnapshot(this Hl7.Fhir.Model.StructureDefinition? sd, System.Collections.Generic.ICollection<Hl7.Fhir.Model.ElementDefinition!>? elements) -> void
static Firely.Fhir.Validation.Compilation.StructureDefinitionCorrectionExtensions.WithCorrections<T>(this T? resource) -> T?
static Firely.Fhir.Validation.Compilation.StructureDefinitionToElementSchemaResolver.Create(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! source, System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.Compilation.ISchemaBuilder!>? extraSchemaBuilders = null) -> Firely.Fhir.Validation.IElementSchemaResolver!
static Firely.Fhir.Validation.Compilation.StructureDefinitionToElementSchemaResolver.CreatedCached(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! source, System.Collections.Concurrent.ConcurrentDictionary<Firely.Fhir.Validation.Canonical!, Firely.Fhir.Validation.ElementSchema?>! cache) -> Firely.Fhir.Validation.IElementSchemaResolver!
static Firely.Fhir.Validation.Compilation.StructureDefinitionToElementSchemaResolver.CreatedCached(Hl7.Fhir.Specification.Source.IAsyncResourceResolver! source, System.Collections.Generic.IEnumerable<Firely.Fhir.Validation.Compilation.ISchemaBuilder!>? extraSchemaBuilders = null) -> Firely.Fhir.Validation.IElementSchemaResolver!
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Hl7.Fhir.Model;
using Hl7.Fhir.Specification;
using static Hl7.Fhir.Model.ElementDefinition;

namespace Firely.Fhir.Validation.Compilation;

internal class BundleCorrector : ConstraintsCorrector
{
public BundleCorrector() : base("http://hl7.org/fhir/StructureDefinition/Bundle")
{
RegisterInvalidConstraint("Bundle.entry",
"bdl-8",
"fullUrl.contains('/_history/').not()",
"fullUrl.exists() implies fullUrl.contains('/_history/').not()",
FhirRelease.STU3, FhirRelease.R4);

// See https://github.com/FirelyTeam/firely-validator-api/issues/152
RegisterMissingConstraint("Bundle", "bdl-3a", createBdl3A, FhirRelease.STU3, FhirRelease.R4, FhirRelease.R4B);
RegisterMissingConstraint("Bundle", "bdl-3b", createBdl3B, FhirRelease.STU3, FhirRelease.R4, FhirRelease.R4B);
RegisterMissingConstraint("Bundle", "bdl-3c", createBdl3C, FhirRelease.STU3, FhirRelease.R4, FhirRelease.R4B);
RegisterMissingConstraint("Bundle", "bdl-3d", createBdl3D, FhirRelease.STU3, FhirRelease.R4, FhirRelease.R4B);
RegisterMissingConstraint("Bundle", "bdl-10", createBdl10, FhirRelease.STU3);
RegisterMissingConstraint("Bundle", "bdl-11", createBdl11, FhirRelease.STU3);
RegisterMissingConstraint("Bundle", "bdl-12", createBdl12, FhirRelease.STU3);
RegisterMissingConstraint("Bundle", "bdl-15", createBdl15, FhirRelease.STU3, FhirRelease.R4, FhirRelease.R4B);
}

private static ConstraintComponent createBdl3A()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-3a",
Human = "For collections of type document, message, searchset or collection, all entries must contain resources, and not have request or response elements",
Expression = "type in ('document' | 'message' | 'searchset' | 'collection') implies entry.all(resource.exists() and request.empty() and response.empty())"
};
}

private static ConstraintComponent createBdl3B()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-3b",
Human = "For collections of type history, all entries must contain request or response elements, and resources if the method is POST, PUT or PATCH",
Expression = "type = 'history' implies entry.all(request.exists() and response.exists() and ((request.method in ('POST' | 'PATCH' | 'PUT')) = resource.exists()))"
};
}

private static ConstraintComponent createBdl3C()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-3c",
Human = "For collections of type transaction or batch, all entries must contain request elements, and resources if the method is POST, PUT or PATCH",
Expression = "type in ('transaction' | 'batch') implies entry.all(request.method.exists() and ((request.method in ('POST' | 'PATCH' | 'PUT')) = resource.exists()))"
};
}

private static ConstraintComponent createBdl3D()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-3d",
Human = "For collections of type transaction-response or batch-response, all entries must contain response elements",
Expression = "type in ('transaction-response' | 'batch-response') implies entry.all(response.exists())"
};
}

private static ConstraintComponent createBdl10()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-10",
Human = "A document must have a date",
Expression = "type = 'document' implies (timestamp.hasValue())"
};
}

private static ConstraintComponent createBdl11()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-11",
Human = "A document must have a Composition as the first resource",
Expression = "type = 'document' implies entry.first().resource.is(Composition)"
};
}

private static ConstraintComponent createBdl12()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-12",
Human = "A message must have a MessageHeader as the first resource",
Expression = "type = 'message' implies entry.first().resource.is(MessageHeader)"
};
}

private static ConstraintComponent createBdl15()
{
return new ConstraintComponent
{
Severity = ConstraintSeverity.Error,
Key = "bdl-15",
Human = "Bundle resources where type is not transaction, transaction-response, batch, or batch-response or when the request is a POST SHALL have Bundle.entry.fullUrl populated",
Expression = "type='transaction' or type='transaction-response' or type='batch' or type='batch-response' or entry.all(fullUrl.exists() or request.method='POST')"
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#if R4_AND_LATER

using Hl7.Fhir.Specification;

namespace Firely.Fhir.Validation.Compilation;

internal class CareTeamCorrector : ConstraintsCorrector
{
public CareTeamCorrector() : base("http://hl7.org/fhir/StructureDefinition/CareTeam")
{
RegisterInvalidConstraint("CareTeam.participant",
"ctm-1",
"onBehalfOf.exists() implies (member.resolve().iif(empty(), true, ofType(Practitioner).exists()))",
"onBehalfOf.exists() implies (member.resolve() is Practitioner)",
FhirRelease.R4);
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Hl7.Fhir.Model;
using Hl7.Fhir.Specification;
using System;
using System.Collections.Generic;
using System.Linq;
using static Hl7.Fhir.Model.ElementDefinition;

namespace Firely.Fhir.Validation.Compilation;

internal abstract class ConstraintsCorrector(string baseUrl) : Corrector
{
private class ConstraintExpressionCorrector(string oldExpression, string newExpression)
{
public void Correct(ConstraintComponent constraint)
{
if (constraint.Expression == oldExpression)
constraint.Expression = newExpression;
}
}

/// <summary>
/// The constraint expression correctors are organized by
/// - FHIR release (e.g. STU3)
/// - then by path (e.g. "Bundle.entry")
/// - then by constraint key (e.g. "eld-1")
/// </summary>
private readonly Dictionary<FhirRelease, Dictionary<string, Dictionary<string, ConstraintExpressionCorrector>>> _constraintExpressionCorrectors = [];

/// <summary>
/// The constraint creators for missing constraints are organized by
/// - FHIR release (e.g. R4)
/// - then by path (e.g. "Bundle")
/// - then by constraint key (e.g. "bdl-3a")
/// </summary>
private readonly Dictionary<FhirRelease, Dictionary<string, Dictionary<string, Func<ConstraintComponent>>>> _constraintCreators = [];

public override void CorrectDifferential(FhirRelease? fhirRelease, ICollection<ElementDefinition> elements, string url) => correct(fhirRelease, elements, url);
public override void CorrectSnapshot(FhirRelease? fhirRelease, ICollection<ElementDefinition> elements) => correct(fhirRelease, elements, null);

private void correct(FhirRelease? fhirRelease, ICollection<ElementDefinition> elements, string? url)
{
if (!fhirRelease.HasValue)
return; // Don't really know what we should do if we don't know the FHIR release so it's probably better to do nothing

correctInvalidConstraints(fhirRelease.Value, elements);
correctMissingConstraints(fhirRelease.Value, elements, url);
}

private void correctInvalidConstraints(FhirRelease fhirRelease, ICollection<ElementDefinition> elements)
{
// Get registered constraint correctors for FHIR release
if (!_constraintExpressionCorrectors.TryGetValue(fhirRelease, out var correctorsForRelease))
return;

// Filter and group elements on path
var pathGroups = elements.Where(e => correctorsForRelease.ContainsKey(e.Path!)).GroupBy(e => e.Path);

foreach (var pathGroup in pathGroups)
{
var correctorsForPath = correctorsForRelease[pathGroup.Key!];

foreach (var constraint in pathGroup.SelectMany(e => e.Constraint))
{
if (correctorsForPath.TryGetValue(constraint.Key!, out var correctorForKey))
correctorForKey.Correct(constraint);
}
}
}

private void correctMissingConstraints(FhirRelease fhirRelease, ICollection<ElementDefinition> elements, string? url)
{
// Get registered constraint creators for FHIR release
if (!_constraintCreators.TryGetValue(fhirRelease, out var creatorsForRelease))
return;

// Always correct snapshot (url == null) and only correct differential (url != null) if the URL of the StructureDefinition matches the base URL
if (url != null && url != baseUrl)
return;

// Filter and group elements on path
var pathGroups = elements.Where(e => creatorsForRelease.ContainsKey(e.Path!)).GroupBy(e => e.Path);

foreach (var pathGroup in pathGroups)
{
var creatorsForPath = creatorsForRelease[pathGroup.Key!];

foreach (var elemDef in pathGroup)
{
var existingKeys = elemDef.Constraint.Select(c => c.Key).ToHashSet();

foreach (var (key, createKey) in creatorsForPath)
{
if (!existingKeys.Contains(key))
elemDef.Constraint.Add(createKey());
}
}
}
}

protected void RegisterInvalidConstraint(string path, string key, string oldExpression, string newExpression, params FhirRelease[] releases)
{
if (releases.Length == 0)
return;

foreach (var release in releases)
{
if (!_constraintExpressionCorrectors.TryGetValue(release, out var constraintCorrectorsForRelease))
{
constraintCorrectorsForRelease = [];
_constraintExpressionCorrectors.Add(release, constraintCorrectorsForRelease);
}

if (!constraintCorrectorsForRelease.TryGetValue(path, out var constraintCorrectorsForPath))
{
constraintCorrectorsForPath = [];
constraintCorrectorsForRelease.Add(path, constraintCorrectorsForPath);
}

constraintCorrectorsForPath[key] = new ConstraintExpressionCorrector(oldExpression, newExpression);
}
}

protected void RegisterMissingConstraint(string path, string key, Func<ConstraintComponent> createConstraint, params FhirRelease[] releases)
{
if (releases.Length == 0)
return;

foreach (var release in releases)
{
if (!_constraintCreators.TryGetValue(release, out var constraintCreatorsForRelease))
{
constraintCreatorsForRelease = [];
_constraintCreators.Add(release, constraintCreatorsForRelease);
}

if (!constraintCreatorsForRelease.TryGetValue(path, out var constraintCreatorsForPath))
{
constraintCreatorsForPath = [];
constraintCreatorsForRelease.Add(path, constraintCreatorsForPath);
}

constraintCreatorsForPath[key] = createConstraint;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Hl7.Fhir.Model;
using Hl7.Fhir.Specification;
using System.Collections.Generic;

namespace Firely.Fhir.Validation.Compilation;

internal abstract class Corrector
{
public void Correct(FhirRelease? fhirRelease, StructureDefinition sd)
{
if (sd.Differential?.Element != null)
CorrectDifferential(fhirRelease, sd.Differential.Element, sd.Url!);

if (sd.Snapshot?.Element != null)
CorrectSnapshot(fhirRelease, sd.Snapshot.Element);
}

public abstract void CorrectDifferential(FhirRelease? fhirRelease, ICollection<ElementDefinition> elements, string url);
public abstract void CorrectSnapshot(FhirRelease? fhirRelease, ICollection<ElementDefinition> elements);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Hl7.Fhir.Model;
using System;
using System.Collections.Generic;

namespace Firely.Fhir.Validation.Compilation.Shared.Correctors
{
internal class CorrectorFactory
{
private static readonly Lazy<Corrector> RESOURCE_CORRECTOR = new(() => new ResourceCorrector());

private static readonly Dictionary<string, Lazy<Corrector>> TYPE_CORRECTORS = new()
{
{ "string", new Lazy<Corrector>(() => new StringCorrector()) },
{ "markdown", new Lazy<Corrector>(() => new MarkdownCorrector()) },
{ "Bundle", new Lazy<Corrector>(() => new BundleCorrector()) },
#if R4_AND_LATER
{ "CareTeam", new Lazy<Corrector>(() => new CareTeamCorrector()) },
{ "ElementDefinition", new Lazy<Corrector>(() => new ElementDefinitionCorrector()) },
{ "ImagingSelection", new Lazy<Corrector>(() => new ImagingSelectionCorrector()) },
{ "OperationDefinition", new Lazy<Corrector>(() => new OperationDefinitionCorrector()) },
#endif
{ "Observation", new Lazy<Corrector>(() => new ObservationCorrector()) },
{ "Reference", new Lazy<Corrector>(() => new ReferenceCorrector()) },
#if R4_AND_LATER
{ "StructureDefinition", new Lazy<Corrector>(() => new StructureDefinitionCorrector()) },
{ "Questionnaire", new Lazy<Corrector>(() => new QuestionnaireCorrector()) },
#endif
};

public static Corrector? Get(StructureDefinition.StructureDefinitionKind? kind)
{
return kind == StructureDefinition.StructureDefinitionKind.Resource
? RESOURCE_CORRECTOR.Value
: null;
}

public static Corrector? Get(string? type)
{
return type != null && TYPE_CORRECTORS.TryGetValue(type, out var typeCorrector)
? typeCorrector.Value
: null;
}
}
}
Loading