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
7 changes: 4 additions & 3 deletions src/Firely.Fhir.Validation/Impl/BindingValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,11 @@ private ResultReport verifyContentRequirements(PocoNode source, DataType bindabl
case Coding cd when string.IsNullOrEmpty(cd.Code) && Strength == BindingStrength.Required:
case CodeableConcept cc when !codeableConceptHasCode(cc) && Strength == BindingStrength.Required:
return new IssueAssertion(Issue.TERMINOLOGY_INCOMPLETE_CODE_ERROR,
$"No code found in {source.Poco.TypeName} with a required binding to valueset '{ValueSetUri}'.").AsResult(s, source, nameof(BindingValidator));
$"No code found in {source.Poco.TypeName} with a required binding to valueset '{ValueSetUri}'.").AsResult(s, source, nameof(BindingValidator), this);
case CodeableConcept cc when !codeableConceptHasCode(cc) && string.IsNullOrEmpty(cc.Text) &&
Strength == BindingStrength.Extensible:
return new IssueAssertion(Issue.TERMINOLOGY_INCOMPLETE_CODE_WARNING,
$"Extensible binding to valueset '{ValueSetUri}' requires code or text.").AsResult(s, source, nameof(BindingValidator));
$"Extensible binding to valueset '{ValueSetUri}' requires code or text.").AsResult(s, source, nameof(BindingValidator), this);
default:
return ResultReport.SUCCESS; // nothing wrong then
}
Expand Down Expand Up @@ -196,7 +196,8 @@ ValidateCodeParameters buildParams()
return result switch
{
(null, _) => ResultReport.SUCCESS,
({ } issue, var message) => new IssueAssertion(issue, (issue.Severity == OperationOutcome.IssueSeverity.Error ? message! + ", but the binding is of strength 'required'" : message!)).AsResult(s, input, nameof(BindingValidator))
({ } issue, var message) => new IssueAssertion(issue, (issue.Severity == OperationOutcome.IssueSeverity.Error ? message! + ", but the binding is of strength 'required'" : message!))
.AsResult(s, input, nameof(BindingValidator), this)
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/Firely.Fhir.Validation/Impl/CanonicalValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida
return canonical.HasAnchor || canonical.IsAbsolute
? ResultReport.SUCCESS
: new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE,
$"Canonical URLs must be absolute URLs if they are not fragment references").AsResult(state, input, nameof(CanonicalValidator));
$"Canonical URLs must be absolute URLs if they are not fragment references").AsResult(state, input, nameof(CanonicalValidator), this);

return new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE,
$"Primitive does not have the correct type ({input.Poco.TypeName})").AsResult(state, input, nameof(CanonicalValidator));
$"Primitive does not have the correct type ({input.Poco.TypeName})").AsResult(state, input, nameof(CanonicalValidator), this);
}
}
}
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/CardinalityValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ ResultReport IGroupValidatable.Validate(IEnumerable<PocoNode> input, ValidationS

private ResultReport buildResult(PocoNode parent, string elemName, int count, ValidationState s) => !inRange(count) ?
new IssueAssertion(Issue.CONTENT_INCORRECT_OCCURRENCE,
$"Instance count at element '{elemName}' is {count}, which is not within the specified cardinality of {CardinalityDisplay}").AsResult(s, parent, nameof(CardinalityValidator))
$"Instance count at element '{elemName}' is {count}, which is not within the specified cardinality of {CardinalityDisplay}").AsResult(s, parent, nameof(CardinalityValidator), this)
: ResultReport.SUCCESS;

/// <inheritdoc />
Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/ChildrenValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida
{
var elementList = string.Join(",", matchResult.UnmatchedInstanceElements.Select(e => $"'{e.Name}'"));
evidence.Add(new IssueAssertion(Issue.CONTENT_ELEMENT_HAS_UNKNOWN_CHILDREN, $"Encountered unknown child elements {elementList}")
.AsResult(state, input, nameof(ChildrenValidator)));
.AsResult(state, input, nameof(ChildrenValidator), this));
}

