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

Changes coming from Shim PR and running the original Tests #205

Merged
merged 9 commits into from
Nov 20, 2023
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 @@ -25,7 +25,7 @@ internal static string GetCodeFromTypeRef(this CommonTypeRefComponent typeRef)
// and there are some R4 profiles in the wild that still use this old schema too.
if (string.IsNullOrEmpty(typeRef.Code))
{
var r3TypeIndicator = typeRef.CodeElement.GetStringExtension(SDXMLTYPEEXTENSION) ?? throw new IncorrectElementDefinitionException($"Encountered a typeref without a code.");
var r3TypeIndicator = typeRef.CodeElement?.GetStringExtension(SDXMLTYPEEXTENSION) ?? throw new IncorrectElementDefinitionException($"Encountered a typeref without a code nor xml-type extension..");
return deriveSystemTypeFromXsdType(r3TypeIndicator);
}
else
Expand All @@ -36,7 +36,7 @@ static string deriveSystemTypeFromXsdType(string xsdTypeName)
// This R3-specific mapping is derived from the possible xsd types from the primitive datatype table
// at http://www.hl7.org/fhir/stu3/datatypes.html, and the mapping of these types to
// FhirPath from http://hl7.org/fhir/fhirpath.html#types
return makeSystemType(xsdTypeName switch
var systemType = xsdTypeName switch
{
"xsd:boolean" => "Boolean",
"xsd:int" => "Integer",
Expand All @@ -54,9 +54,9 @@ static string deriveSystemTypeFromXsdType(string xsdTypeName)
"xsd:positiveInteger" => "Integer",
"xhtml:div" => "String", // used in R3 xhtml
_ => throw new NotSupportedException($"The xsd type {xsdTypeName} is not supported as a primitive type in R3.")
});
};

static string makeSystemType(string name) => SYSTEMTYPEURI + name;
return SYSTEMTYPEURI + systemType;
}
}

