diff --git a/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs b/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs index bf508b3588..593fed0cda 100644 --- a/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs +++ b/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs @@ -156,7 +156,7 @@ void merge(ElementDefinition snap, ElementDefinition diff, bool mergeElementId, // Constraints are cumulative, so they are always "new" (hence a constant false for the comparer) // [WMR 20160917] Note: constraint keys must be unique. The validator will detect duplicate keys, so the derived // profile author can correct the conflicting constraint key. - // [WMR 20160918] MUST merge indentical constraints, otherwise each derived profile accumulates + // [WMR 20160918] MUST merge identical constraints, otherwise each derived profile accumulates // additional identical constraints inherited from e.g. BackboneElement. // snap.Constraint = mergeCollection(snap.Constraint, diff.Constraint, (a, b) => false); // [WMR 20190723] R4 NEW: Initialize Constraint.source property @@ -283,7 +283,7 @@ ElementDefinition.TypeRefComponent mergeElementType(ElementDefinition.TypeRefCom // TODO: Move logic to MergeTo method on partial class TypeRefComponent // TODO: Copy diff annotations...? - if (diff.ElementId != null) { result.ElementId = diff.ElementId; } + result.ElementId = mergeString(snap.ElementId, diff.ElementId); result.Extension = mergeExtensions(snap.Extension, diff.Extension); result.CodeElement = mergePrimitiveElement(snap.CodeElement, diff.CodeElement); @@ -366,6 +366,7 @@ private ElementDefinition.ElementDefinitionBindingComponent mergeBinding(Element snap.StrengthElement = mergePrimitiveElement(snap.StrengthElement, diff.StrengthElement); snap.DescriptionElement = mergePrimitiveElement(snap.DescriptionElement, diff.DescriptionElement); snap.ValueSetElement = mergeComplexAttribute(snap.ValueSetElement, diff.ValueSetElement); + snap.ElementId = mergeString(snap.ElementId, diff.ElementId); snap.Extension = mergeExtensions(snap.Extension, diff.Extension); snap.Additional = mergeCollection(snap.Additional, diff.Additional, matchExactly); onConstraint(result); @@ -670,7 +671,8 @@ T mergePrimitiveElement(T snap, T diff, bool allowAppend = false) where T : P { result.ObjectValue = diffValue; } - // Also merge extensions on primitives + // Also merge element id and extensions on primitives + result.ElementId = mergeString(snap.ElementId, diff.ElementId); result.Extension = mergeExtensions(snap.Extension, diff.Extension); onConstraint(result); } @@ -692,7 +694,7 @@ static string mergeId(ElementDefinition snap, ElementDefinition diff, bool merge } // Newly introduced named slices NEVER inherit element id // Must always regenerate new unique identifier for named slices - else if (!IsEqualName(diff.SliceName, snap.SliceName)) + if (!IsEqualName(diff.SliceName, snap.SliceName)) { // Regenerate; don't inherit from snap return null; @@ -700,13 +702,13 @@ static string mergeId(ElementDefinition snap, ElementDefinition diff, bool merge // Otherwise inherit existing element id from snap return snap.ElementId; } - else - { - // Don't merge elementId, e.g. for type profiles - return null; - } + + // Don't merge elementId, e.g. for type profiles + return null; } + static string mergeString(string snap, string diff) => diff ?? snap; + // Functions to match snap collection items to diff collection items // Matching key depends on collection type diff --git a/src/Hl7.Fhir.STU3/Specification/Snapshot/ElementDefnMerger.cs b/src/Hl7.Fhir.STU3/Specification/Snapshot/ElementDefnMerger.cs index c115c9bb27..bcd81ac848 100644 --- a/src/Hl7.Fhir.STU3/Specification/Snapshot/ElementDefnMerger.cs +++ b/src/Hl7.Fhir.STU3/Specification/Snapshot/ElementDefnMerger.cs @@ -246,8 +246,9 @@ T mergePrimitiveAttribute(T snap, T diff, bool allowAppend = false) where T : result.ObjectValue = diffText; } - // Also merge extensions on primitives + // Also merge element id and extensions on primitives // [Backported from R4] + result.ElementId = mergeString(snap?.ElementId, diff.ElementId); result.Extension = mergeExtensions(snap?.Extension, diff.Extension); onConstraint(result); return result; @@ -509,6 +510,7 @@ private ElementDefinition.ElementDefinitionBindingComponent mergeBinding(Element snap.StrengthElement = mergePrimitiveAttribute(snap.StrengthElement, diff.StrengthElement); snap.DescriptionElement = mergePrimitiveAttribute(snap.DescriptionElement, diff.DescriptionElement); snap.ValueSet = mergeComplexAttribute(snap.ValueSet, diff.ValueSet); + snap.ElementId = mergeString(snap.ElementId, diff.ElementId); snap.Extension = mergeExtensions(snap.Extension, diff.Extension); onConstraint(result); } @@ -535,7 +537,7 @@ string mergeId(ElementDefinition snap, ElementDefinition diff, bool mergeElement } // Newly introduced named slices NEVER inherit element id // Must always regenerate new unique identifier for named slices - else if (diff.SliceName != snap.SliceName) + if (diff.SliceName != snap.SliceName) { // Regenerate; don't inherit from snap return null; @@ -543,13 +545,13 @@ string mergeId(ElementDefinition snap, ElementDefinition diff, bool mergeElement // Otherwise inherit existing element id from snap return snap.ElementId; } - else - { - // Don't merge elementId, e.g. for type profiles - return null; - } + + // Don't merge elementId, e.g. for type profiles + return null; } + static string mergeString(string snap, string diff) => diff ?? snap; + // [WMR 20180611] NEW static bool isEqualCoding(Coding c, Coding d) { diff --git a/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/ElementDefinitionPropertyProxy.cs b/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/ElementDefinitionPropertyProxy.cs index 2ade34391d..e8b3933e7f 100644 --- a/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/ElementDefinitionPropertyProxy.cs +++ b/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/ElementDefinitionPropertyProxy.cs @@ -12,14 +12,18 @@ namespace Hl7.Fhir.Specification.Tests /// The property can also be a specific item from a property list. /// The property name is specified in the constructor. /// - /// The name is either the name of the property (e.g. "Binding") or the name of the property list - /// with a item selector (e.g. "Constraint[Key:dom-2]"). + /// The name can be + /// - the name of the property (e.g. "Binding") + /// - the name of the property list with an index (e.g. "Type[0]"). + /// - the name of the property list with an item selector (e.g. "Constraint[Key:dom-2]"). /// internal class ElementDefinitionPropertyProxy { + private bool _isList; private string _propertyName; - private string _selectorPropertyName; - private string _selectorValue; + private string _listSelectorPropertyName; + private string _listSelectorValue; + private int? _listIndex; private PropertyInfo _propertyInfo; private PropertyInfo _selectorPropertyInfo; @@ -38,7 +42,7 @@ public object CreateInstance(Element element) { var instance = Activator.CreateInstance(_propertyInfo.PropertyType); - if (_selectorPropertyInfo == null) + if (!_isList) return instance; // Primitive property // Create item for property list @@ -47,8 +51,8 @@ public object CreateInstance(Element element) // Initialize with specified element element?.CopyTo(item); - // Set key value - _selectorPropertyInfo.SetValue(item, _selectorValue); + if (_selectorPropertyInfo != null) + _selectorPropertyInfo.SetValue(item, _listSelectorValue); // Set key value // Add item to property list if (instance is IList items) @@ -74,18 +78,29 @@ public void SetValue(object instance, object value) /// The property value as an Element type. public Element GetValueAsElement(object instance) { - if (_selectorPropertyInfo == null) + if (!_isList) return (Element)_propertyInfo.GetValue(instance); if (_propertyInfo.GetValue(instance, null) is not IList items) return null; - foreach (var item in items) + if (_selectorPropertyInfo != null) { - var value = _selectorPropertyInfo.GetValue(item)?.ToString(); + foreach (var item in items) + { + var value = _selectorPropertyInfo.GetValue(item)?.ToString(); - if (_selectorValue.Equals(value)) - return (Element)item; + if (_listSelectorValue.Equals(value)) + return (Element)item; + } + + return null; + } + + if (_listIndex.HasValue) + { + _listIndex.Value.Should().BeInRange(0, items.Count - 1); + return (Element)items[_listIndex.Value]; } return null; @@ -98,8 +113,9 @@ private void getPropertyInfo(string propertyName) _propertyInfo = typeof(ElementDefinition).GetProperty(_propertyName); _propertyInfo.Should().NotBeNull(); // Check property exists - // ReSharper disable once PossibleNullReferenceException - if (!isList(_propertyInfo.PropertyType)) + _isList = isList(_propertyInfo.PropertyType); + + if (!_isList) { _propertyInfo.PropertyType.IsAssignableTo(typeof(Element)).Should().BeTrue(); // Property type should be derived from Element return; @@ -107,7 +123,10 @@ private void getPropertyInfo(string propertyName) _propertyInfo.PropertyType.GenericTypeArguments[0].IsAssignableTo(typeof(Element)).Should().BeTrue(); // Property type should be derived from Element - _selectorPropertyInfo = _propertyInfo.PropertyType.GenericTypeArguments[0].GetProperty(_selectorPropertyName); + if (_listSelectorPropertyName == null) + return; + + _selectorPropertyInfo = _propertyInfo.PropertyType.GenericTypeArguments[0].GetProperty(_listSelectorPropertyName); _selectorPropertyInfo.Should().NotBeNull(); // Check property exists } @@ -115,17 +134,39 @@ private void parsePropertyName(string propertyName) { _propertyName = propertyName; - var regex = new Regex("(.*)\\[(.*):(.*)\\]|(.*)"); + if (!tryParseListKey(propertyName)) + tryParseListIndex(propertyName); + } + + private static bool isList(Type type) => typeof(IList).IsAssignableFrom(type); + + private bool tryParseListKey(string propertyName) + { + var regex = new Regex("(.+)\\[(.+):(.+)\\]"); var result = regex.Match(propertyName); - if (!result.Success || result.Groups[4].Success) - return; + if (!result.Success) + return false; _propertyName = result.Groups[1].Value; - _selectorPropertyName = result.Groups[2].Value; - _selectorValue = result.Groups[3].Value; + _listSelectorPropertyName = result.Groups[2].Value; + _listSelectorValue = result.Groups[3].Value; + + return true; } - private static bool isList(Type type) => typeof(IList).IsAssignableFrom(type); + private bool tryParseListIndex(string propertyName) + { + var regex = new Regex("(.+)\\[(\\d+)\\]"); + var result = regex.Match(propertyName); + + if (!result.Success) + return false; + + _propertyName = result.Groups[1].Value; + _listIndex = int.Parse(result.Groups[2].Value); + + return true; + } } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/SnapshotGeneratorTest.cs b/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/SnapshotGeneratorTest.cs index 04c280575f..35698065b0 100644 --- a/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/SnapshotGeneratorTest.cs +++ b/src/Hl7.Fhir.Specification.STU3.Tests/Snapshot/SnapshotGeneratorTest.cs @@ -8216,23 +8216,18 @@ public static IEnumerable ElementDefinitionPropertyExtensionTestCasesS { // Modify an existing Binding extension yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("Test")) }}; + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("Test")) }}; // Adding a new Binding extension yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", new FhirBoolean(true)) }}; + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", new FhirBoolean(true)) }}; // Adding a new Constraint extension yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance", "Constraint[Key:dom-2]", Array.Empty(), - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(true)) }}; + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(true)) }}; } } @@ -8346,6 +8341,110 @@ private void logExtensions(string title, IEnumerable extensions, int } } + private const string BaseId = "testBaseId"; + private const string DiffId = "testDiffId"; + + public static IEnumerable ElementDefinitionPropertyElementIdTestCasesStu3 + { + get + { + // Binding + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", null, null, null }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", null, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", BaseId, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", BaseId, null, BaseId }; + + // Primitive element + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", null, null, null }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", null, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", BaseId, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", BaseId, null, BaseId }; + + // Type + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", null, null, null }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", null, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", BaseId, DiffId, DiffId }; + + // To correct the next test case requires backporting of ElementDefnMerger.mergeElementTypes to STU3 + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", BaseId, null, null }; // WRONG! Expected should be BaseId + } + } + + /// + /// Tests whether element definition property element id in the differential is properly merged by the snapshot generator. + /// + /// The profile type under test (e.g. FHIRAllTypes.AllergyIntolerance). + /// The element id of the profile to check (e.g. "AllergyIntolerance.code") + /// The name of the element definition property for which to add or modify the extension in the differential (e.g. "Binding"). + /// The element id that is defined in the base profile for this property. + /// The element id to define in the differential for this property. + /// + [DataTestMethod] + [DynamicData(nameof(ElementDefinitionPropertyElementIdTestCasesStu3), DynamicDataSourceType.Property)] + public async Tasks.Task ElementDefinitionPropertyElementIdTest(FHIRAllTypes profileType, string elementId, string propertyName, string baseId, string diffId, string expectedId) + { + // Arrange + var uri = ModelInfo.CanonicalUriForFhirCoreType(profileType); + + // Create derived profile "myBaseProfile" that will be used as base profile for the test. + // This is necessary to create a base profile that has an element id. + var myBaseProfile = createStructureDefinition("MyBase", profileType, uri); + var myDerivedprofile = createStructureDefinition("MyDerived", profileType, myBaseProfile.Url); + + var source = new CachedResolver(new MultiResolver(_zipSource, new InMemoryResourceResolver(myBaseProfile))); + var generator = new SnapshotGenerator(source, SnapshotGeneratorSettings.CreateDefault()); + var propertyProxy = new ElementDefinitionPropertyProxy(propertyName); + + // Get element from core base profile + var coreElement = await getElementFromStructureDefinition(source, uri, elementId, propertyProxy); + + // Add differential to "myBaseProfile" + var baseElementDefinition = creatElementDefinition(elementId, baseId, propertyProxy, coreElement); + myBaseProfile.Differential.Element = [baseElementDefinition]; + + // Add differential to "myDerivedProfile" + var diffElementDefinition = creatElementDefinition(elementId, diffId, propertyProxy, coreElement); + myDerivedprofile.Differential.Element = [diffElementDefinition]; + + // Act + var elements = await generator.GenerateAsync(myDerivedprofile); + + // Assert + var element = elements.SingleOrDefault(x => x.ElementId == diffElementDefinition.ElementId); + element.Should().NotBeNull(); + var property = propertyProxy.GetValueAsElement(element); + property.ElementId.Should().Be(expectedId); + } + + private static StructureDefinition createStructureDefinition(string name, FHIRAllTypes profileType, string baseDefinition) + { + return new StructureDefinition() + { + Type = profileType.GetLiteral(), + BaseDefinition = baseDefinition, + Name = name, + Url = baseDefinition + name, + Differential = new StructureDefinition.DifferentialComponent() + }; + } + + private static async Tasks.Task getElementFromStructureDefinition(IAsyncResourceResolver source, string uri, string elementId, ElementDefinitionPropertyProxy propertyProxy) + { + var sd = await source.FindStructureDefinitionAsync(uri); // Find base profile + var snapElementDefinition = sd.Snapshot.Element.SingleOrDefault(x => x.ElementId == elementId); // Find specified element in snapshot of base profile + snapElementDefinition.Should().NotBeNull(); + return propertyProxy.GetValueAsElement(snapElementDefinition); // Get the Element property from the snapshot element (typed) + } + + private static ElementDefinition creatElementDefinition(string elementId, string propertyId, ElementDefinitionPropertyProxy propertyProxy, Element element) + { + var elementDefinition = new ElementDefinition(elementId) { ElementId = elementId }; + propertyProxy.SetValue(elementDefinition, propertyProxy.CreateInstance(element)); // Update element definition property value with clone of element + var property = propertyProxy.GetValueAsElement(elementDefinition); // Get the element + property.ElementId = propertyId; // Update property + return elementDefinition; + } + [TestMethod] public async Tasks.Task TestMergingAPreviouslyRemovedElement() { diff --git a/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/ElementDefinitionPropertyProxy.cs b/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/ElementDefinitionPropertyProxy.cs index 2ade34391d..e8b3933e7f 100644 --- a/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/ElementDefinitionPropertyProxy.cs +++ b/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/ElementDefinitionPropertyProxy.cs @@ -12,14 +12,18 @@ namespace Hl7.Fhir.Specification.Tests /// The property can also be a specific item from a property list. /// The property name is specified in the constructor. /// - /// The name is either the name of the property (e.g. "Binding") or the name of the property list - /// with a item selector (e.g. "Constraint[Key:dom-2]"). + /// The name can be + /// - the name of the property (e.g. "Binding") + /// - the name of the property list with an index (e.g. "Type[0]"). + /// - the name of the property list with an item selector (e.g. "Constraint[Key:dom-2]"). /// internal class ElementDefinitionPropertyProxy { + private bool _isList; private string _propertyName; - private string _selectorPropertyName; - private string _selectorValue; + private string _listSelectorPropertyName; + private string _listSelectorValue; + private int? _listIndex; private PropertyInfo _propertyInfo; private PropertyInfo _selectorPropertyInfo; @@ -38,7 +42,7 @@ public object CreateInstance(Element element) { var instance = Activator.CreateInstance(_propertyInfo.PropertyType); - if (_selectorPropertyInfo == null) + if (!_isList) return instance; // Primitive property // Create item for property list @@ -47,8 +51,8 @@ public object CreateInstance(Element element) // Initialize with specified element element?.CopyTo(item); - // Set key value - _selectorPropertyInfo.SetValue(item, _selectorValue); + if (_selectorPropertyInfo != null) + _selectorPropertyInfo.SetValue(item, _listSelectorValue); // Set key value // Add item to property list if (instance is IList items) @@ -74,18 +78,29 @@ public void SetValue(object instance, object value) /// The property value as an Element type. public Element GetValueAsElement(object instance) { - if (_selectorPropertyInfo == null) + if (!_isList) return (Element)_propertyInfo.GetValue(instance); if (_propertyInfo.GetValue(instance, null) is not IList items) return null; - foreach (var item in items) + if (_selectorPropertyInfo != null) { - var value = _selectorPropertyInfo.GetValue(item)?.ToString(); + foreach (var item in items) + { + var value = _selectorPropertyInfo.GetValue(item)?.ToString(); - if (_selectorValue.Equals(value)) - return (Element)item; + if (_listSelectorValue.Equals(value)) + return (Element)item; + } + + return null; + } + + if (_listIndex.HasValue) + { + _listIndex.Value.Should().BeInRange(0, items.Count - 1); + return (Element)items[_listIndex.Value]; } return null; @@ -98,8 +113,9 @@ private void getPropertyInfo(string propertyName) _propertyInfo = typeof(ElementDefinition).GetProperty(_propertyName); _propertyInfo.Should().NotBeNull(); // Check property exists - // ReSharper disable once PossibleNullReferenceException - if (!isList(_propertyInfo.PropertyType)) + _isList = isList(_propertyInfo.PropertyType); + + if (!_isList) { _propertyInfo.PropertyType.IsAssignableTo(typeof(Element)).Should().BeTrue(); // Property type should be derived from Element return; @@ -107,7 +123,10 @@ private void getPropertyInfo(string propertyName) _propertyInfo.PropertyType.GenericTypeArguments[0].IsAssignableTo(typeof(Element)).Should().BeTrue(); // Property type should be derived from Element - _selectorPropertyInfo = _propertyInfo.PropertyType.GenericTypeArguments[0].GetProperty(_selectorPropertyName); + if (_listSelectorPropertyName == null) + return; + + _selectorPropertyInfo = _propertyInfo.PropertyType.GenericTypeArguments[0].GetProperty(_listSelectorPropertyName); _selectorPropertyInfo.Should().NotBeNull(); // Check property exists } @@ -115,17 +134,39 @@ private void parsePropertyName(string propertyName) { _propertyName = propertyName; - var regex = new Regex("(.*)\\[(.*):(.*)\\]|(.*)"); + if (!tryParseListKey(propertyName)) + tryParseListIndex(propertyName); + } + + private static bool isList(Type type) => typeof(IList).IsAssignableFrom(type); + + private bool tryParseListKey(string propertyName) + { + var regex = new Regex("(.+)\\[(.+):(.+)\\]"); var result = regex.Match(propertyName); - if (!result.Success || result.Groups[4].Success) - return; + if (!result.Success) + return false; _propertyName = result.Groups[1].Value; - _selectorPropertyName = result.Groups[2].Value; - _selectorValue = result.Groups[3].Value; + _listSelectorPropertyName = result.Groups[2].Value; + _listSelectorValue = result.Groups[3].Value; + + return true; } - private static bool isList(Type type) => typeof(IList).IsAssignableFrom(type); + private bool tryParseListIndex(string propertyName) + { + var regex = new Regex("(.+)\\[(\\d+)\\]"); + var result = regex.Match(propertyName); + + if (!result.Success) + return false; + + _propertyName = result.Groups[1].Value; + _listIndex = int.Parse(result.Groups[2].Value); + + return true; + } } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/SnapshotGeneratorTest.cs b/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/SnapshotGeneratorTest.cs index 48fb3b9acd..66bb0f1b9f 100644 --- a/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/SnapshotGeneratorTest.cs +++ b/src/Hl7.Fhir.Specification.Shared.Tests/Snapshot/SnapshotGeneratorTest.cs @@ -36,6 +36,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -9555,31 +9556,24 @@ public static IEnumerable ElementDefinitionPropertyExtensionTestCasesR { // Modify an existing Binding extension yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("Test")) }}; + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("Test")) }}; // Adding a new Binding extension yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", new FhirBoolean(true)) }}; + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", new FhirString("AllergyIntoleranceCode")) }, + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", new FhirBoolean(true)) }}; // Adding a new Constraint extension yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance", "Constraint[Key:dom-2]", Array.Empty(), - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(true)) }}; + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(true)) }}; // Modifying an existing Constraint extension yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance", "Constraint[Key:dom-6]", - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(true)), - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice-explanation", new Markdown("When a resource has no narrative, only systems that fully understand the data can display the resource to a human safely. Including a human readable representation in the resource makes for a much more robust eco-system and cheaper handling of resources by intermediary systems. Some ecosystems restrict distribution of resources to only those systems that do fully understand the resources, and as a consequence implementers may believe that the narrative is superfluous. However experience shows that such eco-systems often open up to new participants over time.")) }, - new[] { - new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(false)) }}; + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(true)), + new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice-explanation", new Markdown("When a resource has no narrative, only systems that fully understand the data can display the resource to a human safely. Including a human readable representation in the resource makes for a much more robust eco-system and cheaper handling of resources by intermediary systems. Some ecosystems restrict distribution of resources to only those systems that do fully understand the resources, and as a consequence implementers may believe that the narrative is superfluous. However experience shows that such eco-systems often open up to new participants over time.")) }, + new[] { new Extension("http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", new FhirBoolean(false)) }}; } } @@ -9673,6 +9667,127 @@ public async Tasks.Task ElementDefinitionPropertyExtensionTest(FHIRAllTypes prof elementProperty.Extension.OrderBy(x => x.Url).Should().Equal(expectedExtensions.OrderBy(x => x.Url), (e1, e2) => e1.IsExactly(e2)); } + private void logExtensions(string title, IEnumerable extensions, int level = 1) + { + Debug.WriteLine(title); + + if (!extensions.Any()) + { + Debug.WriteLine($"{new string(' ', level * 3)}none"); + return; + } + + foreach (Extension extension in extensions) + { + if (extension.Extension != null && extension.Extension.Count > 0) + logExtensions(extension.Url, extension.Extension, level + 1); + else + Debug.WriteLine($"{new string(' ', level * 3)}{extension.Url} : {extension.Value}"); + } + } + + private const string BaseId = "testBaseId"; + private const string DiffId = "testDiffId"; + + public static IEnumerable ElementDefinitionPropertyElementIdTestCasesR4 + { + get + { + // Binding + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", null, null, null }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", null, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", BaseId, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Binding", BaseId, null, BaseId }; + + // Primitive element + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", null, null, null }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", null, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", BaseId, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "ShortElement", BaseId, null, BaseId }; + + // Type + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", null, null, null }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", null, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", BaseId, DiffId, DiffId }; + yield return new object[] { FHIRAllTypes.AllergyIntolerance, "AllergyIntolerance.code", "Type[0]", BaseId, null, BaseId }; + } + } + + /// + /// Tests whether element definition property element id in the differential is properly merged by the snapshot generator. + /// + /// The profile type under test (e.g. FHIRAllTypes.AllergyIntolerance). + /// The element id of the profile to check (e.g. "AllergyIntolerance.code") + /// The name of the element definition property for which to add or modify the extension in the differential (e.g. "Binding"). + /// The element id that is defined in the base profile for this property. + /// The element id to define in the differential for this property. + /// + [DataTestMethod] + [DynamicData(nameof(ElementDefinitionPropertyElementIdTestCasesR4), DynamicDataSourceType.Property)] + public async Tasks.Task ElementDefinitionPropertyElementIdTest(FHIRAllTypes profileType, string elementId, string propertyName, string baseId, string diffId, string expectedId) + { + // Arrange + var uri = ModelInfo.CanonicalUriForFhirCoreType(profileType); + + // Create derived profile "myBaseProfile" that will be used as base profile for the test. + // This is necessary to create a base profile that has an element id. + var myBaseProfile = createStructureDefinition("MyBase", profileType, uri); + var myDerivedprofile = createStructureDefinition("MyDerived", profileType, myBaseProfile.Url); + + var source = new CachedResolver(new MultiResolver(_standardFhirSource, new InMemoryResourceResolver(myBaseProfile))); + var generator = new SnapshotGenerator(source, SnapshotGeneratorSettings.CreateDefault()); + var propertyProxy = new ElementDefinitionPropertyProxy(propertyName); + + // Get element from core base profile + var coreElement = await getElementFromStructureDefinition(source, uri, elementId, propertyProxy); + + // Add differential to "myBaseProfile" + var baseElementDefinition = creatElementDefinition(elementId, baseId, propertyProxy, coreElement); + myBaseProfile.Differential.Element = [baseElementDefinition]; + + // Add differential to "myDerivedProfile" + var diffElementDefinition = creatElementDefinition(elementId, diffId, propertyProxy, coreElement); + myDerivedprofile.Differential.Element = [diffElementDefinition]; + + // Act + var elements = await generator.GenerateAsync(myDerivedprofile); + + // Assert + var element = elements.SingleOrDefault(x => x.ElementId == diffElementDefinition.ElementId); + element.Should().NotBeNull(); + var property = propertyProxy.GetValueAsElement(element); + property.ElementId.Should().Be(expectedId); + } + + private static StructureDefinition createStructureDefinition(string name, FHIRAllTypes profileType, string baseDefinition) + { + return new StructureDefinition() + { + Type = profileType.GetLiteral(), + BaseDefinition = baseDefinition, + Name = name, + Url = baseDefinition + name, + Differential = new StructureDefinition.DifferentialComponent() + }; + } + + private static async Tasks.Task getElementFromStructureDefinition(IAsyncResourceResolver source, string uri, string elementId, ElementDefinitionPropertyProxy propertyProxy) + { + var sd = await source.FindStructureDefinitionAsync(uri); // Find base profile + var snapElementDefinition = sd.Snapshot.Element.SingleOrDefault(x => x.ElementId == elementId); // Find specified element in snapshot of base profile + snapElementDefinition.Should().NotBeNull(); + return propertyProxy.GetValueAsElement(snapElementDefinition); // Get the Element property from the snapshot element (typed) + } + + private static ElementDefinition creatElementDefinition(string elementId, string propertyId, ElementDefinitionPropertyProxy propertyProxy, Element element) + { + var elementDefinition = new ElementDefinition(elementId) { ElementId = elementId }; + propertyProxy.SetValue(elementDefinition, propertyProxy.CreateInstance(element)); // Update element definition property value with clone of element + var property = propertyProxy.GetValueAsElement(elementDefinition); // Get the element + property.ElementId = propertyId; // Update property + return elementDefinition; + } + [TestMethod] public async Tasks.Task ConstrainChoiceTypeWithExplicitSlicesInDerivedProfileCorrectly() { @@ -9812,7 +9927,6 @@ public async Tasks.Task BindingRemovedAfterTypeSlicing() element = elements.Should().ContainSingle(e => e.ElementId == "MedicationRequest.substitution.allowed[x]:allowedCodeableConcept").Subject; element.Type.Should().OnlyContain(t => t.Code == "CodeableConcept"); element.Binding.Should().NotBeNull(); - } [TestMethod] @@ -9850,25 +9964,6 @@ public async Tasks.Task TestConstraintSource() constraint.Source.Should().Be("http://hl7.org/fhir/StructureDefinition/Reference"); } - private void logExtensions(string title, IEnumerable extensions, int level = 1) - { - Debug.WriteLine(title); - - if (!extensions.Any()) - { - Debug.WriteLine($"{new string(' ', level * 3)}none"); - return; - } - - foreach (Extension extension in extensions) - { - if (extension.Extension != null && extension.Extension.Count > 0) - logExtensions(extension.Url, extension.Extension, level + 1); - else - Debug.WriteLine($"{new string(' ', level * 3)}{extension.Url} : {extension.Value}"); - } - } - //Tests Github issue #2211, see TestData/Issue-2211 for test artifacts. [TestMethod] public async Tasks.Task TestMergingAPreviouslyRemovedElement()