diff --git a/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml b/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml index f0caf560f1..4964b56bda 100644 --- a/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0001 @@ -113,6 +113,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0001 + T:Hl7.Fhir.Serialization.DeserializerModes + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0001 T:Hl7.Fhir.Serialization.FhirJsonBuilderExtensions @@ -169,6 +176,20 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0001 + T:Hl7.Fhir.Serialization.PrimitiveParseHandler + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0001 + T:Hl7.Fhir.Serialization.SerializationEngineExtensions + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0001 T:Hl7.Fhir.Serialization.SerializerSettings @@ -344,6 +365,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0001 + T:Hl7.Fhir.Serialization.DeserializerModes + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0001 T:Hl7.Fhir.Serialization.FhirJsonBuilderExtensions @@ -400,6 +428,20 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0001 + T:Hl7.Fhir.Serialization.PrimitiveParseHandler + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0001 + T:Hl7.Fhir.Serialization.SerializationEngineExtensions + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0001 T:Hl7.Fhir.Serialization.SerializerSettings @@ -477,6 +519,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + F:Hl7.Fhir.Serialization.BaseFhirParser.Settings + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 F:Hl7.Fhir.Serialization.FhirJsonException.INCORRECT_BASE64_DATA_CODE @@ -2024,6 +2073,41 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.#ctor(Hl7.Fhir.Introspection.ModelInspector,Hl7.Fhir.Serialization.ParserSettings) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse(Hl7.Fhir.ElementModel.ISourceNode,System.Type) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse(Hl7.Fhir.ElementModel.ITypedElement) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse``1(Hl7.Fhir.ElementModel.ISourceNode) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse``1(Hl7.Fhir.ElementModel.ITypedElement) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.BaseFhirXmlPocoSerializer.#ctor(Hl7.Fhir.Specification.FhirRelease) @@ -2052,6 +2136,20 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Serialization.DataAnnotationDeserialzationValidator.#ctor(Hl7.Fhir.Validation.NarrativeValidationKind) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.DataAnnotationDeserialzationValidator.get_NarrativeValidation + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.DataAnnotationDeserialzationValidator.ValidateInstance(System.Object,Hl7.Fhir.Serialization.InstanceDeserializationContext@,Hl7.Fhir.Validation.CodedValidationException[]@) @@ -2451,6 +2549,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + F:Hl7.Fhir.Serialization.BaseFhirParser.Settings + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 F:Hl7.Fhir.Serialization.FhirJsonException.INCORRECT_BASE64_DATA_CODE @@ -3998,6 +4103,41 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.#ctor(Hl7.Fhir.Introspection.ModelInspector,Hl7.Fhir.Serialization.ParserSettings) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse(Hl7.Fhir.ElementModel.ISourceNode,System.Type) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse(Hl7.Fhir.ElementModel.ITypedElement) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse``1(Hl7.Fhir.ElementModel.ISourceNode) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.BaseFhirParser.Parse``1(Hl7.Fhir.ElementModel.ITypedElement) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.BaseFhirXmlPocoSerializer.#ctor(Hl7.Fhir.Specification.FhirRelease) @@ -4026,6 +4166,20 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Serialization.DataAnnotationDeserialzationValidator.#ctor(Hl7.Fhir.Validation.NarrativeValidationKind) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.DataAnnotationDeserialzationValidator.get_NarrativeValidation + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.DataAnnotationDeserialzationValidator.ValidateInstance(System.Object,Hl7.Fhir.Serialization.InstanceDeserializationContext@,Hl7.Fhir.Validation.CodedValidationException[]@) diff --git a/src/Hl7.Fhir.Base/ElementModel/PocoBuilderSettings.cs b/src/Hl7.Fhir.Base/ElementModel/PocoBuilderSettings.cs index 011579862f..6d814406dd 100644 --- a/src/Hl7.Fhir.Base/ElementModel/PocoBuilderSettings.cs +++ b/src/Hl7.Fhir.Base/ElementModel/PocoBuilderSettings.cs @@ -6,71 +6,70 @@ * available at https://github.com/FirelyTeam/firely-net-sdk/blob/master/LICENSE */ - +#nullable enable using Hl7.Fhir.Utility; using System; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// Configuration settings for the PocoBuilder class. +public class PocoBuilderSettings { - /// Configuration settings for the PocoBuilder class. - public class PocoBuilderSettings - { - /// - /// Do not throw when encountering values not parseable as a member of an enumeration in a Poco. - /// - public bool AllowUnrecognizedEnums { get; set; } // = false + /// + /// Do not throw when encountering values not parseable as a member of an enumeration in a Poco. + /// + public bool AllowUnrecognizedEnums { get; set; } // = false - /// - /// Do not throw when the data has an element that does not map to a property in the Poco. - /// - public bool IgnoreUnknownMembers { get; set; } // = false + /// + /// Do not throw when the data has an element that does not map to a property in the Poco. + /// + public bool IgnoreUnknownMembers { get; set; } // = false - /// - /// Allow to parse a FHIR dateTime values into an element of type date. - /// - /// - /// Needed for backward compatibility with old parser for resources which were saved and considered valid in the past. - /// > - [Obsolete("Needed for backward compatibility with old parser for resources which were saved and considered valid in the past. " + - "Should not be used in new code.")] - public bool TruncateDateTimeToDate { get; set; } + /// + /// Allow to parse a FHIR dateTime values into an element of type date. + /// + /// + /// Needed for backward compatibility with old parser for resources which were saved and considered valid in the past. + /// > + [Obsolete("Needed for backward compatibility with old parser for resources which were saved and considered valid in the past. " + + "Should not be used in new code.")] + public bool TruncateDateTimeToDate { get; set; } - /// - /// A Handler to permit intercepting Exceptions during parsing - /// - public ExceptionNotificationHandler ExceptionHandler { get; set; } + /// + /// A Handler to permit intercepting Exceptions during parsing + /// + public ExceptionNotificationHandler? ExceptionHandler { get; set; } - /// Default constructor. Creates a new instance with default property values. - public PocoBuilderSettings() { } + /// Default constructor. Creates a new instance with default property values. + public PocoBuilderSettings() { } - /// Clone constructor. Generates a new instance initialized from the state of the specified instance. - /// The specified argument is null. - public PocoBuilderSettings(PocoBuilderSettings other) - { - if (other == null) throw Error.ArgumentNull(nameof(other)); - other.CopyTo(this); - } + /// Clone constructor. Generates a new instance initialized from the state of the specified instance. + /// The specified argument is null. + public PocoBuilderSettings(PocoBuilderSettings other) + { + if (other == null) throw Error.ArgumentNull(nameof(other)); + other.CopyTo(this); + } - /// Copy all configuration settings to another instance. - /// Another instance. - /// The specified argument is null. - public void CopyTo(PocoBuilderSettings other) - { - if (other == null) throw Error.ArgumentNull(nameof(other)); + /// Copy all configuration settings to another instance. + /// Another instance. + /// The specified argument is null. + public void CopyTo(PocoBuilderSettings other) + { + if (other == null) throw Error.ArgumentNull(nameof(other)); - other.AllowUnrecognizedEnums = AllowUnrecognizedEnums; - other.IgnoreUnknownMembers = IgnoreUnknownMembers; + other.AllowUnrecognizedEnums = AllowUnrecognizedEnums; + other.IgnoreUnknownMembers = IgnoreUnknownMembers; #pragma warning disable CS0618 // Type or member is obsolete - other.TruncateDateTimeToDate = TruncateDateTimeToDate; + other.TruncateDateTimeToDate = TruncateDateTimeToDate; #pragma warning restore CS0618 // Type or member is obsolete - other.ExceptionHandler = ExceptionHandler; - } + other.ExceptionHandler = ExceptionHandler; + } - /// Creates a new object that is a copy of the current instance. - public PocoBuilderSettings Clone() => new PocoBuilderSettings(this); + /// Creates a new object that is a copy of the current instance. + public PocoBuilderSettings Clone() => new(this); - /// Creates a new instance with default property values. - public static PocoBuilderSettings CreateDefault() => new PocoBuilderSettings(); - } + /// Creates a new instance with default property values. + public static PocoBuilderSettings CreateDefault() => new PocoBuilderSettings(); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Rest/FhirClientSerializationEngineExtensions.cs b/src/Hl7.Fhir.Base/Rest/FhirClientSerializationEngineExtensions.cs index 7b9432e2e7..3bef34dc37 100644 --- a/src/Hl7.Fhir.Base/Rest/FhirClientSerializationEngineExtensions.cs +++ b/src/Hl7.Fhir.Base/Rest/FhirClientSerializationEngineExtensions.cs @@ -96,7 +96,11 @@ public static BaseFhirClient WithOstrichModeSerializer(this BaseFhirClient clien public static BaseFhirClient WithCustomIgnoreListSerializer(this BaseFhirClient client, string[] ignoreList) { - client.Settings.SerializationEngine = FhirSerializationEngineFactory.Custom(client.Inspector, ignoreList.IsInList()); + var xmlSettings = new FhirXmlPocoDeserializerSettings().Ignoring(ignoreList); + var jsonSettings = new FhirJsonConverterOptions().Ignoring(ignoreList); + + client.Settings.SerializationEngine = FhirSerializationEngineFactory.Custom(client.Inspector, jsonSettings, xmlSettings); + return client; } diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs index 697cc55783..e044200dee 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs @@ -33,31 +33,22 @@ public class BaseFhirJsonPocoDeserializer /// Initializes an instance of the deserializer. /// /// Assembly containing the POCO classes to be used for deserialization. - public BaseFhirJsonPocoDeserializer(Assembly assembly) : this(assembly, new()) + [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()) { - // nothing + // Nothing } /// /// Initializes an instance of the deserializer. /// /// The containing the POCO classes to be used for deserialization. - public BaseFhirJsonPocoDeserializer(ModelInspector inspector) : this(inspector, new()) + public BaseFhirJsonPocoDeserializer(ModelInspector inspector) : this(inspector, new FhirJsonConverterOptions()) { // nothing } - /// - /// Initializes an instance of the deserializer. - /// - /// Assembly containing the POCO classes to be used for deserialization. - /// A settings object to be used by this instance. - public BaseFhirJsonPocoDeserializer(Assembly assembly, FhirJsonConverterOptions settings) - { - Settings = settings; - _inspector = ModelInspector.ForAssembly(assembly ?? throw new ArgumentNullException(nameof(assembly))); - } - /// /// Initializes an instance of the deserializer. /// @@ -85,6 +76,7 @@ public BaseFhirJsonPocoDeserializer(ModelInspector inspector, FhirJsonConverterO /// The result of deserialization. May be incomplete when there are issues. /// Issues encountered while deserializing. Will be empty when the function returns true. /// false if there are issues, true otherwise. + /// The influences which issues are returned. public bool TryDeserializeResource(ref Utf8JsonReader reader, [NotNullWhen(true)] out Resource? instance, out IEnumerable issues) { if (reader.CurrentState.Options.CommentHandling is not JsonCommentHandling.Skip and not JsonCommentHandling.Disallow) @@ -96,9 +88,11 @@ public bool TryDeserializeResource(ref Utf8JsonReader reader, [NotNullWhen(true) FhirJsonPocoDeserializerState state = new(); instance = DeserializeResourceInternal(ref reader, state, stayOnLastToken: true); - issues = state.Errors; + issues = Settings.ExceptionFilter is { } filter + ? state.Errors.Remove(filter) + : state.Errors; - return !state.Errors.HasExceptions; + return !issues.Any(); } /// @@ -109,6 +103,7 @@ public bool TryDeserializeResource(ref Utf8JsonReader reader, [NotNullWhen(true) /// The result of deserialization. May be incomplete when there are issues. /// Issues encountered while deserializing. Will be empty when the function returns true. /// false if there are issues, true otherwise. + /// The influences which issues are returned. public bool TryDeserializeObject(Type targetType, ref Utf8JsonReader reader, [NotNullWhen(true)] out Base? instance, out IEnumerable issues) { if (reader.CurrentState.Options.CommentHandling is not JsonCommentHandling.Skip and not JsonCommentHandling.Disallow) @@ -122,17 +117,20 @@ public bool TryDeserializeObject(Type targetType, ref Utf8JsonReader reader, [No $"therefore not be used for deserialization. " + reader.GenerateLocationMessage(), nameof(targetType)); // Create a new instance of the object to read the members into. - if (mapping.Factory() is Base result) - { - var state = new FhirJsonPocoDeserializerState(); - deserializeObjectInto(result, mapping, ref reader, DeserializedObjectKind.Complex, state, stayOnLastToken: true); + if (mapping.Factory() is not Base result) + throw new ArgumentException( + $"Can only deserialize into subclasses of class {nameof(Base)}. " + reader.GenerateLocationMessage(), + nameof(targetType)); - instance = result; - issues = state.Errors; - return !state.Errors.HasExceptions; - } - else - throw new ArgumentException($"Can only deserialize into subclasses of class {nameof(Base)}. " + reader.GenerateLocationMessage(), nameof(targetType)); + var state = new FhirJsonPocoDeserializerState(); + deserializeObjectInto(result, mapping, ref reader, DeserializedObjectKind.Complex, state, stayOnLastToken: true); + + instance = result; + issues = Settings.ExceptionFilter is { } filter + ? state.Errors.Remove(filter) + : state.Errors; + + return !issues.Any(); } internal Resource? DeserializeResourceInternal(ref Utf8JsonReader reader, FhirJsonPocoDeserializerState state, bool stayOnLastToken) @@ -308,7 +306,7 @@ private void deserializeObjectInto( // to avoid spurious error messages. if (Settings.Validator is not null && kind != DeserializedObjectKind.FhirPrimitive && (Settings.ValidateOnFailedParse || state.Errors.Count == oldErrorCount)) { - var context = new InstanceDeserializationContext(state.Path, line, pos, mapping); + var context = new InstanceDeserializationContext(state.Path, line, pos, mapping, Settings.NarrativeValidation); PocoDeserializationHelper.RunInstanceValidation(target, Settings.Validator, context, state.Errors); } } @@ -397,7 +395,8 @@ FhirJsonPocoDeserializerState state state.Path, propertyName, line, pos, - propertyMapping); + propertyMapping, + Settings.NarrativeValidation); // If this is a FhirPrimitive, make sure we delay validation until we had the // chance to encounter both the `name` and `_name` property. @@ -650,7 +649,7 @@ FhirJsonPocoDeserializerState state // the `name` and `_name` property. if (Settings.Validator is not null && (Settings.ValidateOnFailedParse || oldErrorCount == state.Errors.Count)) { - var context = new InstanceDeserializationContext(state.Path, line, pos, propertyValueMapping); + var context = new InstanceDeserializationContext(state.Path, line, pos, propertyValueMapping, Settings.NarrativeValidation); if (parsingState is null) PocoDeserializationHelper.RunInstanceValidation(targetPrimitive, Settings.Validator, context, state.Errors); else diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirParser.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirParser.cs index 226444ea3d..43e0357f88 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirParser.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirParser.cs @@ -6,57 +6,30 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ -using Hl7.Fhir.ElementModel; -using Hl7.Fhir.Introspection; -using Hl7.Fhir.Model; -using System; +#nullable enable -namespace Hl7.Fhir.Serialization -{ - public class BaseFhirParser - { - public readonly ParserSettings Settings; - private readonly ModelInspector _inspector; +namespace Hl7.Fhir.Serialization; - public BaseFhirParser(ModelInspector inspector, ParserSettings settings = null) +public class BaseFhirParser +{ + internal static PocoBuilderSettings BuildPocoBuilderSettings(ParserSettings ps) => + new() { - Settings = settings?.Clone() ?? new ParserSettings(); - _inspector = inspector; - } - - internal static PocoBuilderSettings BuildPocoBuilderSettings(ParserSettings ps) => - new() - { - AllowUnrecognizedEnums = ps.AllowUnrecognizedEnums, - IgnoreUnknownMembers = ps.AcceptUnknownMembers, - ExceptionHandler = ps.ExceptionHandler, + AllowUnrecognizedEnums = ps.AllowUnrecognizedEnums, + IgnoreUnknownMembers = ps.AcceptUnknownMembers, + ExceptionHandler = ps.ExceptionHandler, #pragma warning disable CS0618 // Type or member is obsolete - TruncateDateTimeToDate = ps.TruncateDateTimeToDate + TruncateDateTimeToDate = ps.TruncateDateTimeToDate #pragma warning restore CS0618 // Type or member is obsolete - }; - - internal static FhirXmlParsingSettings BuildXmlParsingSettings(ParserSettings settings) => - new() - { - DisallowSchemaLocation = settings.DisallowXsiAttributesOnRoot, - PermissiveParsing = settings.PermissiveParsing, - }; - - internal static FhirJsonParsingSettings BuildJsonParserSettings(ParserSettings settings) => - new() - { - AllowJsonComments = false, - PermissiveParsing = settings.PermissiveParsing - }; + }; + internal static FhirXmlParsingSettings BuildXmlParsingSettings(ParserSettings settings) => + new() + { + DisallowSchemaLocation = settings.DisallowXsiAttributesOnRoot, + PermissiveParsing = settings.PermissiveParsing, + }; - public Base Parse(ITypedElement element) => element.ToPoco(_inspector, BuildPocoBuilderSettings(Settings)); - - public T Parse(ITypedElement element) where T : Base => element.ToPoco(_inspector, BuildPocoBuilderSettings(Settings)); - - public Base Parse(ISourceNode node, Type type = null) => node.ToPoco(_inspector, type, BuildPocoBuilderSettings(Settings)); - - public T Parse(ISourceNode node) where T : Base => node.ToPoco(_inspector, BuildPocoBuilderSettings(Settings)); - } - + internal static FhirJsonParsingSettings BuildJsonParserSettings(ParserSettings settings) => + new() { AllowJsonComments = false, PermissiveParsing = settings.PermissiveParsing }; } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs index 4783b58b8c..a52722383e 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs @@ -70,6 +70,7 @@ public BaseFhirXmlPocoDeserializer(ModelInspector inspector, FhirXmlPocoDeserial /// The result of deserialization. May be incomplete when there are issues. /// Issues encountered while deserializing. Will be empty when the function returns true. /// false if there are issues, true otherwise. + /// The influences which issues are returned. public bool TryDeserializeResource(XmlReader reader, [NotNullWhen(true)] out Resource? instance, out IEnumerable issues) { FhirXmlPocoDeserializerState state = new(); @@ -84,9 +85,11 @@ public bool TryDeserializeResource(XmlReader reader, [NotNullWhen(true)] out Res } instance = DeserializeResourceInternal(reader, state); - issues = state.Errors; + issues = Settings.ExceptionFilter is { } filter + ? state.Errors.Remove(filter) + : state.Errors; - return !state.Errors.HasExceptions; + return !issues.Any(); } /// @@ -97,6 +100,7 @@ public bool TryDeserializeResource(XmlReader reader, [NotNullWhen(true)] out Res /// The result of deserialization. May be incomplete when there are issues. /// Issues encountered while deserializing. Will be empty when the function returns true. /// false if there are issues, true otherwise. + /// The influences which issues are returned. public bool TryDeserializeElement(Type targetType, XmlReader reader, [NotNullWhen(true)] out Base? instance, out IEnumerable issues) { FhirXmlPocoDeserializerState state = new(); @@ -108,8 +112,11 @@ public bool TryDeserializeElement(Type targetType, XmlReader reader, [NotNullWhe } instance = DeserializeElementInternal(targetType, reader, state); - issues = state.Errors; - return !state.Errors.HasExceptions; + issues = Settings.ExceptionFilter is { } filter + ? state.Errors.Remove(filter) + : state.Errors; + + return !issues.Any(); } internal Resource? DeserializeResourceInternal(XmlReader reader, FhirXmlPocoDeserializerState state) @@ -291,7 +298,8 @@ internal void DeserializeElementInto(Base target, ClassMapping mapping, XmlReade var context = new InstanceDeserializationContext( state.Path, lineNumber, position, - mapping!); + mapping, + Settings.NarrativeValidation); PocoDeserializationHelper.RunInstanceValidation(target, Settings.Validator, context, state.Errors); } @@ -336,7 +344,8 @@ private void deserializePropertyValue(Base target, XmlReader reader, FhirXmlPoco state.Path, // should this path GetPath or this? name, lineNumber, position, - propMapping); + propMapping, + Settings.NarrativeValidation); PocoDeserializationHelper.RunPropertyValidation(result, Settings.Validator, context, state.Errors); } diff --git a/src/Hl7.Fhir.Base/Serialization/CodedExceptionFilters.cs b/src/Hl7.Fhir.Base/Serialization/CodedExceptionFilters.cs new file mode 100644 index 0000000000..f307ae7ade --- /dev/null +++ b/src/Hl7.Fhir.Base/Serialization/CodedExceptionFilters.cs @@ -0,0 +1,95 @@ +#nullable enable + +using Hl7.Fhir.Utility; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Hl7.Fhir.Serialization; + +/// +/// Provides utility methods for building and combining s used to filter s. +/// +public static class CodedExceptionFilters +{ + /// + /// Creates a predicate that returns true if a is recoverable. + /// + public static readonly Predicate IsRecoverableIssue = + // Note that CodedValidationExceptions are coming from property validation, and so are by definition + // recoverable, since the data was already safely in the POCO by that time. + ce => FhirJsonException.RECOVERABLE_ISSUES.Contains(ce.ErrorCode) || + FhirXmlException.RECOVERABLE_ISSUES.Contains(ce.ErrorCode); + + /// + /// Creates a predicate that returns true if a signifies a backwards compatibility issue. + /// + public static readonly Predicate IsBackwardsCompatibilityIssue = + ce => FhirJsonException.BACKWARDS_COMPATIBILITY_ALLOWED_ISSUES.Contains(ce.ErrorCode) || + FhirXmlException.BACKWARDS_COMPATIBILITY_ALLOWED_ISSUES.Contains(ce.ErrorCode); + + /// + /// Combines two predicates for a with a logical AND. + /// + [return: NotNullIfNotNull(nameof(a))] + [return: NotNullIfNotNull(nameof(b))] + public static Predicate? And(this Predicate? a, Predicate? b) => + (a, b) switch + { + (a: null, b: not null) => b, + (a: not null, b: null) => a, + (a: not null, b: not null) => ce => a(ce) && b(ce), + _ => null + }; + + /// + /// Combines two predicates for a with a logical OR. + /// + [return: NotNullIfNotNull(nameof(a))] + [return: NotNullIfNotNull(nameof(b))] + public static Predicate? Or(this Predicate? a, Predicate? b) => + (a, b) switch + { + (a: null, b: not null) => b, + (a: not null, b: null) => a, + (a: not null, b: not null) => ce => a(ce) || b(ce), + _ => null + }; + + /// + /// Negates a predicate for a . + /// + /// + /// + [return: NotNullIfNotNull(nameof(a))] + public static Predicate? Negate(this Predicate? a) => + a is not null ? ce => !a(ce) : null; + + /// + /// Will restrict the predicate to only return true for the given list of error codes. + /// + public static Predicate Enforce(this Predicate? a, IEnumerable toEnforce) + { + var enforcer = toEnforce.IsInList().Negate(); + return a is null ? enforcer : a.And(enforcer); + } + + /// + /// Will relax the pedicate to also return true for the given list of error codes. + /// + public static Predicate Ignore(this Predicate? a, IEnumerable toAccept) + { + var ignorer = toAccept.IsInList(); + return a is null ? ignorer : a.Or(ignorer); + } + + /// + /// Returns a list of CodedException that do not match the filter. + /// + public static IEnumerable Remove(this IEnumerable issues, Predicate filter) => + issues.Where(ce => !filter(ce)); + + internal static Predicate IsInList(this IEnumerable ignoreList) => + ce => ignoreList.Contains(ce.ErrorCode); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs b/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs index c30ebdb5ac..4bf5a785bd 100644 --- a/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs +++ b/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs @@ -16,13 +16,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -#pragma warning disable CS1580 // Invalid type for parameter in XML comment cref attribute -#pragma warning disable CS1584 // XML comment has syntactically incorrect cref attribute namespace Hl7.Fhir.Serialization; - /// /// This validator uses the System.ComponentModel.DataAnnotations attributes to validate an instance, /// but simulates Validator.ValidateObject(), to avoid using reflection and use the cached reflection @@ -32,26 +29,12 @@ public class DataAnnotationDeserialzationValidator : IDeserializationValidator { public static readonly DataAnnotationDeserialzationValidator Default = new(); - /// - /// For performance reasons, validation of Xhtml again the rules specified in the FHIR - /// specification for Narrative (http://hl7.org/fhir/narrative.html#2.4.0) is turned off by - /// default. Set this property to any other value than - /// to perform validation. - /// - public NarrativeValidationKind NarrativeValidation { get; } = NarrativeValidationKind.None; - - public DataAnnotationDeserialzationValidator( - NarrativeValidationKind narrativeValidation = NarrativeValidationKind.None) - { - NarrativeValidation = narrativeValidation; - } - /// public virtual void ValidateProperty(object? propertyValue, in PropertyDeserializationContext context, out CodedValidationException[]? reportedErrors) { var validationContext = new ValidationContext(context.ObjectInstance) .SetValidateRecursively(false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. - .SetNarrativeValidationKind(NarrativeValidation) + .SetNarrativeValidationKind(context.NarrativeValidation) .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) .SetLocationProducer(context.PathStack.GetInstancePath); @@ -64,7 +47,7 @@ public virtual void ValidateInstance(Base instance, in InstanceDeserializationCo { var validationContext = new ValidationContext(instance) .SetValidateRecursively(false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. - .SetNarrativeValidationKind(NarrativeValidation) + .SetNarrativeValidationKind(context.NarrativeValidation) .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) .SetLocationProducer(context.PathStack.GetInstancePath); diff --git a/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs b/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs index 1a49c95e2b..f03e49cb45 100644 --- a/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs +++ b/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs @@ -9,6 +9,7 @@ using Hl7.Fhir.Introspection; using Hl7.Fhir.Model; +using Hl7.Fhir.Validation; #nullable enable @@ -27,7 +28,9 @@ internal PropertyDeserializationContext( string propertyName, long lineNumber, long linePosition, - PropertyMapping propMapping) + PropertyMapping propMapping, + NarrativeValidationKind narrativeValidation + ) { PathStack = path; ObjectInstance = objectInstance; @@ -35,6 +38,7 @@ internal PropertyDeserializationContext( LineNumber = lineNumber; LinePosition = linePosition; ElementMapping = propMapping; + NarrativeValidation = narrativeValidation; } internal PathStack PathStack { get; } @@ -68,6 +72,14 @@ internal PropertyDeserializationContext( /// The metadata for the element that is currently being deserialized. /// public PropertyMapping ElementMapping { get; } + + /// + /// For performance reasons, validation of Xhtml again the rules specified in the FHIR + /// specification for Narrative (http://hl7.org/fhir/narrative.html#2.4.0) is turned off by + /// default. Set this property to any other value than + /// to perform validation. + /// + public NarrativeValidationKind NarrativeValidation { get; } = NarrativeValidationKind.None; } /// @@ -80,12 +92,14 @@ internal InstanceDeserializationContext( PathStack path, long lineNumber, long linePosition, - ClassMapping instanceMapping) + ClassMapping instanceMapping, + NarrativeValidationKind narrativeValidation) { PathStack = path; LineNumber = lineNumber; LinePosition = linePosition; InstanceMapping = instanceMapping; + NarrativeValidation = narrativeValidation; } internal PathStack PathStack { get; } @@ -109,4 +123,12 @@ internal InstanceDeserializationContext( /// The metadata for the type of which the current property is part of. /// public ClassMapping InstanceMapping { get; } + + /// + /// For performance reasons, validation of Xhtml again the rules specified in the FHIR + /// specification for Narrative (http://hl7.org/fhir/narrative.html#2.4.0) is turned off by + /// default. Set this property to any other value than + /// to perform validation. + /// + public NarrativeValidationKind NarrativeValidation { get; } = NarrativeValidationKind.None; } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/DeserializerModes.cs b/src/Hl7.Fhir.Base/Serialization/DeserializerModes.cs new file mode 100644 index 0000000000..c802a0c809 --- /dev/null +++ b/src/Hl7.Fhir.Base/Serialization/DeserializerModes.cs @@ -0,0 +1,32 @@ +#nullable enable +namespace Hl7.Fhir.Serialization; + +/// +/// Enumerates the modes with which a deserializer can be configured +/// +public enum DeserializationMode +{ + /// + /// Do not ignore any errors (default behaviour for most implementations) + /// + Strict, + + /// + /// An issue is recoverable if all data present in the parsed data could be retrieved and + /// captured in the POCO model, even if the syntax or the data was not fully FHIR compliant. + /// + Recoverable, + + /// + /// An issue is allowable for backwards compatibility if it could be caused because an older parser encounters data coming from a newer + /// FHIR release. This means allowing unknown elements, attributes, codes and types in a choice element. Note that the POCO model cannot capture + /// these newer elements and data, so this means data loss may occur. + /// + BackwardsCompatible, + + /// + /// Ignore all errors. Useful for debugging, when you know the data to be parsed is a correct instance or + /// when catching errors is not important. + /// + Ostrich, +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs index a05970679b..40e455bc20 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs @@ -11,7 +11,6 @@ using Hl7.Fhir.Introspection; using Hl7.Fhir.Model; using System; -using System.Collections.Generic; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; @@ -23,66 +22,34 @@ namespace Hl7.Fhir.Serialization; /// public class FhirJsonConverterFactory(ModelInspector inspector, FhirJsonConverterOptions converterOptions) : JsonConverterFactory { - internal PocoSerializationEngine? Engine { get; private set; } + private BaseFhirJsonPocoDeserializer _deserializer = new(inspector, converterOptions); + private readonly BaseFhirJsonSerializer _serializer = new(inspector); + private SerializationFilter? _serializationFilter = converterOptions.SummaryFilter; - private PocoSerializationEngine createDefaultEngine() - { - return (PocoSerializationEngine)FhirSerializationEngineFactory.Strict(inspector, converterOptions); - } - - public FhirJsonConverterFactory( - Assembly assembly, FhirJsonConverterOptions converterOptions) : this(ModelInspector.ForAssembly(assembly), converterOptions) - { - // Nothing - } - - internal void SetEnforcedErrors(IEnumerable toEnforce) - { - Engine ??= createDefaultEngine(); - Engine.IgnoreFilter = Engine.IgnoreFilter.And(toEnforce.IsInList().Negate()); - } - - internal void SetIgnoredErrors(IEnumerable toIgnore) - { - Engine ??= createDefaultEngine(); - Engine.IgnoreFilter = Engine.IgnoreFilter.Or(toIgnore.IsInList()); - } + internal FhirJsonConverterOptions CurrentOptions = converterOptions; - internal void SetMode(DeserializerModes mode) + public void Reconfigure(FhirJsonConverterOptions newOptions) { - Engine = mode switch - { - DeserializerModes.Recoverable => (PocoSerializationEngine)FhirSerializationEngineFactory.Recoverable(inspector, converterOptions), - DeserializerModes.BackwardsCompatible => (PocoSerializationEngine)FhirSerializationEngineFactory.BackwardsCompatible(inspector, converterOptions), - DeserializerModes.Ostrich => (PocoSerializationEngine)FhirSerializationEngineFactory.Ostrich(inspector, converterOptions), - _ => createDefaultEngine() - }; + _deserializer = new BaseFhirJsonPocoDeserializer(inspector, newOptions); + _serializationFilter = newOptions.SummaryFilter; + CurrentOptions = newOptions; } - public override bool CanConvert(Type typeToConvert) => typeof(Base).IsAssignableFrom(typeToConvert); public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Engine ??= createDefaultEngine(); return (JsonConverter?)Activator.CreateInstance( - typeof(FhirJsonConverter<>).MakeGenericType(typeToConvert), BindingFlags.NonPublic | BindingFlags.Instance, null, - [Engine], null, null); + typeof(FhirJsonConverter<>).MakeGenericType(typeToConvert), BindingFlags.Public | BindingFlags.Instance, null, + [_deserializer, _serializer, _serializationFilter], null, null); } } /// /// FHIR Resource and datatype converter for FHIR deserialization. /// -public class FhirJsonConverter : JsonConverter +internal class FhirJsonConverter(BaseFhirJsonPocoDeserializer deserializer, BaseFhirJsonSerializer serializer, SerializationFilter? summaryFilter = null) : JsonConverter where TF : Base { - private readonly PocoSerializationEngine _engine; - - private FhirJsonConverter(IFhirSerializationEngine engine) - { - this._engine = (PocoSerializationEngine)engine; - } - /// /// Determines whether the specified type can be converted. /// @@ -93,16 +60,14 @@ private FhirJsonConverter(IFhirSerializationEngine engine) /// public override void Write(Utf8JsonWriter writer, TF poco, JsonSerializerOptions options) { - _engine.SerializeToJsonWriter(poco, writer); + serializer.Serialize(poco, writer, summaryFilter); } /// /// Reads and converts the JSON to a typed object. /// - public override TF Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return typeof(Resource).IsAssignableFrom(typeToConvert) - ? (TF)(Base)_engine.DeserializeFromJson(ref reader) - : (TF)_engine.DeserializeObjectFromJson(typeToConvert, ref reader); - } + public override TF Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + typeof(Resource).IsAssignableFrom(typeToConvert) + ? (TF)(Base)deserializer.DeserializeResource(ref reader) + : (TF)deserializer.DeserializeObject(typeToConvert, ref reader); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs index 2536d07928..726bfb9329 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs @@ -9,9 +9,10 @@ #nullable enable -using Hl7.Fhir.Model; +using Hl7.Fhir.Utility; +using Hl7.Fhir.Validation; using System; -using System.Text.Json; +using System.Collections.Generic; namespace Hl7.Fhir.Serialization; @@ -23,7 +24,7 @@ public record FhirJsonConverterOptions /// /// Specifies the filter to use for summary serialization. /// - public SerializationFilter? SummaryFilter { get; set; } = null; + public SerializationFilter? SummaryFilter { get; init; } = null; /// /// If set, this validator is invoked before the value is set in the object under construction to validate @@ -31,11 +32,17 @@ public record FhirJsonConverterOptions /// public IDeserializationValidator? Validator { get; init; } = DataAnnotationDeserialzationValidator.Default; + /// + /// Specifies a filter that can be used to filter out exceptions that are not considered fatal. The filter + /// returns true for exceptions that should be ignored, and false otherwise. + /// + public Predicate? ExceptionFilter { get; init; } = null; + /// /// Perform the parse time validation on the deserialized object even if parsing issues occurred. /// /// - /// This is useful for "strict mode" once pass validators and may result in spurious error messages + /// This is useful for "strict mode" single-pass validators and may result in spurious error messages /// from validating incomplete content. /// public bool ValidateOnFailedParse { get; init; } = false; @@ -50,20 +57,57 @@ public record FhirJsonConverterOptions /// resource was clean and possibly ok to process). /// public bool AnnotateResourceParseExceptions { get; init; } = false; -} -/// -/// A callback that can handle parsing failures for primitive types. -/// -/// A json reader positioned on the primitive value that failed to parse. -/// The .NET type the deserializer needs this handler to return to be able to update the POCO under construction. -/// The value the deserializer would have returned if this handler was not installed. -/// The exception the deserializer would have raised if this handler was not installed. -/// Returns an object, an error or both depending on whether the handler succeeded in parsing. The returned exception -/// may be the originalException and the returned value the originalValue. -/// If both are returned, the parsing is considered to have failed, with the returned object as the partially parsed result. -public delegate (object?, FhirJsonException?) PrimitiveParseHandler( - ref Utf8JsonReader reader, - Type targetType, - object? originalValue, - FhirJsonException originalException); \ No newline at end of file + /// + /// For performance reasons, validation of Xhtml again the rules specified in the FHIR + /// specification for Narrative (http://hl7.org/fhir/narrative.html#2.4.0) is turned off by + /// default. Set this property to any other value than + /// to perform validation. + /// + public NarrativeValidationKind NarrativeValidation { get; init; } = NarrativeValidationKind.None; + + /// + /// Enables all validation rules that are available. + /// + /// The selected mode to use, see . + /// How strict to validate the XHtml in FHIR Narrative. Only relevant in mode + public FhirJsonConverterOptions WithMode(DeserializationMode mode, + NarrativeValidationKind nvk = NarrativeValidationKind.FhirXhtml) => + mode switch + { + DeserializationMode.Strict => this with + { + ExceptionFilter = null, // No exceptions are ignored + NarrativeValidation = nvk + }, + DeserializationMode.BackwardsCompatible => this with + { + ExceptionFilter = CodedExceptionFilters.IsBackwardsCompatibilityIssue, + NarrativeValidation = NarrativeValidationKind.None + }, + DeserializationMode.Recoverable => this with + { + ExceptionFilter = CodedExceptionFilters.IsRecoverableIssue, + NarrativeValidation = NarrativeValidationKind.None + }, + DeserializationMode.Ostrich => this with + { + Validator = null, // Disable all validations, we don't care. + ExceptionFilter = _ => true, // If there are still errors, ignore. + NarrativeValidation = NarrativeValidationKind.None // We don't care about the narrative. + }, + _ => throw Error.NotSupported("Unknown deserialization mode.") + }; + + /// + /// Alters the options to enforce specific parsing exceptions. + /// + public FhirJsonConverterOptions Enforcing(IEnumerable toEnforce) => + this with { ExceptionFilter = this.ExceptionFilter.Enforce(toEnforce) }; + + /// + /// Alters the options to ignore specific parsing exceptions. + /// + public FhirJsonConverterOptions Ignoring(IEnumerable toIgnore) => + this with { ExceptionFilter = this.ExceptionFilter.Ignore(toIgnore) }; +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptionsExtensions.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptionsExtensions.cs index 26be1142c1..a17143cb5d 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptionsExtensions.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptionsExtensions.cs @@ -77,7 +77,7 @@ public static JsonSerializerOptions ForFhir( FhirJsonConverterOptions converterOptions ) { - var converter = new FhirJsonConverterFactory(modelAssembly, converterOptions); + var converter = new FhirJsonConverterFactory(ModelInspector.ForAssembly(modelAssembly), converterOptions); return options.ForFhir(converter); } @@ -108,25 +108,17 @@ public static JsonSerializerOptions ForFhir(this JsonSerializerOptions options, } /// - /// Initialize the options to serialize using the JsonFhirConverter, producing compact output without whitespace. - /// - public static JsonSerializerOptions ForFhir(this JsonSerializerOptions options, FhirJsonConverter converter) where F : Base - { - options.Converters.Add(converter); - options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; - - return options; - } - - /// - /// Modify the options to use a preset list of errors to ignore by specifying a mode. This can be any member of + /// Modify the options to use a preset list of errors to ignore by specifying a mode. + /// This can be any member of /// /// /// Modifying the options is always left-associative. This means that defining custom constraints should probably be done AFTER setting the mode. /// - public static JsonSerializerOptions UsingMode(this JsonSerializerOptions options, DeserializerModes mode) + public static JsonSerializerOptions UsingMode(this JsonSerializerOptions options, DeserializationMode mode) { - getCustomFactoryFromList(options.Converters).SetMode(mode); + var ourConverter = getCustomFactoryFromList(options.Converters); + ourConverter.Reconfigure(ourConverter.CurrentOptions.WithMode(mode)); + return options; } @@ -139,7 +131,9 @@ public static JsonSerializerOptions UsingMode(this JsonSerializerOptions options /// public static JsonSerializerOptions Enforcing(this JsonSerializerOptions options, IEnumerable toEnforce) { - getCustomFactoryFromList(options.Converters).SetEnforcedErrors(toEnforce); + var ourConverter = getCustomFactoryFromList(options.Converters); + ourConverter.Reconfigure(ourConverter.CurrentOptions.Enforcing(toEnforce)); + return options; } @@ -152,7 +146,9 @@ public static JsonSerializerOptions Enforcing(this JsonSerializerOptions options /// public static JsonSerializerOptions Ignoring(this JsonSerializerOptions options, IEnumerable toIgnore) { - getCustomFactoryFromList(options.Converters).SetIgnoredErrors(toIgnore); + var ourConverter = getCustomFactoryFromList(options.Converters); + ourConverter.Reconfigure(ourConverter.CurrentOptions.Ignoring(toIgnore)); + return options; } @@ -185,33 +181,4 @@ private static FhirJsonConverterFactory getCustomFactoryFromList(IEnumerable -/// Enumerates the modes with which a deserializer can be configured -/// -public enum DeserializerModes -{ - /// - /// Do not ignore any errors (default behaviour for most implementations) - /// - Strict, - - /// - /// An issue is recoverable if all data present in the parsed data could be retrieved and - /// captured in the POCO model, even if the syntax or the data was not fully FHIR compliant. - /// - Recoverable, - - /// - /// An issue is allowable for backwards compatibility if it could be caused because an older parser encounters data coming from a newer - /// FHIR release. This means allowing unknown elements, attributes, codes and types in a choice element. Note that the POCO model cannot capture - /// these newer elements and data, so this means data loss may occur. - /// - BackwardsCompatible, - - /// - /// Ignore all errors. Useful for debugging and/or when you know the data to be parsed is a correct instance. - /// - Ostrich, } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs index 04405e4347..21a5ee5f6c 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs @@ -11,6 +11,7 @@ using Hl7.Fhir.Utility; using Hl7.Fhir.Validation; using System; +using System.Collections.Generic; using System.Text.Json; using OO_Sev = Hl7.Fhir.Model.OperationOutcome.IssueSeverity; using OO_Typ = Hl7.Fhir.Model.OperationOutcome.IssueType; @@ -92,8 +93,9 @@ public class FhirJsonException : ExtendedCodedException /// captured in the POCO model, even if the syntax or the data was not fully FHIR compliant. /// #pragma warning disable CS0618 // Type or member is obsolete - internal static string[] RecoverableIssues = + internal static readonly HashSet RECOVERABLE_ISSUES = [ + ..CodedValidationException.POCO_VALIDATION_ISSUES, EXPECTED_PRIMITIVE_NOT_NULL_CODE, EXPECTED_START_OF_ARRAY_CODE, USE_OF_UNDERSCORE_ILLEGAL_CODE, @@ -101,7 +103,7 @@ public class FhirJsonException : ExtendedCodedException ARRAYS_CANNOT_BE_EMPTY_CODE, PRIMITIVE_ARRAYS_ONLY_NULL_CODE, PROPERTY_MAY_NOT_BE_EMPTY_CODE, - DUPLICATE_ARRAY_CODE, + DUPLICATE_ARRAY_CODE ]; #pragma warning restore CS0618 // Type or member is obsolete @@ -110,12 +112,12 @@ public class FhirJsonException : ExtendedCodedException /// FHIR release. This means allowing unknown elements, codes and types in a choice element. Note that the POCO model cannot capture /// these newer elements and data, so this means data loss may occur. /// - internal static string[] BackwardsCompatibilityAllowedIssues = - [ + internal static readonly string[] BACKWARDS_COMPATIBILITY_ALLOWED_ISSUES = + { CodedValidationException.INVALID_CODED_VALUE_CODE, CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE, UNKNOWN_PROPERTY_FOUND_CODE - ]; + }; public FhirJsonException(string code, string message) : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown) diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonParsingSettings.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonParsingSettings.cs index 51a200d680..829d69f611 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonParsingSettings.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonParsingSettings.cs @@ -6,58 +6,58 @@ * available at https://github.com/FirelyTeam/firely-net-sdk/blob/master/LICENSE */ +#nullable enable using Hl7.Fhir.Utility; using System; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// Parser configuration settings for the class. +public class FhirJsonParsingSettings { - /// Parser configuration settings for the class. - public class FhirJsonParsingSettings + /// + /// Do not raise exceptions for recoverable errors. + /// + public bool PermissiveParsing { get; set; } = true; + + /// + /// Allow DSTU2-style Json comment members. + /// + public bool AllowJsonComments { get; set; } // = false; + + /// + /// Validate narrative against the FHIR Xhtml schema. + /// + /// Validation of xhtml is expensive, so turned off by default. + public bool ValidateFhirXhtml { get; set; } // = false; + + /// Default constructor. Creates a new instance with default property values. + public FhirJsonParsingSettings() { } + + /// Clone constructor. Generates a new instance initialized from the state of the specified instance. + /// The specified argument is null. + public FhirJsonParsingSettings(FhirJsonParsingSettings other) { - /// - /// Do not raise exceptions for recoverable errors. - /// - public bool PermissiveParsing { get; set; } = true; - - /// - /// Allow DSTU2-style Json comment members. - /// - public bool AllowJsonComments { get; set; } // = false; - - /// - /// Validate narrative against the FHIR Xhtml schema. - /// - /// Validation of xhtml is expensive, so turned off by default. - public bool ValidateFhirXhtml { get; set; } // = false; - - /// Default constructor. Creates a new instance with default property values. - public FhirJsonParsingSettings() { } - - /// Clone constructor. Generates a new instance initialized from the state of the specified instance. - /// The specified argument is null. - public FhirJsonParsingSettings(FhirJsonParsingSettings other) - { - if (other == null) throw Error.ArgumentNull(nameof(other)); - other.CopyTo(this); - } - - /// Copy all configuration settings to another instance. - /// Another instance. - /// The specified argument is null. - public void CopyTo(FhirJsonParsingSettings other) - { - if (other == null) throw Error.ArgumentNull(nameof(other)); - - other.PermissiveParsing = PermissiveParsing; - other.AllowJsonComments = AllowJsonComments; - other.ValidateFhirXhtml = ValidateFhirXhtml; - } - - /// Creates a new object that is a copy of the current instance. - public FhirJsonParsingSettings Clone() => new FhirJsonParsingSettings(this); - - /// Creates a new instance with default property values. - public static FhirJsonParsingSettings CreateDefault() => new FhirJsonParsingSettings(); + if (other == null) throw Error.ArgumentNull(nameof(other)); + other.CopyTo(this); } + + /// Copy all configuration settings to another instance. + /// Another instance. + /// The specified argument is null. + public void CopyTo(FhirJsonParsingSettings other) + { + if (other == null) throw Error.ArgumentNull(nameof(other)); + + other.PermissiveParsing = PermissiveParsing; + other.AllowJsonComments = AllowJsonComments; + other.ValidateFhirXhtml = ValidateFhirXhtml; + } + + /// Creates a new object that is a copy of the current instance. + public FhirJsonParsingSettings Clone() => new FhirJsonParsingSettings(this); + + /// Creates a new instance with default property values. + public static FhirJsonParsingSettings CreateDefault() => new(); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FhirXmlException.cs b/src/Hl7.Fhir.Base/Serialization/FhirXmlException.cs index 2ee77bf15d..1ad94f3dfc 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirXmlException.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirXmlException.cs @@ -4,6 +4,7 @@ using Hl7.Fhir.Utility; using Hl7.Fhir.Validation; using System; +using System.Collections.Generic; using System.Xml; using OO_Sev = Hl7.Fhir.Model.OperationOutcome.IssueSeverity; using OO_Typ = Hl7.Fhir.Model.OperationOutcome.IssueType; @@ -85,8 +86,9 @@ internal static FhirXmlException ELEMENT_HAS_NO_VALUE_OR_CHILDREN(string instanc /// List of issues which do NOT lead to data loss. Recoverable issues mean that all data present in the parsed data could be retrieved and /// captured in the POCO model, even if the syntax or the data was not fully FHIR compliant. /// - internal static string[] RecoverableIssues = + internal static readonly HashSet RECOVERABLE_ISSUES = [ + ..CodedValidationException.POCO_VALIDATION_ISSUES, EMPTY_ELEMENT_NAMESPACE_CODE, INCORRECT_ELEMENT_NAMESPACE_CODE, INCORRECT_XHTML_NAMESPACE_CODE, @@ -96,7 +98,7 @@ internal static FhirXmlException ELEMENT_HAS_NO_VALUE_OR_CHILDREN(string instanc ATTRIBUTE_HAS_EMPTY_VALUE_CODE, ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE, SCHEMALOCATION_DISALLOWED_CODE, - ENCOUNTERED_DTD_REFERENCES_CODE, + ENCOUNTERED_DTD_REFERENCES_CODE ]; /// @@ -104,7 +106,7 @@ internal static FhirXmlException ELEMENT_HAS_NO_VALUE_OR_CHILDREN(string instanc /// FHIR release. This means allowing unknown elements, attributes, codes and types in a choice element. Note that the POCO model cannot capture /// these newer elements and data, so this means data loss may occur. /// - internal static string[] BackwardsCompatibilityAllowedIssues = + internal static readonly string[] BACKWARDS_COMPATIBILITY_ALLOWED_ISSUES = [ CodedValidationException.INVALID_CODED_VALUE_CODE, UNKNOWN_ELEMENT_CODE, diff --git a/src/Hl7.Fhir.Base/Serialization/FhirXmlParsingSettings.cs b/src/Hl7.Fhir.Base/Serialization/FhirXmlParsingSettings.cs index 5742edc5d9..fc8dae8ef6 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirXmlParsingSettings.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirXmlParsingSettings.cs @@ -6,65 +6,66 @@ * available at https://github.com/FirelyTeam/firely-net-sdk/blob/master/LICENSE */ +#nullable enable + using Hl7.Fhir.Utility; using System; using System.Xml.Linq; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// Parser configuration settings for the class. +public class FhirXmlParsingSettings { - /// Parser configuration settings for the class. - public class FhirXmlParsingSettings - { - /// - /// A list of namespaces which are allowed in addition to the normal FHIR namespaces. - /// - /// Normally, the only allowed namespaces are 'http://hl7.org/fhir' and the XHTML namespace. - public XNamespace[] AllowedExternalNamespaces { get; set; } + /// + /// A list of namespaces which are allowed in addition to the normal FHIR namespaces. + /// + /// Normally, the only allowed namespaces are 'http://hl7.org/fhir' and the XHTML namespace. + public XNamespace[]? AllowedExternalNamespaces { get; set; } - /// - /// Raise an errors when an xsi:schemaLocation attribute is found on the root. - /// - public bool DisallowSchemaLocation { get; set; } // = false; + /// + /// Raise an errors when an xsi:schemaLocation attribute is found on the root. + /// + public bool DisallowSchemaLocation { get; set; } // = false; - /// - /// Do not raise exceptions for recoverable errors. - /// - public bool PermissiveParsing { get; set; } = true; + /// + /// Do not raise exceptions for recoverable errors. + /// + public bool PermissiveParsing { get; set; } = true; - /// - /// Validate narrative against the FHIR Xhtml schema. - /// - /// Validation of xhtml is expensive, so turned off by default. - public bool ValidateFhirXhtml { get; set; } // = false; + /// + /// Validate narrative against the FHIR Xhtml schema. + /// + /// Validation of xhtml is expensive, so turned off by default. + public bool ValidateFhirXhtml { get; set; } // = false; - /// Default constructor. Creates a new instance with default property values. - public FhirXmlParsingSettings() { } + /// Default constructor. Creates a new instance with default property values. + public FhirXmlParsingSettings() { } - /// Clone constructor. Generates a new instance initialized from the state of the specified instance. - /// The specified argument is null. - public FhirXmlParsingSettings(FhirXmlParsingSettings other) - { - if (other == null) throw Error.ArgumentNull(nameof(other)); - other.CopyTo(this); - } + /// Clone constructor. Generates a new instance initialized from the state of the specified instance. + /// The specified argument is null. + public FhirXmlParsingSettings(FhirXmlParsingSettings other) + { + if (other == null) throw Error.ArgumentNull(nameof(other)); + other.CopyTo(this); + } - /// Copy all configuration settings to another instance. - /// Another instance. - /// The specified argument is null. - public void CopyTo(FhirXmlParsingSettings other) - { - if (other == null) throw Error.ArgumentNull(nameof(other)); + /// Copy all configuration settings to another instance. + /// Another instance. + /// The specified argument is null. + public void CopyTo(FhirXmlParsingSettings other) + { + if (other == null) throw Error.ArgumentNull(nameof(other)); - other.AllowedExternalNamespaces = (XNamespace[])AllowedExternalNamespaces?.Clone(); - other.DisallowSchemaLocation = DisallowSchemaLocation; - other.PermissiveParsing = PermissiveParsing; - other.ValidateFhirXhtml = ValidateFhirXhtml; - } + other.AllowedExternalNamespaces = (XNamespace[]?)AllowedExternalNamespaces?.Clone(); + other.DisallowSchemaLocation = DisallowSchemaLocation; + other.PermissiveParsing = PermissiveParsing; + other.ValidateFhirXhtml = ValidateFhirXhtml; + } - /// Creates a new object that is a copy of the current instance. - public FhirXmlParsingSettings Clone() => new(this); + /// Creates a new object that is a copy of the current instance. + public FhirXmlParsingSettings Clone() => new(this); - /// Creates a new instance with default property values. - public static FhirXmlParsingSettings CreateDefault() => new(); - } + /// Creates a new instance with default property values. + public static FhirXmlParsingSettings CreateDefault() => new(); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FhirXmlPocoDeserializerSettings.cs b/src/Hl7.Fhir.Base/Serialization/FhirXmlPocoDeserializerSettings.cs index f0c5555628..8b68de85c4 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirXmlPocoDeserializerSettings.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirXmlPocoDeserializerSettings.cs @@ -1,37 +1,97 @@ #nullable enable -using Hl7.Fhir.Model; +using Hl7.Fhir.Utility; +using Hl7.Fhir.Validation; +using Hl7.FhirPath.Sprache; +using System; +using System.Collections.Generic; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +public record FhirXmlPocoDeserializerSettings { - public class FhirXmlPocoDeserializerSettings - { - /// - /// If set, this validator is invoked before the value is set in the object under construction to validate - /// and possibly alter the value. Setting this property to null will disable validation completely. - /// - public IDeserializationValidator? Validator { get; init; } = DataAnnotationDeserialzationValidator.Default; - - /// - /// Perform the parse time validation on the deserialized object even if parsing issues occurred. - /// - /// - /// This is useful for "strict mode" once pass validators and may result in spurious error messages - /// from validating incomplete content. - /// - public bool ValidateOnFailedParse { get; init; } = false; - - /// - /// During parsing any contained resources (such as those in a bundle) that encounter some form of parse/validation exception - /// will have a List<CodedException> of these exceptions added as an annotation to the child resource. - /// - /// - /// This is primarily added to ease the processing of bundles during a batch submission. - /// (without requiring processing fhirpath expressions in the issues in the parsing operation outcome to determine if a - /// resource was clean and possibly ok to process). - /// - public bool AnnotateResourceParseExceptions { get; init; } = false; - } -} - -#nullable restore \ No newline at end of file + /// + /// If set, this validator is invoked before the value is set in the object under construction to validate + /// and possibly alter the value. Setting this property to null will disable validation completely. + /// + public IDeserializationValidator? Validator { get; init; } = DataAnnotationDeserialzationValidator.Default; + + /// + /// Specifies a filter that can be used to filter out exceptions that are not considered fatal. The filter + /// returns true for exceptions that should be ignored, and false otherwise. + /// + public Predicate? ExceptionFilter { get; set; } = null; + + /// + /// Perform the parse time validation on the deserialized object even if parsing issues occurred. + /// + /// + /// This is useful for "strict mode" single-pass validators and may result in spurious error messages + /// from validating incomplete content. + /// + public bool ValidateOnFailedParse { get; init; } = false; + + /// + /// During parsing any contained resources (such as those in a bundle) that encounter some form of parse/validation exception + /// will have a List<CodedException> of these exceptions added as an annotation to the child resource. + /// + /// + /// This is primarily added to ease the processing of bundles during a batch submission. + /// (without requiring processing fhirpath expressions in the issues in the parsing operation outcome to determine if a + /// resource was clean and possibly ok to process). + /// + public bool AnnotateResourceParseExceptions { get; init; } = false; + + /// + /// For performance reasons, validation of Xhtml again the rules specified in the FHIR + /// specification for Narrative (http://hl7.org/fhir/narrative.html#2.4.0) is turned off by + /// default. Set this property to any other value than + /// to perform validation. + /// + public NarrativeValidationKind NarrativeValidation { get; init; } = NarrativeValidationKind.None; + + /// + /// Enables all validation rules that are available. + /// + /// The selected mode to use, see . + /// How strict to validate the XHtml in FHIR Narrative. Only relevant in mode + public FhirXmlPocoDeserializerSettings WithMode(DeserializationMode mode, + NarrativeValidationKind nvk = NarrativeValidationKind.FhirXhtml) => + mode switch + { + DeserializationMode.Strict => this with + { + ExceptionFilter = null, // No exceptions are ignored + NarrativeValidation = nvk + }, + DeserializationMode.BackwardsCompatible => this with + { + ExceptionFilter = CodedExceptionFilters.IsBackwardsCompatibilityIssue, + NarrativeValidation = NarrativeValidationKind.None + }, + DeserializationMode.Recoverable => this with + { + ExceptionFilter = CodedExceptionFilters.IsRecoverableIssue, + NarrativeValidation = NarrativeValidationKind.None + }, + DeserializationMode.Ostrich => this with + { + Validator = null, // Disable all validations, we don't care. + ExceptionFilter = _ => true, // If there are still errors, ignore. + NarrativeValidation = NarrativeValidationKind.None // We don't care about the narrative. + }, + _ => throw Error.NotSupported("Unknown deserialization mode.") + }; + + /// + /// Alters the options to enforce specific parsing exceptions. + /// + public FhirXmlPocoDeserializerSettings Enforcing(IEnumerable toEnforce) => + this with { ExceptionFilter = this.ExceptionFilter.Enforce(toEnforce) }; + + /// + /// Alters the options to ignore specific parsing exceptions. + /// + public FhirXmlPocoDeserializerSettings Ignoring(IEnumerable toIgnore) => + this with { ExceptionFilter = this.ExceptionFilter.Ignore(toIgnore) }; +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FilterPredicateExtensions.cs b/src/Hl7.Fhir.Base/Serialization/FilterPredicateExtensions.cs deleted file mode 100644 index 45c7b28b98..0000000000 --- a/src/Hl7.Fhir.Base/Serialization/FilterPredicateExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable enable - -using Hl7.Fhir.Serialization; -using Hl7.Fhir.Utility; -using Hl7.Fhir.Validation; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Hl7.Fhir.Serialization; - -internal static class FilterPredicateExtensions -{ - // Note that CodedValidationExceptions are coming from property validation, and so are by definition - // recoverable, since the data was already safely in the POCO by that time. - internal static Predicate IsRecoverableIssue => - ce => ce is CodedValidationException || - FhirXmlException.RecoverableIssues.Concat(FhirJsonException.RecoverableIssues).Contains(ce.ErrorCode); - - internal static Predicate IsInList(this IEnumerable ignoreList) => - ce => ignoreList.Contains(ce.ErrorCode); - - internal static Predicate IsBackwardsCompatibilityIssue => - FhirXmlException.BackwardsCompatibilityAllowedIssues - .Concat(FhirJsonException.BackwardsCompatibilityAllowedIssues) - .IsInList(); - - internal static Predicate And(this Predicate a, Predicate? b) => - b is not null ? ce => a(ce) && b(ce) : a; - - internal static Predicate Or(this Predicate a, Predicate? b) => - b is not null ? ce => a(ce) || b(ce) : a; - - internal static Predicate Negate(this Predicate a) => - ce => !a(ce); -} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/FhirSerializationEngineFactory.Legacy.cs b/src/Hl7.Fhir.Base/Serialization/engine/FhirSerializationEngineFactory.Legacy.cs new file mode 100644 index 0000000000..7384016f4f --- /dev/null +++ b/src/Hl7.Fhir.Base/Serialization/engine/FhirSerializationEngineFactory.Legacy.cs @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + + +#nullable enable + +using Hl7.Fhir.Introspection; +using System; + +namespace Hl7.Fhir.Serialization; + +/// +/// Factory methods for creating the default implementation of , as used by the +/// FhirClient. +/// +public static partial class FhirSerializationEngineFactory +{ + /// + /// A named scope for the factory methods that use the legacy ElementModel-based (de)serializers. + /// + public static class Legacy + { + private enum Mode + { + Strict, + Permissive, + BackwardsCompatible, + Ostrich + } + + private static PocoBuilderSettings buildPocoBuilderSettings(Mode mode) => new() + { + AllowUnrecognizedEnums = mode is Mode.BackwardsCompatible or Mode.Ostrich, + IgnoreUnknownMembers = mode is Mode.BackwardsCompatible or Mode.Ostrich, + ExceptionHandler = mode is Mode.Ostrich + ? (_, _) => { } + : null + }; + + + private static FhirXmlParsingSettings buildXmlParsingSettings(Mode mode) => new() + { + DisallowSchemaLocation = mode is Mode.Strict, + PermissiveParsing = mode is Mode.Permissive or Mode.Ostrich, + ValidateFhirXhtml = mode is Mode.Strict + }; + + private static FhirJsonParsingSettings buildJsonParsingSettings(Mode mode) => new() + { + AllowJsonComments = mode is not Mode.Strict, + PermissiveParsing = mode is Mode.Permissive or Mode.Ostrich, + ValidateFhirXhtml = mode is Mode.Strict + }; + + /// + /// Create an implementation of which uses the legacy parser and serializer + /// using set to true. + /// + public static IFhirSerializationEngine FromParserSettings(ModelInspector inspector, ParserSettings settings) => + new ElementModelSerializationEngine(inspector, + BaseFhirParser.BuildXmlParsingSettings(settings), + BaseFhirParser.BuildJsonParserSettings(settings), + BaseFhirParser.BuildPocoBuilderSettings(settings)); + + /// + /// Create an implementation of which uses the legacy parser and serializer + /// using set to true. + /// + public static IFhirSerializationEngine Permissive(ModelInspector inspector) => + new ElementModelSerializationEngine(inspector, + buildXmlParsingSettings(Mode.Permissive), + buildJsonParsingSettings(Mode.Permissive), + buildPocoBuilderSettings(Mode.Permissive)); + + /// + /// Create an implementation of which uses the legacy parser and serializer + /// with set to false. + /// + public static IFhirSerializationEngine Strict(ModelInspector inspector) => + new ElementModelSerializationEngine(inspector, + buildXmlParsingSettings(Mode.Strict), + buildJsonParsingSettings(Mode.Strict), + buildPocoBuilderSettings(Mode.Strict)); + + /// + /// Create an implementation of which uses the legacyt parser and serializer + /// and is configured to allow errors that could occur when reading data from newer releases of FHIR. Note that this + /// parser may drop data that cannot be captured in the POCO model, such as new elements in future FHIR releases. + /// + public static IFhirSerializationEngine BackwardsCompatible(ModelInspector inspector) => + new ElementModelSerializationEngine(inspector, + buildXmlParsingSettings(Mode.BackwardsCompatible), + buildJsonParsingSettings(Mode.BackwardsCompatible), + buildPocoBuilderSettings(Mode.BackwardsCompatible)); + + /// + /// Create an implementation of which uses the legacy parser and serializer + /// configured to allow errors and just continue parsing. Note that this may mean data loss. + /// + public static IFhirSerializationEngine Ostrich(ModelInspector inspector) => + new ElementModelSerializationEngine(inspector, + buildXmlParsingSettings(Mode.Ostrich), + buildJsonParsingSettings(Mode.Ostrich), + buildPocoBuilderSettings(Mode.Ostrich)); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/FhirSerializationEngineFactory.cs b/src/Hl7.Fhir.Base/Serialization/engine/FhirSerializationEngineFactory.cs index 867c22003b..47b790daf3 100644 --- a/src/Hl7.Fhir.Base/Serialization/engine/FhirSerializationEngineFactory.cs +++ b/src/Hl7.Fhir.Base/Serialization/engine/FhirSerializationEngineFactory.cs @@ -10,7 +10,6 @@ #nullable enable using Hl7.Fhir.Introspection; -using Hl7.Fhir.Utility; using System; namespace Hl7.Fhir.Serialization; @@ -19,113 +18,25 @@ namespace Hl7.Fhir.Serialization; /// Factory methods for creating the default implementation of , as used by the /// FhirClient. /// -public static class FhirSerializationEngineFactory +public static partial class FhirSerializationEngineFactory { - /// - /// A named scope for the factory methods that use the legacy ElementModel-based (de)serializers. - /// - public static class Legacy - { - private enum Mode - { - Strict, - Permissive, - BackwardsCompatible, - Ostrich - } - - private static PocoBuilderSettings buildPocoBuilderSettings(Mode mode) => new() - { - AllowUnrecognizedEnums = mode is Mode.BackwardsCompatible or Mode.Ostrich, - IgnoreUnknownMembers = mode is Mode.BackwardsCompatible or Mode.Ostrich, - ExceptionHandler = mode is Mode.Ostrich - ? (_, _) => { } - : null - }; - - - private static FhirXmlParsingSettings buildXmlParsingSettings(Mode mode) => new() - { - DisallowSchemaLocation = mode is Mode.Strict, - PermissiveParsing = mode is Mode.Permissive or Mode.Ostrich, - ValidateFhirXhtml = mode is Mode.Strict - }; - - private static FhirJsonParsingSettings buildJsonParsingSettings(Mode mode) => new() - { - AllowJsonComments = mode is not Mode.Strict, - PermissiveParsing = mode is Mode.Permissive or Mode.Ostrich, - ValidateFhirXhtml = mode is Mode.Strict - }; - - /// - /// Create an implementation of which uses the legacy parser and serializer - /// using set to true. - /// - public static IFhirSerializationEngine FromParserSettings(ModelInspector inspector, ParserSettings settings) => - new ElementModelSerializationEngine(inspector, - BaseFhirParser.BuildXmlParsingSettings(settings), - BaseFhirParser.BuildJsonParserSettings(settings), - BaseFhirParser.BuildPocoBuilderSettings(settings)); - - /// - /// Create an implementation of which uses the legacy parser and serializer - /// using set to true. - /// - public static IFhirSerializationEngine Permissive(ModelInspector inspector) => - new ElementModelSerializationEngine(inspector, - buildXmlParsingSettings(Mode.Permissive), - buildJsonParsingSettings(Mode.Permissive), - buildPocoBuilderSettings(Mode.Permissive)); - - /// - /// Create an implementation of which uses the legacy parser and serializer - /// with set to false. - /// - public static IFhirSerializationEngine Strict(ModelInspector inspector) => - new ElementModelSerializationEngine(inspector, - buildXmlParsingSettings(Mode.Strict), - buildJsonParsingSettings(Mode.Strict), - buildPocoBuilderSettings(Mode.Strict)); - - /// - /// Create an implementation of which uses the legacyt parser and serializer - /// and is configured to allow errors that could occur when reading data from newer releases of FHIR. Note that this - /// parser may drop data that cannot be captured in the POCO model, such as new elements in future FHIR releases. - /// - public static IFhirSerializationEngine BackwardsCompatible(ModelInspector inspector) => - new ElementModelSerializationEngine(inspector, - buildXmlParsingSettings(Mode.BackwardsCompatible), - buildJsonParsingSettings(Mode.BackwardsCompatible), - buildPocoBuilderSettings(Mode.BackwardsCompatible)); - - /// - /// Create an implementation of which uses the legacy parser and serializer - /// configured to allow errors and just continue parsing. Note that this may mean data loss. - /// - public static IFhirSerializationEngine Ostrich(ModelInspector inspector) => - new ElementModelSerializationEngine(inspector, - buildXmlParsingSettings(Mode.Ostrich), - buildJsonParsingSettings(Mode.Ostrich), - buildPocoBuilderSettings(Mode.Ostrich)); - } - - /// /// Create an implementation of configured to flag all parsing errors, /// which uses the new Poco-based parser and serializer. /// public static IFhirSerializationEngine Strict(ModelInspector inspector, - FhirJsonConverterOptions? converterOptions = null) => - new PocoSerializationEngine(inspector, converterOptions: converterOptions); + FhirJsonConverterOptions? converterOptions = null, + FhirXmlPocoDeserializerSettings? xmlSettings = null) => + createEngine(inspector, converterOptions, xmlSettings, DeserializationMode.Strict); /// /// Create an implementation of configured to ignore recoverable errors, /// which uses the new Poco-based parser and serializer. /// public static IFhirSerializationEngine Recoverable(ModelInspector inspector, - FhirJsonConverterOptions? converterOptions = null) => - new PocoSerializationEngine(inspector, FilterPredicateExtensions.IsRecoverableIssue, converterOptions: converterOptions); + FhirJsonConverterOptions? converterOptions = null, + FhirXmlPocoDeserializerSettings? xmlSettings = null) => + createEngine(inspector, converterOptions, xmlSettings, DeserializationMode.Recoverable); /// /// Create an implementation of which uses the new Poco-based parser and @@ -134,34 +45,46 @@ public static IFhirSerializationEngine Recoverable(ModelInspector inspector, /// FHIR releases. /// public static IFhirSerializationEngine BackwardsCompatible(ModelInspector inspector, - FhirJsonConverterOptions? converterOptions = null) => - new PocoSerializationEngine(inspector, FilterPredicateExtensions.IsBackwardsCompatibilityIssue, converterOptions); + FhirJsonConverterOptions? converterOptions = null, + FhirXmlPocoDeserializerSettings? xmlSettings = null) => + createEngine(inspector, converterOptions, xmlSettings, DeserializationMode.BackwardsCompatible); /// /// Create an implementation of configured to allow errors /// and just continue parsing. Note that this may mean data loss. /// public static IFhirSerializationEngine Ostrich(ModelInspector inspector, - FhirJsonConverterOptions? converterOptions = null) => - new PocoSerializationEngine( - inspector, - _ => true, - (converterOptions ?? new FhirJsonConverterOptions()) with {Validator = null}, - xmlSettings: new FhirXmlPocoDeserializerSettings {Validator = null}); + FhirJsonConverterOptions? converterOptions = null, + FhirXmlPocoDeserializerSettings? xmlSettings = null) => + createEngine(inspector, converterOptions, xmlSettings, DeserializationMode.Ostrich); /// /// Create an implementation of which allows for manual configuration /// of most behaviour. See parameters for more information. /// /// - /// A predicate specifying which errors to ignore when parsing /// The settings to be used by the engine to deserialize JSON sources /// The settings to be used by the engine to deserialize XML sources /// - public static IFhirSerializationEngine Custom(ModelInspector inspector, Predicate ignoreFilter, - FhirJsonConverterOptions? converterOptions = null, - FhirXmlPocoDeserializerSettings? xmlSerializerSettings = null) + public static IFhirSerializationEngine Custom(ModelInspector inspector, + FhirJsonConverterOptions converterOptions, + FhirXmlPocoDeserializerSettings xmlSerializerSettings) + { + var jsonDeserializer = new BaseFhirJsonPocoDeserializer(inspector, converterOptions); + var xmlDeserializer = new BaseFhirXmlPocoDeserializer(inspector, xmlSerializerSettings); + + return new PocoSerializationEngine(jsonDeserializer, + new BaseFhirJsonSerializer(inspector), + xmlDeserializer, + new BaseFhirXmlSerializer(inspector)); + } + + private static IFhirSerializationEngine createEngine(ModelInspector inspector, + FhirJsonConverterOptions? converterOptions, FhirXmlPocoDeserializerSettings? xmlSettings, DeserializationMode mode) { - return new PocoSerializationEngine(inspector, ignoreFilter, converterOptions); + var jsonOptions = (converterOptions ?? new FhirJsonConverterOptions()).WithMode(mode); + var xmlOptions = (xmlSettings ?? new FhirXmlPocoDeserializerSettings()).WithMode(mode); + + return Custom(inspector, jsonOptions, xmlOptions); } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs index ea07bc335b..e8ad767095 100644 --- a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs +++ b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace Hl7.Fhir.Serialization; @@ -22,27 +21,18 @@ namespace Hl7.Fhir.Serialization; /// This is an implementation of which uses the /// new Poco-based parser and serializer, initialized with the default settings. /// -internal partial class PocoSerializationEngine : IFhirSerializationEngine +public class PocoSerializationEngine(BaseFhirJsonPocoDeserializer jsonDeserializer, BaseFhirJsonSerializer jsonSerializer, + BaseFhirXmlPocoDeserializer xmlDeserializer, BaseFhirXmlSerializer xmlSerializer) : IFhirSerializationEngine { - private delegate (Base?, IEnumerable) TryDeserializer(); + /// + public string SerializeToJson(Resource instance) => jsonSerializer.SerializeToString(instance); - private readonly ModelInspector _inspector; - internal Predicate IgnoreFilter { get; set; } - - internal PocoSerializationEngine(ModelInspector inspector, Predicate? ignoreFilter = null, - FhirJsonConverterOptions? converterOptions = null, FhirXmlPocoDeserializerSettings? xmlSettings = null) - { - _inspector = inspector; - IgnoreFilter = ignoreFilter ?? (_ => false); - _jsonConverterOptions = converterOptions ?? new FhirJsonConverterOptions(); - _xmlSettings = xmlSettings ?? new FhirXmlPocoDeserializerSettings(); - } + /// + public Resource? DeserializeFromJson(string data) => jsonDeserializer.DeserializeResource(data); - private Base deserializeAndFilterErrors(TryDeserializer deserializer) - { - var (instance, issues) = deserializer(); - var relevantIssues = issues.Where(i => !IgnoreFilter(i)).ToList(); + /// + public Resource? DeserializeFromXml(string data) => xmlDeserializer.DeserializeResource(data); - return relevantIssues.Any() ? throw new DeserializationFailedException(instance, relevantIssues) : instance!; - } + /// + public string SerializeToXml(Resource instance) => xmlSerializer.SerializeToString(instance); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Json.cs b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Json.cs deleted file mode 100644 index 4027de8b34..0000000000 --- a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Json.cs +++ /dev/null @@ -1,77 +0,0 @@ -#nullable enable - -using Hl7.Fhir.Model; -using System; -using System.Linq; -using System.Text.Json; - -namespace Hl7.Fhir.Serialization; - -internal partial class PocoSerializationEngine -{ - private readonly FhirJsonConverterOptions? _jsonConverterOptions; - - private BaseFhirJsonPocoDeserializer? _jsonDeserializer; - private BaseFhirJsonSerializer? _jsonSerializer; - - private BaseFhirJsonPocoDeserializer getJsonDeserializer() => - _jsonDeserializer ??= new BaseFhirJsonPocoDeserializer(_inspector, _jsonConverterOptions!); - - private BaseFhirJsonSerializer getJsonSerializer() => - _jsonSerializer ??= new BaseFhirJsonSerializer(_inspector); - - /// - public Resource DeserializeFromJson(string data) - { - return (Resource)deserializeAndFilterErrors(() => - { - _ = getJsonDeserializer().TryDeserializeResource(data, out var instance, out var issues); - return (instance, issues); - }); - } - - /// - public string SerializeToJson(Resource instance) => getJsonSerializer().SerializeToString(instance); - - /// - /// Deserializes a resource from a JSON reader - /// - /// The JSON reader - /// The parsed resource - public Resource DeserializeFromJson(ref Utf8JsonReader reader) => deserializeAndFilterErrors(getJsonDeserializer(), ref reader); - - /// - /// Deserializes an object from a JSON reader - /// - /// The target type of the object - /// The JSON reader - /// The parsed object - public Base DeserializeObjectFromJson(Type targetType, ref Utf8JsonReader reader) => - deserializeObjectAndFilterErrors(targetType, getJsonDeserializer(), ref reader); - - /// - /// Serializes an instance of any child of base to the supplied writer - /// - /// An instance of Base or any of its children - /// The JSON writer - public void SerializeToJsonWriter(Base instance, Utf8JsonWriter writer) => - getJsonSerializer().Serialize(instance, writer, _jsonConverterOptions?.SummaryFilter); - - // overload necessary since ref structs cannot be captured in the lambda - private Resource deserializeAndFilterErrors(BaseFhirJsonPocoDeserializer deserializer, ref Utf8JsonReader reader) - { - _ = deserializer.TryDeserializeResource(ref reader, out var instance, out var issues); - var relevantIssues = issues.Where(i => !IgnoreFilter(i)).ToList(); - - return relevantIssues.Any() ? throw new DeserializationFailedException(instance, relevantIssues) : instance!; - } - - // overload necessary since ref structs cannot be captured in the lambda - private Base deserializeObjectAndFilterErrors(Type targetType, BaseFhirJsonPocoDeserializer deserializer, ref Utf8JsonReader reader) - { - _ = deserializer.TryDeserializeObject(targetType, ref reader, out var instance, out var issues); - var relevantIssues = issues.Where(i => !IgnoreFilter(i)).ToList(); - - return relevantIssues.Any() ? throw new DeserializationFailedException(instance, relevantIssues) : instance!; - } -} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Xml.cs b/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Xml.cs deleted file mode 100644 index 52d7741a4a..0000000000 --- a/src/Hl7.Fhir.Base/Serialization/engine/PocoSerializationEngine_Xml.cs +++ /dev/null @@ -1,64 +0,0 @@ -#nullable enable - -using Hl7.Fhir.Introspection; -using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; -using System; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Xml; - -namespace Hl7.Fhir.Serialization; - -internal partial class PocoSerializationEngine -{ - private readonly FhirXmlPocoDeserializerSettings _xmlSettings; - - private BaseFhirXmlPocoDeserializer? _xmlDeserializer; - private BaseFhirXmlSerializer? _xmlSerializer; - - private BaseFhirXmlPocoDeserializer getXmlDeserializer() => - _xmlDeserializer ??= new BaseFhirXmlPocoDeserializer(_inspector, _xmlSettings); - - private BaseFhirXmlSerializer getXmlSerializer() => - _xmlSerializer ??= new BaseFhirXmlSerializer(_inspector); - - /// - public Resource DeserializeFromXml(string data) - { - return (Resource)deserializeAndFilterErrors(() => - { - _ = getXmlDeserializer().TryDeserializeResource(data, out var instance, out var issues); - return (instance, issues); - }); - } - - /// - public string SerializeToXml(Resource instance) => getXmlSerializer().SerializeToString(instance); - - - internal string SerializeElementToXml(Base instance) => getXmlSerializer().SerializeToString(instance); - - /// - /// Deserializes a resource from an XML reader - /// - /// The XML reader - /// The parsed resource - public Resource DeserializeFromXml(XmlReader reader) - { - return (Resource)deserializeAndFilterErrors(() => - { - _ = getXmlDeserializer().TryDeserializeResource(reader, out var instance, out var issues); - return (instance, issues); - }); - } - - - /// - /// Serializes an element to the supplied writer - /// - /// An instance of Base or any of its children - /// The XML writer - internal void SerializeToXmlWriter(Base instance, XmlWriter writer) => getXmlSerializer().Serialize(instance, writer); -} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/engine/SerializationEngineExtensions.cs b/src/Hl7.Fhir.Base/Serialization/engine/SerializationEngineExtensions.cs deleted file mode 100644 index 6e85fca356..0000000000 --- a/src/Hl7.Fhir.Base/Serialization/engine/SerializationEngineExtensions.cs +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2024, Firely (info@fire.ly) and contributors - * See the file CONTRIBUTORS for details. - * - * This file is licensed under the BSD 3-Clause license - * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE - */ - -#nullable enable -using Hl7.Fhir.Model; -using System; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Xml; - -namespace Hl7.Fhir.Serialization; - -/// -/// Extension methods for the interface when the underlying engine is NOT a legacy engine. -/// -public static class SerializationEngineExtensions -{ - /// - /// Deserialize a FHIR Resource from a . - /// - /// The resource, or null if the operation failed - /// Thrown if the underlying engine is a legacy engine - /// Thrown if a FHIR error was encountered in the data - public static Resource DeserializeFromJsonReader(this IFhirSerializationEngine engine, ref Utf8JsonReader reader) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream reading is not supported by legacy engines"); - } - - return pse.DeserializeFromJson(ref reader); - } - - /// - /// Deserialize a FHIR Resource from an . - /// - /// Thrown if the underlying engine is a legacy engine - /// Thrown if a FHIR error was encountered in the data - public static Resource DeserializeFromXmlReader(this IFhirSerializationEngine engine, XmlReader reader) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream reading is not supported by legacy engines"); - } - - return pse.DeserializeFromXml(reader); - } - - /// - /// Serialize a FHIR Resource to a JSON writer. - /// - /// - public static void SerializeToJsonWriter(this IFhirSerializationEngine engine, Resource instance, Utf8JsonWriter writer) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream writing is not supported by legacy engines"); - } - - pse.SerializeToJsonWriter(instance, writer); - } - - /// - /// Serialize a FHIR Resource to an . - /// - /// Thrown if the underlying engine is a legacy engine - public static void SerializeToXmlWriter(this IFhirSerializationEngine engine, Resource instance, XmlWriter writer) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream writing is not supported by legacy engines"); - } - - pse.SerializeToXmlWriter(instance, writer); - } - - /// - /// Serialize an element (a child of a resource) to a . - /// - /// This method allows you to serialize - /// a subtree of data to XML, which can be useful for debug and display purposes. Note that the - /// FHIR standard does not prescribe how this is done, so the output of this function is by definition - /// non standard and should not be used for production purposes. - public static void SerializeElementToXmlWriter(this IFhirSerializationEngine engine, Base instance, XmlWriter writer) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream writing is not supported by legacy engines"); - } - - pse.SerializeToXmlWriter(instance, writer); - } - - /// - /// Serialize an element (a child of a resource) to a string of Xml. - /// - /// This method allows you to serialize - /// a subtree of data to XML, which can be useful for debug and display purposes. Note that the - /// FHIR standard does not prescribe how this is done, so the output of this function is by definition - /// non standard and should not be used for production purposes. - public static void SerializeElementToXml(this IFhirSerializationEngine engine, Base instance) - { - if (engine is not PocoSerializationEngine pse) - { - throw new InvalidOperationException("stream writing is not supported by legacy engines"); - } - - pse.SerializeElementToXml(instance); - } -} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs b/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs index bc2fe59429..33be453b33 100644 --- a/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs +++ b/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs @@ -10,6 +10,7 @@ using Hl7.Fhir.Serialization; using Hl7.Fhir.Utility; using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using COVE = Hl7.Fhir.Validation.CodedValidationException; using OO_Sev = Hl7.Fhir.Model.OperationOutcome.IssueSeverity; @@ -40,6 +41,26 @@ public class CodedValidationException : ExtendedCodedException public const string POSITIVE_INT_MUST_BE_POSITIVE_CODE = "PVAL125"; public const string UNSIGNED_INT_MUST_NOT_BE_NEGATIVE_CODE = "PVAL126"; + // A list of all issues mentioned above, to we can filter on them. + internal static readonly HashSet POCO_VALIDATION_ISSUES = + [ + CHOICE_TYPE_NOT_ALLOWED_CODE, + INCORRECT_CARDINALITY_MIN_CODE, + INCORRECT_CARDINALITY_MAX_CODE, + REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE, + MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, + NARRATIVE_XML_IS_MALFORMED_CODE, + NARRATIVE_XML_IS_INVALID_CODE, + INVALID_CODED_VALUE_CODE, + CONTAINED_RESOURCES_CANNOT_BE_NESTED_CODE, + INVALID_STRING_LENGTH_CODE, + INVALID_BASE64_VALUE_CODE, + INCORRECT_LITERAL_VALUE_TYPE_CODE, + LITERAL_INVALID_CODE, + POSITIVE_INT_MUST_BE_POSITIVE_CODE, + UNSIGNED_INT_MUST_NOT_BE_NEGATIVE_CODE + ]; + internal static COVE CHOICE_TYPE_NOT_ALLOWED(ValidationContext context, string typeName) => Initialize(context, CHOICE_TYPE_NOT_ALLOWED_CODE, $"Value is of type '{typeName}', which is not an allowed choice.", OO_Sev.Error, OO_Typ.Structure); internal static COVE INCORRECT_CARDINALITY_MIN(ValidationContext context, int count, int Min) => Initialize(context, INCORRECT_CARDINALITY_MIN_CODE, $"Element has {count} elements, but minimum cardinality is {Min}.", OO_Sev.Error, OO_Typ.Required); internal static COVE INCORRECT_CARDINALITY_MAX(ValidationContext context, int count, int Max) => Initialize(context, INCORRECT_CARDINALITY_MAX_CODE, $"Element has {count} elements, but maximum cardinality is {Max}.", OO_Sev.Error, OO_Typ.BusinessRule); diff --git a/src/Hl7.Fhir.Conformance/CompatibilitySuppressions.xml b/src/Hl7.Fhir.Conformance/CompatibilitySuppressions.xml index 0483fa0362..e1badae5f7 100644 --- a/src/Hl7.Fhir.Conformance/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.Conformance/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0002 diff --git a/src/Hl7.Fhir.R4/CompatibilitySuppressions.xml b/src/Hl7.Fhir.R4/CompatibilitySuppressions.xml index e30a9d3be1..770bbb3ca4 100644 --- a/src/Hl7.Fhir.R4/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.R4/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0001 diff --git a/src/Hl7.Fhir.R4B/CompatibilitySuppressions.xml b/src/Hl7.Fhir.R4B/CompatibilitySuppressions.xml index bb07989e1c..94c3323a15 100644 --- a/src/Hl7.Fhir.R4B/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.R4B/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0001 diff --git a/src/Hl7.Fhir.R5/CompatibilitySuppressions.xml b/src/Hl7.Fhir.R5/CompatibilitySuppressions.xml index 31beff887b..ea156e4321 100644 --- a/src/Hl7.Fhir.R5/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.R5/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0001 diff --git a/src/Hl7.Fhir.STU3.Tests/Serialization/SummarySerializationTests.cs b/src/Hl7.Fhir.STU3.Tests/Serialization/SummarySerializationTests.cs index 2b9b53b1b2..98bfa54887 100644 --- a/src/Hl7.Fhir.STU3.Tests/Serialization/SummarySerializationTests.cs +++ b/src/Hl7.Fhir.STU3.Tests/Serialization/SummarySerializationTests.cs @@ -130,7 +130,7 @@ public async Tasks.Task TestSummary() // make sure we accept the crappy output with empty groups var nav = await FhirXmlNode.ParseAsync(qText, new FhirXmlParsingSettings { PermissiveParsing = true }); - var qInflate = FhirXmlParser.Parse(nav); + var qInflate = nav.ToPoco(); Assert.AreEqual(1, qInflate.Meta.Tag.Count(t => t.System == "http://hl7.org/fhir/v3/ObservationValue" && t.Code == "SUBSETTED"), "Subsetted Tag should not still be there."); } diff --git a/src/Hl7.Fhir.STU3/CompatibilitySuppressions.xml b/src/Hl7.Fhir.STU3/CompatibilitySuppressions.xml index 9f7bf48c1a..541e3b0577 100644 --- a/src/Hl7.Fhir.STU3/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.STU3/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0001 diff --git a/src/Hl7.Fhir.Shared.Tests/Serialization/SummarySerializationTests.cs b/src/Hl7.Fhir.Shared.Tests/Serialization/SummarySerializationTests.cs index 4e0ee32650..15b4aa92ac 100644 --- a/src/Hl7.Fhir.Shared.Tests/Serialization/SummarySerializationTests.cs +++ b/src/Hl7.Fhir.Shared.Tests/Serialization/SummarySerializationTests.cs @@ -124,7 +124,7 @@ public async Tasks.Task TestSummary() // make sure we accept the crappy output with empty groups var nav = await FhirXmlNode.ParseAsync(qText, new FhirXmlParsingSettings { PermissiveParsing = true }); - var qInflate = FhirXmlParser.Parse(nav); + var qInflate = nav.ToPoco(); Assert.AreEqual(1, qInflate.Meta.Tag.Count(t => t.System == "http://terminology.hl7.org/CodeSystem/v3-ObservationValue" && t.Code == "SUBSETTED"), "Subsetted Tag should not still be there."); } diff --git a/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonParser.cs b/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonParser.cs index 789cc436f7..49d4a9b2ae 100644 --- a/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonParser.cs +++ b/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonParser.cs @@ -6,60 +6,62 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +#nullable enable + +using Hl7.Fhir.ElementModel; using Hl7.Fhir.Model; using Newtonsoft.Json; using System; using Tasks = System.Threading.Tasks; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +public class FhirJsonParser(ParserSettings? settings = null) : BaseFhirParser { - public class FhirJsonParser : BaseFhirParser - { - public FhirJsonParser(ParserSettings settings = null) : base(ModelInfo.ModelInspector, settings) - { - // - } + public ParserSettings Settings { get; set; } = settings ?? new ParserSettings(); - /// - public T Parse(string json) where T : Base => (T)Parse(json, typeof(T)); + /// + public T Parse(string json) where T : Base => (T)Parse(json, typeof(T)); - public async Tasks.Task ParseAsync(string json) where T : Base - => (T)await ParseAsync(json, typeof(T)).ConfigureAwait(false); + public async Tasks.Task ParseAsync(string json) where T : Base + => (T)await ParseAsync(json, typeof(T)).ConfigureAwait(false); - /// - public T Parse(JsonReader reader) where T : Base => (T)Parse(reader, typeof(T)); + /// + public T Parse(JsonReader reader) where T : Base => (T)Parse(reader, typeof(T)); - public async Tasks.Task ParseAsync(JsonReader reader) where T : Base - => (T)await ParseAsync(reader, typeof(T)).ConfigureAwait(false); + public async Tasks.Task ParseAsync(JsonReader reader) where T : Base + => (T)await ParseAsync(reader, typeof(T)).ConfigureAwait(false); - /// - public Base Parse(string json, Type dataType = null) - { - var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; - var jsonReader = FhirJsonNode.Parse(json, rootName, BuildJsonParserSettings(Settings)); - return Parse(jsonReader, dataType); - } + /// + public Base Parse(string json, Type? dataType = null) + { + var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; + var jsonReader = FhirJsonNode.Parse(json, rootName, BuildJsonParserSettings(Settings)); + return parse(jsonReader, dataType); + } - public async Tasks.Task ParseAsync(string json, Type dataType = null) - { - var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; - var jsonReader = await FhirJsonNode.ParseAsync(json, rootName, BuildJsonParserSettings(Settings)).ConfigureAwait(false); - return Parse(jsonReader, dataType); - } + public async Tasks.Task ParseAsync(string json, Type? dataType = null) + { + var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; + var jsonReader = await FhirJsonNode.ParseAsync(json, rootName, BuildJsonParserSettings(Settings)).ConfigureAwait(false); + return parse(jsonReader, dataType); + } - /// - public Base Parse(JsonReader reader, Type dataType = null) - { - var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; - var jsonReader = FhirJsonNode.Read(reader, rootName, BuildJsonParserSettings(Settings)); - return Parse(jsonReader, dataType); - } + /// + public Base Parse(JsonReader reader, Type? dataType = null) + { + var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; + var jsonReader = FhirJsonNode.Read(reader, rootName, BuildJsonParserSettings(Settings)); + return parse(jsonReader, dataType); + } - public async Tasks.Task ParseAsync(JsonReader reader, Type dataType = null) - { - var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; - var jsonReader = await FhirJsonNode.ReadAsync(reader, rootName, BuildJsonParserSettings(Settings)).ConfigureAwait(false); - return Parse(jsonReader, dataType); - } + public async Tasks.Task ParseAsync(JsonReader reader, Type? dataType = null) + { + var rootName = dataType != null ? ModelInfo.GetFhirTypeNameForType(dataType) : null; + var jsonReader = await FhirJsonNode.ReadAsync(reader, rootName, BuildJsonParserSettings(Settings)).ConfigureAwait(false); + return parse(jsonReader, dataType); } -} + + private Base parse(ISourceNode node, Type? type = null) => + node.ToPoco(ModelInfo.ModelInspector, type, BuildPocoBuilderSettings(Settings)); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirXmlParser.cs b/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirXmlParser.cs index af40a428f5..6980d5aa87 100644 --- a/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirXmlParser.cs +++ b/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirXmlParser.cs @@ -6,58 +6,58 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +#nullable enable + +using Hl7.Fhir.ElementModel; using Hl7.Fhir.Model; using System; using System.Xml; using Tasks = System.Threading.Tasks; +namespace Hl7.Fhir.Serialization; -namespace Hl7.Fhir.Serialization +public class FhirXmlParser(ParserSettings? settings = null) : BaseFhirParser { - public class FhirXmlParser : BaseFhirParser + public ParserSettings Settings { get; set; } = settings ?? new ParserSettings(); + + /// + public T Parse(XmlReader reader) where T : Base => (T)Parse(reader, typeof(T)); + + public async Tasks.Task ParseAsync(XmlReader reader) where T : Base + => await ParseAsync(reader, typeof(T)).ConfigureAwait(false) as T; + + /// + public T Parse(string xml) where T : Base => (T)Parse(xml, typeof(T)); + + public async Tasks.Task ParseAsync(string xml) where T : Base + => await ParseAsync(xml, typeof(T)).ConfigureAwait(false) as T; + + /// + public Base Parse(string xml, Type? dataType = null) + { + var xmlReader = FhirXmlNode.Parse(xml, BuildXmlParsingSettings(Settings)); + return parse(xmlReader, dataType); + } + + public async Tasks.Task ParseAsync(string xml, Type? dataType = null) + { + var xmlReader = await FhirXmlNode.ParseAsync(xml, BuildXmlParsingSettings(Settings)).ConfigureAwait(false); + return parse(xmlReader, dataType); + } + + /// + public Base Parse(XmlReader reader, Type? dataType = null) + { + var xmlReader = FhirXmlNode.Read(reader, BuildXmlParsingSettings(Settings)); + return parse(xmlReader, dataType); + } + + public async Tasks.Task ParseAsync(XmlReader reader, Type? dataType = null) { - public FhirXmlParser(ParserSettings settings = null) : base(ModelInfo.ModelInspector, settings) - { - // - } - - /// - public T Parse(XmlReader reader) where T : Base => (T)Parse(reader, typeof(T)); - - public async Tasks.Task ParseAsync(XmlReader reader) where T : Base - => await ParseAsync(reader, typeof(T)).ConfigureAwait(false) as T; - - /// - public T Parse(string xml) where T : Base => (T)Parse(xml, typeof(T)); - - public async Tasks.Task ParseAsync(string xml) where T : Base - => await ParseAsync(xml, typeof(T)).ConfigureAwait(false) as T; - - /// - public Base Parse(string xml, Type dataType = null) - { - var xmlReader = FhirXmlNode.Parse(xml, BuildXmlParsingSettings(Settings)); - return Parse(xmlReader, dataType); - } - - public async Tasks.Task ParseAsync(string xml, Type dataType = null) - { - var xmlReader = await FhirXmlNode.ParseAsync(xml, BuildXmlParsingSettings(Settings)).ConfigureAwait(false); - return Parse(xmlReader, dataType); - } - - /// - public Base Parse(XmlReader reader, Type dataType = null) - { - var xmlReader = FhirXmlNode.Read(reader, BuildXmlParsingSettings(Settings)); - return Parse(xmlReader, dataType); - } - - public async Tasks.Task ParseAsync(XmlReader reader, Type dataType = null) - { - var xmlReader = await FhirXmlNode.ReadAsync(reader, BuildXmlParsingSettings(Settings)).ConfigureAwait(false); - return Parse(xmlReader, dataType); - } + var xmlReader = await FhirXmlNode.ReadAsync(reader, BuildXmlParsingSettings(Settings)).ConfigureAwait(false); + return parse(xmlReader, dataType); } -} + private Base parse(ISourceNode node, Type? type = null) => + node.ToPoco(ModelInfo.ModelInspector, type, BuildPocoBuilderSettings(Settings)); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Specification.STU3.Tests/Source/ArtifactSummaryTests.cs b/src/Hl7.Fhir.Specification.STU3.Tests/Source/ArtifactSummaryTests.cs index e6156a8d1b..9506c8e6b7 100644 --- a/src/Hl7.Fhir.Specification.STU3.Tests/Source/ArtifactSummaryTests.cs +++ b/src/Hl7.Fhir.Specification.STU3.Tests/Source/ArtifactSummaryTests.cs @@ -335,8 +335,7 @@ public void TestLoadResourceFromZipStream() if (nav != null) { // Parse target resource from navigator - var parser = new BaseFhirParser(ModelInfo.ModelInspector); - var corePatient = parser.Parse(nav); + var corePatient = nav.ToPoco(); Assert.IsNotNull(corePatient); Assert.AreEqual(corePatientUrl, corePatient.Url); } @@ -449,4 +448,4 @@ public void TestIsConformanceSummary(bool typeNameFound) result.Should().BeFalse(); } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Specification.Shared.Tests/Source/ArtifactSummaryTests.cs b/src/Hl7.Fhir.Specification.Shared.Tests/Source/ArtifactSummaryTests.cs index d1b5bc7996..e3fe76c9ed 100644 --- a/src/Hl7.Fhir.Specification.Shared.Tests/Source/ArtifactSummaryTests.cs +++ b/src/Hl7.Fhir.Specification.Shared.Tests/Source/ArtifactSummaryTests.cs @@ -382,7 +382,7 @@ public void TestLoadResourceFromZipStream() { // Parse target resource from navigator var parser = new FhirXmlParser(); - var corePatient = parser.Parse(nav); + var corePatient = nav.ToPoco(); Assert.IsNotNull(corePatient); Assert.AreEqual(corePatientUrl, corePatient.Url); } @@ -495,4 +495,4 @@ public void TestIsConformanceSummary(bool typeNameFound) result.Should().BeFalse(); } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs index 8758aacced..65f7d0887e 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs @@ -26,7 +26,7 @@ namespace Hl7.Fhir.Support.Poco.Tests; public class FhirJsonDeserializationTests { private static BaseFhirJsonPocoDeserializer getTestDeserializer(FhirJsonConverterOptions settings) => - new(typeof(Patient).Assembly, settings); + new(ModelInspector.ForType(typeof(Patient)), settings); [TestMethod] @@ -102,8 +102,8 @@ public void ParsePrimitiveValue(object value, Type targetType, string? errorcode PrimitiveType test() { - var inspector = ModelInspector.ForAssembly(typeof(Patient).Assembly); - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly); + var inspector = ModelInspector.ForType(typeof(Patient)); + var deserializer = new BaseFhirJsonPocoDeserializer(inspector); var mapping = inspector.ImportType(targetType)!; var reader = constructReader(value); @@ -135,7 +135,7 @@ private static (Base?, IReadOnlyCollection) deserializeComplex(T FhirJsonConverterOptions settings) { // For the tests, enable full XHML validation so we can test it when necessary. - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly, settings); + var deserializer = new BaseFhirJsonPocoDeserializer(ModelInspector.ForType(), settings); Utf8JsonReader reader = constructReader(testObject); reader.Read(); @@ -183,7 +183,7 @@ public void TestDeserializeResource(object testObject, JsonTokenType tokenAfterP var reader = constructReader(testObject); reader.Read(); - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly); + var deserializer = new BaseFhirJsonPocoDeserializer(ModelInspector.ForType()); var state = new FhirJsonPocoDeserializerState(); _ = deserializer.DeserializeResourceInternal(ref reader, state, stayOnLastToken: false); assertErrors(state.Errors, errors); @@ -266,8 +266,7 @@ public void TestData(Type t, object testObject, JsonTokenType token, Action().And.Subject.Should().Be("SGkh"); @@ -872,20 +871,20 @@ private static IEnumerable getExtensionOptionsAndExpectedErrors() yield return [ new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .UsingMode(DeserializerModes.Ostrich), + .UsingMode(DeserializationMode.Ostrich), new Predicate>(errs => !errs.Any()) ]; yield return [ new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .UsingMode(DeserializerModes.Recoverable), - new Predicate>(errs => !errs.Any(e => FilterPredicateExtensions.IsRecoverableIssue(e))) + .UsingMode(DeserializationMode.Recoverable), + new Predicate>(errs => !errs.Any(e => CodedExceptionFilters.IsRecoverableIssue(e))) ]; yield return [ new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .UsingMode(DeserializerModes.BackwardsCompatible), - new Predicate>(errs => !errs.Any(e => FilterPredicateExtensions.IsBackwardsCompatibilityIssue(e))) + .UsingMode(DeserializationMode.BackwardsCompatible), + new Predicate>(errs => !errs.Any(e => CodedExceptionFilters.IsBackwardsCompatibilityIssue(e))) ]; yield return [ @@ -895,7 +894,7 @@ private static IEnumerable getExtensionOptionsAndExpectedErrors() ]; yield return [ - new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly).UsingMode(DeserializerModes.Ostrich) + new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly).UsingMode(DeserializationMode.Ostrich) .Enforcing([ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, COVE.LITERAL_INVALID_CODE]), new Predicate>(errs => { @@ -926,7 +925,7 @@ public void TestExtensionMethods(JsonSerializerOptions options, Predicate getPredicateFromOptions(JsonSerializerOptions options) { var factory = options.Converters.FindCustomConverter() as FhirJsonConverterFactory ?? throw new InvalidOperationException(); - return factory.Engine!.IgnoreFilter; + return factory.CurrentOptions.ExceptionFilter!; } private static IEnumerable getIgnoreEnforceTests() @@ -964,7 +963,7 @@ ce.ErrorCode is COVE.INVALID_BASE64_VALUE_CODE or ERR.ARRAYS_CANNOT_BE_EMPTY_COD private static IEnumerable getErrorsList() { var testDeserializerOptions = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .UsingMode(DeserializerModes.Strict); + .UsingMode(DeserializationMode.Strict); string testJson = File.ReadAllText(Path.Combine("TestData", "fp-test-patient-errors.json")); try @@ -993,7 +992,7 @@ public void TestIgnoreEnforcePrevalence(Predicate actual, Predic [TestMethod] public void TestInvalidCustomization() { - var shouldThrow = () => (_ = new JsonSerializerOptions().UsingMode(DeserializerModes.Ostrich)); + var shouldThrow = () => (_ = new JsonSerializerOptions().UsingMode(DeserializationMode.Ostrich)); shouldThrow.Should().Throw("Expected error trying to set the mode of a non-existent converter"); } } \ No newline at end of file