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

3049 Use poco based parsers #3073

Merged
merged 11 commits into from
Mar 19, 2025
418 changes: 370 additions & 48 deletions src/Hl7.Fhir.Base/CompatibilitySuppressions.xml

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions src/Hl7.Fhir.Base/ElementModel/ElementNodeComparator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ public static TreeComparisonResult IsEqualTo(this ITypedElement expected, ITyped
{
if (expected.Name != actual.Name)
return TreeComparisonResult.Fail(actual.Location, $"name: was '{actual.Name}', expected '{expected.Name}'");
if (!Object.Equals(expected.Value, actual.Value))
return TreeComparisonResult.Fail(actual.Location, $"value: was '{actual.Value}', expected '{expected.Value}'");

var cleanedValueL = expected.Value is string el ? el.Replace("\r", "") : expected.Value;
var cleanedValueR = actual.Value is string er ? er.Replace("\r", "") : actual.Value;

if (!Equals(cleanedValueL, cleanedValueR))
return TreeComparisonResult.Fail(actual.Location, $"value: was '{cleanedValueL}', expected '{cleanedValueR}'");
if (expected.InstanceType != actual.InstanceType && actual.InstanceType != null)
return TreeComparisonResult.Fail(actual.Location, $"type: was '{actual.InstanceType}', expected '{expected.InstanceType}'");
if (expected.Location != actual.Location) TreeComparisonResult.Fail(actual.Location, $"Path: was '{actual.Location}', expected '{expected.Location}'");
if (expected.Location != actual.Location)
TreeComparisonResult.Fail(actual.Location, $"Path: was '{actual.Location}', expected '{expected.Location}'");

// Ignore ordering (only relevant to xml)
var childrenExp = expected.Children().OrderBy(e => e.Name);
Expand All @@ -51,4 +56,4 @@ public static TreeComparisonResult IsEqualTo(this ITypedElement expected, ITyped
return TreeComparisonResult.OK;
}
}
}
}
11 changes: 11 additions & 0 deletions src/Hl7.Fhir.Base/ElementModel/PocoBuilderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ public void CopyTo(PocoBuilderSettings other)
other.ExceptionHandler = ExceptionHandler;
}

/// <summary>
/// Initializes the current instance from the specified <see cref="PocoBuilderSettings"/> instance.
/// </summary>
public void CopyFrom(ParserSettings settings)
{
if (settings == null) throw Error.ArgumentNull(nameof(settings));

AllowUnrecognizedEnums = settings.AllowUnrecognizedEnums;
IgnoreUnknownMembers = settings.AcceptUnknownMembers;
}

/// <summary>Creates a new <see cref="PocoBuilderSettings"/> object that is a copy of the current instance.</summary>
public PocoBuilderSettings Clone() => new(this);

Expand Down
20 changes: 10 additions & 10 deletions src/Hl7.Fhir.Base/Rest/BaseFhirClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ public async Task DeleteHistoryVersionAsync(string location, CancellationToken?

public Task<TResource?> executeAsync<TResource>(Model.Bundle tx, HttpStatusCode expect, CancellationToken? ct) where TResource : Model.Resource
{
return executeAsync<TResource>(tx, new[] { expect }, ct);
return executeAsync<TResource>(tx, [expect], ct);
}

private async Task<TResource?> executeAsync<TResource>(Bundle tx, IEnumerable<HttpStatusCode> expect, CancellationToken? ct) where TResource : Resource
Expand Down Expand Up @@ -974,7 +974,9 @@ private static bool isPostOrPutOrPatch(HttpMethod method) =>

private IFhirSerializationEngine getSerializationEngine()
{
return Settings.SerializationEngine ?? FhirSerializationEngineFactory.Legacy.FromParserSettings(Inspector, Settings.ParserSettings ?? new());
#pragma warning disable CS0618 // Type or member is obsolete
return Settings.SerializationEngine ?? FhirSerializationEngineFactory.Legacy.FromParserSettings(Inspector, Settings.ParserSettings);
#pragma warning restore CS0618 // Type or member is obsolete
}

private async Task verifyServerVersion(CancellationToken ct)
Expand All @@ -985,12 +987,12 @@ private async Task verifyServerVersion(CancellationToken ct)
_versionChecked = true; // So we can now start calling Conformance() without getting into a loop

string? serverVersion;
var settings = Settings;
var originalEngine = Settings.SerializationEngine;

try
{
Settings = Settings.Clone();
Settings.ParserSettings = new() { AllowUnrecognizedEnums = true };
// Temporarily set the engine to ignore most errors.
Settings.SerializationEngine = FhirSerializationEngineFactory.Ostrich(Inspector);
serverVersion = await getFhirVersionOfServer(ct).ConfigureAwait(false);
}
catch (FormatException fe)
Expand All @@ -1000,8 +1002,8 @@ private async Task verifyServerVersion(CancellationToken ct)
}
finally
{
// put back the original settings
Settings = settings;
// put back the original engine
Settings.SerializationEngine = originalEngine;
}

