Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Base POCO parsers/serializers on POCO, not IROD #2950

Open
wants to merge 7 commits into
base: develop-6.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Hl7.Fhir.Base/Model/Base.Dictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using System.Collections.Generic;
using System.Linq;

#nullable enable

namespace Hl7.Fhir.Model;

public abstract partial class Base: IReadOnlyDictionary<string,object>, IDictionary<string, object>
Expand Down
504 changes: 250 additions & 254 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoSerializer.cs

Large diffs are not rendered by default.

284 changes: 137 additions & 147 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,188 +19,178 @@
using System.Linq;
using System.Xml;

namespace Hl7.Fhir.Serialization
namespace Hl7.Fhir.Serialization;

/// <summary>
/// Serializes the contents of a POCO according to the rules of FHIR Xml serialization.
/// </summary>
/// <remarks>The serializer uses the format documented in https://www.hl7.org/fhir/xml.html.
/// </remarks>
public class BaseFhirXmlPocoSerializer
{
/// <summary>
/// Serializes the contents of an IReadOnlyDictionary[string,object] according to the rules of FHIR Xml serialization.
/// The release of FHIR for which this serializer is configured.
/// </summary>
/// <remarks>The serializer uses the format documented in https://www.hl7.org/fhir/xml.html. Since all POCOs included
/// in the SDK implement IReadOnlyDictionary, these methods can be used to serialize POCOs to Xml.
/// </remarks>
public class BaseFhirXmlPocoSerializer
public FhirRelease Release { get; }

/// <summary>
/// Construct a new serializer for a specific release of FHIR.
/// </summary>
public BaseFhirXmlPocoSerializer(FhirRelease release)
{
/// <summary>
/// The release of FHIR for which this serializer is configured.
/// </summary>
public FhirRelease Release { get; }

/// <summary>
/// Construct a new serializer for a specific release of FHIR.
/// </summary>
public BaseFhirXmlPocoSerializer(FhirRelease release)
{
Release = release;
}
Release = release;
}

/// <summary>
/// Serializes the given dictionary with FHIR data into Json.
/// </summary>
public void Serialize(IReadOnlyDictionary<string, object> members, XmlWriter writer, SerializationFilter? summary = default)
/// <summary>
/// Serializes the given dictionary with FHIR data into Json.
/// </summary>
public void Serialize(
Base element,
XmlWriter writer,
SerializationFilter? summary = default)
{
writer.WriteStartDocument();

// If we are serializing a non-resource, or we are serializing a nested resource,
// we need to pick a name for the root element.
var pickElementName = element is not Resource or IScopedNode { Parent: not null };
if (pickElementName)
{
writer.WriteStartDocument();
// If we are an element with a name, pick that, otherwise us the name of the type.
var nodeName = element is ITypedElement ite ? ite.Name : element.TypeName;

var simulateRoot = ((IScopedNode)members).Parent is not null || members is not Resource;
if (simulateRoot)
{
// Serialization in XML of non-resources is problematic, since there's no root.
// It's a common usecase though, so "invent" a root that's the name of the element's type.
var rootElementName = members is Base b ? ((ITypedElement)b).Name : members.GetType().Name;
writer.WriteStartElement(rootElementName, XmlNs.FHIR);
}
writer.WriteStartElement(nodeName, XmlNs.FHIR);
}

serializeInternal(members, writer, summary);
serializeInternal(element, writer, summary);

if (simulateRoot) writer.WriteEndElement();
writer.WriteEndDocument();
}
if (pickElementName) writer.WriteEndElement();
writer.WriteEndDocument();
}

/// <summary>
/// Serializes the given dictionary with FHIR data into UTF8 encoded Json.
/// </summary>
public string SerializeToString(IReadOnlyDictionary<string, object> members, SerializationFilter? summary = default) =>
SerializationUtil.WriteXmlToString(w => Serialize(members, w, summary));

