Skip to content

Commit

Permalink
Merge pull request #2374 from FirelyTeam/feature/2373-support-for-sin…
Browse files Browse the repository at this point in the history
…ce-on-choice-element

2373 Added support elements that have changed from choice to non-choice.
  • Loading branch information
ewoutkramer authored Jan 27, 2023
2 parents 99b3410 + 7684c22 commit b09a315
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 17 deletions.
25 changes: 21 additions & 4 deletions src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AllowedTypesAttribute>(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<AllowedTypesAttribute>(prop, release) : null;

var fhirTypes = allowedTypes?.Types?.Any() == true ?
allowedTypes.Types : new[] { fhirType };
Expand All @@ -243,6 +243,23 @@ public static bool TryCreate(PropertyInfo prop, out PropertyMapping? result, Cla
return true;
}

/// <summary>
/// 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 <see cref="ImplementingType"/>.
/// </summary>
internal Type GetInstantiableType()
{
if (!ImplementingType.IsAbstract)
return ImplementingType;

// Ok, so we're in abstract type land, maybe FhirType can help us
if (FhirType.Length == 1)
return FhirType[0];

// No, just return ImplementingType then.
return ImplementingType;
}

private static bool isPrimitiveValueElement(FhirElementAttribute valueElementAttr, PropertyInfo prop)
{
var isValueElement = valueElementAttr != null && valueElementAttr.IsPrimitiveValue;
Expand Down
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/Introspection/ReferencesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 17 additions & 9 deletions src/Hl7.Fhir.Base/Model/Generated/Signature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,32 +103,40 @@ public DateTimeOffset? When
/// <summary>
/// Who signed. Note: Since R5 the cardinality is expanded to 0..1 (previous it was 1..1)
/// </summary>
[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;

/// <summary>
/// The party represented
/// </summary>
[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;

/// <summary>
/// The technical format of the signed resources
Expand Down
15 changes: 14 additions & 1 deletion src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -99,6 +100,18 @@ public Resource DeserializeResource(ref Utf8JsonReader reader)
: throw new DeserializationFailedException(result, state.Errors);
}

/// <summary>
/// Deserialize the FHIR Json from the reader and create a new POCO object containing the data from the reader.
/// </summary>
/// <param name="json">A string of json.</param>
/// <returns>A fully initialized POCO with the data from the reader.</returns>
public Resource DeserializeResource(string json)
{
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json), new() { CommentHandling = JsonCommentHandling.Skip });

return DeserializeResource(ref reader);
}

/// <summary>
/// Reads a (subtree) of serialized FHIR Json data into a POCO object.
/// </summary>
Expand Down Expand Up @@ -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),
Expand Down
15 changes: 15 additions & 0 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,6 +66,18 @@ public BaseFhirJsonPocoSerializer(FhirRelease release, FhirJsonPocoSerializerSet
public void Serialize(IReadOnlyDictionary<string, object> members, Utf8JsonWriter writer) =>
serializeInternal(members, writer, skipValue: false);

/// <summary>
/// Serializes the given dictionary with FHIR data into a Json string.
/// </summary>
public string SerializeToString(IReadOnlyDictionary<string, object> members)
{
var stream = new MemoryStream();
var writer = new Utf8JsonWriter(stream);
serializeInternal(members, writer, skipValue: false);
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}

/// <summary>
/// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 3 additions & 1 deletion src/Hl7.Fhir.Base/Serialization/DispatchingReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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

Expand Down
83 changes: 83 additions & 0 deletions src/Hl7.Fhir.Serialization.R4.Tests/RoundtripSignature.cs
Original file line number Diff line number Diff line change
@@ -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(ModelInfo.CanonicalUriForFhirCoreType("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("<who>");
var sig3 = new FhirXmlPocoDeserializer().DeserializeResource(xml);
sig.IsExactly(sig3).Should().BeTrue();

}
}
}
82 changes: 82 additions & 0 deletions src/Hl7.Fhir.Serialization.STU3.Tests/RoundtripSignature.cs
Original file line number Diff line number Diff line change
@@ -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("<whoReference>");
var sig3 = new FhirXmlPocoDeserializer().DeserializeResource(xml);
sig.IsExactly(sig3).Should().BeTrue();
}
}
}

0 comments on commit b09a315

Please sign in to comment.