diff --git a/src/Firely.Fhir.Validation.Compilation.Shared/CommonTypeRefComponentExtensions.cs b/src/Firely.Fhir.Validation.Compilation.Shared/CommonTypeRefComponentExtensions.cs index 95e33b76..a451ab9d 100644 --- a/src/Firely.Fhir.Validation.Compilation.Shared/CommonTypeRefComponentExtensions.cs +++ b/src/Firely.Fhir.Validation.Compilation.Shared/CommonTypeRefComponentExtensions.cs @@ -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 @@ -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", @@ -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; } } diff --git a/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilder.cs b/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilder.cs index 442d6d78..bd3dbc6d 100644 --- a/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilder.cs +++ b/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilder.cs @@ -65,7 +65,7 @@ public IEnumerable 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}", @@ -139,58 +139,67 @@ private ElementSchema convertElementToSchema(Canonical schemaId, ElementDefiniti /// sibling slice elements, if the current element is a slice intro. internal List 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 convert( @@ -281,7 +290,7 @@ private IReadOnlyDictionary 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) @@ -376,7 +385,7 @@ private static IEnumerable 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)) { diff --git a/src/Firely.Fhir.Validation.Compilation.Shared/StructureDefinitionCorrectionsResolver.cs b/src/Firely.Fhir.Validation.Compilation.Shared/StructureDefinitionCorrectionsResolver.cs index 1695ade0..b1da20d5 100644 --- a/src/Firely.Fhir.Validation.Compilation.Shared/StructureDefinitionCorrectionsResolver.cs +++ b/src/Firely.Fhir.Validation.Compilation.Shared/StructureDefinitionCorrectionsResolver.cs @@ -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)) @@ -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(). @@ -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 @@ -147,9 +155,13 @@ static void correctConstraints(IElementList elements) } /// - public Resource? ResolveByUri(string uri) => ResolveByCanonicalUri(uri); + public Resource? ResolveByUri(string uri) => TaskHelper.Await(() => ResolveByUriAsync(uri)); /// - public Task ResolveByUriAsync(string uri) => ResolveByCanonicalUriAsync(uri); + public async Task ResolveByUriAsync(string uri) + { + var result = await Nested.ResolveByUriAsync(uri).ConfigureAwait(false); + return correctStructureDefinition(result); + } } } \ No newline at end of file diff --git a/src/Firely.Fhir.Validation/Impl/BindingValidator.cs b/src/Firely.Fhir.Validation/Impl/BindingValidator.cs index 357e806c..62a6501c 100644 --- a/src/Firely.Fhir.Validation/Impl/BindingValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/BindingValidator.cs @@ -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}"; diff --git a/src/Firely.Fhir.Validation/Impl/DatatypeSchema.cs b/src/Firely.Fhir.Validation/Impl/DatatypeSchema.cs index 4524a85c..d3e57952 100644 --- a/src/Firely.Fhir.Validation/Impl/DatatypeSchema.cs +++ b/src/Firely.Fhir.Validation/Impl/DatatypeSchema.cs @@ -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!; } diff --git a/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs b/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs index 78ada9f3..d510dc50 100644 --- a/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs +++ b/src/Firely.Fhir.Validation/Impl/ExtensionSchema.cs @@ -64,7 +64,8 @@ public override ResultReport Validate(IEnumerable 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) { @@ -118,8 +119,8 @@ public override ResultReport Validate(IEnumerable 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); } /// diff --git a/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs b/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs index 002cb1f3..ebede144 100644 --- a/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs +++ b/src/Firely.Fhir.Validation/Impl/IssueAssertion.cs @@ -38,7 +38,7 @@ public class IssueAssertion : IFixedResult, IValidatable, IEquatable when creating an . /// [DataMember] - public string Message { get; } + public string Message { get; set; } /// /// The severity of the issue. @@ -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; /// - 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); /// public static bool operator ==(IssueAssertion? left, IssueAssertion? right) => EqualityComparer.Default.Equals(left!, right!); diff --git a/src/Firely.Fhir.Validation/Impl/PatternValidator.cs b/src/Firely.Fhir.Validation/Impl/PatternValidator.cs index 059f45df..5274f2a6 100644 --- a/src/Firely.Fhir.Validation/Impl/PatternValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/PatternValidator.cs @@ -48,10 +48,10 @@ public PatternValidator(object patternPrimitive) : this(ElementNode.ForPrimitive /// 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; } diff --git a/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs b/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs index 79a828ee..61c7cba0 100644 --- a/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs @@ -88,15 +88,16 @@ public ResultReport Validate(ITypedElement input, ValidationContext vc, Validati var (evidence, resolution) = fetchReference(input, reference, vc, state); // If the reference was resolved (either internally or externally), validate it - return resolution.ReferencedResource switch + var referenceResolutionReport = resolution.ReferencedResource switch { + null when vc.ResolveExternalReference is null => ResultReport.SUCCESS, null => new IssueAssertion( Issue.UNAVAILABLE_REFERENCED_RESOURCE, $"Cannot resolve reference {reference}").AsResult(state), - _ => ResultReport.FromEvidence( - evidence.Append( - validateReferencedResource(reference, vc, resolution, state)).ToList()) + _ => validateReferencedResource(reference, vc, resolution, state) }; + + return ResultReport.FromEvidence(evidence.Append(referenceResolutionReport).ToList()); } else return ResultReport.SUCCESS; @@ -141,7 +142,7 @@ private record ResolutionResult(ITypedElement? ReferencedResource, AggregationMo if (resolution.ReferenceKind == AggregationMode.Referenced) { // Bail out if we are asked to follow an *external reference* when this is disabled in the settings - if (vc.ExternalReferenceResolver is null) + if (vc.ResolveExternalReference is null) return (evidence, resolution); // If we are supposed to resolve the reference externally, then do so now. @@ -149,7 +150,7 @@ private record ResolutionResult(ITypedElement? ReferencedResource, AggregationMo { try { - var externalReference = TaskHelper.Await(() => vc.ExternalReferenceResolver!(reference)); + var externalReference = vc.ResolveExternalReference!(reference, input.Location); resolution = resolution with { ReferencedResource = externalReference }; } catch (Exception e) diff --git a/src/Firely.Fhir.Validation/Impl/ResourceSchema.cs b/src/Firely.Fhir.Validation/Impl/ResourceSchema.cs index 333815ba..bb0e62a6 100644 --- a/src/Firely.Fhir.Validation/Impl/ResourceSchema.cs +++ b/src/Firely.Fhir.Validation/Impl/ResourceSchema.cs @@ -40,7 +40,7 @@ public ResourceSchema(StructureDefinitionInformation structureDefinition, IEnume /// /// Gets the canonical of the profile(s) referred to in the Meta.profile property of the resource. /// - internal static Canonical[] GetMetaProfileSchemas(ITypedElement instance, ValidationContext vc) + internal static Canonical[] GetMetaProfileSchemas(ITypedElement instance, ValidationContext.MetaProfileSelector? selector) { var profiles = instance .Children("meta") @@ -49,10 +49,10 @@ internal static Canonical[] GetMetaProfileSchemas(ITypedElement instance, Valida .OfType() .Select(s => new Canonical(s)); - return callback(vc).Invoke(instance.Location, profiles.ToArray()); + return callback(selector).Invoke(instance.Location, profiles.ToArray()); - static ValidationContext.MetaProfileSelector callback(ValidationContext context) - => context.SelectMetaProfiles ?? ((_, m) => m); + static ValidationContext.MetaProfileSelector callback(ValidationContext.MetaProfileSelector? selector) + => selector ?? ((_, m) => m); } /// @@ -74,7 +74,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.UpdateLocation(d => d.InvokeSchema(this)), typeProfile); return fetchResult.Success ? fetchResult.Schema!.Validate(input, vc, state) : fetchResult.Error!; } @@ -85,7 +85,7 @@ public override ResultReport Validate(ITypedElement input, ValidationContext vc, // FHIR has a few occasions where the schema needs to read into the instance to obtain additional schemas to // validate against (Resource.meta.profile, Extension.url). Fetch these from the instance and combine them into // a coherent set to validate against. - var additionalCanonicals = GetMetaProfileSchemas(input, vc); + var additionalCanonicals = GetMetaProfileSchemas(input, vc.SelectMetaProfiles); if (additionalCanonicals.Any() && vc.ElementSchemaResolver is null) throw new ArgumentException($"Cannot validate profiles in meta.profile because {nameof(ValidationContext)} does not contain an ElementSchemaResolver."); diff --git a/src/Firely.Fhir.Validation/Impl/SchemaReferenceValidator.cs b/src/Firely.Fhir.Validation/Impl/SchemaReferenceValidator.cs index c55c8e95..7caa1c0f 100644 --- a/src/Firely.Fhir.Validation/Impl/SchemaReferenceValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/SchemaReferenceValidator.cs @@ -48,8 +48,8 @@ public ResultReport Validate(IEnumerable input, ValidationContext return FhirSchemaGroupAnalyzer.FetchSchema(vc.ElementSchemaResolver, state, SchemaUri) switch { - (var schema, null) => schema!.Validate(input, vc, state), - (_, var error) => error + (var schema, null, _) => schema!.Validate(input, vc, state), + (_, var error, _) => error }; } diff --git a/src/Firely.Fhir.Validation/Impl/SliceValidator.cs b/src/Firely.Fhir.Validation/Impl/SliceValidator.cs index b840e16d..e6f9956a 100644 --- a/src/Firely.Fhir.Validation/Impl/SliceValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/SliceValidator.cs @@ -133,6 +133,7 @@ public ResultReport Validate(IEnumerable input, ValidationContext var buckets = new Buckets(Slices, Default); var candidateNumber = -1; // instead of location - replace this with location later. + var sliceLocation = state.Location.InstanceLocation.ToString(); // Go over the elements in the instance, in order foreach (var candidate in input) @@ -153,7 +154,7 @@ public ResultReport Validate(IEnumerable input, ValidationContext // The instance matched a slice that we have already passed, if order matters, // this is not allowed if (sliceNumber < lastMatchingSlice && Ordered) - evidence.Add(new IssueAssertion(Issue.CONTENT_ELEMENT_SLICING_OUT_OF_ORDER, $"Element matches slice '{sliceName}', but this is out of order for this group, since a previous element already matched slice '{Slices[lastMatchingSlice].Name}'") + 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)); else lastMatchingSlice = sliceNumber; @@ -161,7 +162,7 @@ public ResultReport Validate(IEnumerable input, ValidationContext if (defaultInUse && DefaultAtEnd) { // 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 '{sliceName}', but it appears after a non-match, which is not allowed for an open-at-end group") + 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)); } diff --git a/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs b/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs index 116ee763..516030f1 100644 --- a/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs +++ b/src/Firely.Fhir.Validation/Schema/AssertionToOperationOutcomeExtensions.cs @@ -55,5 +55,39 @@ public static ResultReport RemoveDuplicateEvidence(this ResultReport report) var issues = report.Evidence.Distinct().ToList(); // Those assertions for which equivalence is relevant will have implemented IEqualityComparer return new ResultReport(report.Result, issues); } + + + /// + /// Cleans up the by adding the slice context to the error messages and removes duplicate evidence. + /// + /// + /// + public static ResultReport CleanUp(this ResultReport report) + { + return report.addSliceContextToErrorMessages() + .RemoveDuplicateEvidence(); + } + + private static ResultReport addSliceContextToErrorMessages(this ResultReport report) + { + if (report.Evidence.All(item => item is IssueAssertion)) + { + var issues = report.Evidence.OfType().ToList(); + + //for each issue, check if it has SliceInfo, and if so, add it to the message + foreach (var issue in issues) + { + if (issue.DefinitionPath?.TryGetSliceInfo(out var sliceInfo) == true) + { + issue.Message += $" (for slice {sliceInfo})"; + } + } + + return new ResultReport(report.Result, issues); + } + + return report; + } + } } diff --git a/src/Firely.Fhir.Validation/Schema/DefinitionPath.cs b/src/Firely.Fhir.Validation/Schema/DefinitionPath.cs index 8f95b7cb..51ebc2a0 100644 --- a/src/Firely.Fhir.Validation/Schema/DefinitionPath.cs +++ b/src/Firely.Fhir.Validation/Schema/DefinitionPath.cs @@ -52,6 +52,34 @@ static string combine(string left, PathStackEvent right) => right is InvokeProfileEvent ipe ? left + "->" + ipe.Render() : left + right.Render(); } + + /// + /// Returns whether the path contains slice information, and if so, returns the slice information in the sliceInfo out parameter. + /// + /// Slice information. + /// Whether the path contains slice information. + public bool TryGetSliceInfo(out string? sliceInfo) + { + var scan = Current; + sliceInfo = null; + + while (scan is not null) + { + if (scan is CheckSliceEvent cse) + { + sliceInfo = sliceInfo == null ? cse.SliceName : $"{cse.SliceName}, subslice {sliceInfo}"; + } + else if (sliceInfo != null) + { + return true; + } + + scan = scan.Previous; + } + + return false; + } + /// /// Start a new DefinitionPath. /// diff --git a/src/Firely.Fhir.Validation/Schema/ValidationContext.cs b/src/Firely.Fhir.Validation/Schema/ValidationContext.cs index 1c5467fb..84e41795 100644 --- a/src/Firely.Fhir.Validation/Schema/ValidationContext.cs +++ b/src/Firely.Fhir.Validation/Schema/ValidationContext.cs @@ -70,11 +70,17 @@ public ValidationContext(IElementSchemaResolver schemaResolver, ICodeValidationT /// public ICodeValidationTerminologyService ValidateCodeService; + /// + /// A function that maps a type name found in and TypeRefComponent.Code to a resolvable canonical. + /// If not set, it will prefix the type with the standard http://hl7.org/fhir/StructureDefinition prefix. + /// + public TypeNameMapper? TypeNameMapper { get; set; } + /// /// The to invoke when the validator calls out to a terminology service and this call /// results in an exception. When no function is set, the validator defaults to returning a warning. /// - public ValidateCodeServiceFailureHandler? OnValidateCodeServiceFailure = null; + public ValidateCodeServiceFailureHandler? HandleValidateCodeServiceFailure = null; /// The function has 2 input parameters: /// - valueSetUrl (of type Canonical): the valueSetUrl of the Binding @@ -86,7 +92,7 @@ public ValidationContext(IElementSchemaResolver schemaResolver, ICodeValidationT /// /// A delegate that determines the result of a failed terminology service call by the validator. /// - /// The object that was passed to the terminology service. + /// The object that was passed to the terminology service. /// The as returned by the service. public delegate TerminologyServiceExceptionResult ValidateCodeServiceFailureHandler(ValidateCodeParameters p, FhirOperationException e); @@ -99,6 +105,7 @@ public ValidationContext(IElementSchemaResolver schemaResolver, ICodeValidationT /// public IElementSchemaResolver ElementSchemaResolver; + /// /// A function that resolves an url to an external instance, parsed as an . /// @@ -108,7 +115,13 @@ public ValidationContext(IElementSchemaResolver schemaResolver, ICodeValidationT /// to contained resources will always be followed. If this property is not set, references will be /// ignored. /// - public Func>? ExternalReferenceResolver = null; + public ExternalReferenceResolver? ResolveExternalReference = null; + + + /// + /// A delegate that resolves a reference to another resource, outside of the current instance under validation. + /// + public delegate ITypedElement? ExternalReferenceResolver(string reference, string location); /// /// An instance of the FhirPath compiler to use when evaluating constraints @@ -162,7 +175,12 @@ public ValidationContext(IElementSchemaResolver schemaResolver, ICodeValidationT /// A function to exclude the assertion in the validation or not. If the function is left empty (null) then all the /// assertions are processed in the validation. /// - public Predicate? ExcludeFilter = a => (a is FhirPathValidator fhirPathAssertion && fhirPathAssertion.Key == "dom-6"); + public Predicate? ExcludeFilter = DEFAULT_EXCLUDE_FILTER; + + /// + /// The default for , which will exclude FhirPath invariant dom-6 from triggering. + /// + public static readonly Predicate DEFAULT_EXCLUDE_FILTER = a => a is FhirPathValidator fhirPathAssertion && fhirPathAssertion.Key == "dom-6"; /// /// Determines whether a given assertion is included in the validation. The outcome is determined by diff --git a/src/Firely.Fhir.Validation/Support/FhirSchemaGroupAnalyzer.cs b/src/Firely.Fhir.Validation/Support/FhirSchemaGroupAnalyzer.cs index a3d4b87d..a11eec1a 100644 --- a/src/Firely.Fhir.Validation/Support/FhirSchemaGroupAnalyzer.cs +++ b/src/Firely.Fhir.Validation/Support/FhirSchemaGroupAnalyzer.cs @@ -22,7 +22,7 @@ public static class FhirSchemaGroupAnalyzer /// /// The result of resolving a schema: either a schema, or a detailing the failure. /// - public record SchemaFetchResult(ElementSchema? Schema, ResultReport? Error) + public record SchemaFetchResult(ElementSchema? Schema, ResultReport? Error, Canonical Canonical) { /// /// Indicates whether fetching the schema by canonical has succeeded. @@ -47,18 +47,18 @@ public static SchemaFetchResult FetchSchema(IElementSchemaResolver resolver, Val var (coreSchema, version, anchor) = canonical; if (coreSchema is null) - return new(null, makeUnresolvableError($"Resolving to local anchors is unsupported: '{canonical}'.")); + return new(null, makeUnresolvableError($"Resolving to local anchors is unsupported: '{canonical}'."), canonical); // Resolve the uri - without the anchor part. if (resolver.GetSchema(new Canonical(coreSchema, version, null)) is not { } schema) - return new(null, makeUnresolvableError($"Unable to resolve reference to profile '{canonical}'.")); + return new(null, makeUnresolvableError($"Unable to resolve reference to profile '{canonical}'."), canonical); // If there is a subschema set, try to locate it. return anchor switch { - not null when schema.FindFirstByAnchor(anchor) is { } subschema => new(subschema, null), - not null => new(null, makeUnresolvableError($"Unable to locate anchor {anchor} within profile '{canonical}'.")), - _ => new(schema, null) + not null when schema.FindFirstByAnchor(anchor) is { } subschema => new(subschema, null, canonical), + not null => new(null, makeUnresolvableError($"Unable to locate anchor {anchor} within profile '{canonical}'."), canonical), + _ => new(schema, null, canonical) }; } diff --git a/src/Firely.Fhir.Validation/Support/TypeNameMapper.cs b/src/Firely.Fhir.Validation/Support/TypeNameMapper.cs new file mode 100644 index 00000000..24945141 --- /dev/null +++ b/src/Firely.Fhir.Validation/Support/TypeNameMapper.cs @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021, Firely (info@fire.ly) - All Rights Reserved + * Proprietary and confidential. Unauthorized copying of this file, + * via any medium is strictly prohibited. + */ + +using Hl7.Fhir.ElementModel; + +namespace Firely.Fhir.Validation +{ + /// + /// Function that maps a local type name (as found on e.g. or ElementDefinition.TypeRef.Code) to + /// to a resolvable url. + /// + /// May return null if the local name has no mapping to a resolvable canonical url. + public delegate Canonical? TypeNameMapper(string local); + + /// + /// Extension methods for use on top of . + /// + public static class TypeNameMapperExtensions + { + /// + /// Will invoke the , if set. If there is no mapper, or the mapper returns + /// null, is used instead. + /// + public static Canonical MapTypeName(this TypeNameMapper? mapper, string local) => + mapper switch + { + TypeNameMapper m => m(local) ?? Canonical.ForCoreType(local), + _ => Canonical.ForCoreType(local) + }; + } +} diff --git a/src/Hl7.Fhir.Validation.R4/Hl7.Fhir.Validation.R4.csproj b/src/Hl7.Fhir.Validation.R4/Hl7.Fhir.Validation.R4.csproj index 6a1aeeaa..61d791cc 100644 --- a/src/Hl7.Fhir.Validation.R4/Hl7.Fhir.Validation.R4.csproj +++ b/src/Hl7.Fhir.Validation.R4/Hl7.Fhir.Validation.R4.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/Hl7.Fhir.Validation.Shared/ValidationSettings.cs b/src/Hl7.Fhir.Validation.Shared/ValidationSettings.cs index 51d43b1f..f3daae48 100644 --- a/src/Hl7.Fhir.Validation.Shared/ValidationSettings.cs +++ b/src/Hl7.Fhir.Validation.Shared/ValidationSettings.cs @@ -29,7 +29,7 @@ public class ValidationSettings public StructureDefinitionSummaryProvider.TypeNameMapper? ResourceMapping { get; set; } /// - /// The resolver to use when references to other resources are encountered in the instance. + /// The resolver to use when canonicals or references to other resources are encountered in the instance. /// /// Most of the time this resolver is used when resolving canonicals to profiles or valuesets, /// but it may also be used to resolve references encountered in instance data. @@ -61,7 +61,7 @@ public class ValidationSettings /// (if the property is enabled). /// Never returns null. Assigning null reverts back to default settings. /// - public SnapshotGeneratorSettings GenerateSnapshotSettings + public SnapshotGeneratorSettings? GenerateSnapshotSettings { get => _generateSnapshotSettings; set => _generateSnapshotSettings = value?.Clone() ?? SnapshotGeneratorSettings.CreateDefault(); @@ -84,7 +84,7 @@ public SnapshotGeneratorSettings GenerateSnapshotSettings /// /// A list of constraints to be ignored by the validator. Default values are dom-6, rng-2, "bdl-8" and "cnl-0" /// - public string[] ConstraintsToIgnore { get; set; } = new string[] { "dom-6", "rng-2", "bdl-8", "cnl-0" }; + public string[]? ConstraintsToIgnore { get; set; } = new string[] { "dom-6", "rng-2", "bdl-8", "cnl-0" }; /// /// If a reference is encountered that references to a resource outside of the current instance being validated, @@ -135,7 +135,7 @@ public void CopyTo(ValidationSettings other) other.ConstraintBestPracticesSeverity = ConstraintBestPracticesSeverity; other.GenerateSnapshot = GenerateSnapshot; - other.GenerateSnapshotSettings = GenerateSnapshotSettings.Clone(); + other.GenerateSnapshotSettings = GenerateSnapshotSettings?.Clone(); other.EnableXsdValidation = EnableXsdValidation; other.ResolveExternalReferences = ResolveExternalReferences; other.ResourceResolver = ResourceResolver; diff --git a/test/Benchmarks/ValidatorBenchmarks.cs b/test/Benchmarks/ValidatorBenchmarks.cs index 791cc76f..a2fa473e 100644 --- a/test/Benchmarks/ValidatorBenchmarks.cs +++ b/test/Benchmarks/ValidatorBenchmarks.cs @@ -11,7 +11,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Threading.Tasks; //using Validator = Hl7.Fhir.Validation.Validator; namespace Firely.Sdk.Benchmarks @@ -19,7 +18,7 @@ namespace Firely.Sdk.Benchmarks [MemoryDiagnoser] public class ValidatorBenchmarks { - private static readonly IResourceResolver ZIPSOURCE = new CachedResolver(ZipSource.CreateValidationSource()); + private static readonly IResourceResolver ZIPSOURCE = new CachedResolver(new StructureDefinitionCorrectionsResolver(ZipSource.CreateValidationSource())); private static readonly IStructureDefinitionSummaryProvider PROVIDER = new StructureDefinitionSummaryProvider(ZIPSOURCE); private static readonly string TEST_DIRECTORY = Path.GetFullPath(@"TestData\DocumentComposition"); @@ -41,7 +40,7 @@ public void GlobalSetup() var testFilesResolver = new DirectorySource(TEST_DIRECTORY); TestResolver = new CachedResolver(new SnapshotSource(new CachedResolver(new MultiResolver(testFilesResolver, ZIPSOURCE))))!; - SchemaResolver = StructureDefinitionToElementSchemaResolver.CreatedCached(TestResolver.AsAsync())!; + SchemaResolver = StructureDefinitionToElementSchemaResolver.CreatedCached(TestResolver.AsAsync()!); TestSchema = SchemaResolver.GetSchema(InstanceTypeProfile); // To avoid warnings about bi-model distributions, run the (slow) first-time run here in setup @@ -69,7 +68,7 @@ private static ResultReport validateWip(ITypedElement typedElement, ElementSchem var constraintsToBeIgnored = new string[] { "rng-2", "dom-6" }; var validationContext = new ValidationContext(schemaResolver, new LocalTerminologyService(arr.AsAsync())) { - ExternalReferenceResolver = u => Task.FromResult(arr.ResolveByUri(u)?.ToTypedElement()), + ResolveExternalReference = (u, _) => arr.ResolveByUri(u)?.ToTypedElement(), // IncludeFilter = Settings.SkipConstraintValidation ? (Func)(a => !(a is FhirPathAssertion)) : (Func)null, // 20190703 Issue 447 - rng-2 is incorrect in DSTU2 and STU3. EK // should be removed from STU3/R4 once we get the new normative version diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/ProfiledQuestionnaire.json b/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/ProfiledQuestionnaire.json index 94e66236..3657e764 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/ProfiledQuestionnaire.json +++ b/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/ProfiledQuestionnaire.json @@ -48,7 +48,7 @@ }, "fhirPath-que-0": { "key": "que-0", - "expression": "name.matches('[A-Z]([A-Za-z0-9_]){0,254}')", + "expression": "name.exists() implies name.matches('[A-Z]([A-Za-z0-9_]){0,254}')", "severity": "warning", "bestPractice": false, "humanDescription": "Name should be usable as an identifier for the module by machine processing applications such as code generation" diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/Questionnaire.json b/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/Questionnaire.json index 3da46bdd..a6d8c583 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/Questionnaire.json +++ b/test/Firely.Fhir.Validation.Compilation.Tests.R4/SchemaSnaps/Questionnaire.json @@ -55,7 +55,7 @@ }, "fhirPath-que-0": { "key": "que-0", - "expression": "name.matches('[A-Z]([A-Za-z0-9_]){0,254}')", + "expression": "name.exists() implies name.matches('[A-Z]([A-Za-z0-9_]){0,254}')", "severity": "warning", "bestPractice": false, "humanDescription": "Name should be usable as an identifier for the module by machine processing applications such as code generation" diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/BasicSchemaBuilderTests.cs b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/BasicSchemaBuilderTests.cs index 657aa633..66bc66a6 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/BasicSchemaBuilderTests.cs +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/BasicSchemaBuilderTests.cs @@ -29,7 +29,7 @@ public class BasicSchemaBuilderTests : IClassFixture private readonly ITestOutputHelper _output; #pragma warning restore IDE0052 // I'd like to keep the output handy when I need it - private readonly string _schemaSnapDirectory = "SchemaSnaps"; + private readonly string _schemaSnapDirectory = "..\\..\\..\\SchemaSnaps"; public BasicSchemaBuilderTests(SchemaBuilderFixture fixture, ITestOutputHelper oh) => (_output, _fixture) = (oh, fixture); @@ -65,7 +65,7 @@ private void compareToSchemaSnaps(bool overwrite) var actualJson = generated!.ToJson().ToString(); if (overwrite) { - File.WriteAllText(@"..\..\..\" + file, actualJson); + File.WriteAllText(file, actualJson); continue; } @@ -222,11 +222,11 @@ public void UseSelfDefinedSchemaBuilderTest() assertions.Should().ContainSingle(a => a is SelfDefinedValidator); } - private IEnumerable flattenSchema(ElementSchema schema) + private static IEnumerable flattenSchema(ElementSchema schema) { return flattenMembers(schema.Members); - IEnumerable flattenMembers(IEnumerable assertions) => + static IEnumerable flattenMembers(IEnumerable assertions) => !assertions.Any() ? Enumerable.Empty() : assertions diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTestCases b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTestCases index b41082c2..344f1bdc 160000 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTestCases +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTestCases @@ -1 +1 @@ -Subproject commit b41082c2f7b6f1df62d7eb7c2d402dfb418c5d97 +Subproject commit 344f1bdc7d1c43220a7e68c028ac733172033910 diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/TestCaseRunner.cs b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/TestCaseRunner.cs index 2f174a39..b9d5a6f0 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/TestCaseRunner.cs +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/TestCaseRunner.cs @@ -22,7 +22,8 @@ namespace Firely.Fhir.Validation.Compilation.Tests public enum AssertionOptions { NoAssertion = 1 << 1, - OutputTextAssertion = 1 << 2 + OutputTextAssertion = 1 << 2, + OnlyErrorCount = 2 << 3 } internal class TestCaseRunner diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/WipValidator.cs b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/WipValidator.cs index 50847425..af4e2ba6 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/WipValidator.cs +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/FhirTests/WipValidator.cs @@ -3,6 +3,7 @@ using Hl7.Fhir.Specification.Source; using Hl7.Fhir.Specification.Terminology; using Hl7.Fhir.Support; +using Hl7.Fhir.Utility; using System; using System.Collections.Generic; using System.Diagnostics; @@ -55,7 +56,9 @@ public OperationOutcome Validate(ITypedElement instance, IResourceResolver? reso result.Add(validate(instance, profileUri)); } - outcome.Add(ResultReport.FromEvidence(result).ToOperationOutcome()); + outcome.Add(ResultReport.FromEvidence(result) + .CleanUp() + .ToOperationOutcome()); return outcome; ResultReport validate(ITypedElement typedElement, string canonicalProfile) @@ -67,7 +70,7 @@ ResultReport validate(ITypedElement typedElement, string canonicalProfile) var constraintsToBeIgnored = new string[] { "rng-2", "dom-6" }; var validationContext = new ValidationContext(schemaResolver, new LocalTerminologyService(asyncResolver)) { - ExternalReferenceResolver = async u => (await asyncResolver.ResolveByUriAsync(u))?.ToTypedElement(), + ResolveExternalReference = (u, _) => TaskHelper.Await(() => asyncResolver.ResolveByUriAsync(u))?.ToTypedElement(), // IncludeFilter = Settings.SkipConstraintValidation ? (Func)(a => !(a is FhirPathAssertion)) : (Func)null, // 20190703 Issue 447 - rng-2 is incorrect in DSTU2 and STU3. EK // should be removed from STU3/R4 once we get the new normative version @@ -79,7 +82,7 @@ ResultReport validate(ITypedElement typedElement, string canonicalProfile) _stopWatch.Start(); var result = schema!.Validate(typedElement, validationContext); _stopWatch.Stop(); - return result.RemoveDuplicateEvidence(); + return result; } catch (Exception ex) { diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs index 748aac27..b460e3dc 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs @@ -11,9 +11,7 @@ using Hl7.Fhir.Support; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Xunit; -using Task = System.Threading.Tasks.Task; namespace Firely.Fhir.Validation.Tests { @@ -24,13 +22,11 @@ public class ResourceSchemaValidationTests : IClassFixture public ResourceSchemaValidationTests(SchemaBuilderFixture fixture) => _fixture = fixture; - private Task resolveTestData(string uri) + private ITypedElement? resolveTestData(string uri, string location) { string Url = "http://test.org/fhir/Organization/3141"; - Organization dummy = new() { Id = "3141", Name = "Dummy" }; - - return Task.FromResult(uri == Url ? dummy.ToTypedElement() : null); + return uri == Url ? dummy.ToTypedElement() : null; } @@ -88,7 +84,7 @@ public void AvoidsRedoingProfileValidation() var schemaElement = _fixture.SchemaResolver.GetSchema("http://hl7.org/fhir/StructureDefinition/Bundle"); var vc = _fixture.NewValidationContext(); - vc.ExternalReferenceResolver = resolveTestData; + vc.ResolveExternalReference = resolveTestData; var validationState = new ValidationState(); var result = schemaElement!.Validate(new ScopedNode(all.ToTypedElement()), vc, validationState); diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/TestProfileArtifactSource.cs b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/TestProfileArtifactSource.cs index b4799e54..3eb2237f 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/TestProfileArtifactSource.cs +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/TestProfileArtifactSource.cs @@ -38,6 +38,8 @@ internal class TestProfileArtifactSource : IResourceResolver public const string PROFILEDBOOL = "http://validationtest.org/fhir/StructureDefinition/booleanProfile"; public const string PROFILEDSTRING = "http://validationtest.org/fhir/StructureDefinition/stringProfile"; public const string PATIENTWITHPROFILEDREFS = "http://validationtest.org/fhir/StructureDefinition/PatientWithReferences"; + public const string BUNDLEWITHCONSTRAINEDCONTAINED = "http://validationtest.org/fhir/StructureDefinition/BundleWithConstrainedContained"; + public List TestProfiles = new() { @@ -63,7 +65,8 @@ internal class TestProfileArtifactSource : IResourceResolver buildFlagWithProfiledReferences(), createTestSD(PROFILEDSTRING, "NoopStringProfile", "A noop profile for a string", FHIRAllTypes.String), createTestSD(PROFILEDBOOL, "NoopBoolProfile", "A noop profile for a bool", FHIRAllTypes.Boolean), - buildPatientWithProfiledReferences() + buildPatientWithProfiledReferences(), + bundleWithConstrainedContained() }; private static StructureDefinition buildFlagWithProfiledReferences() @@ -501,6 +504,20 @@ private static StructureDefinition buildObservationWithTargetProfilesAndChildDef return result; } + private static StructureDefinition bundleWithConstrainedContained() + { + var result = createTestSD(BUNDLEWITHCONSTRAINEDCONTAINED, + $"Bundle with a constraint on the Bundle.entry.resource", + $"Bundle with a constraint on the Bundle.entry.resource", FHIRAllTypes.Bundle); + + var cons = result.Differential.Element; + + cons.Add(new ElementDefinition("Bundle").OfType(FHIRAllTypes.Bundle)); + cons.Add(new ElementDefinition("Bundle.entry.resource.meta").Required()); + + return result; + } + private static StructureDefinition createTestSD(string url, string name, string description, FHIRAllTypes constrainedType, string? baseUri = null) { var result = new StructureDefinition diff --git a/test/Firely.Fhir.Validation.Tests/Impl/BindingValidatorTests.cs b/test/Firely.Fhir.Validation.Tests/Impl/BindingValidatorTests.cs index 7fc4438e..eb2637a9 100644 --- a/test/Firely.Fhir.Validation.Tests/Impl/BindingValidatorTests.cs +++ b/test/Firely.Fhir.Validation.Tests/Impl/BindingValidatorTests.cs @@ -240,7 +240,7 @@ public void ValidateCodeWithUnreachableTerminologyServerAndUserIntervention() { setup(new FhirOperationException("Dummy", System.Net.HttpStatusCode.NotFound)); var validationContext = ValidationContext.BuildMinimalContext(_validateCodeService.Object); - validationContext.OnValidateCodeServiceFailure = userIntervention; + validationContext.HandleValidateCodeServiceFailure = userIntervention; var input = createCoding("http://terminology.hl7.org/CodeSystem/data-absent-reason", "UNKNOWN"); var result = _bindingAssertion.Validate(input, validationContext); @@ -263,7 +263,7 @@ public void ValidateConceptWithUnreachableTerminologyServerAndUserIntervention() { setup(new FhirOperationException("Dummy message", System.Net.HttpStatusCode.NotFound)); var validationContext = ValidationContext.BuildMinimalContext(_validateCodeService.Object); - validationContext.OnValidateCodeServiceFailure = userIntervention; + validationContext.HandleValidateCodeServiceFailure = userIntervention; var codings = new[] { createCoding("http://terminology.hl7.org/CodeSystem/data-absent-reason", "masked") , diff --git a/test/Firely.Fhir.Validation.Tests/Impl/CircularReferenceTests.cs b/test/Firely.Fhir.Validation.Tests/Impl/CircularReferenceTests.cs index 6a267473..cd39a1c7 100644 --- a/test/Firely.Fhir.Validation.Tests/Impl/CircularReferenceTests.cs +++ b/test/Firely.Fhir.Validation.Tests/Impl/CircularReferenceTests.cs @@ -8,7 +8,6 @@ using Hl7.Fhir.ElementModel; using Hl7.Fhir.Support; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Threading.Tasks; namespace Firely.Fhir.Validation.Tests { @@ -46,15 +45,15 @@ public void CircularInReferencedResources() var resolver = new TestResolver() { SCHEMA }; var vc = ValidationContext.BuildMinimalContext(schemaResolver: resolver); - Task resolveExample(string example) => - Task.FromResult(example switch - { - "http://example.com/pat1" => pat1, - "http://example.com/pat2" => pat2, - _ => null - }); + ITypedElement? resolveExample(string example, string location) => + example switch + { + "http://example.com/pat1" => pat1, + "http://example.com/pat2" => pat2, + _ => null + }; - vc.ExternalReferenceResolver = resolveExample; + vc.ResolveExternalReference = resolveExample; var result = SCHEMA.Validate(pat1, vc); result.IsSuccessful.Should().BeTrue(); // this is a warning result.Evidence.Should().ContainSingle().Which.Should().BeOfType() diff --git a/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs b/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs index ccfbb57c..de4b1d70 100644 --- a/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs +++ b/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs @@ -8,7 +8,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace Firely.Fhir.Validation.Tests { @@ -84,12 +83,13 @@ public static object createInstance(string reference) => [DataTestMethod] public void ValidateInstance(object instance, ReferencedInstanceValidator testee, bool success, string fragment) { - static Task resolve(string url) => - Task.FromResult(url.StartsWith("http://example.com/hit") ? - (new { t = "irrelevant" }).ToTypedElement() : default); + + static ITypedElement? resolve(string url, string _) => + url.StartsWith("http://example.com/hit") ? + (new { t = "irrelevant" }).ToTypedElement() : default; var vc = ValidationContext.BuildMinimalContext(schemaResolver: new TestResolver() { SCHEMA }); - vc.ExternalReferenceResolver = resolve; + vc.ResolveExternalReference = resolve; var result = test(instance, testee, vc); diff --git a/test/Firely.Fhir.Validation.Tests/Impl/ResourceSchemaTests.cs b/test/Firely.Fhir.Validation.Tests/Impl/ResourceSchemaTests.cs index b93543e8..89cd697f 100644 --- a/test/Firely.Fhir.Validation.Tests/Impl/ResourceSchemaTests.cs +++ b/test/Firely.Fhir.Validation.Tests/Impl/ResourceSchemaTests.cs @@ -11,9 +11,6 @@ public class ResourceSchemaTests [TestMethod] public void FollowMetaProfileTest() { - var context = ValidationContext.BuildMinimalContext(); - context.SelectMetaProfiles = callback; - var instance = new { resourceType = "Patient", @@ -24,16 +21,14 @@ public void FollowMetaProfileTest() } }.ToTypedElement(); - var result = ResourceSchema.GetMetaProfileSchemas(instance, context); + var result = ResourceSchema.GetMetaProfileSchemas(instance, callback); result.Should().BeEquivalentTo(new Canonical[] { "userprofile2", "profile3", "profile4", "userprofile5" }); - context.SelectMetaProfiles = declineAll; - result = ResourceSchema.GetMetaProfileSchemas(instance, context); + result = ResourceSchema.GetMetaProfileSchemas(instance, declineAll); result.Should().BeEmpty(); - // remove the callback: - context.SelectMetaProfiles = null; - result = ResourceSchema.GetMetaProfileSchemas(instance, context); + // without a callback + result = ResourceSchema.GetMetaProfileSchemas(instance, null); result.Should().BeEquivalentTo(new Canonical[] { "profile1", "profile2", "profile3", "profile4" }); static Canonical[] callback(string location, Canonical[] orignalMetaProfiles) diff --git a/test/Firely.Fhir.Validation.Tests/Impl/TestDefinitionPath.cs b/test/Firely.Fhir.Validation.Tests/Impl/TestDefinitionPath.cs index b8319215..43773a06 100644 --- a/test/Firely.Fhir.Validation.Tests/Impl/TestDefinitionPath.cs +++ b/test/Firely.Fhir.Validation.Tests/Impl/TestDefinitionPath.cs @@ -85,5 +85,34 @@ public void NavigateAcrossProfile() testee.ToString().Should().Be("Patient.name->HumanName(http://test.org/humanname-profile).family"); } + + [TestMethod] + public void TestGetSliceInfo() + { + var fhirTypeSchema = new ResourceSchema(new StructureDefinitionInformation("http://test.org/resource", null, "Patient", StructureDefinitionInformation.TypeDerivationRule.Specialization, false)); + + var testee = DefinitionPath.Start().InvokeSchema(fhirTypeSchema); + testee = testee.ToChild("name"); + testee.TryGetSliceInfo(out _).Should().Be(false); + + testee = testee.CheckSlice("vv"); + + testee.ToString().Should().Be("Patient.name[vv]"); + string? sliceInfo; + testee.TryGetSliceInfo(out sliceInfo).Should().Be(true); + sliceInfo.Should().Be("vv"); + + testee = testee.CheckSlice("xx"); + + testee.ToString().Should().Be("Patient.name[vv][xx]"); + testee.TryGetSliceInfo(out sliceInfo).Should().Be(true); + sliceInfo.Should().Be("vv, subslice xx"); + + testee = testee.ToChild("family"); + testee.ToString().Should().Be("Patient.name[vv][xx].family"); + testee.TryGetSliceInfo(out sliceInfo).Should().Be(true); + sliceInfo.Should().Be("vv, subslice xx"); + + } } }