/// <summary>
/// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element.
/// </summary>
/// <remarks>Not serializing the "value" element is useful when serializing FHIR primitives into two properties, one
/// with just the value, and one with the id/extensions.</remarks>
private void serializeInternal(
IReadOnlyDictionary<string, object> members,
XmlWriter writer,
SerializationFilter? filter)
{
if (members is Resource r)
writer.WriteStartElement(r.TypeName, XmlNs.FHIR);
/// <summary>
/// Serializes the given dictionary with FHIR data into UTF8 encoded Json.
/// </summary>
public string SerializeToString(
Base element,
SerializationFilter? summary = default) =>
SerializationUtil.WriteXmlToString(element, (o,w) => Serialize(o, w, summary));

// Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base.
if (!ClassMapping.TryGetMappingForType(members.GetType(), Release, out var mapping) && members is Base)
throw new InvalidOperationException($"Encountered type {members.GetType()}, which is a support POCO for FHIR, but does not " +
$"have sufficient metadata to be used by the serializer.");
/// <summary>
/// Serializes the given dictionary with FHIR data into Json, optionally skipping the "value" element.
/// </summary>
/// <remarks>Not serializing the "value" element is useful when serializing FHIR primitives into two properties, one
/// with just the value, and one with the id/extensions.</remarks>
private void serializeInternal(
Base element,
XmlWriter writer,
SerializationFilter? filter)
{
if (element is Resource r)
writer.WriteStartElement(r.TypeName, XmlNs.FHIR);

filter?.EnterObject(members, mapping);
// Only throw if we don't have a mapping where we are expected to: when this is a subclass of Base.
if (!ClassMapping.TryGetMappingForType(element.GetType(), Release, out var mapping))
throw new InvalidOperationException($"Encountered type {element.GetType()}, which is a support POCO for FHIR, but does not " +
$"have sufficient metadata to be used by the serializer.");

serializeElement(members, writer, filter, mapping);
filter?.EnterObject(element, mapping);

filter?.LeaveObject(members, mapping);
serializeElement(element, writer, filter, mapping);

if (members is Resource) writer.WriteEndElement();
}
filter?.LeaveObject(element, mapping);

private void serializeElement(IReadOnlyDictionary<string, object> members, XmlWriter writer, SerializationFilter? filter, ClassMapping? mapping)
{
// Make sure that elements with attributes are serialized first.
var orderedMembers = members
.Select(m => (m, mapping: mapping?.FindMappedElementByName(m.Key)))
.OrderBy(p => p.mapping?.SerializationHint != XmlRepresentation.XmlAttr);
if (element is Resource) writer.WriteEndElement();
}

foreach (var ((mKey, mValue), propertyMapping) in orderedMembers)
{
if (filter?.TryEnterMember(mKey, mValue, propertyMapping) == false)
continue;
private void serializeElement(Base element, XmlWriter writer, SerializationFilter? filter, ClassMapping? mapping)
{
// Make sure that elements with attributes are serialized first.
var orderedMembers = element
.Select(m => (m, mapping: mapping?.FindMappedElementByName(m.Key)))
.OrderBy(p => p.mapping?.SerializationHint != XmlRepresentation.XmlAttr);

var elementName = propertyMapping?.Choice == ChoiceType.DatatypeChoice ?
addSuffixToElementName(mKey, mValue) : mKey;
foreach (var ((mKey, mValue), propertyMapping) in orderedMembers)
{
if (filter?.TryEnterMember(mKey, mValue, propertyMapping) == false)
continue;

if (mValue is ICollection coll and not byte[])
{
foreach (var value in coll)
serializeMemberValue(elementName, value, writer, filter);
}
else
serializeMemberValue(elementName, mValue, writer, filter);
var elementName = propertyMapping?.Choice == ChoiceType.DatatypeChoice ?
addSuffixToElementName(mKey, mValue) : mKey;

filter?.LeaveMember(mKey, mValue, propertyMapping);
if (mValue is ICollection coll and not byte[])
{
foreach (var value in coll)
serializeMemberValue(elementName, value, writer, filter);
}
else
serializeMemberValue(elementName, mValue, writer, filter);

filter?.LeaveMember(mKey, mValue, propertyMapping);
}
}

private static string addSuffixToElementName(string elementName, object elementValue)
private static string addSuffixToElementName(string elementName, object elementValue)
{
var typeName = elementValue switch
{
var typeName = elementValue switch
{
IEnumerable<Base> ib => ib.FirstOrDefault()?.TypeName,
Base b => b.TypeName,
_ => null
};
IEnumerable<Base> ib => ib.FirstOrDefault()?.TypeName,
Base b => b.TypeName,
_ => null
};

return typeName is null ? elementName : elementName + char.ToUpperInvariant(typeName[0]) + typeName.Substring(1);
}
return typeName is null ? elementName : elementName + char.ToUpperInvariant(typeName[0]) + typeName[1..];
}


private void serializeMemberValue(string elementName, object value, XmlWriter writer, SerializationFilter? filter)
private void serializeMemberValue(string elementName, object value, XmlWriter writer, SerializationFilter? filter)
{
switch (value)
{
if (value is XHtml xhtml)
{
case XHtml xhtml:
writer.WriteRaw(xhtml.Value);
}
else if (value is IReadOnlyDictionary<string, object> complex)
{
break;
case Base complex:
writer.WriteStartElement(elementName, XmlNs.FHIR);
serializeInternal(complex, writer, filter);
writer.WriteEndElement();
}
else
break;
default:
SerializePrimitiveValue(elementName, value, writer);
}

/// <summary>
/// Serialize a primitive .NET value that may occur in the POCOs into XML.
/// </summary>
/// <remarks>
/// To allow for future additions to the POCOs the list of primitives supported here
/// is larger than the set used by the current POCOs. Note that <c>DateTimeOffset</c>c> and
/// <c>byte[]</c> are considered to be "primitive" values here (used as the value in
/// <see cref="Instant"/> and <see cref="Base64Binary"/>).
/// </remarks>
protected virtual void SerializePrimitiveValue(string elementName, object value, XmlWriter writer)
{
if (value is null) return; // Don't write a null property

var literal = value switch
{
int i32 => XmlConvert.ToString(i32),
uint ui32 => XmlConvert.ToString(ui32),
long i64 => XmlConvert.ToString(i64),
ulong ui64 => XmlConvert.ToString(ui64),
float si => XmlConvert.ToString(si),
double dbl => XmlConvert.ToString(dbl),
decimal dec => XmlConvert.ToString(dec),
// A little note about trimming and whitespaces. The spec says:
// "Implementers SHOULD trim leading and trailing whitespace before writing and SHOULD trim leading
// and trailing whitespace when reading attribute values (for XML schema conformance)"
string s => s.Trim(),
bool b => XmlConvert.ToString(b),
DateTimeOffset dto => ElementModel.Types.DateTime.FormatDateTimeOffset(dto),
byte[] bytes => Convert.ToBase64String(bytes),
_ => throw new FormatException($"There is no known serialization for type {value.GetType()} into an Xml primitive property value.")
};

writer.WriteAttributeString(elementName, ns: null, value: literal);
break;
}
}
}