evidence.AddRange(
Expand Down
10 changes: 5 additions & 5 deletions src/Firely.Fhir.Validation/Impl/ExtensionContextValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace Firely.Fhir.Validation;
#endif
public class ExtensionContextValidator : IValidatable
{
private const string CONTEXT_INVARIANT_KEY = "ctx-inv";
/// <summary>
/// Creates a new ExtensionContextValidator with the given allowed contexts and invariants.
/// </summary>
Expand Down Expand Up @@ -69,7 +70,7 @@ public ResultReport Validate(PocoNode input, ValidationSettings vc, ValidationSt
{
return new IssueAssertion(Issue.CONTENT_INCORRECT_OCCURRENCE,
$"Extension used outside of appropriate contexts. Expected context to be one of: {RenderExpectedContexts}")
.AsResult(state, input, nameof(ExtensionContextValidator));
.AsResult(state, input, nameof(ExtensionContextValidator), this);
}

var invariantResults = Invariants
Expand All @@ -86,9 +87,8 @@ public ResultReport Validate(PocoNode input, ValidationSettings vc, ValidationSt
{
// If eval to false, throw an error
(false, null) =>
new IssueAssertion(
Issue.CONTENT_ELEMENT_FAILS_ERROR_CONSTRAINT,
$"Extension context failed invariant constraint {res.Invariant}").AsResult(state, input, nameof(ExtensionContextValidator)),
new IssueAssertion(Issue.CONTENT_ELEMENT_FAILS_ERROR_CONSTRAINT, $"Extension context failed invariant constraint {res.Invariant}")
.AsResult(state, input, nameof(ExtensionContextValidator), new FhirPathValidator(res.Invariant ?? CONTEXT_INVARIANT_KEY, "")),
// If evalutation threw an exception, return that exception
(_, { } report) => report,
// Otherwise return success
Expand Down Expand Up @@ -287,7 +287,7 @@ private static InvariantValidator.InvariantResult runContextInvariant(PocoNode i
{
// our invariant is defined with %extension, but the FhirPathValidator expects %%extension because that is our syntax for environment variables
// TODO investigate changing this in the SDK
var fhirPathValidator = new FhirPathValidator("ctx-inv", invariant.Replace("%extension", "%%extension"));
var fhirPathValidator = new FhirPathValidator(CONTEXT_INVARIANT_KEY, invariant.Replace("%extension", "%%extension"));
return fhirPathValidator.RunInvariant(input.ToPocoNode().Parent!, vc, state, ("extension", [input.ToPocoNode()]));
}

Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ internal override ResultReport ValidateInternal(IEnumerable<PocoNode> input, Val

evidence.Add(new ResultReport(vr,
new IssueAssertion(issue, $"Unable to resolve reference to extension '{group.Key}'.")
.AsResult(state, group.First(), nameof(ExtensionSchema)).Evidence));
.AsResult(state, group.First(), nameof(ExtensionSchema), this).Evidence));

// No url available - validate the Extension schema itself.
evidence.Add(ValidateExtensionSchema(group, vc, state));
Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ private InvariantResult runInvariantInternal(PocoNode input, ValidationSettings
{
return new(false, new IssueAssertion(Issue.PROFILE_ELEMENTDEF_INVALID_FHIRPATH_EXPRESSION,
$"Evaluation of FhirPath for constraint '{Key}' failed: {e.Message}")
.AsResult(s, input, nameof(FhirPathValidator)));
.AsResult(s, input, nameof(FhirPathValidator), this));
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Firely.Fhir.Validation/Impl/FhirStringValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida
{
if (input is not PrimitiveNode { Primitive: FhirString str })
return new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE,
$"Primitive does not have the correct type ({input.Poco.TypeName})").AsResult(state, input, nameof(FhirStringValidator));
$"Primitive does not have the correct type ({input.Poco.TypeName})").AsResult(state, input, nameof(FhirStringValidator), this);
if (!str.HasValidValue())
return new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE,
$"String values cannot be empty").AsResult(state, input, nameof(FhirStringValidator));
$"String values cannot be empty").AsResult(state, input, nameof(FhirStringValidator), this);
return ResultReport.SUCCESS;
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Firely.Fhir.Validation/Impl/FhirTxt1Validator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal override InvariantResult RunInvariant(PocoNode input, ValidationSetting
if (primitive is not {Poco: XHtml xhtml})
return new(false,
new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE,
$"Narrative should be of type string, but is of type ({primitive.Poco.GetType()})").AsResult(state, input));
$"Narrative should be of type string, but is of type ({primitive.Poco.GetType()})").AsResult(state, input, nameof(FhirTxt1Validator), this));

// Check if the narrative contains only the basic HTML formatting elements and attributesvar result = XHtml.IsValidNarrativeXhtml(input.Value.ToString()!, out var malformedError, out var narrativeIssues);

Expand All @@ -63,8 +63,8 @@ internal override InvariantResult RunInvariant(PocoNode input, ValidationSetting
else
{
var issueReports = malformedError is null
? narrativeIssues.Select(e => new IssueAssertion(Issue.XSD_VALIDATION_ERROR, e).AsResult(state, input)).ToArray()
: [new IssueAssertion(Issue.XSD_VALIDATION_ERROR, malformedError).AsResult(state, input)];
? narrativeIssues.Select(e => new IssueAssertion(Issue.XSD_VALIDATION_ERROR, e).AsResult(state, input, nameof(FhirTxt1Validator), this)).ToArray()
: [new IssueAssertion(Issue.XSD_VALIDATION_ERROR, malformedError).AsResult(state, input, nameof(FhirTxt1Validator), this)];
return new(false, ResultReport.Combine(issueReports));
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/FhirTypeLabelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal override ResultReport BasicValidate(PocoNode input, ValidationSettings
ResultReport.SUCCESS :
new IssueAssertion(Issue.CONTENT_ELEMENT_HAS_INCORRECT_TYPE,
$"The declared type of the element ({Label}) is incompatible with that of the instance ({input.Poco.TypeName}).")
.AsResult(s, input, nameof(FhirTypeLabelValidator));
.AsResult(s, input, nameof(FhirTypeLabelValidator), this);
//
return result;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/FhirUriValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class FhirUriValidator : BasicValidator
{
internal override ResultReport BasicValidate(PocoNode input, ValidationSettings vc, ValidationState state) =>
(input is PrimitiveNode {Poco: FhirUri uri})
? uri.HasValidValue() && !string.IsNullOrWhiteSpace(uri.Value) ? ResultReport.SUCCESS : new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE, $"Value '{uri}' is not a valid URI").AsResult(state, input, nameof(FhirUriValidator))
? uri.HasValidValue() && !string.IsNullOrWhiteSpace(uri.Value) ? ResultReport.SUCCESS : new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE, $"Value '{uri}' is not a valid URI").AsResult(state, input, nameof(FhirUriValidator), this)
: ResultReport.SUCCESS; // TODO remove this check when the SDK is updated

/// <inheritdoc />
Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/FixedValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat
{
return new IssueAssertion(Issue.CONTENT_DOES_NOT_MATCH_FIXED_VALUE,
$"Value '{displayValue(input)}' is not exactly equal to fixed value '{displayValue(FixedValue)}'")
.AsResult(s, input, nameof(FixedValidator));
.AsResult(s, input, nameof(FixedValidator), this);
}

return ResultReport.SUCCESS;
Expand Down
3 changes: 2 additions & 1 deletion src/Firely.Fhir.Validation/Impl/InvariantValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida
return new IssueAssertion(sev == IssueSeverity.Error ?
Issue.CONTENT_ELEMENT_FAILS_ERROR_CONSTRAINT :
Issue.CONTENT_ELEMENT_FAILS_WARNING_CONSTRAINT,
$"Instance failed constraint {getDescription()}").AsResult(s, input, nameof(InvariantValidator));
$"Instance failed constraint {getDescription()}").AsResult(s, input, nameof(InvariantValidator),
this);
}
else
return ResultReport.SUCCESS;
Expand Down
39 changes: 30 additions & 9 deletions src/Firely.Fhir.Validation/Impl/IssueAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@
* available at https://github.com/FirelyTeam/firely-validator-api/blob/main/LICENSE
*/

using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Support;
using Hl7.Fhir.Utility;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using static Hl7.Fhir.Model.OperationOutcome;

Expand Down Expand Up @@ -88,6 +86,11 @@ public class IssueAssertion : IFixedResult, IValidatable, IEquatable<IssueAssert
/// </summary>
internal string? IssueSource { get; private set; }

/// <summary>
/// An assertion that resulted in the raised issue.
/// </summary>
internal IAssertion? Assertion { get; private set; }

/// <summary>
/// Interprets the <see cref="IssueSeverity" /> of the assertion as a <see cref="ValidationResult" />
/// to be used by the validator for deriving the result of the validation.
Expand Down Expand Up @@ -125,7 +128,7 @@ public IssueAssertion(int issueNumber, string message, IssueSeverity severity, I
{
}

private IssueAssertion(int issueNumber, string? location, DefinitionPath? definitionPath, string message, IssueSeverity severity, IssueType? type = null, IPositionInfo? positionInfo = null, string? issueSource = null)
private IssueAssertion(int issueNumber, string? location, DefinitionPath? definitionPath, string message, IssueSeverity severity, IssueType? type = null, IPositionInfo? positionInfo = null, string? issueSource = null, IAssertion? assertion = null)
{
IssueNumber = issueNumber;
Location = location;
Expand All @@ -135,6 +138,7 @@ private IssueAssertion(int issueNumber, string? location, DefinitionPath? defini
Type = type;
PositionInfo = positionInfo;
IssueSource = issueSource;
Assertion = assertion;
}

/// <inheritdoc />
Expand Down Expand Up @@ -190,7 +194,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat
// Also, we replace some "magic" tags in the message with common runtime data
var message = Message.Replace(Pattern.INSTANCETYPE, input.Poco.TypeName).Replace(Pattern.RESOURCEURL, state.Instance.ResourceUrl);

return new IssueAssertion(IssueNumber, message, Severity, Type).AsResult(state, input, IssueSource);
return new IssueAssertion(IssueNumber, message, Severity, Type).AsResult(state, input, IssueSource, this);
}

/// <summary>
Expand All @@ -200,7 +204,8 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat
/// <param name="input"></param>
/// <returns></returns>
#pragma warning disable CS0618 // Type or member is obsolete
public ResultReport AsResult(ValidationState state, PocoNode input) => asResult(input.GetLocation(), state.Location.DefinitionPath);
public ResultReport AsResult(ValidationState state, PocoNode input)
=> AsResult(state, input, issueSource: null, assertion: null);

/// <summary>
/// Package this <see cref="IssueAssertion"/> as a <see cref="ResultReport"/>, adding information from the current state of <paramref name="instance"/>.
Expand All @@ -209,17 +214,33 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat
/// <param name="instance"></param>
/// <param name="issueSource"></param>
/// <returns></returns>
public ResultReport AsResult(ValidationState state, PocoNode instance, string? issueSource)
public ResultReport AsResult(ValidationState state, PocoNode instance, string? issueSource)
=> AsResult(state, instance, issueSource, assertion: null);

/// <summary>
/// Package this <see cref="IssueAssertion"/> as a <see cref="ResultReport"/>, adding information from the current
/// state of <paramref name="instance"/>, and registering a callback that will be invoked with the
/// <see cref="IssueComponent"/> generated from this assertion when it is turned into an
/// <see cref="OperationOutcome"/> (e.g. to add extensions), without coupling this class to that concern.
/// </summary>
/// <param name="state"></param>
/// <param name="instance"></param>
/// <param name="issueSource"></param>
/// <param name="assertion">A callback invoked with the generated <see cref="IssueComponent"/>
/// when this assertion is turned into an <see cref="OperationOutcome"/>.</param>
/// <returns></returns>
public ResultReport AsResult(ValidationState state, PocoNode instance, string? issueSource, IAssertion? assertion)
{
this.PositionInfo ??= ((IAnnotated)instance).Annotation<JsonSerializationDetails>();
this.PositionInfo ??= ((IAnnotated)instance).Annotation<XmlSerializationDetails>();
this.PositionInfo ??= ((IAnnotated)instance).Annotation<PositionInfo>();

this.IssueSource = issueSource;

this.Assertion = assertion;

return asResult(instance.GetLocation(), state.Location.DefinitionPath);
}

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

/// <summary>
Expand All @@ -231,7 +252,7 @@ public ResultReport AsResult(ValidationState state, PocoNode instance, string? i
/// Package this <see cref="IssueAssertion"/> as a <see cref="ResultReport"/>
/// </summary>
internal ResultReport asResult(string location, DefinitionPath? definitionPath) =>
new(Result, new IssueAssertion(IssueNumber, location, definitionPath, Message, Severity, Type, PositionInfo, IssueSource));
new(Result, new IssueAssertion(IssueNumber, location, definitionPath, Message, Severity, Type, PositionInfo, IssueSource, Assertion));

/// <inheritdoc/>
public override bool Equals(object? obj) => Equals(obj as IssueAssertion);
Expand Down
2 changes: 1 addition & 1 deletion src/Firely.Fhir.Validation/Impl/MaxLengthValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal override ResultReport BasicValidate(PocoNode input, ValidationSettings
{
return str.Length > MaximumLength
? new IssueAssertion(Issue.CONTENT_ELEMENT_VALUE_TOO_LONG,
$"Value '{str}' is too long (maximum length is {MaximumLength})").AsResult(s, input, nameof(MaxLengthValidator))
$"Value '{str}' is too long (maximum length is {MaximumLength})").AsResult(s, input, nameof(MaxLengthValidator), this)
: ResultReport.SUCCESS;
}
else
Expand Down
Loading