if (serverVersion == null)
Expand Down Expand Up @@ -1059,6 +1061,4 @@ public void Dispose()
GC.SuppressFinalize(this);
}
#endregion
}

#nullable restore
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ public static class FhirClientSerializationEngineExtensions
public static BaseFhirClient WithLegacySerializer(this BaseFhirClient client)
{
client.Settings.SerializationEngine =
FhirSerializationEngineFactory.Legacy.FromParserSettings(client.Inspector, client.Settings.ParserSettings ?? new());
FhirSerializationEngineFactory.Legacy.FromParserSettings(client.Inspector,
#pragma warning disable CS0618 // Type or member is obsolete
client.Settings.ParserSettings);
#pragma warning restore CS0618 // Type or member is obsolete
return client;
}

Expand Down Expand Up @@ -96,8 +99,8 @@ public static BaseFhirClient WithOstrichModeSerializer(this BaseFhirClient clien

public static BaseFhirClient WithCustomIgnoreListSerializer(this BaseFhirClient client, string[] ignoreList)
{
var xmlSettings = new FhirXmlPocoDeserializerSettings().Ignoring(ignoreList);
var jsonSettings = new FhirJsonConverterOptions().Ignoring(ignoreList);
var xmlSettings = new ParserSettings().Ignoring(ignoreList);
var jsonSettings = (FhirJsonConverterOptions)new FhirJsonConverterOptions().Ignoring(ignoreList);

client.Settings.SerializationEngine = FhirSerializationEngineFactory.Custom(client.Inspector, jsonSettings, xmlSettings);

Expand Down
7 changes: 5 additions & 2 deletions src/Hl7.Fhir.Base/Rest/FhirClientSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ public class FhirClientSettings
/// <summary>
/// ParserSettings for the pre-5.0 SDK parsers. Are only used when <see cref="SerializationEngine"/> is not set.
/// </summary>
public ParserSettings? ParserSettings = ParserSettings.CreateDefault();
[Obsolete(
"Use the SerializationEngine setting instead, chosing one of the options on FhirSerializationEngineFactory.")]
public ParserSettings ParserSettings = new ParserSettings().UsingMode(DeserializationMode.Recoverable);

/// <summary>
/// How to transfer binary data when sending data to a Binary endpoint.
Expand All @@ -104,7 +106,6 @@ public class FhirClientSettings
/// Whether we ask the server to return us binary data or a Binary resource.
/// </summary>
public BinaryTransferBehaviour BinaryReceivePreference = BinaryTransferBehaviour.UseData;


public FhirClientSettings() { }

Expand All @@ -123,7 +124,9 @@ public void CopyTo(FhirClientSettings other)
{
if (other == null) throw Error.ArgumentNull(nameof(other));

#pragma warning disable CS0618 // Type or member is obsolete
other.ParserSettings = ParserSettings;
#pragma warning restore CS0618 // Type or member is obsolete
other.PreferCompressedResponses = PreferCompressedResponses;
other.PreferredFormat = PreferredFormat;
other.ReturnPreference = ReturnPreference;
Expand Down
50 changes: 37 additions & 13 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,16 @@

namespace Hl7.Fhir.Serialization;

/// <summary>
/// Deserializes a byte stream into FHIR POCO objects.
/// </summary>
/// <remarks>The serializer uses the format documented in https://www.hl7.org/fhir/json.html. </remarks>
public class BaseFhirJsonPocoDeserializer
public class BaseFhirJsonPocoDeserializer : BaseFhirJsonParser
{
/// <summary>
/// Initializes an instance of the deserializer.
/// </summary>
/// <param name="assembly">Assembly containing the POCO classes to be used for deserialization.</param>
[Obsolete("Use the constructor that takes a ModelInspector instead. " +
"You can find the right ModelInspector for an assembly by calling ModelInspector.ForAssembly(assembly).")]
public BaseFhirJsonPocoDeserializer(Assembly assembly) : this(ModelInspector.ForAssembly(assembly), new FhirJsonConverterOptions())
public BaseFhirJsonPocoDeserializer(Assembly assembly) : this(ModelInspector.ForAssembly(assembly),
new FhirJsonConverterOptions())
{
// Nothing
}
Expand All @@ -55,15 +52,43 @@ public class BaseFhirJsonPocoDeserializer
/// <param name="inspector">The <see cref="ModelInspector"/> containing the POCO classes to be used for deserialization.</param>
/// <param name="settings">A settings object to be used by this instance.</param>
public BaseFhirJsonPocoDeserializer(ModelInspector inspector, FhirJsonConverterOptions settings)
: base(inspector, settings)
{
// nothing
}
}


/// <summary>
/// Deserializes Json into FHIR POCO objects.
/// </summary>
/// <remarks>The serializer uses the format documented in https://www.hl7.org/fhir/json.html. </remarks>
public class BaseFhirJsonParser
{
/// <summary>
/// Initializes an instance of the deserializer.
/// </summary>
/// <param name="inspector">The <see cref="ModelInspector"/> containing the POCO classes to be used for deserialization.</param>
public BaseFhirJsonParser(ModelInspector inspector) : this(inspector, new ParserSettings())
{
// nothing
}

/// <summary>
/// Initializes an instance of the deserializer.
/// </summary>
/// <param name="inspector">The <see cref="ModelInspector"/> containing the POCO classes to be used for deserialization.</param>
/// <param name="settings">A settings object to be used by this instance.</param>
public BaseFhirJsonParser(ModelInspector inspector, ParserSettings? settings)
{
Settings = settings;
Settings = settings ?? new ParserSettings();
_inspector = inspector;
}

/// <summary>
/// The settings that were passed to the constructor.
/// </summary>
public FhirJsonConverterOptions Settings { get; }
public ParserSettings Settings { get; set; }

private const string INSTANCE_VALIDATION_KEY_SUFFIX = ":instance";
private const string PROPERTY_VALIDATION_KEY_SUFFIX = ":property";
Expand All @@ -76,7 +101,7 @@ public BaseFhirJsonPocoDeserializer(ModelInspector inspector, FhirJsonConverterO
/// <param name="instance">The result of deserialization. May be incomplete when there are issues.</param>
/// <param name="issues">Issues encountered while deserializing. Will be empty when the function returns true.</param>
/// <returns><c>false</c> if there are issues, <c>true</c> otherwise.</returns>
/// <remarks>The <see cref="FhirXmlPocoDeserializerSettings.ExceptionFilter"/> influences which issues are returned.</remarks>
/// <remarks>The <see cref="ParserSettings.ExceptionFilter"/> influences which issues are returned.</remarks>
public bool TryDeserializeResource(ref Utf8JsonReader reader, [NotNullWhen(true)] out Resource? instance, out IEnumerable<CodedException> issues)
{
if (reader.CurrentState.Options.CommentHandling is not JsonCommentHandling.Skip and not JsonCommentHandling.Disallow)
Expand All @@ -103,7 +128,7 @@ public bool TryDeserializeResource(ref Utf8JsonReader reader, [NotNullWhen(true)
/// <param name="instance">The result of deserialization. May be incomplete when there are issues.</param>
/// <param name="issues">Issues encountered while deserializing. Will be empty when the function returns true.</param>
/// <returns><c>false</c> if there are issues, <c>true</c> otherwise.</returns>
/// <remarks>The <see cref="FhirXmlPocoDeserializerSettings.ExceptionFilter"/> influences which issues are returned.</remarks>
/// <remarks>The <see cref="ParserSettings.ExceptionFilter"/> influences which issues are returned.</remarks>
public bool TryDeserializeObject(Type targetType, ref Utf8JsonReader reader, [NotNullWhen(true)] out Base? instance, out IEnumerable<CodedException> issues)
{
if (reader.CurrentState.Options.CommentHandling is not JsonCommentHandling.Skip and not JsonCommentHandling.Disallow)
Expand Down Expand Up @@ -496,7 +521,6 @@ public void ScheduleDelayedValidation(string key, Action validation)
_validations[key] = validation;
}

//public CodedValidationException[] Run() => _validations.Values.SelectMany(delayed => delayed()).ToArray();
public void RunDelayedValidation()
{
foreach (var validation in _validations.Values) validation();
Expand Down Expand Up @@ -713,7 +737,7 @@ FhirJsonPocoDeserializerState state
(object? partial, ERR? error) result = reader.TokenType switch
{
JsonTokenType.Null => (null, ERR.EXPECTED_PRIMITIVE_NOT_NULL(ref reader, pathStack.GetInstancePath())),
JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()) => (reader.GetString(), ERR.PROPERTY_MAY_NOT_BE_EMPTY(ref reader, pathStack.GetInstancePath())),
JsonTokenType.String when string.IsNullOrWhiteSpace(reader.GetString()) => (reader.GetString(), ERR.PROPERTY_MAY_NOT_BE_EMPTY(ref reader, pathStack.GetInstancePath())),
JsonTokenType.String => (reader.GetString(), null),
JsonTokenType.Number => (tryGetMatchingNumber(ref reader, valuePropertyType), null),
JsonTokenType.True or JsonTokenType.False => (reader.GetBoolean(), null),
Expand All @@ -737,7 +761,7 @@ JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()) => (reader.Ge
/// This function tries to map from the json-format "generic" number to the kind of numeric type defined in the POCO.
/// </summary>
/// <remarks>Reader must be positioned on a number token. This function will not move the reader to the next token.</remarks>
private static object? tryGetMatchingNumber(ref Utf8JsonReader reader, Type implementingType)
private static object tryGetMatchingNumber(ref Utf8JsonReader reader, Type implementingType)
{
if (reader.TokenType != JsonTokenType.Number)
throw new InvalidOperationException($"Cannot read a numeric when reader is on a {reader.TokenType}. " +
Expand Down
35 changes: 0 additions & 35 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirParser.cs

This file was deleted.

Loading