diff --git a/src/Firely.Fhir.Validation/Impl/BindingValidator.cs b/src/Firely.Fhir.Validation/Impl/BindingValidator.cs index 63cfefb6..e6aa02f2 100644 --- a/src/Firely.Fhir.Validation/Impl/BindingValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/BindingValidator.cs @@ -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 } @@ -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) }; } diff --git a/src/Firely.Fhir.Validation/Impl/CanonicalValidator.cs b/src/Firely.Fhir.Validation/Impl/CanonicalValidator.cs index 03856ca9..1246f28a 100644 --- a/src/Firely.Fhir.Validation/Impl/CanonicalValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/CanonicalValidator.cs @@ -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); } } } \ No newline at end of file diff --git a/src/Firely.Fhir.Validation/Impl/CardinalityValidator.cs b/src/Firely.Fhir.Validation/Impl/CardinalityValidator.cs index f2fe9f61..5105fb9d 100644 --- a/src/Firely.Fhir.Validation/Impl/CardinalityValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/CardinalityValidator.cs @@ -101,7 +101,7 @@ ResultReport IGroupValidatable.Validate(IEnumerable 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; /// diff --git a/src/Firely.Fhir.Validation/Impl/ChildrenValidator.cs b/src/Firely.Fhir.Validation/Impl/ChildrenValidator.cs index b45cefa3..1c8040c7 100644 --- a/src/Firely.Fhir.Validation/Impl/ChildrenValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/ChildrenValidator.cs @@ -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( diff --git a/src/Firely.Fhir.Validation/Impl/ExtensionContextValidator.cs b/src/Firely.Fhir.Validation/Impl/ExtensionContextValidator.cs index 8a029c1c..cd53ccdc 100644 --- a/src/Firely.Fhir.Validation/Impl/ExtensionContextValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/ExtensionContextValidator.cs @@ -27,6 +27,7 @@ namespace Firely.Fhir.Validation; #endif public class ExtensionContextValidator : IValidatable { + private const string CONTEXT_INVARIANT_KEY = "ctx-inv"; /// /// Creates a new ExtensionContextValidator with the given allowed contexts and invariants. /// @@ -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 @@ -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 @@ -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()])); } diff --git a/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs b/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs index 8213f5c8..73b27360 100644 --- a/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs +++ b/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs @@ -98,7 +98,7 @@ internal override ResultReport ValidateInternal(IEnumerable 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)); diff --git a/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs b/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs index 3611b76a..f3367858 100644 --- a/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs @@ -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)); } } diff --git a/src/Firely.Fhir.Validation/Impl/FhirStringValidator.cs b/src/Firely.Fhir.Validation/Impl/FhirStringValidator.cs index 81ffcb3f..437733cb 100644 --- a/src/Firely.Fhir.Validation/Impl/FhirStringValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/FhirStringValidator.cs @@ -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; } } diff --git a/src/Firely.Fhir.Validation/Impl/FhirTxt1Validator.cs b/src/Firely.Fhir.Validation/Impl/FhirTxt1Validator.cs index 669d2615..f242a585 100644 --- a/src/Firely.Fhir.Validation/Impl/FhirTxt1Validator.cs +++ b/src/Firely.Fhir.Validation/Impl/FhirTxt1Validator.cs @@ -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); @@ -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)); } } diff --git a/src/Firely.Fhir.Validation/Impl/FhirTypeLabelValidator.cs b/src/Firely.Fhir.Validation/Impl/FhirTypeLabelValidator.cs index 3e30fdde..ec0b4a70 100644 --- a/src/Firely.Fhir.Validation/Impl/FhirTypeLabelValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/FhirTypeLabelValidator.cs @@ -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; } diff --git a/src/Firely.Fhir.Validation/Impl/FhirUriValidator.cs b/src/Firely.Fhir.Validation/Impl/FhirUriValidator.cs index bb320466..491195bd 100644 --- a/src/Firely.Fhir.Validation/Impl/FhirUriValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/FhirUriValidator.cs @@ -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 /// diff --git a/src/Firely.Fhir.Validation/Impl/FixedValidator.cs b/src/Firely.Fhir.Validation/Impl/FixedValidator.cs index ee0e768c..d1ae645c 100644 --- a/src/Firely.Fhir.Validation/Impl/FixedValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/FixedValidator.cs @@ -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; diff --git a/src/Firely.Fhir.Validation/Impl/InvariantValidator.cs b/src/Firely.Fhir.Validation/Impl/InvariantValidator.cs index d1ebb016..0f780211 100644 --- a/src/Firely.Fhir.Validation/Impl/InvariantValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/InvariantValidator.cs @@ -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; diff --git a/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs b/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs index 57851e5e..251f5c56 100644 --- a/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs +++ b/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs @@ -6,7 +6,6 @@ * 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; @@ -14,7 +13,6 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; using static Hl7.Fhir.Model.OperationOutcome; @@ -88,6 +86,11 @@ public class IssueAssertion : IFixedResult, IValidatable, IEquatable internal string? IssueSource { get; private set; } + /// + /// An assertion that resulted in the raised issue. + /// + internal IAssertion? Assertion { get; private set; } + /// /// Interprets the of the assertion as a /// to be used by the validator for deriving the result of the validation. @@ -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; @@ -135,6 +138,7 @@ private IssueAssertion(int issueNumber, string? location, DefinitionPath? defini Type = type; PositionInfo = positionInfo; IssueSource = issueSource; + Assertion = assertion; } /// @@ -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); } /// @@ -200,7 +204,8 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat /// /// #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); /// /// Package this as a , adding information from the current state of . @@ -209,17 +214,33 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat /// /// /// - public ResultReport AsResult(ValidationState state, PocoNode instance, string? issueSource) + public ResultReport AsResult(ValidationState state, PocoNode instance, string? issueSource) + => AsResult(state, instance, issueSource, assertion: null); + + /// + /// Package this as a , adding information from the current + /// state of , and registering a callback that will be invoked with the + /// generated from this assertion when it is turned into an + /// (e.g. to add extensions), without coupling this class to that concern. + /// + /// + /// + /// + /// A callback invoked with the generated + /// when this assertion is turned into an . + /// + public ResultReport AsResult(ValidationState state, PocoNode instance, string? issueSource, IAssertion? assertion) { this.PositionInfo ??= ((IAnnotated)instance).Annotation(); this.PositionInfo ??= ((IAnnotated)instance).Annotation(); this.PositionInfo ??= ((IAnnotated)instance).Annotation(); this.IssueSource = issueSource; - + this.Assertion = assertion; + return asResult(instance.GetLocation(), state.Location.DefinitionPath); } - + #pragma warning restore CS0618 // Type or member is obsolete /// @@ -231,7 +252,7 @@ public ResultReport AsResult(ValidationState state, PocoNode instance, string? i /// Package this as a /// 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)); /// public override bool Equals(object? obj) => Equals(obj as IssueAssertion); diff --git a/src/Firely.Fhir.Validation/Impl/MaxLengthValidator.cs b/src/Firely.Fhir.Validation/Impl/MaxLengthValidator.cs index 5558f800..b10a34c8 100644 --- a/src/Firely.Fhir.Validation/Impl/MaxLengthValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/MaxLengthValidator.cs @@ -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 diff --git a/src/Firely.Fhir.Validation/Impl/MinMaxValueValidator.cs b/src/Firely.Fhir.Validation/Impl/MinMaxValueValidator.cs index 425de438..40902d50 100644 --- a/src/Firely.Fhir.Validation/Impl/MinMaxValueValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/MinMaxValueValidator.cs @@ -118,7 +118,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat else if (!Any.TryConvert(input.GetValue(), out instanceValue!)) { return new IssueAssertion(Issue.CONTENT_ELEMENT_PRIMITIVE_VALUE_NOT_COMPARABLE, - $"Value '{input.GetValue()}' cannot be compared with {_minMaxAnyValue}.").AsResult(s, input, nameof(MinMaxValueValidator)); + $"Value '{input.GetValue()}' cannot be compared with {_minMaxAnyValue}.").AsResult(s, input, nameof(MinMaxValueValidator), this); } try { @@ -133,19 +133,19 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat if (intResult == _comparisonOutcome) { return new IssueAssertion(_comparisonIssue, $"Value '{instanceValue}' is {_comparisonLabel} {_minMaxAnyValue}.") - .AsResult(s, input, nameof(MinMaxValueValidator)); + .AsResult(s, input, nameof(MinMaxValueValidator), this); } } catch (ArgumentException){ return new IssueAssertion(Issue.CONTENT_ELEMENT_PRIMITIVE_VALUE_NOT_COMPARABLE, $"Value '{instanceValue}' cannot be compared with {_minMaxAnyValue})") - .AsResult(s, input); + .AsResult(s, input, nameof(MinMaxValueValidator), this); } catch (InvalidOperationException) { return new IssueAssertion(Issue.CONTENT_ELEMENT_PRIMITIVE_VALUE_NOT_COMPARABLE, $"Value '{instanceValue}' cannot be compared with {_minMaxAnyValue}.") - .AsResult(s, input, nameof(MinMaxValueValidator)); + .AsResult(s, input, nameof(MinMaxValueValidator), this); } return ResultReport.SUCCESS; diff --git a/src/Firely.Fhir.Validation/Impl/PatternValidator.cs b/src/Firely.Fhir.Validation/Impl/PatternValidator.cs index 57b53f23..3cf24c3d 100644 --- a/src/Firely.Fhir.Validation/Impl/PatternValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/PatternValidator.cs @@ -59,7 +59,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings _, Validat return ResultReport.SUCCESS; return new IssueAssertion(Issue.CONTENT_DOES_NOT_MATCH_PATTERN_VALUE.Code, $"Value '{displayValue(input)}' does not match pattern '{displayValue(PatternValue)}'", OperationOutcome.IssueSeverity.Error, OperationOutcome.IssueType.Invalid) - .AsResult(s, input, nameof(PatternValidator)); + .AsResult(s, input, nameof(PatternValidator), this); static string displayValue(ITypedElement te) => te.Children().Any() ? te.ToJson() : te.Value?.ToString()!; diff --git a/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs b/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs index e93c1693..f8ae7a8f 100644 --- a/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs @@ -77,7 +77,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida if (!IsSupportedReferenceType(input.Poco.TypeName)) return new IssueAssertion(Issue.CONTENT_REFERENCE_OF_INVALID_KIND, $"Expected a reference type here (reference or canonical) not a {input.Poco.TypeName}.") - .AsResult(state, input, nameof(ReferencedInstanceValidator)); + .AsResult(state, input, nameof(ReferencedInstanceValidator), this); // Get the actual reference from the instance by the pre-configured name. // The name is usually "reference" in case we are dealing with a FHIR reference type, @@ -107,7 +107,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida null when vc.ResolveExternalReference is null => ResultReport.SUCCESS, null => new IssueAssertion( Issue.UNAVAILABLE_REFERENCED_RESOURCE, - $"Cannot resolve reference {reference}").AsResult(state, input, nameof(ReferencedInstanceValidator)), + $"Cannot resolve reference {reference}").AsResult(state, input, nameof(ReferencedInstanceValidator), this), _ => validateReferencedResource(reference, vc, resolution, state) }; @@ -139,7 +139,7 @@ private record ResolutionResult(PocoNode? ReferencedResource, AggregationMode? R var allowed = string.Join(", ", AggregationRules); evidence.Add(new IssueAssertion(Issue.CONTENT_REFERENCE_OF_INVALID_KIND, $"Encountered a reference ({reference}) of kind '{resolution.ReferenceKind}', which is not one of the allowed kinds ({allowed}).") - .AsResult(s, input, nameof(ReferencedInstanceValidator))); + .AsResult(s, input, nameof(ReferencedInstanceValidator), this)); } if (VersioningRules is not null && VersioningRules != ReferenceVersionRules.Either) @@ -147,7 +147,7 @@ private record ResolutionResult(PocoNode? ReferencedResource, AggregationMode? R if (VersioningRules != resolution.VersioningKind) evidence.Add(new IssueAssertion(Issue.CONTENT_REFERENCE_OF_INVALID_KIND, $"Expected a {VersioningRules} versioned reference but found {resolution.VersioningKind}.") - .AsResult(s, input, nameof(ReferencedInstanceValidator))); + .AsResult(s, input, nameof(ReferencedInstanceValidator), this)); } if (resolution.ReferenceKind == AggregationMode.Referenced) @@ -169,7 +169,7 @@ private record ResolutionResult(PocoNode? ReferencedResource, AggregationMode? R evidence.Add(new IssueAssertion( Issue.UNAVAILABLE_REFERENCED_RESOURCE, $"Resolution of external reference {reference} failed. Message: {e.Message}") - .AsResult(s, input, nameof(ReferencedInstanceValidator))); + .AsResult(s, input, nameof(ReferencedInstanceValidator), this)); } } } @@ -180,7 +180,7 @@ private record ResolutionResult(PocoNode? ReferencedResource, AggregationMode? R /// /// Try to fetch the resource within this instance (e.g. a contained or bundled resource). /// - private static ResultReport resolveLocally(PocoNode instance, string reference, ValidationState s, out ResolutionResult resolution) + private ResultReport resolveLocally(PocoNode instance, string reference, ValidationState s, out ResolutionResult resolution) { resolution = new ResolutionResult(null, null, null); var identity = new ResourceIdentity(reference); @@ -193,7 +193,7 @@ private static ResultReport resolveLocally(PocoNode instance, string reference, if (!Uri.IsWellFormedUriString(Uri.EscapeDataString(reference), UriKind.RelativeOrAbsolute)) { return new IssueAssertion(Issue.CONTENT_UNPARSEABLE_REFERENCE, - $"Encountered an unparseable reference ({reference}").AsResult(s, instance.ToPocoNode(), nameof(ReferencedInstanceValidator)); + $"Encountered an unparseable reference ({reference}").AsResult(s, instance.ToPocoNode(), nameof(ReferencedInstanceValidator), this); } } @@ -206,7 +206,7 @@ private static ResultReport resolveLocally(PocoNode instance, string reference, catch (Exception e) { return new IssueAssertion(Issue.CONTENT_REFERENCE_NOT_RESOLVABLE, - $"Encountered an issue during reference resolution. Message: {e.Message}").AsResult(s, instance.ToPocoNode(), nameof(ReferencedInstanceValidator)); + $"Encountered an issue during reference resolution. Message: {e.Message}").AsResult(s, instance.ToPocoNode(), nameof(ReferencedInstanceValidator), this); } diff --git a/src/Firely.Fhir.Validation/Impl/RegExValidator.cs b/src/Firely.Fhir.Validation/Impl/RegExValidator.cs index 8836d49e..4a710e3e 100644 --- a/src/Firely.Fhir.Validation/Impl/RegExValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/RegExValidator.cs @@ -65,7 +65,7 @@ internal override ResultReport BasicValidate(PocoNode input, ValidationSettings return ResultReport.SUCCESS; return new IssueAssertion(Issue.CONTENT_ELEMENT_INVALID_PRIMITIVE_VALUE.Code, $"Value '{value}' does not match regex '{Pattern}'", OperationOutcome.IssueSeverity.Error, OperationOutcome.IssueType.Invalid) - .AsResult(s, input, nameof(RegExValidator)); + .AsResult(s, input, nameof(RegExValidator), this); } private static string? toStringRepresentation(PocoNode vp) diff --git a/src/Firely.Fhir.Validation/Impl/RenderingXhtmlValidator.cs b/src/Firely.Fhir.Validation/Impl/RenderingXhtmlValidator.cs index 6099a16e..0e773a34 100644 --- a/src/Firely.Fhir.Validation/Impl/RenderingXhtmlValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/RenderingXhtmlValidator.cs @@ -55,8 +55,8 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida if (isValid) return ResultReport.SUCCESS; var issueReports = malformedError is null - ? narrativeIssues.Select(e => new IssueAssertion(Issue.XSD_VALIDATION_ERROR, e).AsResult(state, valueNode)).ToArray() - : [new IssueAssertion(Issue.XSD_VALIDATION_ERROR, malformedError).AsResult(state, valueNode)]; + ? narrativeIssues.Select(e => new IssueAssertion(Issue.XSD_VALIDATION_ERROR, e).AsResult(state, valueNode, nameof(RenderingXhtmlValidator), this)).ToArray() + : [new IssueAssertion(Issue.XSD_VALIDATION_ERROR, malformedError).AsResult(state, valueNode, nameof(RenderingXhtmlValidator), this)]; return ResultReport.Combine(issueReports); } diff --git a/src/Firely.Fhir.Validation/Impl/RequiredValidator.cs b/src/Firely.Fhir.Validation/Impl/RequiredValidator.cs index ec09e2ad..17eeaee0 100644 --- a/src/Firely.Fhir.Validation/Impl/RequiredValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/RequiredValidator.cs @@ -56,7 +56,7 @@ ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, Valida _requiredMembers .Where(memberName => input.Child(memberName) is null) .Select(memberName => new IssueAssertion(Issue.CONTENT_INCORRECT_OCCURRENCE, $"Missing required member: '{memberName}'") - .AsResult(state, input, nameof(RequiredValidator)) + .AsResult(state, input, nameof(RequiredValidator), this) ).ToList(); return ResultReport.Combine(evidence); diff --git a/src/Firely.Fhir.Validation/Impl/SliceValidator.cs b/src/Firely.Fhir.Validation/Impl/SliceValidator.cs index dd29bccc..5ca3ce08 100644 --- a/src/Firely.Fhir.Validation/Impl/SliceValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/SliceValidator.cs @@ -172,7 +172,7 @@ ResultReport IGroupValidatable.Validate(IEnumerable input, ValidationS if (sliceNumber < lastMatchingSlice && Ordered) evidence.Add(new IssueAssertion(Issue.CONTENT_ELEMENT_SLICING_OUT_OF_ORDER, $"Element matches slice {sliceLocation!}:{sliceName}', but this is out of order for group {sliceLocation!}, since a previous element already matched slice '{sliceLocation!}:{Slices[lastMatchingSlice].Name}'") - .AsResult(state, candidate, nameof(SliceValidator))); + .AsResult(state, candidate, nameof(SliceValidator), this)); else lastMatchingSlice = sliceNumber; @@ -181,7 +181,7 @@ ResultReport IGroupValidatable.Validate(IEnumerable input, ValidationS // We found a match while we already added a non-match to a "open at end" slicegroup, that's not allowed evidence.Add(new IssueAssertion(Issue.CONTENT_ELEMENT_FAILS_SLICING_RULE, $"Element matched slice '{sliceLocation!}:{sliceName}', but it appears after a non-match, which is not allowed for an open-at-end group") - .AsResult(state, candidate, nameof(SliceValidator))); + .AsResult(state, candidate, nameof(SliceValidator), this)); } hasSucceeded = true; @@ -212,7 +212,7 @@ ResultReport IGroupValidatable.Validate(IEnumerable input, ValidationS evidence.AddRange(buckets .Where(slice => slice.Value is null && slice.Key.Required) .Select(slice => new IssueAssertion(Issue.CONTENT_INCORRECT_OCCURRENCE, $"No elements matched required slice: '{pn.Name}:{slice.Key.Name}'") - .AsResult(state, pn.Parent!, nameof (SliceValidator)))); + .AsResult(state, pn.Parent!, nameof (SliceValidator), this))); evidence.AddRange(buckets.Validate(vc, state)); return ResultReport.Combine(evidence); diff --git a/src/Firely.Fhir.Validation/PublicAPI.Unshipped.txt b/src/Firely.Fhir.Validation/PublicAPI.Unshipped.txt index c9dc4f6f..b88b276c 100644 --- a/src/Firely.Fhir.Validation/PublicAPI.Unshipped.txt +++ b/src/Firely.Fhir.Validation/PublicAPI.Unshipped.txt @@ -175,6 +175,7 @@ Firely.Fhir.Validation.InvariantValidator.InvariantValidator() -> void Firely.Fhir.Validation.IssueAssertion Firely.Fhir.Validation.IssueAssertion.AsResult(Firely.Fhir.Validation.ValidationState! state, Hl7.Fhir.Model.PocoNode! input) -> Firely.Fhir.Validation.ResultReport! Firely.Fhir.Validation.IssueAssertion.AsResult(Firely.Fhir.Validation.ValidationState! state, Hl7.Fhir.Model.PocoNode! instance, string? issueSource) -> Firely.Fhir.Validation.ResultReport! +Firely.Fhir.Validation.IssueAssertion.AsResult(Firely.Fhir.Validation.ValidationState! state, Hl7.Fhir.Model.PocoNode! instance, string? issueSource, Firely.Fhir.Validation.IAssertion? assertion) -> Firely.Fhir.Validation.ResultReport! Firely.Fhir.Validation.IssueAssertion.AsResult(string! location) -> Firely.Fhir.Validation.ResultReport! Firely.Fhir.Validation.IssueAssertion.Equals(Firely.Fhir.Validation.IssueAssertion? other) -> bool Firely.Fhir.Validation.IssueAssertion.IssueAssertion(Hl7.Fhir.Support.Issue! issue, string! message) -> void diff --git a/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs b/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs index 0ed0dae4..6bd4003b 100644 --- a/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs +++ b/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs @@ -65,6 +65,13 @@ public static OperationOutcome ToOperationOutcome(this ResultReport result) { newIssueComponent.AddExtension("http://hl7.org/fhir/StructureDefinition/operationoutcome-issue-source", new FhirString(item.IssueSource)); } + + #pragma warning disable CS0618 // Type or member is obsolete + if (item.Assertion is InvariantValidator invariant) + { + #pragma warning restore CS0618 // Type or member is obsolete + newIssueComponent.AddExtension("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id", new FhirString(invariant.Key)); + } } return outcome; diff --git a/src/Firely.Fhir.Validation/Support/OperationOutcomeDefinitionExtensions.cs b/src/Firely.Fhir.Validation/Support/OperationOutcomeDefinitionExtensions.cs index eb914019..39c0c943 100644 --- a/src/Firely.Fhir.Validation/Support/OperationOutcomeDefinitionExtensions.cs +++ b/src/Firely.Fhir.Validation/Support/OperationOutcomeDefinitionExtensions.cs @@ -24,4 +24,6 @@ public static void SetStructureDefinitionPath(this OperationOutcome.IssueCompone public static string? GetStructureDefinitionPath(this OperationOutcome.IssueComponent ic) => ic.Details?.Coding.FirstOrDefault(c => c.System == OPERATION_OUTCOME_SDREF)?.Code; } + + } \ No newline at end of file diff --git a/test/Firely.Fhir.Validation.Tests/Impl/FhirPathValidatorTests.cs b/test/Firely.Fhir.Validation.Tests/Impl/FhirPathValidatorTests.cs index 08475062..dbc77d6b 100644 --- a/test/Firely.Fhir.Validation.Tests/Impl/FhirPathValidatorTests.cs +++ b/test/Firely.Fhir.Validation.Tests/Impl/FhirPathValidatorTests.cs @@ -61,6 +61,25 @@ public void ValidateSuccess() Assert.IsTrue(result.IsSuccessful, "the FhirPath Expression must be valid for this input"); } + [TestMethod] + public void ValidateFailureExposesInvariantIdOnOperationOutcome() + { + var validatable = new FhirPathValidator("ele-1", "$this = 'other'", "human description", IssueSeverity.Error, false); + + var input = PocoNode.ForPrimitive("test"); + + var minimalContextWithFp = ValidationSettings.BuildMinimalContext(fpCompiler: _fpCompiler); + var result = validatable.Validate(input, minimalContextWithFp); + + Assert.IsFalse(result.IsSuccessful); + + var outcome = result.ToOperationOutcome(); + var extension = outcome.Issue.Single().GetExtension("http://hl7.org/fhir/StructureDefinition/operationoutcome-message-id"); + + Assert.IsNotNull(extension); + Assert.AreEqual("ele-1", (extension.Value as FhirString)?.Value); + } + [TestMethod] public void ValidateIncorrectFhirPath() {