#if NETSTANDARD

file static class KvpExtensions
{
public static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
/// <summary>
/// Serialize a primitive .NET value that may occur in the POCOs into XML.
/// </summary>
/// <remarks>
/// To allow for future additions to the POCOs the list of primitives supported here
/// is larger than the set used by the current POCOs. Note that <c>DateTimeOffset</c>c> and
/// <c>byte[]</c> are considered to be "primitive" values here (used as the value in
/// <see cref="Instant"/> and <see cref="Base64Binary"/>).
/// </remarks>
protected virtual void SerializePrimitiveValue(string elementName, object value, XmlWriter writer)
{
key = kvp.Key;
value = kvp.Value;
var literal = value switch
{
int i32 => XmlConvert.ToString(i32),
uint ui32 => XmlConvert.ToString(ui32),
long i64 => XmlConvert.ToString(i64),
ulong ui64 => XmlConvert.ToString(ui64),
float si => XmlConvert.ToString(si),
double dbl => XmlConvert.ToString(dbl),
decimal dec => XmlConvert.ToString(dec),
// A little note about trimming and whitespaces. The spec says:
// "Implementers SHOULD trim leading and trailing whitespace before writing and SHOULD trim leading
// and trailing whitespace when reading attribute values (for XML schema conformance)"
string s => s.Trim(),
bool b => XmlConvert.ToString(b),
DateTimeOffset dto => ElementModel.Types.DateTime.FormatDateTimeOffset(dto),
byte[] bytes => Convert.ToBase64String(bytes),
_ => throw new FormatException($"There is no known serialization for type {value.GetType()} into an Xml primitive property value.")
};

writer.WriteAttributeString(elementName, ns: null, value: literal);
}
}

#endif

#nullable restore
}
44 changes: 0 additions & 44 deletions src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,50 +85,6 @@ private FhirJsonConverter(IFhirSerializationEngine engine)
this._engine = (PocoSerializationEngine)engine;
}

/// <summary>
/// Constructs a <see cref="JsonConverter{T}"/> that (de)serializes FHIR json for the
/// POCOs in a given assembly.
/// </summary>
/// <param name="assembly">The assembly containing classes to be used for deserialization.</param>
[Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")]
public FhirJsonConverter(
Assembly assembly) : this(ModelInspector.ForAssembly(assembly))
{
// nothing
}

[Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")]
public FhirJsonConverter(
Assembly assembly, FhirJsonPocoSerializerSettings? serializerSettings = null, FhirJsonPocoDeserializerSettings? deserializerSettings = null,
Predicate<CodedException>? ignoreFilter = null) :
this(FhirSerializationEngineFactory.Custom(ModelInspector.ForAssembly(assembly), ignoreFilter ?? (_ => false), deserializerSettings, serializerSettings))
{ }

/// <summary>
/// Constructs a <see cref="JsonConverter{T}"/> that (de)serializes FHIR json for the
/// POCOs in a given assembly.
/// </summary>
/// <param name="inspector">The <see cref="ModelInspector" /> containing classes to be used for deserialization.</param>
[Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")]
public FhirJsonConverter(
ModelInspector inspector) : this(FhirSerializationEngineFactory.Strict(inspector))
{
}

/// <summary>
/// Constructs a <see cref="JsonConverter{T}"/> that (de)serializes FHIR json for the
/// POCOs in a given assembly.
/// </summary>
/// <param name="deserializer">A custom deserializer to be used by the json converter.</param>
/// <param name="serializer">A customer serializer to be used by the json converter.</param>
/// <remarks>Since the standard serializer/deserializer will allow you to override its behaviour to produce
/// custom behaviour, this constructor will allow the developer to use such custom serializers/deserializers instead
/// of the defaults.</remarks>
[Obsolete("Using this directly is not recommended. Instead, try creating a converter using the .ForFhir static method of the JsonSerializerOptions class")]
public FhirJsonConverter(BaseFhirJsonPocoDeserializer deserializer, BaseFhirJsonPocoSerializer serializer) : this(FhirSerializationEngineFactory.WithCustomJsonSerializers(deserializer, serializer))
{
}

/// <summary>
/// Determines whether the specified type can be converted.
/// </summary>
Expand Down
Loading
Loading