diff --git a/src/Firely.Fhir.Validation.Compilation.Shared/EnterpriseSchemaBuilders/StructureDefinitionBuilder.cs b/src/Firely.Fhir.Validation.Compilation.Shared/EnterpriseSchemaBuilders/StructureDefinitionBuilder.cs new file mode 100644 index 00000000..69db32c2 --- /dev/null +++ b/src/Firely.Fhir.Validation.Compilation.Shared/EnterpriseSchemaBuilders/StructureDefinitionBuilder.cs @@ -0,0 +1,20 @@ +using Hl7.Fhir.Specification.Navigation; +using System.Collections.Generic; + +namespace Firely.Fhir.Validation.Compilation +{ + /// + /// The schema builder for the . + /// + public class StructureDefinitionBuilder : ISchemaBuilder + { + /// + public IEnumerable Build(ElementDefinitionNavigator nav, ElementConversionMode? conversionMode = ElementConversionMode.Full) + { + if (nav.Current.Path == "StructureDefinition") + { + yield return new StructureDefinitionValidator(); + }; + } + } +} diff --git a/src/Firely.Fhir.Validation.Compilation.Shared/Firely.Fhir.Validation.Compilation.Shared.projitems b/src/Firely.Fhir.Validation.Compilation.Shared/Firely.Fhir.Validation.Compilation.Shared.projitems index e9fea457..87d6dd7d 100644 --- a/src/Firely.Fhir.Validation.Compilation.Shared/Firely.Fhir.Validation.Compilation.Shared.projitems +++ b/src/Firely.Fhir.Validation.Compilation.Shared/Firely.Fhir.Validation.Compilation.Shared.projitems @@ -11,6 +11,7 @@ + diff --git a/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilders/StandardBuilders.cs b/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilders/StandardBuilders.cs index c23ae57d..63f68d43 100644 --- a/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilders/StandardBuilders.cs +++ b/src/Firely.Fhir.Validation.Compilation.Shared/SchemaBuilders/StandardBuilders.cs @@ -33,7 +33,8 @@ public StandardBuilders(IAsyncResourceResolver source) new ContentReferenceBuilder(), new TypeReferenceBuilder(source), new CanonicalBuilder(), - new ElementDefinitionBuilder() + new ElementDefinitionBuilder(), + new StructureDefinitionBuilder() }; } diff --git a/src/Firely.Fhir.Validation/EnterpriseValidators/StructureDefinitionValidator.cs b/src/Firely.Fhir.Validation/EnterpriseValidators/StructureDefinitionValidator.cs new file mode 100644 index 00000000..6c793624 --- /dev/null +++ b/src/Firely.Fhir.Validation/EnterpriseValidators/StructureDefinitionValidator.cs @@ -0,0 +1,68 @@ +using Hl7.Fhir.ElementModel; +using Hl7.Fhir.Support; +using Hl7.FhirPath.Sprache; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Firely.Fhir.Validation +{ + /// + /// An that represents a FHIR StructureDefinition + /// + [DataContract] + public class StructureDefinitionValidator : IValidatable + { + /// + public JToken ToJson() => new JProperty("elementDefinition", new JObject()); + + /// + public ResultReport Validate(ITypedElement input, ValidationContext vc, ValidationState state) + { + //this can be expanded with other validate functionality + var evidence = validateInvariantUniqueness(input, state); + + return ResultReport.FromEvidence(evidence); + } + + /// + /// Validates if the invariants defined in the snapshot and differentials have a unique key. + /// Duplicate keys can exist when the element paths are also the same (e.g. in slices). + /// + /// + /// + /// + private static List validateInvariantUniqueness(ITypedElement input, ValidationState state) + { + var snapshotElements = input.Children("snapshot").SelectMany(c => c.Children("element")); + var diffElements = input.Children("differential").SelectMany(c => c.Children("element")); + + var snapshotEvidence = validateInvariantUniqueness(snapshotElements); + var diffEvidence = validateInvariantUniqueness(diffElements); + + return snapshotEvidence.Concat(diffEvidence).Select(i => i.AsResult(input, state)).ToList(); + } + + private static List validateInvariantUniqueness(IEnumerable elements) + { + //Selects the combination of key and elementDefintion path for the duplicate keys where the paths are not also the same. + + IEnumerable<(string Key, string Path)> PathsPerInvariantKey = elements + .SelectMany(e => e.Children("constraint") + .Select(c => (Key: c.Children("key") + .Single().Value.ToString(), + Path: e.Children("path") + .Single().Value.ToString()))); + + IEnumerable<(string Key, IEnumerable Paths)> PathsPerDuplicateInvariantKey = PathsPerInvariantKey.GroupBy(pair => pair.Key) + .Select(group => (Key: group.Key, Paths: group.Select(pair => pair.Path) // select all paths, per invariant key + .Distinct())) //Distinct to remove paths that are encountered multiple times per invariant + .Where(kv => kv.Paths.Count() > 1); //Remove entries that only have a single path. These are not incorrect. + + return PathsPerDuplicateInvariantKey.Select(c => new IssueAssertion(Issue.PROFILE_ELEMENTDEF_INCORRECT, $"Duplicate key '{c.Key}' in paths: {string.Join(", ", c.Paths)}")).ToList(); + } + } +} + +#nullable restore diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/Firely.Fhir.Validation.Compilation.Tests.Shared.projitems b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/Firely.Fhir.Validation.Compilation.Tests.Shared.projitems index 24f3077d..e472bdf7 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/Firely.Fhir.Validation.Compilation.Tests.Shared.projitems +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/Firely.Fhir.Validation.Compilation.Tests.Shared.projitems @@ -1804,6 +1804,7 @@ + diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/StructureDefinitionSchemaValidationTests.cs b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/StructureDefinitionSchemaValidationTests.cs new file mode 100644 index 00000000..42aefb31 --- /dev/null +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/StructureDefinitionSchemaValidationTests.cs @@ -0,0 +1,105 @@ +using Firely.Fhir.Validation.Compilation.Tests; +using FluentAssertions; +using Hl7.Fhir.ElementModel; +using Hl7.Fhir.Model; +using System.Linq; +using Xunit; + +namespace Firely.Fhir.Validation.Tests.Impl +{ + public class StructureDefinitionSchemaTests : IClassFixture + { + internal SchemaBuilderFixture _fixture; + + public StructureDefinitionSchemaTests(SchemaBuilderFixture fixture) => _fixture = fixture; + + + [Fact] + public void ValidateElementDefinitioninProfileValueType() + { + var structureDefSchema = _fixture.SchemaResolver.GetSchema("http://hl7.org/fhir/StructureDefinition/StructureDefinition"); + + var elementDef1 = new ElementDefinition + { + Path = "Patient.gender", + Constraint = new() + { + new() + { + Key = "key1", + Expression = "foo.bar" + }, + new() + { + Key = "key2", + Expression = "bar.foo" + } + } + }; + var elementDef2 = new ElementDefinition + { + Path = "Patient.gender", + Constraint = new() + { + new() + { + Key = "key1", + Expression = "foo.bar" + }, + } + }; + var elementDef3 = new ElementDefinition + { + Path = "Patient.birthdate", + Constraint = new() + { + new() + { + Key = "key2", + Expression = "bar.foo" + } + } + }; + var elementDef4 = new ElementDefinition + { + Path = "Patient.birthdate", + Constraint = new() + { + new() + { + Key = "key4", + Expression = "bar.foo" + } + } + }; + + var profile = new StructureDefinition + { + Snapshot = new() + { + Element = new() { + + elementDef1, + elementDef2, + elementDef3, + elementDef4 + } + }, + Differential = new() + { + Element = new() { + + elementDef1, + elementDef2, + elementDef3, + elementDef4 + } + } + }; + + var results = structureDefSchema!.Validate(profile.ToTypedElement(), _fixture.NewValidationContext()); + + results.Warnings.Select(w => w.Message).Should().Contain($"Duplicate key 'key2' in paths: Patient.gender, Patient.birthdate"); + } + } +}