From 8a94be7e605fbb8774b2fc9baadbe74f60d77f6d Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 26 Jan 2023 16:07:58 +0100 Subject: [PATCH 1/3] Added support elements that have changed from choice to non-choice. --- .../Introspection/PropertyMapping.cs | 22 ++++- .../Introspection/ReferencesAttribute.cs | 2 +- .../Model/Generated/Signature.cs | 26 ++++-- .../BaseFhirJsonPocoDeserializer.cs | 15 +++- .../BaseFhirJsonPocoSerializer.cs | 15 ++++ .../BaseFhirXmlPocoDeserializer.cs | 2 +- .../Serialization/DispatchingReader.cs | 4 +- .../RoundtripSignature.cs | 83 +++++++++++++++++++ .../RoundtripSignature.cs | 82 ++++++++++++++++++ 9 files changed, 234 insertions(+), 17 deletions(-) create mode 100644 src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs create mode 100644 src/Hl7.Fhir.Serialization.STU3.Tests/RoundtripSignature.cs diff --git a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs index 418289858d..d67c4f4a78 100644 --- a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs @@ -215,10 +215,10 @@ public static bool TryCreate(PropertyInfo prop, out PropertyMapping? result, Cla throw new InvalidOperationException($"Property {prop.Name} in class {prop.DeclaringType!.Name} is of type " + $"{fhirType}, for which a classmapping cannot be found."); - // The [AllowedElements] attribute can specify a set of allowed types - // for this element. Take this list as the declared list of FHIR types. - // If not present assume this is the implementing FHIR type above - var allowedTypes = ClassMapping.GetAttribute(prop, release); + // The [AllowedElements] attribute can specify a set of allowed types for this element. + // If this is a choice element, then take this list as the declared list of FHIR types, + // otherwise assume this is the implementing FHIR type above + var allowedTypes = elementAttr.Choice != ChoiceType.None ? ClassMapping.GetAttribute(prop, release) : null; var fhirTypes = allowedTypes?.Types?.Any() == true ? allowedTypes.Types : new[] { fhirType }; @@ -243,6 +243,20 @@ public static bool TryCreate(PropertyInfo prop, out PropertyMapping? result, Cla return true; } + internal Type GetInstantiableType() + { + if (Choice != ChoiceType.None) + throw new InvalidOperationException("This internal function can only be used on non-choice properties."); + + if (!ImplementingType.IsAbstract) + return ImplementingType; + + if (FhirType.Length != 1) + throw new InvalidOperationException("Property is not a choice, so FhirType.Length should be 1"); + + return FhirType.Single(); + } + private static bool isPrimitiveValueElement(FhirElementAttribute valueElementAttr, PropertyInfo prop) { var isValueElement = valueElementAttr != null && valueElementAttr.IsPrimitiveValue; diff --git a/src/Hl7.Fhir.Base/Introspection/ReferencesAttribute.cs b/src/Hl7.Fhir.Base/Introspection/ReferencesAttribute.cs index 78648958f5..e4d1a43948 100644 --- a/src/Hl7.Fhir.Base/Introspection/ReferencesAttribute.cs +++ b/src/Hl7.Fhir.Base/Introspection/ReferencesAttribute.cs @@ -33,7 +33,7 @@ POSSIBILITY OF SUCH DAMAGE. namespace Hl7.Fhir.Introspection { [CLSCompliant(false)] - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)] public class ReferencesAttribute : VersionedAttribute { public ReferencesAttribute(params string[] resources) diff --git a/src/Hl7.Fhir.Base/Model/Generated/Signature.cs b/src/Hl7.Fhir.Base/Model/Generated/Signature.cs index f66838ad85..5300cf9c7c 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Signature.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Signature.cs @@ -103,32 +103,40 @@ public DateTimeOffset? When /// /// Who signed. Note: Since R5 the cardinality is expanded to 0..1 (previous it was 1..1) /// - [FhirElement("who", InSummary=true, Order=50)] + [FhirElement("who", InSummary = true, Order = 50, Choice = ChoiceType.DatatypeChoice)] + [FhirElement("who", InSummary = true, Order = 50, Since = FhirRelease.R4)] [CLSCompliant(false)] - [References("Practitioner","PractitionerRole","RelatedPerson","Patient","Device","Organization")] - [DataMember] - public Hl7.Fhir.Model.ResourceReference Who + [References("Practitioner", "RelatedPerson", "Patient", "Device", "Organization")] + [References("Practitioner", "PractitionerRole", "RelatedPerson", "Patient", "Device", "Organization",Since=FhirRelease.R4)] + [AllowedTypes(typeof(Hl7.Fhir.Model.FhirUri), typeof(Hl7.Fhir.Model.ResourceReference))] + [DeclaredType(Type = typeof(ResourceReference), Since=FhirRelease.R4)] + [DataMember] + public Hl7.Fhir.Model.DataType Who { get { return _Who; } set { _Who = value; OnPropertyChanged("Who"); } } - private Hl7.Fhir.Model.ResourceReference _Who; + private Hl7.Fhir.Model.DataType _Who; /// /// The party represented /// - [FhirElement("onBehalfOf", InSummary=true, Order=60)] + [FhirElement("onBehalfOf", InSummary = true, Order = 60, Choice = ChoiceType.DatatypeChoice)] + [FhirElement("onBehalfOf", InSummary=true, Order=60, Since = FhirRelease.R4)] [CLSCompliant(false)] - [References("Practitioner","PractitionerRole","RelatedPerson","Patient","Device","Organization")] + [References("Practitioner", "RelatedPerson", "Patient", "Device", "Organization")] + [References("Practitioner","PractitionerRole","RelatedPerson","Patient","Device","Organization",Since=FhirRelease.R4)] + [AllowedTypes(typeof(Hl7.Fhir.Model.FhirUri), typeof(Hl7.Fhir.Model.ResourceReference))] + [DeclaredType(Type = typeof(ResourceReference), Since = FhirRelease.R4)] [DataMember] - public Hl7.Fhir.Model.ResourceReference OnBehalfOf + public Hl7.Fhir.Model.DataType OnBehalfOf { get { return _OnBehalfOf; } set { _OnBehalfOf = value; OnPropertyChanged("OnBehalfOf"); } } - private Hl7.Fhir.Model.ResourceReference _OnBehalfOf; + private Hl7.Fhir.Model.DataType _OnBehalfOf; /// /// The technical format of the signed resources diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs index 10c390e853..419c23f104 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text; using System.Text.Json; using ERR = Hl7.Fhir.Serialization.FhirJsonException; @@ -99,6 +100,18 @@ public Resource DeserializeResource(ref Utf8JsonReader reader) : throw new DeserializationFailedException(result, state.Errors); } + /// + /// Deserialize the FHIR Json from the reader and create a new POCO object containing the data from the reader. + /// + /// A string of json. + /// A fully initialized POCO with the data from the reader. + public Resource DeserializeResource(string json) + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json), new() { CommentHandling = JsonCommentHandling.Skip }); + + return DeserializeResource(ref reader); + } + /// /// Reads a (subtree) of serialized FHIR Json data into a POCO object. /// @@ -882,7 +895,7 @@ private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, Fh (ClassMapping? propertyValueMapping, FhirJsonException? error) = propertyMapping.Choice switch { ChoiceType.None or ChoiceType.ResourceChoice => - inspector.FindOrImportClassMapping(propertyMapping.ImplementingType) is ClassMapping m + inspector.FindOrImportClassMapping(propertyMapping.GetInstantiableType()) is ClassMapping m ? (m, null) : throw new InvalidOperationException($"Encountered property type {propertyMapping.ImplementingType} for which no mapping was found in the model assemblies. " + reader.GenerateLocationMessage()), ChoiceType.DatatypeChoice => getChoiceClassMapping(ref reader), diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs index 4a509bedd1..7977bf477f 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs @@ -17,7 +17,10 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices.ComTypes; +using System.Text; using System.Text.Json; namespace Hl7.Fhir.Serialization @@ -63,6 +66,18 @@ public BaseFhirJsonPocoSerializer(FhirRelease release, FhirJsonPocoSerializerSet public void Serialize(IReadOnlyDictionary members, Utf8JsonWriter writer) => serializeInternal(members, writer, skipValue: false); + /// + /// Serializes the given dictionary with FHIR data into a Json string. + /// + public string SerializeToString(IReadOnlyDictionary members) + { + var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream); + serializeInternal(members, writer, skipValue: false); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); + } + /// /// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element. /// diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs index 735d1268d7..4df04b0e7e 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs @@ -650,7 +650,7 @@ private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, Fh (ClassMapping? propertyValueMapping, FhirXmlException? error) = propertyMapping.Choice switch { ChoiceType.None or ChoiceType.ResourceChoice => - inspector.FindOrImportClassMapping(propertyMapping.ImplementingType) is ClassMapping m + inspector.FindOrImportClassMapping(propertyMapping.GetInstantiableType()) is ClassMapping m ? (m, null) : throw new InvalidOperationException($"Encountered property type {propertyMapping.ImplementingType} for which no mapping was found in the model assemblies. " + reader.GenerateLocationMessage()), ChoiceType.DatatypeChoice => getChoiceClassMapping(reader), diff --git a/src/Hl7.Fhir.Base/Serialization/DispatchingReader.cs b/src/Hl7.Fhir.Base/Serialization/DispatchingReader.cs index 3dfd1c220d..6eb6247b21 100644 --- a/src/Hl7.Fhir.Base/Serialization/DispatchingReader.cs +++ b/src/Hl7.Fhir.Base/Serialization/DispatchingReader.cs @@ -10,7 +10,9 @@ using Hl7.Fhir.Introspection; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; +using System; using System.Collections; +using System.Linq; namespace Hl7.Fhir.Serialization { @@ -70,7 +72,7 @@ public object Deserialize(PropertyMapping prop, string memberName, object existi ClassMapping mapping = prop.Choice == ChoiceType.DatatypeChoice ? getMappingForType(memberName, _current.InstanceType) - : _inspector.FindOrImportClassMapping(prop.ImplementingType); + : _inspector.FindOrImportClassMapping(prop.GetInstantiableType()); // Handle other Choices having any datatype or a list of datatypes diff --git a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs new file mode 100644 index 0000000000..30f1dbd55f --- /dev/null +++ b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs @@ -0,0 +1,83 @@ +using FluentAssertions; +using Hl7.Fhir.ElementModel; +using Hl7.Fhir.Model; +using Hl7.Fhir.Specification; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; + +namespace Hl7.Fhir.Serialization.Tests +{ + // Signature is special, since it's the first case where + // an element changed from a choice element in STU3 to a non-choice element in R4. + // These unit-tests will exercise those parts that are sensitive to such a change. + [TestClass] + public class RoundtripSignature + { + [TestMethod] + public void ModelInfoHasCorrectInfo() + { + var cls = ModelInfo.ModelInspector.FindClassMapping("Signature"); + cls.FindMappedElementByChoiceName("whoReference").Should().BeNull(); + var prop = cls.FindMappedElementByName("who"); + prop.Should().NotBeNull(); + + prop.Choice.Should().Be(Introspection.ChoiceType.None); + prop.ImplementingType.Should().Be(typeof(DataType)); + prop.PropertyTypeMapping.Name.Should().Be("Reference"); + prop.FhirType.Should().BeEquivalentTo(new[] { typeof(ResourceReference) }); + } + + [TestMethod] + public void SDSHasCorrectInfo() + { + var cls = new PocoStructureDefinitionSummaryProvider().Provide("http://hl7.org/fhir/StructureDefinition/Signature"); + var elem = cls.GetElements().Single(e => e.ElementName == "who"); + cls.GetElements().Where(e => e.ElementName == "whoReference").Should().BeEmpty(); + validateEDS(elem); + } + + private void validateEDS(IElementDefinitionSummary eds) + { + eds.IsChoiceElement.Should().BeFalse(); + eds.Type.Select(t => t.GetTypeName()).Should().BeEquivalentTo("Reference"); + } + + [TestMethod] + public void TypedElementHasCorrectInfo() + { + var cls = new Signature() { Who = new ResourceReference("http://nu.nl") }.ToTypedElement(); + + var who = cls.Children("who").Single(); + who.Name.Should().Be("who"); + who.InstanceType.Should().Be("Reference"); + validateEDS(who.Definition); + } + + [TestMethod] + public void WorksWithTypedElementSerializers() + { + var sig = new Bundle() { Signature = new Signature() { Who = new ResourceReference("http://nu.nl") } }; + var json = sig.ToTypedElement().ToJson(); + json.Should().Contain("\"who\""); + var sig2 = FhirJsonNode.Parse(json).ToPoco(); + sig.IsExactly(sig2).Should().BeTrue(); + } + + [TestMethod] + public void WorksWithPocoSerializers() + { + var sig = new Bundle() { Type = Bundle.BundleType.Document, Signature = new Signature() { Who = new ResourceReference("http://nu.nl") } }; + var json = new FhirJsonPocoSerializer().SerializeToString(sig); + json.Should().Contain("\"who\""); + var sig2 = new FhirJsonPocoDeserializer().DeserializeResource(json); + sig.IsExactly(sig2).Should().BeTrue(); + + var xml = new FhirXmlPocoSerializer().SerializeToString(sig); + xml.Should().Contain(""); + var sig3 = new FhirXmlPocoDeserializer().DeserializeResource(xml); + sig.IsExactly(sig3).Should().BeTrue(); + + } + } +} diff --git a/src/Hl7.Fhir.Serialization.STU3.Tests/RoundtripSignature.cs b/src/Hl7.Fhir.Serialization.STU3.Tests/RoundtripSignature.cs new file mode 100644 index 0000000000..79344b6751 --- /dev/null +++ b/src/Hl7.Fhir.Serialization.STU3.Tests/RoundtripSignature.cs @@ -0,0 +1,82 @@ +using FluentAssertions; +using Hl7.Fhir.ElementModel; +using Hl7.Fhir.Model; +using Hl7.Fhir.Specification; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; + +namespace Hl7.Fhir.Serialization.Tests +{ + // Signature is special, since it's the first case where + // an element changed from a choice element in STU3 to a non-choice element in R4. + // These unit-tests will exercise those parts that are sensitive to such a change. + [TestClass] + public class RoundtripSignature + { + [TestMethod] + public void ModelInfoHasCorrectInfo() + { + var cls = ModelInfo.ModelInspector.FindClassMapping("Signature"); + cls.FindMappedElementByName("who").Should().NotBeNull(); + var prop = cls.FindMappedElementByChoiceName("whoReference"); + prop.Should().NotBeNull(); + + prop.Choice.Should().Be(Introspection.ChoiceType.DatatypeChoice); + prop.ImplementingType.Should().Be(typeof(DataType)); + prop.PropertyTypeMapping.Name.Should().Be("DataType"); + prop.FhirType.Should().BeEquivalentTo(new Type[] { typeof(FhirUri), typeof(ResourceReference) }); + } + + [TestMethod] + public void SDSHasCorrectInfo() + { + var cls = new PocoStructureDefinitionSummaryProvider().Provide("http://hl7.org/fhir/StructureDefinition/Signature"); + var elem = cls.GetElements().Single(e => e.ElementName == "who"); + cls.GetElements().Where(e => e.ElementName == "whoReference").Should().BeEmpty(); + validateEDS(elem); + } + + private void validateEDS(IElementDefinitionSummary eds) + { + eds.IsChoiceElement.Should().BeTrue(); + eds.Type.Select(t => t.GetTypeName()).Should().BeEquivalentTo("uri", "Reference"); + } + + [TestMethod] + public void TypedElementHasCorrectInfo() + { + var cls = new Signature() { Who = new ResourceReference("http://nu.nl") }.ToTypedElement(); + + var who = cls.Children("who").Single(); + who.Name.Should().Be("who"); + who.InstanceType.Should().Be("Reference"); + validateEDS(who.Definition); + } + + [TestMethod] + public void WorksWithTypedElementSerializers() + { + var sig = new Bundle() { Signature = new Signature() { Who = new ResourceReference("http://nu.nl") } }; + var json = sig.ToTypedElement().ToJson(); + json.Should().Contain("\"whoReference\""); + var sig2 = FhirJsonNode.Parse(json).ToPoco(); + sig.IsExactly(sig2).Should().BeTrue(); + } + + [TestMethod] + public void WorksWithPocoSerializers() + { + var sig = new Bundle() { Type = Bundle.BundleType.Document, Signature = new Signature() { Who = new ResourceReference("http://nu.nl") } }; + var json = new FhirJsonPocoSerializer().SerializeToString(sig); + json.Should().Contain("\"whoReference\""); + var sig2 = new FhirJsonPocoDeserializer().DeserializeResource(json); + sig.IsExactly(sig2).Should().BeTrue(); + + var xml = new FhirXmlPocoSerializer().SerializeToString(sig); + xml.Should().Contain(""); + var sig3 = new FhirXmlPocoDeserializer().DeserializeResource(xml); + sig.IsExactly(sig3).Should().BeTrue(); + } + } +} From 24aacd6876d5c8bb5f2c59d8ef8e97a2c26b663e Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 26 Jan 2023 16:43:28 +0100 Subject: [PATCH 2/3] Resource choices should return Resource, even though that is abstract. --- .../Introspection/PropertyMapping.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs index d67c4f4a78..de1c623e64 100644 --- a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs @@ -243,18 +243,21 @@ public static bool TryCreate(PropertyInfo prop, out PropertyMapping? result, Cla return true; } + /// + /// This function tried to figure out a concrete type for the element represented by this property. + /// If it cannot derive a concrete type, it will just return . + /// internal Type GetInstantiableType() { - if (Choice != ChoiceType.None) - throw new InvalidOperationException("This internal function can only be used on non-choice properties."); - if (!ImplementingType.IsAbstract) return ImplementingType; - if (FhirType.Length != 1) - throw new InvalidOperationException("Property is not a choice, so FhirType.Length should be 1"); + // Ok, so we're in abstract type land, maybe FhirType can help us + if (FhirType.Length == 1) + return FhirType[0]; - return FhirType.Single(); + // No, just return ImplementingType then. + return ImplementingType; } private static bool isPrimitiveValueElement(FhirElementAttribute valueElementAttr, PropertyInfo prop) From 7684c227d73f5aa9d5971f3609a2e5342ef08152 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Fri, 27 Jan 2023 16:34:35 +0100 Subject: [PATCH 3/3] Update src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs Co-authored-by: Marco Visser --- src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs index 30f1dbd55f..3977b133ff 100644 --- a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs +++ b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs @@ -31,7 +31,7 @@ public void ModelInfoHasCorrectInfo() [TestMethod] public void SDSHasCorrectInfo() { - var cls = new PocoStructureDefinitionSummaryProvider().Provide("http://hl7.org/fhir/StructureDefinition/Signature"); + var cls = new PocoStructureDefinitionSummaryProvider().Provide(ModelInfo.CanonicalUriForFhirCoreType("Signature")); var elem = cls.GetElements().Single(e => e.ElementName == "who"); cls.GetElements().Where(e => e.ElementName == "whoReference").Should().BeEmpty(); validateEDS(elem);