Expand Down
109 changes: 59 additions & 50 deletions src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public IEnumerable<IAssertion> Build(ElementDefinitionNavigator nav, ElementConv
// Generate the right subclass of ElementSchema for the kind of SD
schema = generateFhirSchema(nav.StructureDefinition, converted);
}
catch (Exception e)
catch (Exception e) when (e is not InvalidOperationException)
{
throw new InvalidOperationException($"Failed to convert ElementDefinition at " +
$"{nav.Current.ElementId ?? nav.Current.Path} in profile {nav.StructureDefinition.Url}: {e.Message}",
Expand Down Expand Up @@ -139,58 +139,67 @@ private ElementSchema convertElementToSchema(Canonical schemaId, ElementDefiniti
/// sibling slice elements, if the current element is a slice intro.</remarks>
internal List<IAssertion> ConvertElement(ElementDefinitionNavigator nav, SubschemaCollector? subschemas = null)
{
// We will generate a separate schema for backbones in resource/type definitions, so
// a contentReference can reference it. Note: contentReference always refers to the
// unconstrained base type, not the constraints in this profile. See
// https://chat.fhir.org/#narrow/stream/179252-IG-creation/topic/Clarification.20on.20contentReference
bool generateBackbone = nav.Current.IsBackboneElement()
&& nav.StructureDefinition.Derivation != StructureDefinition.TypeDerivationRule.Constraint
&& subschemas?.NeedsSchemaFor("#" + nav.Current.Path) == true;

// This will generate most of the assertions for the current ElementDefinition,
// except for the Children and slicing assertions (done below). The exact set of
// assertions generated depend on whether this is going to be the schema
// for a normal element or for a subschema representing a Backbone element.
var conversionMode = generateBackbone ?
ElementConversionMode.BackboneType :
ElementConversionMode.Full;

var schemaMembers = convert(nav, conversionMode);

// Children need special treatment since the definition of this assertion does not
// depend on the current ElementNode, but on its descendants in the ElementDefNavigator.
if (nav.HasChildren)
try
{
var childrenAssertion = createChildrenAssertion(nav, subschemas);
schemaMembers.Add(childrenAssertion);
}
// We will generate a separate schema for backbones in resource/type definitions, so
// a contentReference can reference it. Note: contentReference always refers to the
// unconstrained base type, not the constraints in this profile. See
// https://chat.fhir.org/#narrow/stream/179252-IG-creation/topic/Clarification.20on.20contentReference
bool generateBackbone = nav.Current.IsBackboneElement()
&& nav.StructureDefinition.Derivation != StructureDefinition.TypeDerivationRule.Constraint
&& subschemas?.NeedsSchemaFor("#" + nav.Current.Path) == true;

// This will generate most of the assertions for the current ElementDefinition,
// except for the Children and slicing assertions (done below). The exact set of
// assertions generated depend on whether this is going to be the schema
// for a normal element or for a subschema representing a Backbone element.
var conversionMode = generateBackbone ?
ElementConversionMode.BackboneType :
ElementConversionMode.Full;

var schemaMembers = convert(nav, conversionMode);

// Children need special treatment since the definition of this assertion does not
// depend on the current ElementNode, but on its descendants in the ElementDefNavigator.
if (nav.HasChildren)
{
var childrenAssertion = createChildrenAssertion(nav, subschemas);
schemaMembers.Add(childrenAssertion);
}

// Slicing also needs to navigate to its sibling ElementDefinitions,
// so we are dealing with it here separately.
if (nav.Current.Slicing != null)
{
var sliceAssertion = CreateSliceValidator(nav);
if (!sliceAssertion.IsAlways(ValidationResult.Success))
schemaMembers.Add(sliceAssertion);
}
// Slicing also needs to navigate to its sibling ElementDefinitions,
// so we are dealing with it here separately.
if (nav.Current.Slicing != null)
{
var sliceAssertion = CreateSliceValidator(nav);
if (!sliceAssertion.IsAlways(ValidationResult.Success))
schemaMembers.Add(sliceAssertion);
}

if (generateBackbone)
{
// If the schema generated is to be a subschema, put it in the
// list of subschemas we're creating.
var anchor = "#" + nav.Current.Path;

subschemas?.AddSchema(new ElementSchema(anchor, schemaMembers));

// Then represent the current backbone element exactly the
// way we would do for elements with a contentReference (without
// the contentReference itself, this backbone won't have one) + add
// a reference to the schema we just generated for the element.
schemaMembers = convert(nav, ElementConversionMode.ContentReference);
schemaMembers.Add(new SchemaReferenceValidator(nav.StructureDefinition.Url + anchor));
}

if (generateBackbone)
return schemaMembers;
}
catch (Exception e) when (e is not InvalidOperationException)
{
// If the schema generated is to be a subschema, put it in the
// list of subschemas we're creating.
var anchor = "#" + nav.Current.Path;

subschemas?.AddSchema(new ElementSchema(anchor, schemaMembers));

// Then represent the current backbone element exactly the
// way we would do for elements with a contentReference (without
// the contentReference itself, this backbone won't have one) + add
// a reference to the schema we just generated for the element.
schemaMembers = convert(nav, ElementConversionMode.ContentReference);
schemaMembers.Add(new SchemaReferenceValidator(nav.StructureDefinition.Url + anchor));
throw new InvalidOperationException($"Failed to convert ElementDefinition at " +
$"{nav.Current.ElementId ?? nav.Current.Path} in profile {nav.StructureDefinition.Url}: {e.Message}",
e);
}

return schemaMembers;
}

private List<IAssertion> convert(
Expand Down Expand Up @@ -281,7 +290,7 @@ private IReadOnlyDictionary<string, IAssertion> harvestChildren(
// After we're done processing the previous child, our next elment still appears to have the same path...
// This means the previous element was sliced, without us being able to correctly parse the slice. We rather fail than
// produce incorrect schemas here....
throw new InvalidOperationException($"Encountered an invalid or incomplete slice at element '{childNav.Path}', which cannot be understood by the validation.");
throw new IncorrectElementDefinitionException($"Encountered an invalid or incomplete slice at element '{childNav.Path}', which cannot be understood by the validation.");
}

// Don't add empty schemas (i.e. empty ElementDefs in a differential)
Expand Down Expand Up @@ -376,7 +385,7 @@ private static IEnumerable<Bookmark> findMemberSlices(ElementDefinitionNavigator
while (intro.MoveToNext(pathName))
{
var currentSliceName = intro.Current.SliceName ??
throw new InvalidOperationException($"Encountered a slice that has no slice name.");
throw new IncorrectElementDefinitionException($"Encountered a slice that has no slice name.");

if (ElementDefinitionNavigator.IsDirectSliceOf(currentSliceName, introSliceName))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,15 @@ public StructureDefinitionCorrectionsResolver(ISyncOrAsyncResourceResolver neste
correctIdElement(sd.Differential); correctIdElement(sd.Snapshot);
}


if (sd.Type == "string")
{
correctStringRegex(sd.Differential); correctStringRegex(sd.Snapshot);
correctStringTextRegex("string", sd.Differential); correctStringTextRegex("string", sd.Snapshot);
}

if (sd.Type == "markdown")
{
correctStringTextRegex("markdown", sd.Differential); correctStringTextRegex("markdown", sd.Snapshot);
}

if (new[] { "StructureDefinition", "ElementDefinition", "Reference", "Questionnaire" }.Contains(sd.Type))
Expand All @@ -81,11 +87,11 @@ static void correctIdElement(IElementList elements)
}
}

static void correctStringRegex(IElementList elements)
static void correctStringTextRegex(string datatype, IElementList elements)
{
if (elements is null) return;

var valueElement = elements.Element.Where(e => e.Path == "string.value");
var valueElement = elements.Element.Where(e => e.Path == $"{datatype}.value");
if (valueElement.Count() == 1 && valueElement.Single().Type.Count == 1)
{
valueElement.Single().Type.Single().
Expand Down Expand Up @@ -137,6 +143,8 @@ static void correctConstraints(IElementList elements)
=> @"((kind in 'resource' | 'complex-type') and (derivation= 'specialization')) implies differential.element.where((min != 0 and min != 1) or (max != '1' and max != '*')).empty()",

// correct datatype in expression:
{ Key: "que-0", Expression: @"name.matches('[A-Z]([A-Za-z0-9_]){0,254}')" }
=> @"name.exists() implies name.matches('[A-Z]([A-Za-z0-9_]){0,254}')",
{ Key: "que-7", Expression: @"operator = 'exists' implies (answer is Boolean)" }
=> @"operator = 'exists' implies (answer is boolean)",
var ce => ce.Expression
Expand All @@ -147,9 +155,13 @@ static void correctConstraints(IElementList elements)
}

/// <inheritdoc />
public Resource? ResolveByUri(string uri) => ResolveByCanonicalUri(uri);
public Resource? ResolveByUri(string uri) => TaskHelper.Await(() => ResolveByUriAsync(uri));

/// <inheritdoc />
public Task<Resource?> ResolveByUriAsync(string uri) => ResolveByCanonicalUriAsync(uri);
public async Task<Resource?> ResolveByUriAsync(string uri)
{
var result = await Nested.ResolveByUriAsync(uri).ConfigureAwait(false);
return correctStructureDefinition(result);
}
}
}
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/BindingValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ private static (Issue?, string?) callService(ValidateCodeParameters parameters,
}
catch (FhirOperationException tse)
{
var desiredResult = ctx.OnValidateCodeServiceFailure?.Invoke(parameters, tse)
var desiredResult = ctx.HandleValidateCodeServiceFailure?.Invoke(parameters, tse)
?? ValidationContext.TerminologyServiceExceptionResult.Warning;

var message = $"Terminology service failed while validating {display}: {tse.Message}";
Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/DatatypeSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public override ResultReport Validate(ITypedElement input, ValidationContext vc,
if (vc.ElementSchemaResolver is null)
throw new ArgumentException($"Cannot validate the resource because {nameof(ValidationContext)} does not contain an ElementSchemaResolver.");

var typeProfile = Canonical.ForCoreType(input.InstanceType);
var typeProfile = vc.TypeNameMapper.MapTypeName(input.InstanceType);
var fetchResult = FhirSchemaGroupAnalyzer.FetchSchema(vc.ElementSchemaResolver, state, typeProfile);
return fetchResult.Success ? fetchResult.Schema!.Validate(input, vc, state) : fetchResult.Error!;
}
Expand Down
7 changes: 4 additions & 3 deletions src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public override ResultReport Validate(IEnumerable<ITypedElement> input, Validati
{
if (group.Key is not null)
{
var extensionHandling = callback(vc).Invoke(state.Location.InstanceLocation.ToString(), group.Key);

var extensionHandling = callback(vc.FollowExtensionUrl).Invoke(state.Location.InstanceLocation.ToString(), group.Key);

if (extensionHandling is ExtensionUrlHandling.DontResolve)
{
Expand Down Expand Up @@ -118,8 +119,8 @@ public override ResultReport Validate(IEnumerable<ITypedElement> input, Validati

return ResultReport.FromEvidence(evidence);

static ExtensionUrlFollower callback(ValidationContext context) =>
context.FollowExtensionUrl ?? ((l, c) => ExtensionUrlHandling.WarnIfMissing);
static ExtensionUrlFollower callback(ExtensionUrlFollower? follower) =>
follower ?? ((l, c) => ExtensionUrlHandling.WarnIfMissing);
}

/// <summary>
Expand Down
5 changes: 2 additions & 3 deletions src/Firely.Fhir.Validation/Impl/IssueAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class IssueAssertion : IFixedResult, IValidatable, IEquatable<IssueAssert
/// to <see cref="IssueComponent.Details" /> when creating an <see cref="OperationOutcome" />.
/// </remarks>
[DataMember]
public string Message { get; }
public string Message { get; set; }

/// <summary>
/// The severity of the issue.
Expand Down Expand Up @@ -193,13 +193,12 @@ private ResultReport asResult(string location, DefinitionPath? definitionPath) =
public bool Equals(IssueAssertion? other) => other is not null &&
IssueNumber == other.IssueNumber &&
Location == other.Location &&
DefinitionPath == other.DefinitionPath &&
Message == other.Message &&
Severity == other.Severity &&
Type == other.Type;

/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(IssueNumber, Location, DefinitionPath, Message, Severity, Type, Result);
public override int GetHashCode() => HashCode.Combine(IssueNumber, Location, Message, Severity, Type, Result);

/// <inheritdoc/>
public static bool operator ==(IssueAssertion? left, IssueAssertion? right) => EqualityComparer<IssueAssertion>.Default.Equals(left!, right!);
Expand Down
8 changes: 4 additions & 4 deletions src/Firely.Fhir.Validation/Impl/PatternValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ public PatternValidator(object patternPrimitive) : this(ElementNode.ForPrimitive
/// <inheritdoc/>
public ResultReport Validate(ITypedElement input, ValidationContext _, ValidationState s)
{
var result = !input.Matches(PatternValue)
? new IssueAssertion(Issue.CONTENT_DOES_NOT_MATCH_PATTERN_VALUE, $"Value does not match pattern '{PatternValue.ToJson()}")
.AsResult(s)
: ResultReport.SUCCESS;
var result = input.Matches(PatternValue)
? ResultReport.SUCCESS
: new IssueAssertion(Issue.CONTENT_DOES_NOT_MATCH_PATTERN_VALUE, $"Value does not match pattern '{PatternValue.ToJson()}")
.AsResult(s);

return result;
}
Expand Down
Loading