diff --git a/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml b/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml index 991b943a26..094026abea 100644 --- a/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml +++ b/src/Hl7.Fhir.Base/CompatibilitySuppressions.xml @@ -1,5 +1,5 @@  - + CP0001 @@ -477,6 +477,48 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.INCORRECT_BASE64_DATA_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.LONG_CANNOT_BE_PARSED_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.LONG_INCORRECT_FORMAT_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.NUMBER_CANNOT_BE_PARSED_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.STRING_ISNOTAN_INSTANT_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.UNEXPECTED_JSON_TOKEN_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 F:Hl7.Fhir.Serialization.FhirXmlException.ENCOUNTERED_DTP_REFERENCES_CODE @@ -484,6 +526,83 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + F:Hl7.Fhir.Serialization.FhirXmlException.INCORRECT_BASE64_DATA_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirXmlException.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.CODE_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.CONTAINED_RESOURCE_CANNOT_HAVE_NARRATIVE_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.DATE_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.DATETIME_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.ID_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.OID_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.TIME_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.URI_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.UUID_LITERAL_INVALID_CODE + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 F:Hl7.FhirPath.Functions.EqualityOperators.TypedElementEqualityComparer @@ -890,6 +1009,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.Base.Validate(System.ComponentModel.DataAnnotations.ValidationContext) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.Canonical.CanonicalUriForFhirCoreType(System.String) @@ -953,6 +1079,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.FhirBoolean.IsValidValue(System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.FhirDateTime.op_Equality(Hl7.Fhir.Model.FhirDateTime,Hl7.Fhir.Model.FhirDateTime) @@ -1009,6 +1142,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.FhirDecimal.IsValidValue(System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.Instant.op_Equality(Hl7.Fhir.Model.Instant,Hl7.Fhir.Model.Instant) @@ -1051,6 +1191,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.Integer.IsValidValue(System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.IScopedNode.Children(System.String) @@ -1093,6 +1240,20 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.PositiveInt.IsValidValue(System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Model.PrimitiveType.OnObjectValueChanged + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.Quantity.ToQuantity @@ -1170,6 +1331,27 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.UnsignedInt.IsValidValue(System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Model.XHtml.IsValidNarrativeXhtml(System.String,System.String[]@) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Model.XHtml.IsValidNarrativeXhtml(System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Rest.BaseFhirClient.Continue(Hl7.Fhir.Model.Bundle,Hl7.Fhir.Rest.PageDirection) @@ -1814,6 +1996,13 @@ 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[]@) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.FhirJsonConverterFactory.#ctor(Hl7.Fhir.Introspection.ModelInspector,Hl7.Fhir.Serialization.FhirJsonPocoSerializerSettings,Hl7.Fhir.Serialization.FhirJsonPocoDeserializerSettings) @@ -1863,6 +2052,34 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Serialization.FhirXmlPocoDeserializerSettings.get_DisableBase64Decoding + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.FhirXmlPocoDeserializerSettings.set_DisableBase64Decoding(System.Boolean) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.IDeserializationValidator.ValidateInstance(System.Object,Hl7.Fhir.Serialization.InstanceDeserializationContext@,Hl7.Fhir.Validation.CodedValidationException[]@) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.PropertyDeserializationContext.get_InstancePath + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.SerializationFilter.ForElements(System.String[]) @@ -1905,6 +2122,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Utility.SerializationUtil.RunFhirXhtmlSchemaValidation(System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Utility.SerializationUtil.WriteJsonToBytes(System.Action{Newtonsoft.Json.JsonWriter}) @@ -1968,6 +2192,20 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Validation.NarrativeXhtmlPatternAttribute.IsValid(System.Object,Hl7.Fhir.Validation.NarrativeValidationKind,System.ComponentModel.DataAnnotations.ValidationContext) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Validation.ValidationContextExtensions.SetLocation(System.ComponentModel.DataAnnotations.ValidationContext,System.String) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.FhirPath.CompiledExpression.BeginInvoke(Hl7.Fhir.Model.IScopedNode,Hl7.FhirPath.EvaluationContext,System.AsyncCallback,System.Object) @@ -2157,6 +2395,48 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.INCORRECT_BASE64_DATA_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.LONG_CANNOT_BE_PARSED_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.LONG_INCORRECT_FORMAT_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.NUMBER_CANNOT_BE_PARSED_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.STRING_ISNOTAN_INSTANT_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirJsonException.UNEXPECTED_JSON_TOKEN_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 F:Hl7.Fhir.Serialization.FhirXmlException.ENCOUNTERED_DTP_REFERENCES_CODE @@ -2164,6 +2444,83 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + F:Hl7.Fhir.Serialization.FhirXmlException.INCORRECT_BASE64_DATA_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Serialization.FhirXmlException.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.CODE_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.CONTAINED_RESOURCE_CANNOT_HAVE_NARRATIVE_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.DATE_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.DATETIME_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.ID_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.OID_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.TIME_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.URI_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + F:Hl7.Fhir.Validation.CodedValidationException.UUID_LITERAL_INVALID_CODE + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 F:Hl7.FhirPath.Functions.EqualityOperators.TypedElementEqualityComparer @@ -2570,6 +2927,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.Base.Validate(System.ComponentModel.DataAnnotations.ValidationContext) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.Canonical.CanonicalUriForFhirCoreType(System.String) @@ -2633,6 +2997,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.FhirBoolean.IsValidValue(System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.FhirDateTime.op_Equality(Hl7.Fhir.Model.FhirDateTime,Hl7.Fhir.Model.FhirDateTime) @@ -2689,6 +3060,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.FhirDecimal.IsValidValue(System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.Instant.op_Equality(Hl7.Fhir.Model.Instant,Hl7.Fhir.Model.Instant) @@ -2731,6 +3109,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.Integer.IsValidValue(System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.IScopedNode.Children(System.String) @@ -2773,6 +3158,20 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.PositiveInt.IsValidValue(System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Model.PrimitiveType.OnObjectValueChanged + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Model.Quantity.ToQuantity @@ -2850,6 +3249,27 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Model.UnsignedInt.IsValidValue(System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Model.XHtml.IsValidNarrativeXhtml(System.String,System.String[]@) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Model.XHtml.IsValidNarrativeXhtml(System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Rest.BaseFhirClient.Continue(Hl7.Fhir.Model.Bundle,Hl7.Fhir.Rest.PageDirection) @@ -3494,6 +3914,13 @@ 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[]@) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.FhirJsonConverterFactory.#ctor(Hl7.Fhir.Introspection.ModelInspector,Hl7.Fhir.Serialization.FhirJsonPocoSerializerSettings,Hl7.Fhir.Serialization.FhirJsonPocoDeserializerSettings) @@ -3543,6 +3970,34 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Serialization.FhirXmlPocoDeserializerSettings.get_DisableBase64Decoding + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.FhirXmlPocoDeserializerSettings.set_DisableBase64Decoding(System.Boolean) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.IDeserializationValidator.ValidateInstance(System.Object,Hl7.Fhir.Serialization.InstanceDeserializationContext@,Hl7.Fhir.Validation.CodedValidationException[]@) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Serialization.PropertyDeserializationContext.get_InstancePath + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Serialization.SerializationFilter.ForElements(System.String[]) @@ -3585,6 +4040,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Utility.SerializationUtil.RunFhirXhtmlSchemaValidation(System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.Fhir.Utility.SerializationUtil.WriteJsonToBytes(System.Action{Newtonsoft.Json.JsonWriter}) @@ -3648,6 +4110,20 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0002 + M:Hl7.Fhir.Validation.NarrativeXhtmlPatternAttribute.IsValid(System.Object,Hl7.Fhir.Validation.NarrativeValidationKind,System.ComponentModel.DataAnnotations.ValidationContext) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + + + CP0002 + M:Hl7.Fhir.Validation.ValidationContextExtensions.SetLocation(System.ComponentModel.DataAnnotations.ValidationContext,System.String) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0002 M:Hl7.FhirPath.CompiledExpression.BeginInvoke(Hl7.Fhir.Model.IScopedNode,Hl7.FhirPath.EvaluationContext,System.AsyncCallback,System.Object) @@ -3851,6 +4327,13 @@ lib/net8.0/Hl7.Fhir.Base.dll true + + CP0005 + M:Hl7.Fhir.Model.PrimitiveType.ValidateObjectValue(System.ComponentModel.DataAnnotations.ValidationContext) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + CP0005 P:Hl7.Fhir.ElementModel.PocoNodeOrList.Parent @@ -3886,6 +4369,13 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0005 + M:Hl7.Fhir.Model.PrimitiveType.ValidateObjectValue(System.ComponentModel.DataAnnotations.ValidationContext) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0005 P:Hl7.Fhir.ElementModel.PocoNodeOrList.Parent @@ -3893,6 +4383,20 @@ lib/netstandard2.1/Hl7.Fhir.Base.dll true + + CP0006 + M:Hl7.Fhir.Serialization.IDeserializationValidator.ValidateInstance(Hl7.Fhir.Model.Base,Hl7.Fhir.Serialization.InstanceDeserializationContext@,Hl7.Fhir.Validation.CodedValidationException[]@) + lib/net8.0/Hl7.Fhir.Base.dll + lib/net8.0/Hl7.Fhir.Base.dll + true + + + CP0006 + M:Hl7.Fhir.Serialization.IDeserializationValidator.ValidateInstance(Hl7.Fhir.Model.Base,Hl7.Fhir.Serialization.InstanceDeserializationContext@,Hl7.Fhir.Validation.CodedValidationException[]@) + lib/netstandard2.1/Hl7.Fhir.Base.dll + lib/netstandard2.1/Hl7.Fhir.Base.dll + true + CP0008 T:Hl7.Fhir.ElementModel.PocoNode diff --git a/src/Hl7.Fhir.Base/ElementModel/NewPocoBuilder.cs b/src/Hl7.Fhir.Base/ElementModel/NewPocoBuilder.cs index d7c40f1fd1..2542d6b06a 100644 --- a/src/Hl7.Fhir.Base/ElementModel/NewPocoBuilder.cs +++ b/src/Hl7.Fhir.Base/ElementModel/NewPocoBuilder.cs @@ -53,7 +53,7 @@ private Base readFromElement(ITypedElement node, ClassMapping classMapping) { var objectValue = newInstance is DynamicPrimitive ? value : - convertTypedElementValue(value, node.InstanceType); + convertTypedElementValue(value); if(newInstance is PrimitiveType pt) pt.ObjectValue = objectValue; @@ -289,29 +289,25 @@ private void setOrAddProperty(ITypedElement node, Base target, } catch (InvalidCastException) { - throw Error.InvalidOperation($"Cannot assign data of type {convertedValue.GetType()} to to property '{node.Name}'."); + var typeString = convertedValue is IDynamicType it ? it.DynamicTypeName : convertedValue.GetType().Name; + throw Error.InvalidOperation($"Cannot assign data of type {typeString} to property '{node.Name}'."); } } /// /// Convert the value of a typed element to a value that can be set on a POCO property. /// - private static object convertTypedElementValue(object value, string? instanceType) + private static object convertTypedElementValue(object value) { return value switch { - // Instants are converted to DateTimeOffset, and should by definition have a timezone in their - // serialization, but if it does not, we'll use UTC. - ET.DateTime inst when instanceType == "instant" => inst.ToDateTimeOffset(TimeSpan.Zero), - - // all "other" date/time types are just strings, since that is how the POCO's represent the - // partial date/time types in ObjectValue. + // Some ITypedElement date/time values are strings in the POCO's ObjectValue. ET.DateTime => value.ToString()!, ET.Time => value.ToString()!, ET.Date => value.ToString()!, - // Base64Binary is a string of base64 encoded data, and the POCO's use byte[] for this. - string uuenc when instanceType == "base64Binary" => Convert.FromBase64String(uuenc), + // Integer64 uses string in the POCOs + long l => new ET.Long(l).ToString(), // All other primitives are one-on-one convertible to their .NET counterparts. _ => value diff --git a/src/Hl7.Fhir.Base/ElementModel/PocoElementNode.cs b/src/Hl7.Fhir.Base/ElementModel/PocoElementNode.cs index 2ddd1045dd..855b8670de 100644 --- a/src/Hl7.Fhir.Base/ElementModel/PocoElementNode.cs +++ b/src/Hl7.Fhir.Base/ElementModel/PocoElementNode.cs @@ -195,7 +195,7 @@ internal object ToITypedElementValue() Integer64 fint64 => fint64.Value, PositiveInt pint => pint.Value, UnsignedInt unsint => unsint.Value, - Base64Binary { Value: { } b64 } => PrimitiveTypeConverter.ConvertTo(b64), + Base64Binary { ObjectValue: { } b64 } => b64, PrimitiveType prim => prim.ObjectValue, _ => null }; diff --git a/src/Hl7.Fhir.Base/ElementModel/Types/DateTime.cs b/src/Hl7.Fhir.Base/ElementModel/Types/DateTime.cs index cdcdf1e429..4b07f572b5 100644 --- a/src/Hl7.Fhir.Base/ElementModel/Types/DateTime.cs +++ b/src/Hl7.Fhir.Base/ElementModel/Types/DateTime.cs @@ -58,6 +58,11 @@ public static DateTime FromDateTimeOffset(DateTimeOffset dto, DateTimePrecision public Date TruncateToDate() => Date.FromDateTimeOffset(_value, Precision > DateTimePrecision.Day ? DateTimePrecision.Day : Precision, HasOffset); + /// + /// Whether this DateTime represents a precise instant in time, according to FHIR rules. + /// + public bool IsInstant => HasOffset && Precision >= DateTimePrecision.Second; + public int? Years => Precision >= DateTimePrecision.Year ? _value.Year : null; public int? Months => Precision >= DateTimePrecision.Month ? _value.Month : null; public int? Days => Precision >= DateTimePrecision.Day ? _value.Day : null; @@ -79,7 +84,7 @@ public static DateTime FromDateTimeOffset(DateTimeOffset dto, DateTimePrecision /// /// Whether the time specifies an offset to UTC /// - public bool HasOffset { get; private set; } + public bool HasOffset { get; } /// /// If this instance was constructed using Parse(), this is the original diff --git a/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs b/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs index 5b66831ed5..7acae9de53 100644 --- a/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs +++ b/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs @@ -88,14 +88,14 @@ public static SymbolTable AddFhirExtensions(this SymbolTable t) /// /// /// - public static bool HtmlChecks(this PocoNode focus) => focus is PrimitiveNode {Primitive: XHtml xhtml} && XHtml.IsValidNarrativeXhtml(xhtml.Value); + public static bool HtmlChecks(this PocoNode focus) => + focus is PrimitiveNode {Primitive: XHtml {ObjectValue: string xhtml}} && XHtml.IsValidNarrativeXhtml(xhtml, out _, out _); public static IEnumerable ToFhirValues(this IEnumerable results) { return results.Select(r => r.Poco); } - internal static decimal? AdjustBoundaryDecimal(decimal? input, long? precision, Func op) { if (input is null) return null; diff --git a/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs b/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs index 01c7c4ad06..5061a251f9 100644 --- a/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs +++ b/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs @@ -28,6 +28,7 @@ POSSIBILITY OF SUCH DAMAGE. */ +using Hl7.Fhir.Model; using Hl7.Fhir.Specification; using Hl7.Fhir.Utility; using Hl7.Fhir.Validation; @@ -63,7 +64,7 @@ public FhirElementAttribute(string name, ChoiceType choice, XmlRepresentation re /// /// The name of the element in FHIR this property represents. /// - public string Name { get; private set; } + public string Name { get; } /// /// The element represents the primitive `value` attribute/property in the FHIR serialization @@ -123,10 +124,13 @@ public FhirElementAttribute(string name, ChoiceType choice, XmlRepresentation re return result.FirstOrDefault(); } - private void validateElement(object value, ValidationContext validationContext, List result) + private void validateElement(object value, ValidationContext validationContext, List results) { - DotNetAttributeValidation.TryValidate(value, validationContext.IntoPath(value, validationContext.MemberName ?? Name), result); - } -} + // We will only validate the element's value recursively if it is a POCO, otherwise, this attribute + // will do nothing. + if (value is not Base b) return; -#nullable restore \ No newline at end of file + var nestedContext = validationContext.IntoPath(b, validationContext.MemberName ?? Name); + _ = Validator.TryValidateObject(b, nestedContext, results, validateAllProperties: true); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/Base.cs b/src/Hl7.Fhir.Base/Model/Base.cs index 2a9932b8e2..aef2bd6ed6 100644 --- a/src/Hl7.Fhir.Base/Model/Base.cs +++ b/src/Hl7.Fhir.Base/Model/Base.cs @@ -40,8 +40,7 @@ POSSIBILITY OF SUCH DAMAGE. namespace Hl7.Fhir.Model; -public abstract partial class Base : IAnnotatable, - IValidatableObject, INotifyPropertyChanged +public abstract partial class Base : IAnnotatable, INotifyPropertyChanged { /// /// FHIR Type Name @@ -56,8 +55,6 @@ public abstract partial class Base : IAnnotatable, protected Dictionary Overflow => LazyInitializer.EnsureInitialized(ref _overflow, () => new Dictionary())!; - public virtual IEnumerable Validate(ValidationContext validationContext) => []; - #region << Annotations >> [NonSerialized] private AnnotationList? _annotations = null; diff --git a/src/Hl7.Fhir.Base/Model/Base64Binary.cs b/src/Hl7.Fhir.Base/Model/Base64Binary.cs index 89e80adda6..9e2492e2cd 100644 --- a/src/Hl7.Fhir.Base/Model/Base64Binary.cs +++ b/src/Hl7.Fhir.Base/Model/Base64Binary.cs @@ -29,7 +29,12 @@ POSSIBILITY OF SUCH DAMAGE. */ using Hl7.Fhir.ElementModel.Types; +using Hl7.Fhir.Introspection; +using Hl7.Fhir.Specification; using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using COVE=Hl7.Fhir.Validation.CodedValidationException; using P=Hl7.Fhir.ElementModel.Types; #nullable enable @@ -38,28 +43,104 @@ namespace Hl7.Fhir.Model; public partial class Base64Binary { - public static Base64Binary FromBase64String(string base64Data) => - new(Convert.FromBase64String(base64Data)); + [FhirElement("value", IsPrimitiveValue = true, XmlSerialization = XmlRepresentation.XmlAttr, InSummary = true, + Order = 30)] + [DeclaredType(Type = typeof(P.String))] + [DataMember] + public byte[]? Value + { + get + { + if (ValidateObjectValue(null) is {} error) + throw error; - public static Base64Binary FromText(string text) => - new(System.Text.Encoding.UTF8.GetBytes(text)); + return _parsedValue; + } + + set + { + _parsedValue = value; + base.ObjectValue = null; + OnPropertyChanged("Value"); + } + } + + public override object? ObjectValue + { + get + { + if (_parsedValue is not null && base.ObjectValue is null) + { + base.ObjectValue = Convert.ToBase64String(_parsedValue); + _parsedValue = null; // Clear the parsed value to free up memory + } + + return base.ObjectValue; + } + set + { + base.ObjectValue = value; + _parsedValue = null; + } + } + + [NonSerialized] // To prevent binary serialization from serializing this field + private byte[]? _parsedValue = null; /// - /// Checks whether the given literal is correctly formatted. + /// Validates the JsonValue and updates the internal cached byte[] Value, releasing + /// the data in JsonValue to save memory. /// - public static bool IsValidValue(string value) + protected internal override COVE? ValidateObjectValue(ValidationContext? context) + { + if (_parsedValue is not null || base.ObjectValue is null) return null; + _parsedValue = null; + + if (base.ObjectValue is not string unparsed) + return COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName); + + _parsedValue = doParse(unparsed); + + // Clear the string value to free up memory if we have successfully parsed the value. + if(_parsedValue is not null) + base.ObjectValue = null; + + return _parsedValue is null ? COVE.INVALID_BASE64_VALUE(context, unparsed) : null; + } + + private static byte[]? doParse(string literal) { try { - _ = Convert.FromBase64String(value); - return true; + return Convert.FromBase64String(literal); } catch { - return false; + return null; } } + /// + /// Checks whether the given literal is correctly formatted. + /// + public static bool IsValidValue(string value) => doParse(value) is not null; + + + /// + /// Constructs a Base64Binary instance from a string of base64-encoded data. + /// + public static Base64Binary FromBase64String(string base64Data) => + new() { ObjectValue = base64Data }; + + /// + /// Constructs a Base64Binary instance from a string of human-readable text. + /// + /// + /// + public static Base64Binary FromText(string text) => + new(System.Text.Encoding.UTF8.GetBytes(text)); + + /// /// Converts this binary to a Base64-encoded . /// @@ -69,7 +150,7 @@ public P.String ToSystemString() => (P.String?)TryConvertToSystemTypeInternal() throw new InvalidOperationException("Value is null."); protected internal override Any? TryConvertToSystemTypeInternal() => - Value is not null - ? new P.String(Convert.ToBase64String(Value)) + ObjectValue is string s + ? new P.String(s) : null; } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/Canonical.cs b/src/Hl7.Fhir.Base/Model/Canonical.cs index ecf9966edc..eb0e8a9167 100644 --- a/src/Hl7.Fhir.Base/Model/Canonical.cs +++ b/src/Hl7.Fhir.Base/Model/Canonical.cs @@ -31,6 +31,8 @@ POSSIBILITY OF SUCH DAMAGE. using Hl7.Fhir.Utility; using Hl7.Fhir.Rest; using System; +using System.ComponentModel.DataAnnotations; +using COVE=Hl7.Fhir.Validation.CodedValidationException; #nullable enable @@ -93,6 +95,19 @@ public void Deconstruct(out string? uri, out string? version, out string? fragme /// public static implicit operator string?(Canonical? value) => value?.Value; + /// + /// Validates the JsonValue. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string unparsed when IsValidValue(unparsed) => null, + string unparsed => COVE.LITERAL_INVALID(context, unparsed, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + + /// /// Checks whether the given literal is correctly formatted. /// diff --git a/src/Hl7.Fhir.Base/Model/Code.cs b/src/Hl7.Fhir.Base/Model/Code.cs index e28dc860f0..7978083bac 100644 --- a/src/Hl7.Fhir.Base/Model/Code.cs +++ b/src/Hl7.Fhir.Base/Model/Code.cs @@ -32,9 +32,10 @@ POSSIBILITY OF SUCH DAMAGE. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using P=Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; @@ -54,6 +55,19 @@ Value is not null ? new P.Code(system: null, code: Value, display: null, version: null) : null; + /// + /// Validates the JsonValue. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string unparsed => IsValidValue(unparsed) + ? null + : COVE.LITERAL_INVALID(context, unparsed, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + /// /// Checks whether the given literal is correctly formatted. /// diff --git a/src/Hl7.Fhir.Base/Model/CodeOfT.cs b/src/Hl7.Fhir.Base/Model/CodeOfT.cs index 945fb47d53..119404eaf0 100644 --- a/src/Hl7.Fhir.Base/Model/CodeOfT.cs +++ b/src/Hl7.Fhir.Base/Model/CodeOfT.cs @@ -36,7 +36,6 @@ POSSIBILITY OF SUCH DAMAGE. using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using COVE = Hl7.Fhir.Validation.CodedValidationException; using P = Hl7.Fhir.ElementModel.Types; @@ -68,50 +67,76 @@ public Code(T? value) Value = value; } + [NonSerialized] // To prevent binary serialization from serializing this field + private T? _parsedValue = null; + + public override object? ObjectValue + { + get + { + if (_parsedValue is not null && base.ObjectValue is null) + base.ObjectValue = _parsedValue.GetLiteral(); + + return base.ObjectValue; + } + set + { + base.ObjectValue = value; + _parsedValue = null; + } + } + + // Primitive value of element [FhirElement("value", IsPrimitiveValue = true, XmlSerialization = XmlRepresentation.XmlAttr, InSummary = true, Order = 30)] [DataMember] new public T? Value { - get => TryParseObjectValue(out var value) - ? value - : throw new InvalidCastException($"Value '{ObjectValue}' cannot be cast to a member of enumeration {typeof(T).Name}."); + get + { + if (ValidateObjectValue(null) is { } error) + throw error; + + return _parsedValue; + } + set { - ObjectValue = value?.GetLiteral(); + _parsedValue = value; + base.ObjectValue = null; OnPropertyChanged("Value"); } } - internal bool TryParseObjectValue(out T? value) + /// + /// Validates the JsonValue and updates the internal cached enum Value. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) { - value = null; + if (_parsedValue is not null || base.ObjectValue is null) return null; - if (ObjectValue is string s && EnumUtility.ParseLiteral(s) is { } parsed) - { - value = parsed; - return true; - } - else return ObjectValue is null; + _parsedValue = null; + + if (base.ObjectValue is not string unparsed) + return COVE.INCORRECT_LITERAL_VALUE_TYPE(context, base.ObjectValue, this.TypeName); + + _parsedValue = doParse(unparsed); + return _parsedValue is null ? COVE.INVALID_CODED_VALUE(context, unparsed, EnumUtility.GetName()) : null; } + private static T? doParse(string literal) => EnumUtility.ParseLiteral(literal); + + /// + /// Checks whether the given literal is correctly formatted. + /// + public static new bool IsValidValue(string value) => doParse(value) is not null; + /// public override IEnumerable ToCodings() => [new(Value?.GetSystem(), Value?.GetLiteral())]; protected internal override P.Any? TryConvertToSystemTypeInternal() => Value is not null ? new P.Code(Value.GetSystem(), Value.GetLiteral()!, display: null, version: null) : null; - public override IEnumerable Validate(ValidationContext validationContext) - { - var baseResults = base.Validate(validationContext); - - if (TryParseObjectValue(out _)) - return baseResults; - - var result = COVE.INVALID_CODED_VALUE(validationContext, ObjectValue, EnumUtility.GetName()).AsResult(validationContext); - return baseResults.Append(result); - } - protected internal override Base DeepCopyInternal() { var instance = new Code(); diff --git a/src/Hl7.Fhir.Base/Model/Date.cs b/src/Hl7.Fhir.Base/Model/Date.cs index cce0ac907c..177c8a289b 100644 --- a/src/Hl7.Fhir.Base/Model/Date.cs +++ b/src/Hl7.Fhir.Base/Model/Date.cs @@ -30,24 +30,29 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class Date { public Date(int year, int month, int day) - : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTHDAY, year, month, day)) + : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTHDAY, year, + month, day)) { } public Date(int year, int month) - : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTH, year, month)) + : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEARMONTH, year, + month)) { } - public Date(int year) : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEAR, year)) + public Date(int year) : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FhirDateTime.FMT_YEAR, + year)) { } @@ -63,13 +68,32 @@ public Date(int year) : this(string.Format(System.Globalization.CultureInfo.Inva /// public static Date UtcToday() => FromDateTimeOffset(DateTimeOffset.UtcNow); - [NonSerialized] // To prevent binary serialization from serializing this field + [NonSerialized] // To prevent binary serialization from serializing this field private P.Date? _parsedValue = null; - // This is a sentinel value that marks that the current string representation is - // not parseable, so we don't have to try again. It's value is never used, it's just - // checked by reference. - private static readonly P.Date INVALID_VALUE = P.Date.FromDateTimeOffset(DateTimeOffset.MinValue); + /// + /// Validates the JsonValue and updates the internal cached Date value. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) + { + if (_parsedValue is not null || base.ObjectValue is null) return null; + + _parsedValue = null; + + if (base.ObjectValue is not string unparsed) + return COVE.INCORRECT_LITERAL_VALUE_TYPE(context, base.ObjectValue, this.TypeName); + + _parsedValue = doParse(unparsed); + return _parsedValue is null ? COVE.LITERAL_INVALID(context, base.ObjectValue, this.TypeName) : null; + } + + private static P.Date? doParse(string value) => + P.Date.TryParse(value, out var v) && !v.HasOffset ? v : null; + + /// + /// Checks whether the given literal is correctly formatted. + /// + public static bool IsValidValue(string value) => doParse(value) is not null; /// /// Converts a Fhir Date to a CQL . @@ -77,22 +101,14 @@ public Date(int year) : this(string.Format(System.Globalization.CultureInfo.Inva /// true if the Fhir Date contains a valid date string, false otherwise. public bool TryToSystemDate([NotNullWhen(true)] out P.Date? date) { - if (_parsedValue is null) - { - if (Value is not null && !(P.Date.TryParse(Value, out _parsedValue) && !_parsedValue!.HasOffset)) - _parsedValue = INVALID_VALUE; - } - - if (hasInvalidParsedValue()) + if (ValidateObjectValue(null) is not null || _parsedValue is null) { date = null; return false; } - date = _parsedValue!; + date = _parsedValue; return true; - - bool hasInvalidParsedValue() => ReferenceEquals(_parsedValue, INVALID_VALUE); } /// @@ -102,33 +118,24 @@ public bool TryToSystemDate([NotNullWhen(true)] out P.Date? date) /// Thrown when the Value does not contain a valid FHIR Date. public P.Date ToSystemDate() { - if(Value is null) - throw new InvalidOperationException("Value is null."); + if (ValidateObjectValue(null) is {} error) + throw error; + + if(_parsedValue is null) + throw new InvalidOperationException("Value is null"); - return TryToSystemDate(out var dt) - ? dt - : throw new FormatException($"String '{Value}' was not recognized as a valid date."); + return _parsedValue; } protected internal override P.Any? TryConvertToSystemTypeInternal() => TryToSystemDate(out var date) ? date : null; - protected override void OnObjectValueChanged() - { - _parsedValue = null; - base.OnObjectValueChanged(); - } - /// /// Converts this Fhir Fhir Date to a . /// /// A DateTimeOffset filled out to midnight, january 1 (UTC) in case of a partial date. public DateTimeOffset ToDateTimeOffset() { - if (Value == null) throw new InvalidOperationException("Date's value is null."); - - // TryToDate() will convert partial date/times by filling out to midnight/january 1 UTC - if (!TryToSystemDate(out var dt)) - throw new FormatException($"Date '{Value}' was not recognized as a valid datetime."); + var dt = ToSystemDate(); // Since Value is not null and the parsed value is valid, dto will not be null return dt.ToDateTimeOffset(TimeSpan.Zero); @@ -140,7 +147,7 @@ public DateTimeOffset ToDateTimeOffset() /// True if the value of the Fhir Date is not null and can be parsed as a DateTimeOffset, false otherwise. public bool TryToDateTimeOffset(out DateTimeOffset dto) { - if (Value is not null && TryToSystemDate(out var dt)) + if (TryToSystemDate(out var dt)) { dto = dt.ToDateTimeOffset(TimeSpan.Zero); return true; @@ -150,8 +157,14 @@ public bool TryToDateTimeOffset(out DateTimeOffset dto) return false; } - /// - /// Checks whether the given literal is correctly formatted. - /// - public static bool IsValidValue(string value) => P.Date.TryParse(value, out var parsed) && !parsed.HasOffset; + + public override object? ObjectValue + { + get => base.ObjectValue; + set + { + base.ObjectValue = value; + _parsedValue = null; + } + } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/DomainResource.cs b/src/Hl7.Fhir.Base/Model/DomainResource.cs index 7144768bac..aa9451f89b 100644 --- a/src/Hl7.Fhir.Base/Model/DomainResource.cs +++ b/src/Hl7.Fhir.Base/Model/DomainResource.cs @@ -37,22 +37,20 @@ POSSIBILITY OF SUCH DAMAGE. namespace Hl7.Fhir.Model { [System.Diagnostics.DebuggerDisplay("\\{\"{TypeName,nq}/{Id,nq}\" Identity={DebuggerDisplay}}")] - public abstract partial class DomainResource : IModifierExtendable + public abstract partial class DomainResource : IValidatableObject { [DebuggerBrowsable(DebuggerBrowsableState.Never)] private Rest.ResourceIdentity DebuggerDisplay => this.ResourceIdentity(); - public override IEnumerable Validate(ValidationContext validationContext) + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) { - var result = new List(base.Validate(validationContext)); - if (this.Contained != null) { if (!Contained.OfType().All(cr => cr.Contained == null || !cr.Contained.Any())) - result.Add(COVE.CONTAINED_RESOURCES_CANNOT_BE_NESTED(validationContext).AsResult(validationContext)); + return [COVE.CONTAINED_RESOURCES_CANNOT_BE_NESTED(validationContext).AsResult(validationContext)]; } - return result; + return []; } /// @@ -86,4 +84,4 @@ public Resource FindContainedResource(string reference) return FindContainedResource(new ResourceReference(reference)); } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/DynamicDataType.cs b/src/Hl7.Fhir.Base/Model/DynamicDataType.cs index fda7e5abfb..91c458b279 100644 --- a/src/Hl7.Fhir.Base/Model/DynamicDataType.cs +++ b/src/Hl7.Fhir.Base/Model/DynamicDataType.cs @@ -1,8 +1,11 @@ #nullable enable using Hl7.Fhir.ElementModel.Types; using Hl7.Fhir.Introspection; +using Hl7.Fhir.Validation; using System; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; @@ -77,4 +80,9 @@ protected internal override Base DeepCopyInternal() } protected internal override Any? TryConvertToSystemTypeInternal() => null; + + protected internal override COVE? ValidateObjectValue(ValidationContext? validationContext) => + ObjectValue is string or bool or decimal or int + ? null + : COVE.INCORRECT_LITERAL_VALUE_TYPE(validationContext, ObjectValue, TypeName); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/FhirBoolean.cs b/src/Hl7.Fhir.Base/Model/FhirBoolean.cs index 0b0b5dfa0f..ebfc2bbd3b 100644 --- a/src/Hl7.Fhir.Base/Model/FhirBoolean.cs +++ b/src/Hl7.Fhir.Base/Model/FhirBoolean.cs @@ -31,6 +31,8 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; +using COVE=Hl7.Fhir.Validation.CodedValidationException; using P = Hl7.Fhir.ElementModel.Types; namespace Hl7.Fhir.Model; @@ -38,9 +40,14 @@ namespace Hl7.Fhir.Model; public partial class FhirBoolean { /// - /// Checks whether the given literal is correctly formatted. + /// Validates the JsonValue. /// - public static bool IsValidValue(string value) => P.Boolean.TryParse(value, out _); + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null or bool => null, + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; /// /// Converts this FhirBoolean to a . diff --git a/src/Hl7.Fhir.Base/Model/FhirDateTime.cs b/src/Hl7.Fhir.Base/Model/FhirDateTime.cs index 440d070d4c..c44493490d 100644 --- a/src/Hl7.Fhir.Base/Model/FhirDateTime.cs +++ b/src/Hl7.Fhir.Base/Model/FhirDateTime.cs @@ -32,8 +32,10 @@ POSSIBILITY OF SUCH DAMAGE. using Hl7.Fhir.Serialization; using System; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; @@ -68,7 +70,6 @@ public FhirDateTime(int year, int month, int day, int hr, int min, int sec, Time { } - public FhirDateTime(int year, int month, int day) : this(string.Format(System.Globalization.CultureInfo.InvariantCulture, FMT_YEARMONTHDAY, year, month, day)) { @@ -89,10 +90,31 @@ public FhirDateTime(int year) [NonSerialized] // To prevent binary serialization from serializing this field private P.DateTime? _parsedValue = null; - // This is a sentintel value that marks that the current string representation is - // not parseable, so we don't have to try again. It's value is never used, it's just - // checked by reference. - private static readonly P.DateTime INVALID_VALUE = P.DateTime.FromDateTimeOffset(DateTimeOffset.MinValue); + /// + /// Validates the JsonValue and updates the internal cached DateTime value. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) + { + if (_parsedValue is not null || base.ObjectValue is null) return null; + + _parsedValue = null; + + if (base.ObjectValue is not string unparsed) + return COVE.INCORRECT_LITERAL_VALUE_TYPE(context, base.ObjectValue, this.TypeName); + + _parsedValue = doParse(unparsed); + return _parsedValue is null ? COVE.LITERAL_INVALID(context, base.ObjectValue, this.TypeName) : null; + } + + private static P.DateTime? doParse(string literal) => + P.DateTime.TryParse(literal, out var parsed) && + (parsed.Precision <= P.DateTimePrecision.Day == !parsed.HasOffset) ? parsed : null; + + /// + /// Checks whether the given literal is correctly formatted. + /// + public static bool IsValidValue(string value) => doParse(value) is not null; + /// /// Converts a FhirDateTime to a . @@ -100,24 +122,14 @@ public FhirDateTime(int year) /// true if the FhirDateTime contains a valid date/time string, false otherwise. public bool TryToSystemDateTime([NotNullWhen(true)] out P.DateTime? dateTime) { - if (_parsedValue is null) - { - if (Value is not null && !P.DateTime.TryParse(Value, out _parsedValue)) - _parsedValue = INVALID_VALUE; - } - - if (hasInvalidParsedValue()) + if (ValidateObjectValue(null) is not null || _parsedValue is null) { dateTime = null; return false; } - else - { - dateTime = _parsedValue!; - return true; - } - bool hasInvalidParsedValue() => ReferenceEquals(_parsedValue, INVALID_VALUE); + dateTime = _parsedValue; + return true; } /// @@ -128,18 +140,23 @@ public bool TryToSystemDateTime([NotNullWhen(true)] out P.DateTime? dateTime) /// Thrown when the Value does not contain a valid FHIR DateTime. public P.DateTime ToSystemDateTime() { - if (Value is null) + if (ValidateObjectValue(null) is { } error) + throw error; + + if (_parsedValue is null) throw new InvalidOperationException("Value is null."); - return TryToSystemDateTime(out var dt) - ? dt - : throw new FormatException($"String '{Value}' was not recognized as a valid datetime."); - } + return _parsedValue; + } - protected override void OnObjectValueChanged() + public override object? ObjectValue { - _parsedValue = null; - base.OnObjectValueChanged(); + get => base.ObjectValue; + set + { + base.ObjectValue = value; + _parsedValue = null; + } } /// @@ -154,11 +171,7 @@ protected override void OnObjectValueChanged() /// effect on this, this merely converts the given Fhir datetime to the desired timezone public DateTimeOffset ToDateTimeOffset(TimeSpan zone) { - if (Value == null) throw new InvalidOperationException("FhirDateTime's value is null."); - - // TryToDateTime() will convert partial date/times by filling out to midnight/january 1 UTC. - if (!TryToSystemDateTime(out var dt)) - throw new FormatException($"DateTime '{Value}' was not recognized as a valid datetime."); + var dt = ToSystemDateTime(); // Since Value is not null and the parsed value is valid, dto will not be null return dt.ToDateTimeOffset(TimeSpan.Zero).ToOffset(zone); @@ -171,7 +184,7 @@ public DateTimeOffset ToDateTimeOffset(TimeSpan zone) /// specified timezone, false otherwise. public bool TryToDateTimeOffset(out DateTimeOffset dto) { - if (Value is not null && TryToSystemDateTime(out var dt) && dt.Offset is not null) + if (TryToSystemDateTime(out var dt) && dt.Offset is not null) { dto = dt.ToDateTimeOffset(dt.Offset.Value); return true; @@ -189,7 +202,7 @@ public bool TryToDateTimeOffset(out DateTimeOffset dto) /// True if the value of the FhirDateTime is not null and can be parsed as a DateTimeOffset, false otherwise. public bool TryToDateTimeOffset(TimeSpan defaultOffset, out DateTimeOffset dto) { - if (Value is not null && TryToSystemDateTime(out var dt)) + if (TryToSystemDateTime(out var dt)) { dto = dt.ToDateTimeOffset(defaultOffset); return true; @@ -200,10 +213,4 @@ public bool TryToDateTimeOffset(TimeSpan defaultOffset, out DateTimeOffset dto) } protected internal override P.Any? TryConvertToSystemTypeInternal() => TryToSystemDateTime(out var date) ? date : null; - - /// - /// Checks whether the given literal is correctly formatted. - /// - public static bool IsValidValue(string value) => P.DateTime.TryParse(value, out var parsed) && - (parsed.Precision <= P.DateTimePrecision.Day == !parsed.HasOffset); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/FhirDecimal.cs b/src/Hl7.Fhir.Base/Model/FhirDecimal.cs index 0f1ae6663a..87cca22787 100644 --- a/src/Hl7.Fhir.Base/Model/FhirDecimal.cs +++ b/src/Hl7.Fhir.Base/Model/FhirDecimal.cs @@ -31,16 +31,23 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class FhirDecimal { /// - /// Checks whether the given literal is correctly formatted. + /// Validates the JsonValue. /// - public static bool IsValidValue(string value) => P.Decimal.TryParse(value, out _); + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null or decimal => null, + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; /// /// Converts this FhirDecimal to a . diff --git a/src/Hl7.Fhir.Base/Model/FhirString.cs b/src/Hl7.Fhir.Base/Model/FhirString.cs index 063649e2d5..ab83545f44 100644 --- a/src/Hl7.Fhir.Base/Model/FhirString.cs +++ b/src/Hl7.Fhir.Base/Model/FhirString.cs @@ -31,12 +31,26 @@ POSSIBILITY OF SUCH DAMAGE. using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class FhirString : ICoded { + /// + /// Validates the JsonValue. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string s when IsValidValue(s) => null, + string s => COVE.LITERAL_INVALID(context, s, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + /// /// Checks whether the given literal is correctly formatted. /// diff --git a/src/Hl7.Fhir.Base/Model/FhirUri.cs b/src/Hl7.Fhir.Base/Model/FhirUri.cs index 41ccd55d34..6cd67cd682 100644 --- a/src/Hl7.Fhir.Base/Model/FhirUri.cs +++ b/src/Hl7.Fhir.Base/Model/FhirUri.cs @@ -32,7 +32,9 @@ POSSIBILITY OF SUCH DAMAGE. using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; @@ -43,6 +45,18 @@ public FhirUri(Uri uri) Value = uri.OriginalString; } + /// + /// Validates the JsonValue. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string unparsed when IsValidValue(unparsed) => null, + string unparsed => COVE.LITERAL_INVALID(context, unparsed, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + /// /// Checks whether the given literal is correctly formatted. /// @@ -61,15 +75,14 @@ public static bool IsValidValue(string value) return false; } - if (uri.IsAbsoluteUri) - { - var uris = uri.ToString(); + if (!uri.IsAbsoluteUri) return true; - if (uris.StartsWith("urn:oid:") && !Oid.IsValidValue(uris)) - return false; - else if (uris.StartsWith("urn:uuid:") && !Uuid.IsValidValue(uris)) - return false; - } + var uris = uri.ToString(); + + if (uris.StartsWith("urn:oid:") && !Oid.IsValidValue(uris)) + return false; + if (uris.StartsWith("urn:uuid:") && !Uuid.IsValidValue(uris)) + return false; return true; } diff --git a/src/Hl7.Fhir.Base/Model/FhirUrl.cs b/src/Hl7.Fhir.Base/Model/FhirUrl.cs index f03646dbf2..28a63a295d 100644 --- a/src/Hl7.Fhir.Base/Model/FhirUrl.cs +++ b/src/Hl7.Fhir.Base/Model/FhirUrl.cs @@ -30,7 +30,8 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; -using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; +using COVE=Hl7.Fhir.Validation.CodedValidationException; using P = Hl7.Fhir.ElementModel.Types; namespace Hl7.Fhir.Model; @@ -42,6 +43,18 @@ public FhirUrl(Uri uri) Value = uri.OriginalString; } + /// + /// Validates the JsonValue. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string unparsed when IsValidValue(unparsed) => null, + string unparsed => COVE.LITERAL_INVALID(context, unparsed, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + /// /// Checks whether the given literal is correctly formatted. /// @@ -49,7 +62,7 @@ public static bool IsValidValue(string value) { try { - var uri = new Uri(value, UriKind.RelativeOrAbsolute); + _ = new Uri(value, UriKind.RelativeOrAbsolute); return true; } catch diff --git a/src/Hl7.Fhir.Base/Model/Generated/Base64Binary.cs b/src/Hl7.Fhir.Base/Model/Generated/Base64Binary.cs index 4b347a4856..12a3214ba8 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Base64Binary.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Base64Binary.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -68,18 +69,6 @@ public Base64Binary(byte[] value) public Base64Binary(): this((byte[])null) {} - /// - /// Primitive value of the element - /// - [FhirElement("value", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)] - [DeclaredType(Type = typeof(SystemPrimitive.String))] - [DataMember] - public byte[] Value - { - get { return (byte[])ObjectValue; } - set { ObjectValue = value; OnPropertyChanged("Value"); } - } - protected internal override Base DeepCopyInternal() { var instance = new Base64Binary(); diff --git a/src/Hl7.Fhir.Base/Model/Generated/Canonical.cs b/src/Hl7.Fhir.Base/Model/Generated/Canonical.cs index 4138a22616..4574c57ad2 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Canonical.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Canonical.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -76,7 +77,7 @@ public Canonical(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Code.cs b/src/Hl7.Fhir.Base/Model/Generated/Code.cs index 77110d05f5..5481ebc255 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Code.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Code.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -74,7 +75,7 @@ public Code(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Date.cs b/src/Hl7.Fhir.Base/Model/Generated/Date.cs index 89b1e6a367..efb4b4b05c 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Date.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Date.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -74,7 +75,7 @@ public Date(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/FhirBoolean.cs b/src/Hl7.Fhir.Base/Model/Generated/FhirBoolean.cs index a240bcca1d..fad870b494 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/FhirBoolean.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/FhirBoolean.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -73,7 +74,7 @@ public FhirBoolean(): this((bool?)null) {} [DataMember] public bool? Value { - get { return (bool?)ObjectValue; } + get { return ObjectValue is bool or null ? (bool?)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/FhirDateTime.cs b/src/Hl7.Fhir.Base/Model/Generated/FhirDateTime.cs index 9b00de76dc..d71b13ff9b 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/FhirDateTime.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/FhirDateTime.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -77,7 +78,7 @@ public FhirDateTime(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/FhirDecimal.cs b/src/Hl7.Fhir.Base/Model/Generated/FhirDecimal.cs index fc5dc9574b..35b1dde629 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/FhirDecimal.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/FhirDecimal.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -76,7 +77,7 @@ public FhirDecimal(): this((decimal?)null) {} [DataMember] public decimal? Value { - get { return (decimal?)ObjectValue; } + get { return ObjectValue is decimal or null ? (decimal?)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/FhirString.cs b/src/Hl7.Fhir.Base/Model/Generated/FhirString.cs index 3355a04713..c562952927 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/FhirString.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/FhirString.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -77,7 +78,7 @@ public FhirString(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/FhirUri.cs b/src/Hl7.Fhir.Base/Model/Generated/FhirUri.cs index 1c724187e9..7b61da5f3f 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/FhirUri.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/FhirUri.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -77,7 +78,7 @@ public FhirUri(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/FhirUrl.cs b/src/Hl7.Fhir.Base/Model/Generated/FhirUrl.cs index 1463daae04..605794031b 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/FhirUrl.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/FhirUrl.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -73,7 +74,7 @@ public FhirUrl(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Id.cs b/src/Hl7.Fhir.Base/Model/Generated/Id.cs index ead6728688..7064bba8f6 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Id.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Id.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -77,7 +78,7 @@ public Id(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Instant.cs b/src/Hl7.Fhir.Base/Model/Generated/Instant.cs index 7a814bd780..79028c84d5 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Instant.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Instant.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -68,18 +69,6 @@ public Instant(DateTimeOffset? value) public Instant(): this((DateTimeOffset?)null) {} - /// - /// Primitive value of the element - /// - [FhirElement("value", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)] - [DeclaredType(Type = typeof(SystemPrimitive.DateTime))] - [DataMember] - public DateTimeOffset? Value - { - get { return (DateTimeOffset?)ObjectValue; } - set { ObjectValue = value; OnPropertyChanged("Value"); } - } - protected internal override Base DeepCopyInternal() { var instance = new Instant(); diff --git a/src/Hl7.Fhir.Base/Model/Generated/Integer.cs b/src/Hl7.Fhir.Base/Model/Generated/Integer.cs index d2ddc68f32..0de52b51b2 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Integer.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Integer.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -76,7 +77,7 @@ public Integer(): this((int?)null) {} [DataMember] public int? Value { - get { return (int?)ObjectValue; } + get { return ObjectValue is int or null ? (int?)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Integer64.cs b/src/Hl7.Fhir.Base/Model/Generated/Integer64.cs index 8f637d900a..fa06115526 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Integer64.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Integer64.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -68,26 +69,6 @@ public Integer64(long? value) public Integer64(): this((long?)null) {} - /// - /// Primitive value of the element - /// - [FhirElement("value", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)] - [DeclaredType(Type = typeof(SystemPrimitive.Long))] - [DataMember] - public long? Value - { - get -{ - return ObjectValue switch - { - null => null, - long l => l, - _ => Convert.ToInt64(ObjectValue) - }; -} - set { ObjectValue = value; OnPropertyChanged("Value"); } - } - protected internal override Base DeepCopyInternal() { var instance = new Integer64(); diff --git a/src/Hl7.Fhir.Base/Model/Generated/Markdown.cs b/src/Hl7.Fhir.Base/Model/Generated/Markdown.cs index 257bfba625..d795f61be4 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Markdown.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Markdown.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -77,7 +78,7 @@ public Markdown(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Oid.cs b/src/Hl7.Fhir.Base/Model/Generated/Oid.cs index 48fe22bdd4..70f7baf49b 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Oid.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Oid.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -77,7 +78,7 @@ public Oid(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/PositiveInt.cs b/src/Hl7.Fhir.Base/Model/Generated/PositiveInt.cs index 18eacf29a8..66bb2846e8 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/PositiveInt.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/PositiveInt.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -73,7 +74,7 @@ public PositiveInt(): this((int?)null) {} [DataMember] public int? Value { - get { return (int?)ObjectValue; } + get { return ObjectValue is int or null ? (int?)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/PrimitiveType.cs b/src/Hl7.Fhir.Base/Model/Generated/PrimitiveType.cs index 85ea439959..402e4ec8e4 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/PrimitiveType.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/PrimitiveType.cs @@ -74,11 +74,7 @@ public override bool CompareChildren(Base other, IEqualityComparer compare if(!base.CompareChildren(otherT, comparer)) return false; - var otherValue = otherT.ObjectValue; - if (ObjectValue is byte[] bytes && otherValue is byte[] bytesOther) - return Enumerable.SequenceEqual(bytes, bytesOther); - else - return Equals(ObjectValue, otherT.ObjectValue); + return Equals(ObjectValue, otherT.ObjectValue); } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Time.cs b/src/Hl7.Fhir.Base/Model/Generated/Time.cs index fa9e930ca8..5f910ae09f 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Time.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Time.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -74,7 +75,7 @@ public Time(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/UnsignedInt.cs b/src/Hl7.Fhir.Base/Model/Generated/UnsignedInt.cs index f351637ec7..3366bd7091 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/UnsignedInt.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/UnsignedInt.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -73,7 +74,7 @@ public UnsignedInt(): this((int?)null) {} [DataMember] public int? Value { - get { return (int?)ObjectValue; } + get { return ObjectValue is int or null ? (int?)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/Uuid.cs b/src/Hl7.Fhir.Base/Model/Generated/Uuid.cs index d9e34d00ca..5c0be7f55d 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Uuid.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Uuid.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -77,7 +78,7 @@ public Uuid(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } diff --git a/src/Hl7.Fhir.Base/Model/Generated/XHtml.cs b/src/Hl7.Fhir.Base/Model/Generated/XHtml.cs index e354754f90..a8804a160d 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/XHtml.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/XHtml.cs @@ -8,6 +8,7 @@ using Hl7.Fhir.Specification; using Hl7.Fhir.Validation; using SystemPrimitive = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; /* Copyright (c) 2011+, HL7, Inc. @@ -71,7 +72,7 @@ public XHtml(): this((string)null) {} [DataMember] public string Value { - get { return (string)ObjectValue; } + get { return ObjectValue is string or null ? (string)ObjectValue : throw COVE.INCORRECT_LITERAL_VALUE_TYPE(null, ObjectValue, this.TypeName); } set { ObjectValue = value; OnPropertyChanged("Value"); } } @@ -81,8 +82,9 @@ protected internal override Base DeepCopyInternal() CopyToInternal(instance); return instance; } + } } -// end of file \ No newline at end of file +// end of file diff --git a/src/Hl7.Fhir.Base/Model/IValue.cs b/src/Hl7.Fhir.Base/Model/IValue.cs index 189d29a741..2e5ff68385 100644 --- a/src/Hl7.Fhir.Base/Model/IValue.cs +++ b/src/Hl7.Fhir.Base/Model/IValue.cs @@ -1,28 +1,21 @@ -/* +/* * Copyright (c) 2020, 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 */ -namespace Hl7.Fhir.Model -{ - // [WMR 20160615] - // Motivation: we want to enable polymorphism against subclasses of Primitive, especially the Value property - // However subclasses of Primitive don't implement Value in a consistent way - // e.g. Code is derived from Primitive but exposes a Value property of type T? - // => As workaround, subclasses can implement a common IValue interface - - /// Common generic Value property interface. - /// The value type. - public interface IValue - { - /// Gets or sets the value - T Value { get; set; } - } +namespace Hl7.Fhir.Model; - /// Common generic nullable value property interface. - /// The value type. - public interface INullableValue : IValue where T : struct { } +/// Common generic Value property interface. +/// The value type. +public interface IValue +{ + /// Gets or sets the value + T Value { get; set; } } + +/// Common generic nullable value property interface. +/// The value type. +public interface INullableValue : IValue where T : struct { } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/Id.cs b/src/Hl7.Fhir.Base/Model/Id.cs index ae57ccc130..d355e491e7 100644 --- a/src/Hl7.Fhir.Base/Model/Id.cs +++ b/src/Hl7.Fhir.Base/Model/Id.cs @@ -32,13 +32,27 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class Id { + /// + /// Validates the JsonValue and updates the internal cached Date value. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string unparsed when IsValidValue(unparsed) => null, + string unparsed => COVE.LITERAL_INVALID(context, unparsed, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + /// /// Checks whether the given literal is correctly formatted. /// diff --git a/src/Hl7.Fhir.Base/Model/Instant.cs b/src/Hl7.Fhir.Base/Model/Instant.cs index 6e2466ba59..e8d5352a79 100644 --- a/src/Hl7.Fhir.Base/Model/Instant.cs +++ b/src/Hl7.Fhir.Base/Model/Instant.cs @@ -29,13 +29,41 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable +using Hl7.Fhir.Introspection; +using Hl7.Fhir.Specification; using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class Instant { + [FhirElement("value", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)] + [DeclaredType(Type = typeof(P.DateTime))] + [DataMember] + public DateTimeOffset? Value + { + get + { + if (ValidateObjectValue(null) is {} error) + throw error; + + return _parsedValue?.ToDateTimeOffset(TimeSpan.Zero); + } + + set + { + _parsedValue = value is null ? null : P.DateTime.FromDateTimeOffset(value.Value); + base.ObjectValue = null; + OnPropertyChanged("Value"); + } + } + + public static Instant FromLocalDateTime(int year, int month, int day, int hour, int min, int sec, int millis = 0) => new(new DateTimeOffset(year, month, day, hour, min, sec, millis, DateTimeOffset.Now.Offset)); @@ -51,19 +79,80 @@ public static Instant FromDateTimeUtc(int year, int month, int day, /// public static Instant Now() => new(DateTimeOffset.Now); + [NonSerialized] // To prevent binary serialization from serializing this field + private P.DateTime? _parsedValue = null; + + /// + /// Validates the JsonValue and updates the internal cached CQL DateTime value. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) + { + if (_parsedValue is not null || base.ObjectValue is null) return null; + + _parsedValue = null; + + if (base.ObjectValue is not string unparsed) + return COVE.INCORRECT_LITERAL_VALUE_TYPE(context, base.ObjectValue, this.TypeName); + + _parsedValue = doParse(unparsed); + return _parsedValue is null ? COVE.LITERAL_INVALID(context, base.ObjectValue, this.TypeName) : null; + } + + private static P.DateTime? doParse(string literal) => + P.DateTime.TryParse(literal, out var parsed) && parsed.IsInstant ? parsed : null; + /// /// Checks whether the given literal is correctly formatted. /// - public static bool IsValidValue(string value) => P.DateTime.TryParse(value, out var dateTime) && - dateTime is { Precision: >= P.DateTimePrecision.Second, HasOffset: true }; + public static bool IsValidValue(string value) => doParse(value) is not null; + + public override object? ObjectValue + { + get + { + if (_parsedValue is not null && base.ObjectValue is null) + base.ObjectValue = _parsedValue.ToString(); + + return base.ObjectValue; + } + set + { + base.ObjectValue = value; + _parsedValue = null; + } + } + /// - /// Converts this Instant to a . + /// Converts an Instant to a . /// - /// The Value of this DateTime is null. - public P.DateTime ToSystemDateTime() => - (P.DateTime?)TryConvertToSystemTypeInternal() ?? - throw new InvalidOperationException("Instant's value is null and can therefore not be converted to a System DateTime."); + /// true if the Instant contains a valid date/time string, false otherwise. + public bool TryToSystemDateTime([NotNullWhen(true)] out P.DateTime? dateTime) + { + if (ValidateObjectValue(null) is not null || _parsedValue is null) + { + dateTime = null; + return false; + } + + dateTime = _parsedValue; + return true; + } + +/// +/// Converts this Instant to a . +/// +/// The Value of this DateTime is null. + + public P.DateTime ToSystemDateTime() + { + if (ValidateObjectValue(null) is { } error) + throw error; + + if (_parsedValue is null) + throw new InvalidOperationException("Value is null."); + + return _parsedValue; + } - protected internal override P.Any? TryConvertToSystemTypeInternal() => - Value is not null ? P.DateTime.FromDateTimeOffset(Value.Value) : null; + protected internal override P.Any? TryConvertToSystemTypeInternal() => TryToSystemDateTime(out var date) ? date : null; } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/Integer.cs b/src/Hl7.Fhir.Base/Model/Integer.cs index 69312a4083..4e15364266 100644 --- a/src/Hl7.Fhir.Base/Model/Integer.cs +++ b/src/Hl7.Fhir.Base/Model/Integer.cs @@ -31,16 +31,23 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class Integer { /// - /// Checks whether the given literal is correctly formatted. + /// Validates the JsonValue. /// - public static bool IsValidValue(string value) => P.Integer.TryParse(value, out _); + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null or int => null, + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; /// /// Converts this Integer to a . diff --git a/src/Hl7.Fhir.Base/Model/Integer64.cs b/src/Hl7.Fhir.Base/Model/Integer64.cs index 2507b7023f..e684b93b88 100644 --- a/src/Hl7.Fhir.Base/Model/Integer64.cs +++ b/src/Hl7.Fhir.Base/Model/Integer64.cs @@ -30,28 +30,117 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable +using Hl7.Fhir.Introspection; +using Hl7.Fhir.Specification; using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using System.Xml; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class Integer64 { + [FhirElement("value", IsPrimitiveValue=true, XmlSerialization=XmlRepresentation.XmlAttr, InSummary=true, Order=30)] + [DeclaredType(Type = typeof(P.Long))] + [DataMember] + public long? Value + { + get + { + if (ValidateObjectValue(null) is {} error) + throw error; + + return _parsedValue; + } + + set + { + _parsedValue = value; + base.ObjectValue = null; + OnPropertyChanged("Value"); + } + } + + [NonSerialized] // To prevent binary serialization from serializing this field + private long? _parsedValue = null; + + /// + /// Validates the JsonValue and updates the internal cached long value. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) + { + if (_parsedValue is not null || base.ObjectValue is null) return null; + + _parsedValue = null; + + if (base.ObjectValue is not string unparsed) + return COVE.INCORRECT_LITERAL_VALUE_TYPE(context, base.ObjectValue, this.TypeName); + + _parsedValue = doParse(unparsed); + return _parsedValue is null ? COVE.LITERAL_INVALID(context, base.ObjectValue, this.TypeName) : null; + } + + private static long? doParse(string literal) => + P.Long.TryParse(literal, out var parsedLong) ? parsedLong.Value : null; + /// /// Checks whether the given literal is correctly formatted. /// - public static bool IsValidValue(string value) => ElementModel.Types.Long.TryParse(value, out _); + public static bool IsValidValue(string value) => doParse(value) is not null; + + public override object? ObjectValue + { + get + { + if (_parsedValue is not null && base.ObjectValue is null) + base.ObjectValue = XmlConvert.ToString(_parsedValue.Value); + + return base.ObjectValue; + } + set + { + base.ObjectValue = value; + _parsedValue = null; + } + } + + + + /// + /// Converts an Insteger64 to a . + /// + /// true if the Integer64 contains a valid date/time string, false otherwise. + public bool TryToSystemLong([NotNullWhen(true)] out P.Long? longValue) + { + if (ValidateObjectValue(null) is not null || _parsedValue is null) + { + longValue = null; + return false; + } + + longValue = new P.Long(_parsedValue.Value); + return true; + } /// /// Converts this Integer64 to a . /// /// The Value of this Integer64 is null, /// which is not valid for System longs. - public P.Long ToSystemLong() => - (P.Long?)TryConvertToSystemTypeInternal() - ?? throw new InvalidOperationException("Value is null."); + public P.Long ToSystemLong() + { + if (ValidateObjectValue(null) is { } error) + throw error; + + if (_parsedValue is null) + throw new InvalidOperationException("Value is null."); + + return new P.Long(_parsedValue.Value); + } - protected internal override P.Any? TryConvertToSystemTypeInternal() => - Value is not null - ? new P.Long(Value.Value) : null; + protected internal override P.Any? TryConvertToSystemTypeInternal() => TryToSystemLong(out var longValue) ? longValue : null; } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/Markdown.cs b/src/Hl7.Fhir.Base/Model/Markdown.cs index ac28003fef..8a6e472da0 100644 --- a/src/Hl7.Fhir.Base/Model/Markdown.cs +++ b/src/Hl7.Fhir.Base/Model/Markdown.cs @@ -31,16 +31,25 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class Markdown { /// - /// Checks whether the given literal is correctly formatted. + /// Validates the JsonValue. /// - public static bool IsValidValue(string value) => FhirString.IsValidValue(value); + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string s when IsValidValue(s) => null, + string s => COVE.LITERAL_INVALID(context, s, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; public static implicit operator string?(Markdown? md) => md?.Value; public static implicit operator Markdown?(string? s) => s is not null ? new Markdown(s) : null; @@ -55,4 +64,8 @@ public P.String ToSystemString() => protected internal override P.Any? TryConvertToSystemTypeInternal() => Value is not null ? new P.String(Value) : null; + /// + /// Checks whether the given literal is correctly formatted. + /// + public static bool IsValidValue(string value) => FhirString.IsValidValue(value); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/Oid.cs b/src/Hl7.Fhir.Base/Model/Oid.cs index 973c2011b8..36b37c1933 100644 --- a/src/Hl7.Fhir.Base/Model/Oid.cs +++ b/src/Hl7.Fhir.Base/Model/Oid.cs @@ -30,13 +30,25 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class Oid { + /// Validates the JsonValue. + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string unparsed when IsValidValue(unparsed) => null, + string unparsed => COVE.LITERAL_INVALID(context, unparsed, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + /// /// Creates a new based on this oid. /// diff --git a/src/Hl7.Fhir.Base/Model/PositiveInt.cs b/src/Hl7.Fhir.Base/Model/PositiveInt.cs index 66180572a2..c497925ff2 100644 --- a/src/Hl7.Fhir.Base/Model/PositiveInt.cs +++ b/src/Hl7.Fhir.Base/Model/PositiveInt.cs @@ -31,16 +31,23 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class PositiveInt { - /// - /// Checks whether the given literal is correctly formatted. - /// - public static bool IsValidValue(string value) => P.Integer.TryParse(value, out var parsed) && parsed.Value > 0; + /// Validates the JsonValue. + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + > 0 => null, + int i => COVE.POSITIVE_INT_MUST_BE_POSITIVE(context, i), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; /// /// Converts this PositiveInt to a . diff --git a/src/Hl7.Fhir.Base/Model/PrimitiveType.cs b/src/Hl7.Fhir.Base/Model/PrimitiveType.cs index 6db89f575c..90bd931c6c 100644 --- a/src/Hl7.Fhir.Base/Model/PrimitiveType.cs +++ b/src/Hl7.Fhir.Base/Model/PrimitiveType.cs @@ -9,14 +9,17 @@ #nullable enable using Hl7.Fhir.Serialization; +using Hl7.Fhir.Validation; using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Linq; using P = Hl7.Fhir.ElementModel.Types; namespace Hl7.Fhir.Model; -public partial class PrimitiveType : P.IToSystemPrimitive +public partial class PrimitiveType : IValidatableObject, P.IToSystemPrimitive { /// /// The value of the primitive, stored as an object. Will generally contain the same value as the @@ -28,24 +31,7 @@ public partial class PrimitiveType : P.IToSystemPrimitive /// to store the original serialized string form of the value in the wire format when a parsing error is /// encountered. - private object? _objectValue; - public object? ObjectValue - { - get => _objectValue; - - set - { - if (!ReferenceEquals(value, _objectValue)) - { - _objectValue = value; - OnObjectValueChanged(); - } - } - } - - protected virtual void OnObjectValueChanged() - { - } + public virtual object? ObjectValue { get; set; } /// public override string? ToString() @@ -59,7 +45,7 @@ protected virtual void OnObjectValueChanged() /// Returns true if the primitive has any child elements (currently in FHIR this can /// be only the element id and zero or more extensions). /// - public bool HasElements => ElementId is not null || Extension?.Any() == true; + public bool HasElements => ElementIdElement?.ObjectValue is not null || Extension?.Any() == true; protected internal abstract P.Any? TryConvertToSystemTypeInternal(); @@ -70,6 +56,19 @@ bool P.IToSystemPrimitive.TryConvertToSystemType([NotNullWhen(true)] out P.Any? return result is not null; } + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) => + Validate(validationContext); + + protected virtual IEnumerable Validate(ValidationContext validationContext) => + ValidateObjectValue(validationContext) is { } result ? [result.AsResult(validationContext)] : []; + + /// + /// Validates the JsonValue. Some subclasses will also, as a side-effect, update + /// their internal cache if parsing and validating is expensive. + /// + protected internal abstract CodedValidationException? ValidateObjectValue(ValidationContext? validationContext); + + public bool HasValidValue() => ValidateObjectValue(null) is null; internal object? ToITypedElementValue() { @@ -85,7 +84,7 @@ bool P.IToSystemPrimitive.TryConvertToSystemType([NotNullWhen(true)] out P.Any? Integer64 fint64 => fint64.Value, PositiveInt pint => pint.Value, UnsignedInt unsint => unsint.Value, - Base64Binary { Value: { } b64 } => PrimitiveTypeConverter.ConvertTo(b64), + Base64Binary { ObjectValue: { } b64 } => b64, { } prim => prim.ObjectValue }; } diff --git a/src/Hl7.Fhir.Base/Model/Time.cs b/src/Hl7.Fhir.Base/Model/Time.cs index c675cc8494..091bbdfe79 100644 --- a/src/Hl7.Fhir.Base/Model/Time.cs +++ b/src/Hl7.Fhir.Base/Model/Time.cs @@ -31,9 +31,11 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; @@ -59,10 +61,29 @@ public Time(int hour, int minute, int second) : this(string.Format(CultureInfo.I [NonSerialized] // To prevent binary serialization from serializing this field private P.Time? _parsedValue = null; - // This is a sentintel value that marks that the current string representation is - // not parseable, so we don't have to try again. It's value is never used, it's just - // checked by reference. - private static readonly P.Time INVALID_VALUE = P.Time.FromDateTimeOffset(DateTimeOffset.MinValue); + /// + /// Validates the JsonValue and updates the internal cached Time value. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) + { + if (_parsedValue is not null || base.ObjectValue is null) return null; + + _parsedValue = null; + + if (base.ObjectValue is not string unparsed) + return COVE.INCORRECT_LITERAL_VALUE_TYPE(context, base.ObjectValue, this.TypeName); + + _parsedValue = doParse(unparsed); + return _parsedValue is null ? COVE.LITERAL_INVALID(context, base.ObjectValue, this.TypeName) : null; + } + + private static P.Time? doParse(string value) => + P.Time.TryParse(value, out var v) && !v.HasOffset ? v : null; + + /// + /// Checks whether the given literal is correctly formatted. + /// + public static bool IsValidValue(string value) => doParse(value) is not null; /// /// Converts a Fhir Time to a . @@ -70,24 +91,14 @@ public Time(int hour, int minute, int second) : this(string.Format(CultureInfo.I /// true if the Fhir Time contains a valid time string, false otherwise. public bool TryToSystemTime([NotNullWhen(true)] out P.Time? time) { - if (_parsedValue is null) - { - if (Value is not null && !P.Time.TryParse(Value, out _parsedValue)) - _parsedValue = INVALID_VALUE; - } - - if (hasInvalidParsedValue()) + if (ValidateObjectValue(null) is not null || _parsedValue is null) { time = null; return false; } - else - { - time = _parsedValue!; - return true; - } - bool hasInvalidParsedValue() => ReferenceEquals(_parsedValue, INVALID_VALUE); + time = _parsedValue; + return true; } /// @@ -98,22 +109,25 @@ public bool TryToSystemTime([NotNullWhen(true)] out P.Time? time) /// Thrown when the Value does not contain a valid FHIR Time. public P.Time ToSystemTime() { - if (Value is null) + if (ValidateObjectValue(null) is {} error) + throw error; + + if(_parsedValue is null) throw new InvalidOperationException("Value is null"); - return TryToSystemTime(out var dt) - ? dt - : throw new FormatException($"String '{Value}' was not recognized as a valid time."); + return _parsedValue; } + protected internal override P.Any? TryConvertToSystemTypeInternal() => TryToSystemTime(out var date) ? date : null; - protected internal override P.Any? TryConvertToSystemTypeInternal() => - TryToSystemTime(out var result) ? result : null; - - protected override void OnObjectValueChanged() + public override object? ObjectValue { - _parsedValue = null; - base.OnObjectValueChanged(); + get => base.ObjectValue; + set + { + base.ObjectValue = value; + _parsedValue = null; + } } /// @@ -129,20 +143,13 @@ public TimeSpan ToTimeSpan() => /// True if the value of the Fhir Time is not null and can be parsed as a Time without an offset, false otherwise. public bool TryToTimeSpan(out TimeSpan dto) { - if (Value is not null && TryToSystemTime(out var dt) && !dt.HasOffset) + if (TryToSystemTime(out var dt) && !dt.HasOffset) { dto = dt.ToTimeSpan(); return true; } - else - { - dto = TimeSpan.Zero; - return false; - } - } - /// - /// Checks whether the given literal is correctly formatted. - /// - public static bool IsValidValue(string value) => P.Time.TryParse(value, out var parsed) && !parsed.HasOffset; + dto = TimeSpan.Zero; + return false; + } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/UnsignedInt.cs b/src/Hl7.Fhir.Base/Model/UnsignedInt.cs index cdf648d453..4c466db016 100644 --- a/src/Hl7.Fhir.Base/Model/UnsignedInt.cs +++ b/src/Hl7.Fhir.Base/Model/UnsignedInt.cs @@ -31,16 +31,25 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; public partial class UnsignedInt { /// - /// Checks whether the given literal is correctly formatted. + /// Validates the JsonValue. /// - public static bool IsValidValue(string value) => ElementModel.Types.Integer.TryParse(value, out var parsed) && parsed.Value >= 0; + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + >= 0 => null, + int i => COVE.UNSIGNED_INT_MUST_NOT_BE_NEGATIVE(context, i), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; /// /// Converts this UnsignedInt to a . diff --git a/src/Hl7.Fhir.Base/Model/Uuid.cs b/src/Hl7.Fhir.Base/Model/Uuid.cs index fcb072a242..c00baefa03 100644 --- a/src/Hl7.Fhir.Base/Model/Uuid.cs +++ b/src/Hl7.Fhir.Base/Model/Uuid.cs @@ -31,8 +31,10 @@ POSSIBILITY OF SUCH DAMAGE. #nullable enable using System; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Model; @@ -53,6 +55,19 @@ public static Uuid Generate() /// public FhirUri AsUri() => new(Value); + /// + /// Validates the JsonValue. + /// + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string unparsed when IsValidValue(unparsed) => null, + string unparsed => COVE.LITERAL_INVALID(context, unparsed, this.TypeName), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + + /// /// Checks whether the given literal is correctly formatted. /// @@ -68,5 +83,4 @@ public P.String ToSystemString() => protected internal override P.Any? TryConvertToSystemTypeInternal() => Value is not null ? new P.String(Value) : null; - } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Model/XHtml.cs b/src/Hl7.Fhir.Base/Model/XHtml.cs index 9b7c2b2322..997680c4fe 100644 --- a/src/Hl7.Fhir.Base/Model/XHtml.cs +++ b/src/Hl7.Fhir.Base/Model/XHtml.cs @@ -29,10 +29,13 @@ POSSIBILITY OF SUCH DAMAGE. */ using Hl7.Fhir.Utility; +using System; +using Hl7.Fhir.Validation; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Xml; -using SystemPrimitive = Hl7.Fhir.ElementModel.Types; - +using P = Hl7.Fhir.ElementModel.Types; +using COVE=Hl7.Fhir.Validation.CodedValidationException; #nullable enable namespace Hl7.Fhir.Model; @@ -43,35 +46,74 @@ namespace Hl7.Fhir.Model; public partial class XHtml { /// - /// Verifies the given string of XML against the FHIR narrative requirements from https://www.hl7.org/fhir/narrative.html. + /// Validates the JsonValue. /// - public static bool IsValidNarrativeXhtml(string value, out string[] errors) + protected internal override COVE? ValidateObjectValue(ValidationContext? context) => + ObjectValue switch + { + null => null, + string xml => ValidateXmlLiteral(xml, context), + _ => COVE.INCORRECT_LITERAL_VALUE_TYPE(context, ObjectValue, this.TypeName) + }; + + internal static COVE? ValidateXmlLiteral(string xml, ValidationContext? context) { - errors = SerializationUtil.RunFhirXhtmlSchemaValidation(value); - return !errors.Any(); + return context?.GetNarrativeValidationKind() switch + { + null => null, + NarrativeValidationKind.None => null, + NarrativeValidationKind.Xml => IsValidXml(xml, out var error) ? null : make(error, null), + NarrativeValidationKind.FhirXhtml => IsValidNarrativeXhtml(xml, out var malformedXmlError, + out var invalidNarrativeErrors) ? null : make(malformedXmlError, invalidNarrativeErrors), + var kind => throw new NotSupportedException($"Encountered unknown narrative validation kind '{kind}'.") + }; + + COVE? make(string? malformedXmlError, string[]? invalidNarrativeErrors) => + malformedXmlError is not null + ? COVE.NARRATIVE_XML_IS_MALFORMED(context, malformedXmlError) + : invalidNarrativeErrors?.Any() == true + ? COVE.NARRATIVE_XML_IS_INVALID(context, string.Concat(", ", invalidNarrativeErrors)) + : null; } - /// - public static bool IsValidNarrativeXhtml(string value) => IsValidNarrativeXhtml(value, out _); + /// + /// Verifies the given string of XML against the FHIR narrative requirements from https://www.hl7.org/fhir/narrative.html. + /// + public static bool IsValidNarrativeXhtml(string text, out string? malformedXmlError, out string[] invalidNarrativeErrors) + { + try + { + var doc = SerializationUtil.XDocumentFromXmlText(text); + malformedXmlError = null; + invalidNarrativeErrors = SerializationUtil.RunFhirXhtmlSchemaValidation(doc); + return !invalidNarrativeErrors.Any(); + } + catch (FormatException fe) + { + malformedXmlError = fe.Message; + invalidNarrativeErrors = []; + return false; + } + } /// /// Validates whether the given string of Xml is well-formatted. /// - public static bool IsValidXml(string value, out string? error) + public static bool IsValidXml(string value, out string? malformedXmlError) { try { using var reader = SerializationUtil.XmlReaderFromXmlText(value); while (reader.Read()) ; - error = null; + malformedXmlError = null; return true; } catch (XmlException xmlE) { - error = xmlE.Message; + malformedXmlError = xmlE.Message; return false; } } - protected internal override SystemPrimitive.Any? TryConvertToSystemTypeInternal() => null; + protected internal override P.Any? TryConvertToSystemTypeInternal() => Value is not null ? new P.String(Value) : null; } \ 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 15cdb07c8f..7b9432e2e7 100644 --- a/src/Hl7.Fhir.Base/Rest/FhirClientSerializationEngineExtensions.cs +++ b/src/Hl7.Fhir.Base/Rest/FhirClientSerializationEngineExtensions.cs @@ -96,7 +96,7 @@ public static BaseFhirClient WithOstrichModeSerializer(this BaseFhirClient clien public static BaseFhirClient WithCustomIgnoreListSerializer(this BaseFhirClient client, string[] ignoreList) { - client.Settings.SerializationEngine = FhirSerializationEngineFactory.Custom(client.Inspector, ignoreList.ToPredicate()); + client.Settings.SerializationEngine = FhirSerializationEngineFactory.Custom(client.Inspector, ignoreList.IsInList()); return client; } @@ -112,5 +112,4 @@ public static BaseFhirClient WithOstrichModeLegacySerializer(this BaseFhirClient } } -#nullable restore - +#nullable restore \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs index ecdcb5ee32..697cc55783 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonPocoDeserializer.cs @@ -21,928 +21,846 @@ #nullable enable -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// Deserializes a byte stream into FHIR POCO objects. +/// +/// The serializer uses the format documented in https://www.hl7.org/fhir/json.html. +public class BaseFhirJsonPocoDeserializer { /// - /// Deserializes a byte stream into FHIR POCO objects. + /// Initializes an instance of the deserializer. /// - /// The serializer uses the format documented in https://www.hl7.org/fhir/json.html. - public class BaseFhirJsonPocoDeserializer + /// Assembly containing the POCO classes to be used for deserialization. + public BaseFhirJsonPocoDeserializer(Assembly assembly) : this(assembly, new()) { - /// - /// Initializes an instance of the deserializer. - /// - /// Assembly containing the POCO classes to be used for deserialization. - public BaseFhirJsonPocoDeserializer(Assembly assembly) : this(assembly, new()) - { - // 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()) - { - // nothing - } + /// + /// Initializes an instance of the deserializer. + /// + /// The containing the POCO classes to be used for deserialization. + public BaseFhirJsonPocoDeserializer(ModelInspector inspector) : this(inspector, new()) + { + // 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. + /// + /// 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. - /// - /// The containing the POCO classes to be used for deserialization. - /// A settings object to be used by this instance. - public BaseFhirJsonPocoDeserializer(ModelInspector inspector, FhirJsonConverterOptions settings) - { - Settings = settings; - _inspector = inspector; - } + /// + /// Initializes an instance of the deserializer. + /// + /// The containing the POCO classes to be used for deserialization. + /// A settings object to be used by this instance. + public BaseFhirJsonPocoDeserializer(ModelInspector inspector, FhirJsonConverterOptions settings) + { + Settings = settings; + _inspector = inspector; + } - /// - /// The settings that were passed to the constructor. - /// - public FhirJsonConverterOptions Settings { get; } + /// + /// The settings that were passed to the constructor. + /// + public FhirJsonConverterOptions Settings { get; } - private const string INSTANCE_VALIDATION_KEY_SUFFIX = ":instance"; - private const string PROPERTY_VALIDATION_KEY_SUFFIX = ":property"; - private readonly ModelInspector _inspector; + private const string INSTANCE_VALIDATION_KEY_SUFFIX = ":instance"; + private const string PROPERTY_VALIDATION_KEY_SUFFIX = ":property"; + private readonly ModelInspector _inspector; - /// - /// Deserialize the FHIR Json from the reader and create a new POCO object containing the data from the reader. - /// - /// A json reader positioned on the first token of the object, or the beginning of the stream. - /// 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. - 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) - throw new InvalidOperationException("The reader must be set to ignore or refuse comments."); + /// + /// Deserialize the FHIR Json from the reader and create a new POCO object containing the data from the reader. + /// + /// A json reader positioned on the first token of the object, or the beginning of the stream. + /// 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. + 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) + throw new InvalidOperationException("The reader must be set to ignore or refuse comments."); - // If the stream has just been opened, move to the first token. - if (reader.TokenType == JsonTokenType.None) reader.Read(); + // If the stream has just been opened, move to the first token. + if (reader.TokenType == JsonTokenType.None) reader.Read(); - FhirJsonPocoDeserializerState state = new(); + FhirJsonPocoDeserializerState state = new(); - instance = DeserializeResourceInternal(ref reader, state, stayOnLastToken: true); - issues = state.Errors; + instance = DeserializeResourceInternal(ref reader, state, stayOnLastToken: true); + issues = state.Errors; - return !state.Errors.HasExceptions; - } + return !state.Errors.HasExceptions; + } - /// - /// Reads a (subtree) of serialized FHIR Json data into a POCO object. - /// - /// The type of POCO to construct and deserialize - /// A json reader positioned on the first token of the object, or the beginning of the stream. - /// 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. - 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) - throw new InvalidOperationException("The reader must be set to ignore or refuse comments."); + /// + /// Reads a (subtree) of serialized FHIR Json data into a POCO object. + /// + /// The type of POCO to construct and deserialize + /// A json reader positioned on the first token of the object, or the beginning of the stream. + /// 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. + 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) + throw new InvalidOperationException("The reader must be set to ignore or refuse comments."); - // If the stream has just been opened, move to the first token. - if (reader.TokenType == JsonTokenType.None) reader.Read(); + // If the stream has just been opened, move to the first token. + if (reader.TokenType == JsonTokenType.None) reader.Read(); - var mapping = _inspector.FindOrImportClassMapping(targetType) ?? - throw new ArgumentException($"Type '{targetType}' could not be located and can " + - $"therefore not be used for deserialization. " + reader.GenerateLocationMessage(), nameof(targetType)); + var mapping = _inspector.FindOrImportClassMapping(targetType) ?? + throw new ArgumentException($"Type '{targetType}' could not be located and can " + + $"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); + // 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); - 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)); + 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)); + } - internal Resource? DeserializeResourceInternal(ref Utf8JsonReader reader, FhirJsonPocoDeserializerState state, bool stayOnLastToken) + internal Resource? DeserializeResourceInternal(ref Utf8JsonReader reader, FhirJsonPocoDeserializerState state, bool stayOnLastToken) + { + if (reader.TokenType != JsonTokenType.StartObject) { - if (reader.TokenType != JsonTokenType.StartObject) - { - state.Errors.Add(ERR.EXPECTED_START_OF_OBJECT(ref reader, state.Path.GetInstancePath(), reader.TokenType)); - reader.Recover(); // skip to the end of the construct encountered (value or array) - return null; - } + state.Errors.Add(ERR.EXPECTED_START_OF_OBJECT(ref reader, state.Path.GetInstancePath(), reader.TokenType)); + reader.Recover(); // skip to the end of the construct encountered (value or array) + return null; + } - (ClassMapping? resourceMapping, FhirJsonException? error) = DetermineClassMappingFromInstance(ref reader, _inspector, state.Path); + (ClassMapping? resourceMapping, FhirJsonException? error) = DetermineClassMappingFromInstance(ref reader, _inspector, state.Path); - if (resourceMapping is not null) + if (resourceMapping is not null) + { + // If we have at least a mapping, let's try to continue + var newResource = (Base)resourceMapping.Factory(); + + try { - // If we have at least a mapping, let's try to continue - var newResource = (Base)resourceMapping.Factory(); + state.Path.EnterResource(resourceMapping.Name); + int nErrorCount = state.Errors.Count; + deserializeObjectInto(newResource, resourceMapping, ref reader, DeserializedObjectKind.Resource, state, stayOnLastToken); - try + if (!resourceMapping.IsResource) { - state.Path.EnterResource(resourceMapping.Name); - int nErrorCount = state.Errors.Count; - deserializeObjectInto(newResource, resourceMapping, ref reader, DeserializedObjectKind.Resource, state, stayOnLastToken); - - if (!resourceMapping.IsResource) - { - state.Errors.Add(ERR.RESOURCE_TYPE_NOT_A_RESOURCE(ref reader, state.Path.GetInstancePath(), resourceMapping.Name)); - return null; - } - else - { - if (Settings.AnnotateResourceParseExceptions && state.Errors.Count > nErrorCount) - { - List resourceErrs = state.Errors.Skip(nErrorCount).ToList(); - ((Resource)newResource).SetAnnotation(resourceErrs); - } - return (Resource)newResource; - } + state.Errors.Add(ERR.RESOURCE_TYPE_NOT_A_RESOURCE(ref reader, state.Path.GetInstancePath(), resourceMapping.Name)); + return null; } - finally + else { - state.Path.ExitResource(); + if (Settings.AnnotateResourceParseExceptions && state.Errors.Count > nErrorCount) + { + List resourceErrs = state.Errors.Skip(nErrorCount).ToList(); + ((Resource)newResource).SetAnnotation(resourceErrs); + } + return (Resource)newResource; } } - else + finally { - state.Errors.Add(error!); + state.Path.ExitResource(); + } + } + else + { + state.Errors.Add(error!); - // Read past the end of this object to recover. - reader.Recover(); + // Read past the end of this object to recover. + reader.Recover(); - return null; - } + return null; } + } + /// + /// The kind of object we need to deserialize into, which will influence subtly + /// how the + /// function will operate. + /// + private enum DeserializedObjectKind + { /// - /// The kind of object we need to deserialize into, which will influence subtly - /// how the - /// function will operate. + /// Deserialize into a complex datatype, and complain about the presence of + /// a resourceType element. /// - private enum DeserializedObjectKind - { - /// - /// Deserialize into a complex datatype, and complain about the presence of - /// a resourceType element. - /// - Complex, - - /// - /// Deserialize into a resource - /// - Resource, - - /// - /// Deserialize the non-value part of a FhirPrimitive, and do not call validation of - /// the instance yet, since it will be done when the FhirPrimitive has been constructed - /// completely, including its value part. - /// - FhirPrimitive - } + Complex, /// - /// Reads a complex object into an existing instance of a POCO. + /// Deserialize into a resource /// - /// - /// - /// - /// - /// - /// Normally, the reader will be on the first token *after* the object, however, - /// System.Text.Json converters expect the readers on the last token of the object. Since all logic - /// in this class assumes the first case, we make a special case for the outermost call to this function - /// done by the function, which is in its - /// turn called by System.Text.Json upon a . - /// Reader will be on the first token after the object upon return, but see . - private void deserializeObjectInto( - T target, - ClassMapping mapping, - ref Utf8JsonReader reader, - DeserializedObjectKind kind, - FhirJsonPocoDeserializerState state, - bool stayOnLastToken = false) where T : Base - { - if (reader.TokenType != JsonTokenType.StartObject) - { - state.Errors.Add(ERR.EXPECTED_START_OF_OBJECT(ref reader, state.Path.GetInstancePath(), reader.TokenType)); - reader.Recover(); // skip to the end of the construct encountered (value or array) - return; - } - - // read past start of object into first property or end of object - reader.Read(); - - var empty = true; - var objectParsingState = new ObjectParsingState(); - var oldErrorCount = state.Errors.Count; - var (line, pos) = reader.GetLocation(); - - while (reader.TokenType != JsonTokenType.EndObject) - { - var currentPropertyName = reader.GetString()!; - - // The resourceType property on the level of a resource is used to determine - // the type and should otherwise be skipped when processing a resource. - if (currentPropertyName == "resourceType" && kind is DeserializedObjectKind.Resource) - { - reader.SkipTo(JsonTokenType.PropertyName); - continue; - } - - empty = false; - - // Lookup the metadata for this property by its name to determine the expected type of the value - var (propMapping, propValueMapping, error) = tryGetMappedElementMetadata(_inspector, mapping, ref reader, state.Path, currentPropertyName); + Resource, - if (error is not null) - { - state.Errors.Add(error); - - // try to recover by skipping to the next property. - reader.SkipTo(JsonTokenType.PropertyName); - continue; - } - else - { - // read past the property name into the value - reader.Read(); + /// + /// Deserialize the non-value part of a FhirPrimitive, and do not call validation of + /// the instance yet, since it will be done when the FhirPrimitive has been constructed + /// completely, including its value part. + /// + FhirPrimitive + } - try - { - state.Path.EnterElement(propMapping!.Name, !propMapping.IsCollection ? null : 0, propMapping.IsPrimitive); - deserializePropertyValueInto(target, currentPropertyName, propMapping, propValueMapping!, ref reader, objectParsingState, state); - } - finally - { - state.Path.ExitElement(); - } - } - } + /// + /// Reads a complex object into an existing instance of a POCO. + /// + /// + /// + /// + /// + /// + /// Normally, the reader will be on the first token *after* the object, however, + /// System.Text.Json converters expect the readers on the last token of the object. Since all logic + /// in this class assumes the first case, we make a special case for the outermost call to this function + /// done by the function, which is in its + /// turn called by System.Text.Json upon a . + /// Reader will be on the first token after the object upon return, but see . + private void deserializeObjectInto( + T target, + ClassMapping mapping, + ref Utf8JsonReader reader, + DeserializedObjectKind kind, + FhirJsonPocoDeserializerState state, + bool stayOnLastToken = false) where T : Base + { + if (reader.TokenType != JsonTokenType.StartObject) + { + state.Errors.Add(ERR.EXPECTED_START_OF_OBJECT(ref reader, state.Path.GetInstancePath(), reader.TokenType)); + reader.Recover(); // skip to the end of the construct encountered (value or array) + return; + } - // Now after having deserialized all properties we can run the validations that needed to be - // postponed until after all properties have been seen (e.g. Instance and Property validations for - // primitive properties, since they may be composed from two properties `name` and `_name` in json - // and should only be validated when both have been processed, even if megabytes apart in the json file). - objectParsingState.RunDelayedValidation(); + // read past start of object into first property or end of object + reader.Read(); - // read past object, unless this is the last EndObject in the top-level Deserialize call - if (!stayOnLastToken) reader.Read(); + var empty = true; + var objectParsingState = new ObjectParsingState(); + var oldErrorCount = state.Errors.Count; + var (line, pos) = reader.GetLocation(); - // do not allow empty complex objects. - if (empty) state.Errors.Add(ERR.OBJECTS_CANNOT_BE_EMPTY(ref reader, state.Path.GetInstancePath())); + while (reader.TokenType != JsonTokenType.EndObject) + { + var currentPropertyName = reader.GetString()!; - // Only run instance validation when deserialization yielded no errors - // to avoid spurious error messages. - if (Settings.Validator is not null && kind != DeserializedObjectKind.FhirPrimitive && (Settings.ValidateOnFailedParse || state.Errors.Count == oldErrorCount)) + // The resourceType property on the level of a resource is used to determine + // the type and should otherwise be skipped when processing a resource. + if (currentPropertyName == "resourceType" && kind is DeserializedObjectKind.Resource) { - var context = new InstanceDeserializationContext(state.Path, line, pos, mapping); - PocoDeserializationHelper.RunInstanceValidation(target, Settings.Validator, context, state.Errors); + reader.SkipTo(JsonTokenType.PropertyName); + continue; } - return; - } + empty = false; - /// - /// Reads the value of a json property. - /// - /// The target POCO which property will be set/updated during deserialization. If null, it will be - /// be created based on the , otherwise it will be updated. - /// The literal name of the property in the json serialization. - /// The cached metadata for the property we are setting. - /// The cached metadata for the type of value we are setting the property to. - /// The reader to deserialize from. - /// Validations to be delayed until the target has been fully deserialized. - /// This function will add to this list if necessary. - /// Object used to track all parsing state. - /// - /// Expects the reader to be positioned on the property value. - /// Reader will be on the first token after the property value upon return. - private void deserializePropertyValueInto( - Base target, - string propertyName, - PropertyMapping propertyMapping, - ClassMapping propertyValueMapping, - ref Utf8JsonReader reader, - ObjectParsingState delayedValidations, - FhirJsonPocoDeserializerState state - ) - { - object? result; - var oldErrorCount = state.Errors.Count; - var (line, pos) = reader.CurrentState.GetLocation(); + // Lookup the metadata for this property by its name to determine the expected type of the value + var (propMapping, propValueMapping, error) = tryGetMappedElementMetadata(_inspector, mapping, ref reader, state.Path, currentPropertyName); - // There might be an existing value, since FhirPrimitives may be spread out over two properties - // (one with, and one without the '_') - var existingValue = propertyMapping.GetValue(target); - - if (propertyValueMapping.IsFhirPrimitive) + if (error is not null) { - // fix for https://github.com/FirelyTeam/firely-net-sdk/issues/2701 - use the known native type if it is in the list - var fhirType = propertyMapping.FhirType.Contains(propertyValueMapping.NativeType) - ? propertyValueMapping.NativeType - : propertyMapping.FhirType.FirstOrDefault(); - - // Note that the POCO model will always allocate a new list if the property had not been set before, - // so there is always an existingValue for IList - result = propertyMapping.IsCollection ? - deserializeFhirPrimitiveList((IList)existingValue!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state) : - DeserializeFhirPrimitive(existingValue as PrimitiveType, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state); + state.Errors.Add(error); + + // try to recover by skipping to the next property. + reader.SkipTo(JsonTokenType.PropertyName); } else { - // This is not a FHIR primitive, so we should not be dealing with these weird _name members. - if (propertyName[0] == '_') - state.Errors.Add(ERR.USE_OF_UNDERSCORE_ILLEGAL(ref reader, state.Path.GetInstancePath(), propertyMapping.Name, propertyName)); + // read past the property name into the value + reader.Read(); - // Note that repeating simple elements (like Extension.url) do not currently exist in the FHIR serialization - if (propertyMapping.IsCollection) - { - var l = (IList)existingValue!; - // if the list is already populated, a property with an identical key was encountered earlier - if (l.Count > 0) - { - state.Path.IncrementIndex(l.Count); - state.Errors.Add(ERR.DUPLICATE_ARRAY(ref reader, state.Path.GetInstancePath())); - } - result = deserializeNormalList(l, propertyValueMapping, ref reader, propertyMapping, state); - } - else + try { - // if the property already has a value, its key must have been encountered before - if (existingValue is not null) - { - state.Errors.Add(ERR.DUPLICATE_PROPERTY(ref reader, state.Path.GetInstancePath(), propertyName)); - } - result = deserializeSingleValue(ref reader, propertyValueMapping, propertyMapping, state); + state.Path.EnterElement(propMapping!.Name, !propMapping.IsCollection ? null : 0, propMapping.IsPrimitive); + deserializePropertyValueInto(target, currentPropertyName, propMapping, propValueMapping!, ref reader, objectParsingState, state); } - } - - // Only do validation when no parse errors were encountered, otherwise we'll just - // produce spurious messages. - if (Settings.Validator is not null && (Settings.ValidateOnFailedParse || oldErrorCount == state.Errors.Count)) - { - var deserializationContext = new PropertyDeserializationContext( - state.Path, - propertyName, - line, pos, - propertyMapping); - - // If this is a FhirPrimitive, make sure we delay validation until we had the - // chance to encounter both the `name` and `_name` property. - if (delayedValidations is not null && propertyValueMapping.IsFhirPrimitive) + finally { - delayedValidations.ScheduleDelayedValidation( - propertyMapping.Name + PROPERTY_VALIDATION_KEY_SUFFIX, - () => PocoDeserializationHelper.RunPropertyValidation(ref result, Settings.Validator!, deserializationContext, state.Errors)); + state.Path.ExitElement(); } - else - PocoDeserializationHelper.RunPropertyValidation(ref result, Settings.Validator!, deserializationContext, state.Errors); } - - propertyMapping.SetValue(target, result); - - return; } - /// - /// Reads the content of a list with non-FHIR-primitive content (so, no name/_name pairs to be dealt with). Note - /// that the contents can only be complex in the current FHIR serialization, but we'll be prepared and handle - /// other situations (e.g. repeating Extension.url's, if they would ever exist). - /// - private IList? deserializeNormalList( - IList? existingList, - ClassMapping propertyValueMapping, - ref Utf8JsonReader reader, - PropertyMapping propertyMapping, - FhirJsonPocoDeserializerState state) - { - // Create a list of the type of this property's value. - IList listInstance = existingList ?? propertyValueMapping.ListFactory(); - - // if true, we have encountered a single value where we expected an array. - // we need to recover by creating an array with that single value. - bool oneshot = false; + // Now after having deserialized all properties we can run the validations that needed to be + // postponed until after all properties have been seen (e.g. Instance and Property validations for + // primitive properties, since they may be composed from two properties `name` and `_name` in json + // and should only be validated when both have been processed, even if megabytes apart in the json file). + objectParsingState.RunDelayedValidation(); - if (reader.TokenType != JsonTokenType.StartArray) - { - state.Errors.Add(ERR.EXPECTED_START_OF_ARRAY(ref reader, state.Path.GetInstancePath())); - oneshot = true; - } - else - { - // Read past start of array - reader.Read(); + // read past object, unless this is the last EndObject in the top-level Deserialize call + if (!stayOnLastToken) reader.Read(); - if (reader.TokenType == JsonTokenType.EndArray) - state.Errors.Add(ERR.ARRAYS_CANNOT_BE_EMPTY(ref reader, state.Path.GetInstancePath())); - } + // do not allow empty complex objects. + if (empty) state.Errors.Add(ERR.OBJECTS_CANNOT_BE_EMPTY(ref reader, state.Path.GetInstancePath())); - // Can't make an iterator because of the ref readers struct, so need - // to simply create a list by Adding(). Not the fastest approach :-( - while (reader.TokenType != JsonTokenType.EndArray) - { - var result = deserializeSingleValue(ref reader, propertyValueMapping, propertyMapping, state); - listInstance.Add(result); - state.Path.IncrementIndex(); + // Only run instance validation when deserialization yielded no errors + // 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); + PocoDeserializationHelper.RunInstanceValidation(target, Settings.Validator, context, state.Errors); + } + } - if (oneshot) break; - } + /// + /// Reads the value of a json property. + /// + /// The target POCO which property will be set/updated during deserialization. If null, it will be + /// be created based on the , otherwise it will be updated. + /// The literal name of the property in the json serialization. + /// The cached metadata for the property we are setting. + /// The cached metadata for the type of value we are setting the property to. + /// The reader to deserialize from. + /// Validations to be delayed until the target has been fully deserialized. + /// This function will add to this list if necessary. + /// Object used to track all parsing state. + /// + /// Expects the reader to be positioned on the property value. + /// Reader will be on the first token after the property value upon return. + private void deserializePropertyValueInto( + Base target, + string propertyName, + PropertyMapping propertyMapping, + ClassMapping propertyValueMapping, + ref Utf8JsonReader reader, + ObjectParsingState delayedValidations, + FhirJsonPocoDeserializerState state + ) + { + object? result; + var oldErrorCount = state.Errors.Count; + var (line, pos) = reader.CurrentState.GetLocation(); - // Read past end of array - if (!oneshot) reader.Read(); + // There might be an existing value, since FhirPrimitives may be spread out over two properties + // (one with, and one without the '_') + var existingValue = propertyMapping.GetValue(target); - return listInstance; + if (propertyValueMapping.IsFhirPrimitive) + { + // fix for https://github.com/FirelyTeam/firely-net-sdk/issues/2701 - use the known native type if it is in the list + var fhirType = propertyMapping.FhirType.Contains(propertyValueMapping.NativeType) + ? propertyValueMapping.NativeType + : propertyMapping.FhirType.FirstOrDefault(); + + // Note that the POCO model will always allocate a new list if the property had not been set before, + // so there is always an existingValue for IList + result = propertyMapping.IsCollection ? + deserializeFhirPrimitiveList((IList)existingValue!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state) : + DeserializeFhirPrimitive(existingValue as PrimitiveType, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state); } - - internal class ObjectParsingState + else { - private readonly Dictionary _validations = new(); - private readonly Dictionary _parsedPropValue = new(); - - public int GetPropertyIndex(string memberName) - { - if (_parsedPropValue.ContainsKey(memberName)) - return _parsedPropValue[memberName]; - _parsedPropValue.Add(memberName, 0); - return 0; - } + // This is not a FHIR primitive, so we should not be dealing with these weird _name members. + if (propertyName[0] == '_') + state.Errors.Add(ERR.USE_OF_UNDERSCORE_ILLEGAL(ref reader, state.Path.GetInstancePath(), propertyMapping.Name, propertyName)); - public void SetPropertyIndex(string memberName, int count) + // Note that repeating simple elements (like Extension.url) do not currently exist in the FHIR serialization + if (propertyMapping.IsCollection) { - _parsedPropValue[memberName] = count; - } - - public void ScheduleDelayedValidation(string key, Action validation) - { - // Add or overwrite the entry for the given key. - if (_validations.ContainsKey(key)) _validations.Remove(key); - _validations[key] = validation; + var l = (IList)existingValue!; + // if the list is already populated, a property with an identical key was encountered earlier + if (l.Count > 0) + { + state.Path.IncrementIndex(l.Count); + state.Errors.Add(ERR.DUPLICATE_ARRAY(ref reader, state.Path.GetInstancePath())); + } + result = deserializeNormalList(l, propertyValueMapping, ref reader, state); } - - //public CodedValidationException[] Run() => _validations.Values.SelectMany(delayed => delayed()).ToArray(); - public void RunDelayedValidation() + else { - foreach (var validation in _validations.Values) validation(); + // if the property already has a value, its key must have been encountered before + if (existingValue is not null) + { + state.Errors.Add(ERR.DUPLICATE_PROPERTY(ref reader, state.Path.GetInstancePath(), propertyName)); + } + result = deserializeSingleValue(ref reader, propertyValueMapping, state); } } - /// - /// Reads a list of FHIR primitives (either from a name or _name property). - /// - /// Upon completion, reader will be located at the next token afther the list. - private IList? deserializeFhirPrimitiveList( - IList existingList, - string propertyName, - ClassMapping propertyValueMapping, - Type? fhirType, - ref Utf8JsonReader reader, - ObjectParsingState delayedValidations, - FhirJsonPocoDeserializerState state - ) + // Only do validation when no parse errors were encountered, otherwise we'll just + // produce spurious messages. + if (Settings.Validator is not null && (Settings.ValidateOnFailedParse || oldErrorCount == state.Errors.Count)) { - // if true, we have encountered a single value where we expected an array. - // we need to recover by creating an array with that single value. - bool oneshot = false; - - if (reader.TokenType != JsonTokenType.StartArray) + var deserializationContext = new PropertyDeserializationContext( + target, + state.Path, + propertyName, + line, pos, + propertyMapping); + + // If this is a FhirPrimitive, make sure we delay validation until we had the + // chance to encounter both the `name` and `_name` property. + if (propertyValueMapping.IsFhirPrimitive) { - state.Errors.Add(ERR.EXPECTED_START_OF_ARRAY(ref reader, state.Path.GetInstancePath())); - oneshot = true; + var elementName = propertyMapping.Name; + + delayedValidations.ScheduleDelayedValidation( + elementName + PROPERTY_VALIDATION_KEY_SUFFIX, + () => + { + deserializationContext.PathStack.EnterElement(elementName, null, + propertyValueMapping.IsPrimitive); + PocoDeserializationHelper.RunPropertyValidation(result, Settings.Validator!, + deserializationContext, state.Errors); + deserializationContext.PathStack.ExitElement(); + }); } else - { - // read into array - reader.Read(); + PocoDeserializationHelper.RunPropertyValidation(result, Settings.Validator!, deserializationContext, state.Errors); + } - if (reader.TokenType == JsonTokenType.EndArray) - state.Errors.Add(ERR.ARRAYS_CANNOT_BE_EMPTY(ref reader, state.Path.GetInstancePath())); - } + propertyMapping.SetValue(target, result); + } - int originalSize = existingList.Count; + /// + /// Reads the content of a list with non-FHIR-primitive content (so, no name/_name pairs to be dealt with). Note + /// that the contents can only be complex in the current FHIR serialization, but we'll be prepared and handle + /// other situations (e.g. repeating Extension.url's, if they would ever exist). + /// + private IList deserializeNormalList( + IList? existingList, + ClassMapping propertyValueMapping, + ref Utf8JsonReader reader, + FhirJsonPocoDeserializerState state) + { + // Create a list of the type of this property's value. + IList listInstance = existingList ?? propertyValueMapping.ListFactory(); - // Can't make an iterator because of the ref readers struct, so need - // to simply create a list by Adding(). Not the fastest approach :-( - int elementIndex = 0; - bool? onlyNulls = null; - elementIndex = delayedValidations.GetPropertyIndex(propertyName); - if (elementIndex > 0) - { - state.Path.IncrementIndex(elementIndex); - state.Errors.Add(ERR.DUPLICATE_ARRAY(ref reader, state.Path.GetInstancePath())); - } + // if true, we have encountered a single value where we expected an array. + // we need to recover by creating an array with that single value. + bool oneshot = false; - while (reader.TokenType != JsonTokenType.EndArray) - { - if (elementIndex >= originalSize) - existingList.Add(null); + if (reader.TokenType != JsonTokenType.StartArray) + { + state.Errors.Add(ERR.EXPECTED_START_OF_ARRAY(ref reader, state.Path.GetInstancePath())); + oneshot = true; + } + else + { + // Read past start of array + reader.Read(); - if (reader.TokenType == JsonTokenType.Null) - { - onlyNulls ??= true; + if (reader.TokenType == JsonTokenType.EndArray) + state.Errors.Add(ERR.ARRAYS_CANNOT_BE_EMPTY(ref reader, state.Path.GetInstancePath())); + } - // don't read any new data into the primitive here - reader.Read(); - } - else - { - existingList[elementIndex] ??= propertyValueMapping.Factory(); - onlyNulls = false; - _ = DeserializeFhirPrimitive((PrimitiveType)existingList[elementIndex]!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state); + // Can't make an iterator because of the ref readers struct, so need + // to simply create a list by Adding(). Not the fastest approach :-( + while (reader.TokenType != JsonTokenType.EndArray) + { + var result = deserializeSingleValue(ref reader, propertyValueMapping, state); + listInstance.Add(result); + state.Path.IncrementIndex(); - delayedValidations.SetPropertyIndex(propertyName, existingList.Count); - } + if (oneshot) break; + } - elementIndex += 1; - state.Path.IncrementIndex(); + // Read past end of array + if (!oneshot) reader.Read(); - if (oneshot) break; - } + return listInstance; + } - if (onlyNulls == true) - state.Errors.Add(ERR.PRIMITIVE_ARRAYS_ONLY_NULL(ref reader, state.Path.GetInstancePath())); + internal class ObjectParsingState + { + private readonly Dictionary _validations = new(); + private readonly Dictionary _parsedPropValue = new(); - //[EK 20221027] - According to the new R5 spec, these arrays need not be of the same size, and - //we need to fill out missing elements with null values. - //if (originalSize > 0 && elementIndex != originalSize) - // state.Errors.Add(ERR.PRIMITIVE_ARRAYS_INCOMPAT_SIZE.With(ref reader)); + public int GetPropertyIndex(string memberName) + { + if (_parsedPropValue.TryGetValue(memberName, out int propertyIndex)) + return propertyIndex; + _parsedPropValue.Add(memberName, 0); + return 0; + } - // read past array to next property or end of object - if (!oneshot) reader.Read(); + public void SetPropertyIndex(string memberName, int count) + { + _parsedPropValue[memberName] = count; + } - return existingList; + public void ScheduleDelayedValidation(string key, Action validation) + { + // Add or overwrite the entry for the given key. + if (_validations.ContainsKey(key)) _validations.Remove(key); + _validations[key] = validation; } - /// - /// Deserializes a FHIR primitive, which can be a name or _name property. - /// - /// Upon completion, reader will be located at the next token after the FHIR primitive. - internal PrimitiveType DeserializeFhirPrimitive( - PrimitiveType? existingPrimitive, - string propertyName, - ClassMapping propertyValueMapping, - Type? fhirType, - ref Utf8JsonReader reader, - ObjectParsingState? delayedValidations, - FhirJsonPocoDeserializerState state - ) + //public CodedValidationException[] Run() => _validations.Values.SelectMany(delayed => delayed()).ToArray(); + public void RunDelayedValidation() { - var targetPrimitive = existingPrimitive ?? (PrimitiveType)propertyValueMapping.Factory(); - var oldErrorCount = state.Errors.Count; - var (line, pos) = reader.CurrentState.GetLocation(); + foreach (var validation in _validations.Values) validation(); + } + } - if (propertyName[0] != '_') - { - // No underscore, dealing with the 'value' property here. - var primitiveValueProperty = propertyValueMapping.PrimitiveValueProperty ?? - throw new InvalidOperationException($"All subclasses of {nameof(PrimitiveType)} should have a property representing the value element, " + - $"but {propertyValueMapping.Name} has not. " + reader.GenerateLocationMessage()); + /// + /// Reads a list of FHIR primitives (either from a name or _name property). + /// + /// Upon completion, reader will be located at the next token afther the list. + private IList deserializeFhirPrimitiveList( + IList existingList, + string propertyName, + ClassMapping propertyValueMapping, + Type? fhirType, + ref Utf8JsonReader reader, + ObjectParsingState delayedValidations, + FhirJsonPocoDeserializerState state + ) + { + // if true, we have encountered a single value where we expected an array. + // we need to recover by creating an array with that single value. + bool oneshot = false; - state.Path.EnterElement("value", 0, true); - try - { + if (reader.TokenType != JsonTokenType.StartArray) + { + state.Errors.Add(ERR.EXPECTED_START_OF_ARRAY(ref reader, state.Path.GetInstancePath())); + oneshot = true; + } + else + { + // read into array + reader.Read(); - var (result, error) = DeserializePrimitiveValue(ref reader, primitiveValueProperty.ImplementingType, fhirType, state.Path); + if (reader.TokenType == JsonTokenType.EndArray) + state.Errors.Add(ERR.ARRAYS_CANNOT_BE_EMPTY(ref reader, state.Path.GetInstancePath())); + } - // Only do validation when no parse errors were encountered, otherwise we'll just - // produce spurious messages. - if (error is not null) - state.Errors.Add(error); - else if (Settings.Validator is not null) - { + int originalSize = existingList.Count; - var propertyValueContext = new PropertyDeserializationContext( - state.Path, - "value", - line, pos, - primitiveValueProperty); + // Can't make an iterator because of the ref readers struct, so need + // to simply create a list by Adding(). Not the fastest approach :-( + bool? onlyNulls = null; + int elementIndex = delayedValidations.GetPropertyIndex(propertyName); + if (elementIndex > 0) + { + state.Path.IncrementIndex(elementIndex); + state.Errors.Add(ERR.DUPLICATE_ARRAY(ref reader, state.Path.GetInstancePath())); + } - PocoDeserializationHelper.RunPropertyValidation(ref result, Settings.Validator, propertyValueContext, state.Errors); - } + while (reader.TokenType != JsonTokenType.EndArray) + { + if (elementIndex >= originalSize) + existingList.Add(null); - if (targetPrimitive.ObjectValue is not null) - { - state.Errors.Add(FhirJsonException.DUPLICATE_PROPERTY(ref reader, state.Path.GetInstancePath(), propertyName)); - } - targetPrimitive.ObjectValue = result; - } - finally - { - state.Path.ExitElement(); - } + if (reader.TokenType == JsonTokenType.Null) + { + onlyNulls ??= true; + + // don't read any new data into the primitive here + reader.Read(); } else { - // The complex part of a primitive - read the object's primitives into the target - if (targetPrimitive.Extension.Any() || - targetPrimitive.ElementId is not null) - { - state.Errors.Add(FhirJsonException.DUPLICATE_PROPERTY(ref reader, state.Path.GetInstancePath(), propertyName)); - } - deserializeObjectInto(targetPrimitive, propertyValueMapping, ref reader, DeserializedObjectKind.FhirPrimitive, state, stayOnLastToken: false); - } + existingList[elementIndex] ??= propertyValueMapping.Factory(); + onlyNulls = false; + _ = DeserializeFhirPrimitive((PrimitiveType)existingList[elementIndex]!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state); - // Only do validation on this instance when no parse errors were encountered, otherwise we'll just - // produce spurious messages. Also, delay validation of this instance until we have processed both - // 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); - if (delayedValidations is null) - PocoDeserializationHelper.RunInstanceValidation(targetPrimitive, Settings.Validator, context, state.Errors); - else - delayedValidations.ScheduleDelayedValidation( - propertyName.TrimStart('_') + INSTANCE_VALIDATION_KEY_SUFFIX, - () => - { - context.PathStack.EnterElement(propertyName.TrimStart('_'), null, propertyValueMapping.IsPrimitive); - PocoDeserializationHelper.RunInstanceValidation(targetPrimitive, Settings.Validator, context, state.Errors); - context.PathStack.ExitElement(); - }); + delayedValidations.SetPropertyIndex(propertyName, existingList.Count); } - return targetPrimitive; - } + elementIndex += 1; + state.Path.IncrementIndex(); - /// - /// Deserializes a single object, either a resource, a FHIR primitive or a primitive value. - /// - /// Upon completion, reader will be located at the next token afther the value. - private object? deserializeSingleValue(ref Utf8JsonReader reader, ClassMapping propertyValueMapping, PropertyMapping propertyMapping, FhirJsonPocoDeserializerState state) - { - // Resources - if (propertyValueMapping.IsResource) - { - return DeserializeResourceInternal(ref reader, state, stayOnLastToken: false); - } - - // "normal" complex types & backbones - var newComplex = (Base)propertyValueMapping.Factory(); - deserializeObjectInto(newComplex, propertyValueMapping, ref reader, DeserializedObjectKind.Complex, state, stayOnLastToken: false); - return newComplex; + if (oneshot) break; } - /// - /// Does a best-effort parse of the data available at the reader, given the required type of the property the - /// data needs to be read into. - /// - /// A value without an error if the data could be parsed to the required type, and a value with an error if the - /// value could not be parsed - in which case the value returned is the raw value coming in from the reader. - /// Upon completion, the reader will be positioned on the token after the primitive. - internal (object?, FhirJsonException?) DeserializePrimitiveValue(ref Utf8JsonReader reader, Type implementingType, Type? fhirType, PathStack pathStack) - { - // Check for unexpected non-value types. - if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) - { - var exception = reader.TokenType == JsonTokenType.StartObject - ? ERR.EXPECTED_PRIMITIVE_NOT_OBJECT(ref reader, pathStack.GetInstancePath()) - : ERR.EXPECTED_PRIMITIVE_NOT_ARRAY(ref reader, pathStack.GetInstancePath()); - reader.Recover(); - return (null, exception); - } + if (onlyNulls == true) + state.Errors.Add(ERR.PRIMITIVE_ARRAYS_ONLY_NULL(ref reader, state.Path.GetInstancePath())); - // Check for value types - (object? partial, FhirJsonException? error) result = reader.TokenType switch - { - JsonTokenType.Null => new(null, ERR.EXPECTED_PRIMITIVE_NOT_NULL(ref reader, pathStack.GetInstancePath())), - JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()) => new(reader.GetString(), ERR.PROPERTY_MAY_NOT_BE_EMPTY(ref reader, pathStack.GetInstancePath())), - JsonTokenType.String when implementingType == typeof(string) => new(reader.GetString(), null), - JsonTokenType.String when implementingType == typeof(byte[]) => - !Settings.DisableBase64Decoding ? readBase64(ref reader, pathStack) : new(reader.GetString(), null), - JsonTokenType.String when implementingType == typeof(DateTimeOffset) => readDateTimeOffset(ref reader, pathStack), - JsonTokenType.String when implementingType.IsEnum => new(reader.GetString(), null), - JsonTokenType.String when implementingType == typeof(long) => readLong(ref reader, fhirType, pathStack), - //JsonTokenType.String when requiredType.IsEnum => readEnum(ref reader, requiredType), - JsonTokenType.String => unexpectedToken(ref reader, pathStack.GetInstancePath(), reader.GetString(), implementingType.Name, "string"), - JsonTokenType.Number => tryGetMatchingNumber(ref reader, implementingType, fhirType, pathStack), - JsonTokenType.True or JsonTokenType.False when implementingType == typeof(bool) => new(reader.GetBoolean(), null), - JsonTokenType.True or JsonTokenType.False => unexpectedToken(ref reader, pathStack.GetInstancePath(), reader.GetRawText(), implementingType.Name, "boolean"), - - _ => - // This would be an internal logic error, since our callers should have made sure we're - // on the primitive value after the property name (and the Utf8JsonReader would have complained about any - // other token that one that is a value). - // EK: I think 'Comment' is the only possible non-expected option here.... - throw new InvalidOperationException($"Unexpected token type {reader.TokenType} while parsing a primitive value. " + - reader.GenerateLocationMessage()), - }; - - // If there is a failure, and we have a handler installed, call it - if (Settings.OnPrimitiveParseFailed is not null && result.error is not null) - result = Settings.OnPrimitiveParseFailed(ref reader, implementingType, result.partial, result.error); - - // Read past the value - reader.Read(); + //[EK 20221027] - According to the new R5 spec, these arrays need not be of the same size, and + //we need to fill out missing elements with null values. + //if (originalSize > 0 && elementIndex != originalSize) + // state.Errors.Add(ERR.PRIMITIVE_ARRAYS_INCOMPAT_SIZE.With(ref reader)); - return result; + // read past array to next property or end of object + if (!oneshot) reader.Read(); - static (object?, FhirJsonException?) readBase64(ref Utf8JsonReader reader, PathStack pathStack) => - reader.TryGetBytesFromBase64(out var bytesValue) ? - new(bytesValue, null) : - new(reader.GetString(), ERR.INCORRECT_BASE64_DATA(ref reader, pathStack.GetInstancePath())); + return existingList; + } - static (object?, FhirJsonException?) readDateTimeOffset(ref Utf8JsonReader reader, PathStack pathStack) - { - var contents = reader.GetString()!; + /// + /// Deserializes a FHIR primitive, which can be a name or _name property. + /// + /// Upon completion, reader will be located at the next token after the FHIR primitive. + internal PrimitiveType DeserializeFhirPrimitive( + PrimitiveType? existingPrimitive, + string propertyName, + ClassMapping propertyValueMapping, + Type? fhirType, + ref Utf8JsonReader reader, + ObjectParsingState? parsingState, + FhirJsonPocoDeserializerState state + ) + { + var targetPrimitive = existingPrimitive ?? (PrimitiveType)propertyValueMapping.Factory(); + var oldErrorCount = state.Errors.Count; + var (line, pos) = reader.CurrentState.GetLocation(); - return ElementModel.Types.DateTime.TryParse(contents, out var parsed) ? - new(parsed.ToDateTimeOffset(TimeSpan.Zero), null) : - new(contents, ERR.STRING_ISNOTAN_INSTANT(ref reader, pathStack.GetInstancePath(), contents)); - } + if (propertyName[0] != '_') + { + // No underscore, dealing with the 'value' property here. + var primitiveValueProperty = propertyValueMapping.PrimitiveValueProperty ?? + throw new InvalidOperationException($"All subclasses of {nameof(PrimitiveType)} should have a property representing the value element, " + + $"but {propertyValueMapping.Name} has not. " + reader.GenerateLocationMessage()); - static (object?, FhirJsonException?) readLong(ref Utf8JsonReader reader, Type? fhirType, PathStack pathStack) + try { - // convert string in json to a long. - var contents = reader.GetString()!; + state.Path.EnterElement("value", 0, true); - return long.TryParse(contents, out var parsed) switch - { - true when isInteger64() => new(parsed, null), - true => new(parsed, ERR.LONG_INCORRECT_FORMAT(ref reader, pathStack.GetInstancePath(), "Json string", contents, typeName(), "Json number")), - false when isInteger64() => new(contents, ERR.LONG_CANNOT_BE_PARSED(ref reader, pathStack.GetInstancePath(), contents, nameof(Integer64))), - false => new(contents, ERR.LONG_INCORRECT_FORMAT(ref reader, pathStack.GetInstancePath(), "Json string", contents, typeName(), "Json number")) - }; + var (result, error) = DeserializePrimitiveValue(ref reader, primitiveValueProperty.ImplementingType, state.Path); - string typeName() - => fhirType?.Name ?? string.Empty; + if (error is not null) + state.Errors.Add(error); - bool isInteger64() - => fhirType == typeof(Integer64); + if (targetPrimitive.ObjectValue is not null) + state.Errors.Add(ERR.DUPLICATE_PROPERTY(ref reader, state.Path.GetInstancePath(), propertyName)); + else + // Set the value, validation is done in the ObjectValidation of the PrimitiveType's. + targetPrimitive.ObjectValue = result; + } + finally + { + state.Path.ExitElement(); } } - - private static (object?, FhirJsonException) unexpectedToken(ref Utf8JsonReader reader, string instancePath, string? value, string expected, string actual) => - new(value, ERR.UNEXPECTED_JSON_TOKEN(ref reader, instancePath, expected, actual, value)); - - /// - /// This function tries to map from the json-format "generic" number to the kind of numeric type defined in the POCO. - /// - /// Reader must be positioned on a number token. This function will not move the reader to the next token. - private static (object?, FhirJsonException?) tryGetMatchingNumber(ref Utf8JsonReader reader, Type implementingType, Type? fhirType, PathStack pathStack) + else { - if (reader.TokenType != JsonTokenType.Number) - throw new InvalidOperationException($"Cannot read a numeric when reader is on a {reader.TokenType}. " + - reader.GenerateLocationMessage()); - - object? value = null; - bool success; - - if (implementingType == typeof(decimal)) - success = reader.TryGetDecimal(out decimal dec) && (value = dec) is not null; - else if (implementingType == typeof(short)) - success = reader.TryGetInt16(out short i16) && (value = i16) is not null; - else if (implementingType == typeof(ushort)) - success = reader.TryGetUInt16(out ushort ui16) && (value = ui16) is not null; - else if (implementingType == typeof(int)) - success = reader.TryGetInt32(out int i32) && (value = i32) is not null; - else if (implementingType == typeof(uint)) - success = reader.TryGetUInt32(out uint ui32) && (value = ui32) is not null; - else if (implementingType == typeof(long)) - success = reader.TryGetInt64(out long i64) && (value = i64) is not null; - else if (implementingType == typeof(ulong)) - success = reader.TryGetUInt64(out ulong ui64) && (value = ui64) is not null; - else if (implementingType == typeof(float)) - success = reader.TryGetSingle(out float si) && si.IsNormal() && (value = si) is not null; - else if (implementingType == typeof(double)) - success = reader.TryGetDouble(out double dbl) && dbl.IsNormal() && (value = dbl) is not null; - else + // The complex part of a primitive - read the object's primitives into the target + if (targetPrimitive.Extension.Any() || + targetPrimitive.ElementId is not null) { - var rawValue = reader.GetRawText(); - return unexpectedToken(ref reader, pathStack.GetInstancePath(), rawValue, implementingType.Name, "number"); + state.Errors.Add(ERR.DUPLICATE_PROPERTY(ref reader, state.Path.GetInstancePath(), propertyName)); } + deserializeObjectInto(targetPrimitive, propertyValueMapping, ref reader, DeserializedObjectKind.FhirPrimitive, state, stayOnLastToken: false); + } - // We expected a number, we found a json number, but they don't match (e.g. precision etc) - if (success) - { - return implementingType == typeof(long) && fhirType == typeof(Integer64) - ? new(value, ERR.LONG_INCORRECT_FORMAT(ref reader, pathStack.GetInstancePath(), "Json number", reader.GetRawText(), nameof(Integer64), "Json string")) - : new(value, null); - } + // Only do validation on this instance when no parse errors were encountered, otherwise we'll just + // produce spurious messages. Also, delay validation of this instance until we have processed both + // 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); + if (parsingState is null) + PocoDeserializationHelper.RunInstanceValidation(targetPrimitive, Settings.Validator, context, state.Errors); else { - var rawValue = reader.GetRawText(); - return new(rawValue, ERR.NUMBER_CANNOT_BE_PARSED(ref reader, pathStack.GetInstancePath(), rawValue, implementingType.Name)); + var elementName = state.Path.GetLastPart(); + parsingState.ScheduleDelayedValidation( + elementName + INSTANCE_VALIDATION_KEY_SUFFIX, + () => + { + context.PathStack.EnterElement(elementName, null, + propertyValueMapping.IsPrimitive); + PocoDeserializationHelper.RunInstanceValidation(targetPrimitive, Settings.Validator, context, + state.Errors); + context.PathStack.ExitElement(); + }); } } - /// - /// Returns the for the object to be deserialized using the `resourceType` property. - /// - /// Assumes the reader is on the start of an object. - internal static (ClassMapping?, FhirJsonException?) DetermineClassMappingFromInstance(ref Utf8JsonReader reader, ModelInspector inspector, PathStack path) + return targetPrimitive; + } + + /// + /// Deserializes a single object, either a resource, a FHIR primitive or a primitive value. + /// + /// Upon completion, reader will be located at the next token afther the value. + private object? deserializeSingleValue(ref Utf8JsonReader reader, ClassMapping propertyValueMapping, FhirJsonPocoDeserializerState state) + { + // Resources + if (propertyValueMapping.IsResource) { - var (resourceType, error) = determineResourceType(ref reader); + return DeserializeResourceInternal(ref reader, state, stayOnLastToken: false); + } - if (resourceType is not null) - { - var resourceMapping = inspector.FindClassMapping(resourceType); + // "normal" complex types & backbones + var newComplex = (Base)propertyValueMapping.Factory(); + deserializeObjectInto(newComplex, propertyValueMapping, ref reader, DeserializedObjectKind.Complex, state, stayOnLastToken: false); + return newComplex; + } - return resourceMapping is not null ? - (new(resourceMapping, null)) : - (new(null, ERR.UNKNOWN_RESOURCE_TYPE(ref reader, path.GetInstancePath(), resourceType))); - } - else - return new(null, error); + /// + /// Does a best-effort parse of the data available at the reader, given the required type of the property the + /// data needs to be read into. + /// + /// A value without an error if the data could be parsed to the required type, and a value with an error if the + /// value could not be parsed - in which case the value returned is the raw value coming in from the reader. + /// Upon completion, the reader will be positioned on the token after the primitive. + internal (object?, FhirJsonException?) DeserializePrimitiveValue(ref Utf8JsonReader reader, Type valuePropertyType, + PathStack pathStack) + { + // Check for unexpected non-value types. + if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) + { + var exception = reader.TokenType == JsonTokenType.StartObject + ? ERR.EXPECTED_PRIMITIVE_NOT_OBJECT(ref reader, pathStack.GetInstancePath()) + : ERR.EXPECTED_PRIMITIVE_NOT_ARRAY(ref reader, pathStack.GetInstancePath()); + reader.Recover(); + return (null, exception); } - private static (string?, FhirJsonException?) determineResourceType(ref Utf8JsonReader reader) + // Check for value types + (object? partial, ERR? error) result = reader.TokenType switch { - //TODO: determineResourceType probably won't work with streaming inputs to Utf8JsonReader + JsonTokenType.Null => (null, ERR.EXPECTED_PRIMITIVE_NOT_NULL(ref reader, pathStack.GetInstancePath())), + JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()) => (reader.GetString(), ERR.PROPERTY_MAY_NOT_BE_EMPTY(ref reader, pathStack.GetInstancePath())), + JsonTokenType.String => (reader.GetString(), null), + JsonTokenType.Number => (tryGetMatchingNumber(ref reader, valuePropertyType), null), + JsonTokenType.True or JsonTokenType.False => (reader.GetBoolean(), null), + + _ => + // This would be an internal logic error, since our callers should have made sure we're + // on the primitive value after the property name (and the Utf8JsonReader would have complained about any + // other token that one that is a value). + // EK: I think 'Comment' is the only possible non-expected option here.... + throw new InvalidOperationException($"Unexpected token type {reader.TokenType} while parsing a primitive value. " + + reader.GenerateLocationMessage()), + }; + + // Read past the value + reader.Read(); + + return result; + } - var originalReader = reader; // copy the struct so we can "rewind" - var atDepth = reader.CurrentDepth + 1; + /// + /// This function tries to map from the json-format "generic" number to the kind of numeric type defined in the POCO. + /// + /// Reader must be positioned on a number token. This function will not move the reader to the next token. + private static object? tryGetMatchingNumber(ref Utf8JsonReader reader, Type implementingType) + { + if (reader.TokenType != JsonTokenType.Number) + throw new InvalidOperationException($"Cannot read a numeric when reader is on a {reader.TokenType}. " + + reader.GenerateLocationMessage()); + + // Decimal and integer are the only two types in FHIR where we are using Json native numbers + if (implementingType == typeof(decimal) && reader.TryGetDecimal(out decimal dec)) + return dec; + if (implementingType == typeof(int) && reader.TryGetInt32(out int i32)) + return i32; + + // Ok, an incorrect number, let's try to get it by polling which type of number it is. + if (reader.TryGetInt32(out i32)) + return i32; + if (reader.TryGetDecimal(out dec)) + return dec; + + // Ok, whatever, grab the raw stuff. + return reader.GetRawText(); + } - try + /// + /// Returns the for the object to be deserialized using the `resourceType` property. + /// + /// Assumes the reader is on the start of an object. + internal static (ClassMapping?, FhirJsonException?) DetermineClassMappingFromInstance(ref Utf8JsonReader reader, ModelInspector inspector, PathStack path) + { + var (resourceType, error) = determineResourceType(ref reader); + + if (resourceType is null) return (null, error); + + var resourceMapping = inspector.FindClassMapping(resourceType); + + return resourceMapping is not null ? + (new(resourceMapping, null)) : + (new(null, ERR.UNKNOWN_RESOURCE_TYPE(ref reader, path.GetInstancePath(), resourceType))); + } + + private static (string?, FhirJsonException?) determineResourceType(ref Utf8JsonReader reader) + { + //TODO: determineResourceType probably won't work with streaming inputs to Utf8JsonReader + + var originalReader = reader; // copy the struct so we can "rewind" + var atDepth = reader.CurrentDepth + 1; + + try + { + while (reader.Read() && reader.CurrentDepth >= atDepth) { - while (reader.Read() && reader.CurrentDepth >= atDepth) + if (reader.TokenType == JsonTokenType.PropertyName && reader.CurrentDepth == atDepth) { - if (reader.TokenType == JsonTokenType.PropertyName && reader.CurrentDepth == atDepth) + var propName = reader.GetString(); + + if (propName == "resourceType") { - var propName = reader.GetString(); - - if (propName == "resourceType") - { - reader.Read(); - return (reader.TokenType == JsonTokenType.String) ? - new(reader.GetString()!, null) : - new(null, ERR.RESOURCETYPE_SHOULD_BE_STRING(ref reader, "", reader.TokenType)); - } + reader.Read(); + return (reader.TokenType == JsonTokenType.String) ? + new(reader.GetString()!, null) : + new(null, ERR.RESOURCETYPE_SHOULD_BE_STRING(ref reader, "", reader.TokenType)); } } - - return new(null, ERR.NO_RESOURCETYPE_PROPERTY(ref reader, "")); - } - finally - { - reader = originalReader; } - } - /// - /// Given a possibly suffixed property name (as encountered in the serialized form), lookup the - /// mapping for the property and the mapping for the value of the property. - /// - /// In case the name is a choice type, the type suffix will be used to determine the returned - /// , otherwise the is used. As well, - /// since the property name is from the serialized form it may also be prefixed by '_'. - /// - private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, FhirJsonException? error) tryGetMappedElementMetadata( - ModelInspector inspector, - ClassMapping parentMapping, - ref Utf8JsonReader reader, - PathStack path, - string propertyName) + return new(null, ERR.NO_RESOURCETYPE_PROPERTY(ref reader, "")); + } + finally { - bool startsWithUnderscore = propertyName[0] == '_'; - var elementName = startsWithUnderscore ? propertyName.Substring(1) : propertyName; + reader = originalReader; + } + } + + /// + /// Given a possibly suffixed property name (as encountered in the serialized form), lookup the + /// mapping for the property and the mapping for the value of the property. + /// + /// In case the name is a choice type, the type suffix will be used to determine the returned + /// , otherwise the is used. As well, + /// since the property name is from the serialized form it may also be prefixed by '_'. + /// + private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, FhirJsonException? error) tryGetMappedElementMetadata( + ModelInspector inspector, + ClassMapping parentMapping, + ref Utf8JsonReader reader, + PathStack path, + string propertyName) + { + bool startsWithUnderscore = propertyName[0] == '_'; + var elementName = startsWithUnderscore ? propertyName.Substring(1) : propertyName; - var propertyMapping = parentMapping.FindMappedElementByName(elementName) - ?? parentMapping.FindMappedElementByChoiceName(elementName); + var propertyMapping = parentMapping.FindMappedElementByName(elementName) + ?? parentMapping.FindMappedElementByChoiceName(elementName); - if (propertyMapping is null) - return (null, null, ERR.UNKNOWN_PROPERTY_FOUND(ref reader, path.GetInstancePath(), propertyName)); + if (propertyMapping is null) + return (null, null, ERR.UNKNOWN_PROPERTY_FOUND(ref reader, path.GetInstancePath(), propertyName)); - (ClassMapping? propertyValueMapping, FhirJsonException? error) = propertyMapping.Choice switch - { - ChoiceType.None or ChoiceType.ResourceChoice => - inspector.FindOrImportClassMapping(propertyMapping.GetInstantiableType()) is ClassMapping m - ? (m, null) - : throw new InvalidOperationException($"Encountered property type {propertyMapping.ImplementingType} for which no mapping was found in the model assemblies. " + reader.GenerateLocationMessage()), - ChoiceType.DatatypeChoice => getChoiceClassMapping(ref reader), - _ => throw new NotImplementedException("Unknown choice type in property mapping. " + reader.GenerateLocationMessage()) - }; + (ClassMapping? propertyValueMapping, FhirJsonException? error) = propertyMapping.Choice switch + { + ChoiceType.None or ChoiceType.ResourceChoice => + inspector.FindOrImportClassMapping(propertyMapping.GetInstantiableType()) is { } m + ? (m, null) + : throw new InvalidOperationException($"Encountered property type {propertyMapping.ImplementingType} for which no mapping was found in the model assemblies. " + reader.GenerateLocationMessage()), + ChoiceType.DatatypeChoice => getChoiceClassMapping(ref reader), + _ => throw new NotImplementedException("Unknown choice type in property mapping. " + reader.GenerateLocationMessage()) + }; - return (propertyMapping, propertyValueMapping, error); + return (propertyMapping, propertyValueMapping, error); - (ClassMapping?, FhirJsonException?) getChoiceClassMapping(ref Utf8JsonReader r) - { - string typeSuffix = elementName.Substring(propertyMapping.Name.Length); + (ClassMapping?, FhirJsonException?) getChoiceClassMapping(ref Utf8JsonReader r) + { + string typeSuffix = elementName[propertyMapping.Name.Length..]; - return string.IsNullOrEmpty(typeSuffix) - ? (null, ERR.CHOICE_ELEMENT_HAS_NO_TYPE(ref r, path.GetInstancePath(), propertyMapping.Name)) - : inspector.FindClassMapping(typeSuffix) is ClassMapping cm - ? (cm, null) - : (default, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(ref r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); - } + return string.IsNullOrEmpty(typeSuffix) + ? (null, ERR.CHOICE_ELEMENT_HAS_NO_TYPE(ref r, path.GetInstancePath(), propertyMapping.Name)) + : inspector.FindClassMapping(typeSuffix) is { } cm + ? (cm, null) + : (null, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(ref r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); } } +} - internal class FhirJsonPocoDeserializerState - { - public readonly ExceptionAggregator Errors = new(); - public readonly PathStack Path = new(); - } +internal class FhirJsonPocoDeserializerState +{ + public readonly ExceptionAggregator Errors = new(); + public readonly PathStack Path = new(); } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonSerializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonSerializer.cs index 72067bf6be..cd8d06f8a6 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonSerializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirJsonSerializer.cs @@ -268,20 +268,6 @@ protected virtual void SerializePrimitiveValue(object value, Utf8JsonWriter writ switch (value) { case int i32: writer.WriteNumberValue(i32); break; - case uint ui32: writer.WriteNumberValue(ui32); break; - case long i64: - { - // in case of Integer64, then the value must be serialized as a string due to - // issues with precision in floating point libraries. - if (requiredType == typeof(Integer64)) - writer.WriteStringValue(i64.ToString()); - else - writer.WriteNumberValue(i64); - break; - } - case ulong ui64: writer.WriteNumberValue(ui64); break; - case float si: writer.WriteNumberValue(si); break; - case double dbl: writer.WriteNumberValue(dbl); break; case decimal dec: writer.WriteNumberValue(dec); break; // A little note about trimming and whitespaces. The spec says: // "(...) In JSON and Turtle whitespace in string values is always significant. Primitive types other than @@ -293,12 +279,10 @@ protected virtual void SerializePrimitiveValue(object value, Utf8JsonWriter writ // (or an empty byte[]). case string s: writer.WriteStringValue(s); break; case bool b: writer.WriteBooleanValue(b); break; - case DateTimeOffset dto: writer.WriteStringValue(ElementModel.Types.DateTime.FormatDateTimeOffset(dto)); break; - case Enum e: writer.WriteStringValue(e.GetLiteral()); break; - case byte[] bytes: writer.WriteStringValue(Convert.ToBase64String(bytes)); break; case null: writer.WriteNullValue(); break; default: - throw new FormatException($"There is no known serialization for type {value.GetType()} into a Json primitive property value."); + writer.WriteStringValue(PrimitiveTypeConverter.ConvertTo(value)); + break; } } } diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs index 7cb53bd130..4783b58b8c 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoDeserializer.cs @@ -12,672 +12,587 @@ using System.Xml; using ERR = Hl7.Fhir.Serialization.FhirXmlException; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +public class BaseFhirXmlPocoDeserializer { - public class BaseFhirXmlPocoDeserializer + /// + /// Initializes an instance of the deserializer. + /// + /// Assembly containing the POCO classes to be used for deserialization. + public BaseFhirXmlPocoDeserializer(Assembly assembly) : this(assembly, new FhirXmlPocoDeserializerSettings()) { - /// - /// Initializes an instance of the deserializer. - /// - /// Assembly containing the POCO classes to be used for deserialization. - public BaseFhirXmlPocoDeserializer(Assembly assembly) : this(assembly, new()) - { - // nothing - } + // nothing + } - /// - /// Initializes an instance of the deserializer. - /// - /// The containing the POCO classes to be used for deserialization. - public BaseFhirXmlPocoDeserializer(ModelInspector inspector) : this(inspector, new()) - { - // nothing - } + /// + /// Initializes an instance of the deserializer. + /// + /// The containing the POCO classes to be used for deserialization. + public BaseFhirXmlPocoDeserializer(ModelInspector inspector) : this(inspector, new()) + { + // 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 BaseFhirXmlPocoDeserializer(Assembly assembly, FhirXmlPocoDeserializerSettings settings) - { - Settings = settings; - _inspector = ModelInspector.ForAssembly(assembly ?? throw new ArgumentNullException(nameof(assembly))); - } + /// + /// 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 BaseFhirXmlPocoDeserializer(Assembly assembly, FhirXmlPocoDeserializerSettings settings) + { + Settings = settings; + _inspector = ModelInspector.ForAssembly(assembly ?? throw new ArgumentNullException(nameof(assembly))); + } - /// - /// Initializes an instance of the deserializer. - /// - /// The containing the POCO classes to be used for deserialization. - /// A settings object to be used by this instance. - public BaseFhirXmlPocoDeserializer(ModelInspector inspector, FhirXmlPocoDeserializerSettings settings) - { - Settings = settings; - _inspector = inspector; - } + /// + /// Initializes an instance of the deserializer. + /// + /// The containing the POCO classes to be used for deserialization. + /// A settings object to be used by this instance. + public BaseFhirXmlPocoDeserializer(ModelInspector inspector, FhirXmlPocoDeserializerSettings settings) + { + Settings = settings; + _inspector = inspector; + } - /// - /// The settings that were passed to the constructor. - /// - public FhirXmlPocoDeserializerSettings Settings { get; } - - private readonly ModelInspector _inspector; - - /// - /// Deserialize the FHIR xml from the reader and create a new POCO resource containing the data from the reader. - /// - /// An xml reader positioned on the first element, or the beginning of the stream. - /// 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. - public bool TryDeserializeResource(XmlReader reader, [NotNullWhen(true)] out Resource? instance, out IEnumerable issues) + /// + /// The settings that were passed to the constructor. + /// + public FhirXmlPocoDeserializerSettings Settings { get; } + + private readonly ModelInspector _inspector; + + /// + /// Deserialize the FHIR xml from the reader and create a new POCO resource containing the data from the reader. + /// + /// An xml reader positioned on the first element, or the beginning of the stream. + /// 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. + public bool TryDeserializeResource(XmlReader reader, [NotNullWhen(true)] out Resource? instance, out IEnumerable issues) + { + FhirXmlPocoDeserializerState state = new(); + + // If the stream has just been opened, move to the first token. (skip processing instructions, comments, whitespaces etc.) + reader.MoveToContent(); + + if (reader.Settings?.DtdProcessing == DtdProcessing.Parse) { - FhirXmlPocoDeserializerState state = new(); + state.Errors.Add(ERR.ENCOUNTERED_DTD_REFERENCES(reader, state.Path.GetInstancePath())); + reader.Settings.DtdProcessing = DtdProcessing.Prohibit; + } - // If the stream has just been opened, move to the first token. (skip processing instructions, comments, whitespaces etc.) - reader.MoveToContent(); + instance = DeserializeResourceInternal(reader, state); + issues = state.Errors; - if (reader.Settings?.DtdProcessing == DtdProcessing.Parse) - { - state.Errors.Add(ERR.ENCOUNTERED_DTD_REFERENCES(reader, state.Path.GetInstancePath())); - reader.Settings.DtdProcessing = DtdProcessing.Prohibit; - } + return !state.Errors.HasExceptions; + } - instance = DeserializeResourceInternal(reader, state); - issues = state.Errors; + /// + /// Reads a (subtree) of serialzed FHIR Json data into a POCO object. + /// + /// The type of POCO to construct and deserialize + /// An xml reader positioned on the first element, or the beginning of the stream. + /// 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. + public bool TryDeserializeElement(Type targetType, XmlReader reader, [NotNullWhen(true)] out Base? instance, out IEnumerable issues) + { + FhirXmlPocoDeserializerState state = new(); - return !state.Errors.HasExceptions; + if (reader.Settings?.DtdProcessing == DtdProcessing.Parse) + { + state.Errors.Add(ERR.ENCOUNTERED_DTD_REFERENCES(reader, state.Path.GetInstancePath())); + reader.Settings.DtdProcessing = DtdProcessing.Prohibit; } - /// - /// Reads a (subtree) of serialzed FHIR Json data into a POCO object. - /// - /// The type of POCO to construct and deserialize - /// An xml reader positioned on the first element, or the beginning of the stream. - /// 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. - public bool TryDeserializeElement(Type targetType, XmlReader reader, [NotNullWhen(true)] out Base? instance, out IEnumerable issues) - { - FhirXmlPocoDeserializerState state = new(); + instance = DeserializeElementInternal(targetType, reader, state); + issues = state.Errors; + return !state.Errors.HasExceptions; + } - if (reader.Settings?.DtdProcessing == DtdProcessing.Parse) - { - state.Errors.Add(ERR.ENCOUNTERED_DTD_REFERENCES(reader, state.Path.GetInstancePath())); - reader.Settings.DtdProcessing = DtdProcessing.Prohibit; - } + internal Resource? DeserializeResourceInternal(XmlReader reader, FhirXmlPocoDeserializerState state) + { + //check if we are actually on an opening element. + verifyOpeningElement(reader, state); - instance = DeserializeElementInternal(targetType, reader, state); - issues = state.Errors; - return !state.Errors.HasExceptions; - } + (ClassMapping? resourceMapping, FhirXmlException? error) = determineClassMappingFromInstance(reader, _inspector, state.Path); - internal Resource? DeserializeResourceInternal(XmlReader reader, FhirXmlPocoDeserializerState state) - { - //check if we are actually on an opening element. - verifyOpeningElement(reader, state); + state.Errors.Add(error); - (ClassMapping? resourceMapping, FhirXmlException? error) = DetermineClassMappingFromInstance(reader, _inspector, state.Path); + if (resourceMapping is not null) + { + validateNameSpace(reader, state, null); - state.Errors.Add(error); + // If we have at least a mapping, let's try to continue + var newResource = (Base)resourceMapping.Factory(); - if (resourceMapping is not null) + try { - validateNameSpace(reader, state, null); + state.Path.EnterResource(resourceMapping.Name); + int nErrorCount = state.Errors.Count; + DeserializeElementInto(newResource, resourceMapping, reader, state); - // If we have at least a mapping, let's try to continue - var newResource = (Base)resourceMapping.Factory(); - - try + if (!resourceMapping.IsResource) { - state.Path.EnterResource(resourceMapping.Name); - int nErrorCount = state.Errors.Count; - deserializeElementInto(newResource, resourceMapping, reader, state); - - if (!resourceMapping.IsResource) - { - state.Errors.Add(ERR.RESOURCE_TYPE_NOT_A_RESOURCE(reader, state.Path.GetInstancePath(), resourceMapping.Name)); - return null; - } - else - { - if (Settings.AnnotateResourceParseExceptions && state.Errors.Count > nErrorCount) - { - List resourceErrs = state.Errors.Skip(nErrorCount).ToList(); - ((Resource)newResource).SetAnnotation(resourceErrs); - } - return (Resource)newResource; - } + state.Errors.Add(ERR.RESOURCE_TYPE_NOT_A_RESOURCE(reader, state.Path.GetInstancePath(), resourceMapping.Name)); + return null; } - finally + else { - state.Path.ExitResource(); + if (Settings.AnnotateResourceParseExceptions && state.Errors.Count > nErrorCount) + { + List resourceErrs = state.Errors.Skip(nErrorCount).ToList(); + ((Resource)newResource).SetAnnotation(resourceErrs); + } + return (Resource)newResource; } } - else + finally { - return null; + state.Path.ExitResource(); } } + else + { + return null; + } + } - private static void verifyOpeningElement(XmlReader reader, FhirXmlPocoDeserializerState state) + private static void verifyOpeningElement(XmlReader reader, FhirXmlPocoDeserializerState state) + { + //If not skip all non-content and check again. + reader.MoveToContent(); + if (reader.NodeType != XmlNodeType.Element) { - //If not skip all non-content and check again. - reader.MoveToContent(); - if (reader.NodeType != XmlNodeType.Element) + //if we are still not at an opening element, throw user-error. + state.Errors.Add(ERR.EXPECTED_OPENING_ELEMENT(reader, state.Path.GetInstancePath(), reader.NodeType.GetLiteral())); + //try to recover + while (reader.NodeType != XmlNodeType.Element || !reader.EOF) { - //if we are still not at an opening element, throw user-error. - state.Errors.Add(ERR.EXPECTED_OPENING_ELEMENT(reader, state.Path.GetInstancePath(), reader.NodeType.GetLiteral())); - //try to recover - while (reader.NodeType != XmlNodeType.Element || !reader.EOF) - { - reader.ReadToContent(state); - } + reader.ReadToContent(state); } } + } - private static bool validateNameSpace(XmlReader reader, FhirXmlPocoDeserializerState state, PropertyMapping? propMapping) + private static bool validateNameSpace(XmlReader reader, FhirXmlPocoDeserializerState state, PropertyMapping? propMapping) + { + if (string.IsNullOrEmpty(reader.NamespaceURI)) { - if (string.IsNullOrEmpty(reader.NamespaceURI)) - { - state.Errors.Add(ERR.EMPTY_ELEMENT_NAMESPACE(reader, state.Path.GetInstancePath(), reader.LocalName)); - } - else if (propMapping?.SerializationHint == Specification.XmlRepresentation.XHtml) - { - if (reader.NamespaceURI != XmlNs.XHTML) - { - state.Errors.Add(ERR.INCORRECT_XHTML_NAMESPACE(reader, state.Path.GetInstancePath())); - } - } - else if (reader.NamespaceURI != XmlNs.FHIR) + state.Errors.Add(ERR.EMPTY_ELEMENT_NAMESPACE(reader, state.Path.GetInstancePath(), reader.LocalName)); + } + else if (propMapping?.SerializationHint == Specification.XmlRepresentation.XHtml) + { + if (reader.NamespaceURI != XmlNs.XHTML) { - state.Errors.Add(ERR.INCORRECT_ELEMENT_NAMESPACE(reader, state.Path.GetInstancePath(), reader.LocalName, reader.NamespaceURI)); - return false; // the only case we want to NOT process the content for anyway + state.Errors.Add(ERR.INCORRECT_XHTML_NAMESPACE(reader, state.Path.GetInstancePath())); } - return true; } - - internal Base DeserializeElementInternal(Type targetType, XmlReader reader, FhirXmlPocoDeserializerState state) + else if (reader.NamespaceURI != XmlNs.FHIR) { - var mapping = _inspector.FindOrImportClassMapping(targetType) ?? - throw new ArgumentException($"Type '{targetType}' could not be located and can " + - $"therefore not be used for deserialization. " + reader.GenerateLocationMessage(), nameof(targetType)); + state.Errors.Add(ERR.INCORRECT_ELEMENT_NAMESPACE(reader, state.Path.GetInstancePath(), reader.LocalName, reader.NamespaceURI)); + return false; // the only case we want to NOT process the content for anyway + } + return true; + } - //check if we are at an opening element. - verifyOpeningElement(reader, state); + internal Base DeserializeElementInternal(Type targetType, XmlReader reader, FhirXmlPocoDeserializerState state) + { + var mapping = _inspector.FindOrImportClassMapping(targetType) ?? + throw new ArgumentException($"Type '{targetType}' could not be located and can " + + $"therefore not be used for deserialization. " + reader.GenerateLocationMessage(), nameof(targetType)); - // If we have at least a mapping, let's try to continue - var newDatatype = (Base)mapping.Factory(); - deserializeElementInto(newDatatype, mapping, reader, state); - return newDatatype; - } + //check if we are at an opening element. + verifyOpeningElement(reader, state); - // We expect to start at the open tag op the element. When done, the reader will be at the next token after this element or end of the file. - private void deserializeElementInto(Base target, ClassMapping mapping, XmlReader reader, FhirXmlPocoDeserializerState state) + // If we have at least a mapping, let's try to continue + var newDatatype = (Base)mapping.Factory(); + DeserializeElementInto(newDatatype, mapping, reader, state); + return newDatatype; + } + + // We expect to start at the open tag op the element. When done, the reader will be at the next token after this element or end of the file. + internal void DeserializeElementInto(Base target, ClassMapping mapping, XmlReader reader, FhirXmlPocoDeserializerState state) + { + var oldErrors = state.Errors.Count; + var (lineNumber, position) = reader.GenerateLineInfo(); + var hasValueAttribute = reader.GetAttribute("value") != null; + var depth = reader.Depth; + var name = reader.LocalName; + + //check if on opening tag + if (reader.NodeType != XmlNodeType.Element) + throw new InvalidOperationException($"Xml node of type '{reader.NodeType}' is unexpected at this point"); + + if (reader.HasAttributes) + readAttributes(target, mapping, reader, state); + + //Empty elements have no children e.g. depth))) { - readAttributes(target, mapping, reader, state); + //previous element didn't have a value and the current value is not a child of the previous element. + //error is thrown with the location and the name of the previous element. + var locationMessage = XmlReaderExtensions.GenerateLocationMessage(lineNumber, position); + state.Errors.Add(ERR.ELEMENT_HAS_NO_VALUE_OR_CHILDREN(state.Path.GetInstancePath(), lineNumber, position, locationMessage, name)); } - //Empty elements have no children e.g. = highestOrder) + { + highestOrder = propMapping.Order; } + else + { + state.Errors.Add(ERR.ELEMENT_OUT_OF_ORDER(reader, state.Path.GetInstancePath(), reader.LocalName)); + incorrectOrder = true; + } + return (highestOrder, incorrectOrder); + } + + private void deserializePropertyValue(Base target, XmlReader reader, FhirXmlPocoDeserializerState state, int oldErrors, bool incorrectOrder, PropertyMapping propMapping, ClassMapping? propValueMapping) + { + var (lineNumber, position) = reader.GenerateLineInfo(); + var name = reader.LocalName; + + object? result = propMapping.IsCollection + ? addToList(target, propValueMapping!, propMapping, reader, state) + : readSingleValue(propValueMapping!, propMapping, reader, state); - private static (int highestOrder, bool incorrectOrder) checkOrder(XmlReader reader, FhirXmlPocoDeserializerState state, int highestOrder, PropertyMapping propMapping) + if (!propMapping.IsCollection && propMapping.GetValue(target) != null) { - var incorrectOrder = false; - //check if element is in the correct order. - if (propMapping.Order >= highestOrder) - { - highestOrder = propMapping.Order; - } - else - { - state.Errors.Add(ERR.ELEMENT_OUT_OF_ORDER(reader, state.Path.GetInstancePath(), reader.LocalName)); - incorrectOrder = true; - } - return (highestOrder, incorrectOrder); + state.Errors.Add(ERR.INVALID_DUPLICATE_PROPERTY(reader, state.Path.GetInstancePath(), propMapping.Name)); } - private void deserializePropertyValue(Base target, XmlReader reader, FhirXmlPocoDeserializerState state, int oldErrors, bool incorrectOrder, PropertyMapping propMapping, ClassMapping? propValueMapping) + if (Settings.Validator is not null && (Settings.ValidateOnFailedParse || oldErrors == state.Errors.Count)) { - var (lineNumber, position) = reader.GenerateLineInfo(); - var name = reader.LocalName; + var context = new PropertyDeserializationContext( + target, + state.Path, // should this path GetPath or this? + name, + lineNumber, position, + propMapping); + + PocoDeserializationHelper.RunPropertyValidation(result, Settings.Validator, context, state.Errors); + } - object? result = propMapping.IsCollection - ? addToList(target, propValueMapping!, propMapping, reader, state) - : readSingleValue(propValueMapping!, propMapping, reader, state); + propMapping.SetValue(target, result); + } - if (!propMapping.IsCollection && propMapping.GetValue(target) != null) - { - state.Errors.Add(ERR.INVALID_DUPLICATE_PROPERTY(reader, state.Path.GetInstancePath(), propMapping.Name)); - } - if (Settings.Validator is not null && (Settings.ValidateOnFailedParse || oldErrors == state.Errors.Count)) - { - var context = new PropertyDeserializationContext( - state.Path, // should this path GetPath or this? - name, - lineNumber, position, - propMapping); + private static XHtml readXhtml(XmlReader reader) + { + var xhtml = reader.ReadOuterXml(); + return new XHtml(xhtml); + } - PocoDeserializationHelper.RunPropertyValidation(ref result, Settings.Validator, context, state.Errors); - } + //Will create a new list, or adds encountered values to an already existing list (and reports a user error). + private IList addToList(Base target, ClassMapping propValueMapping, PropertyMapping propMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + { + // We know our POCOs will generate the correct, non-null list. + var currentList = (IList)propMapping.GetValue(target)!; + + readIntoList(currentList,propValueMapping, propMapping, reader, state); + return currentList; + } - propMapping.SetValue(target, result); + //When done, the reader will be at the next token after the last element of the list or end of the file. + private void readIntoList(IList targetList, ClassMapping propValueMapping, PropertyMapping propMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + { + //There was already a list created previously -> User error! + if (targetList.Count > 0) + { + state.Path.IncrementIndex(targetList.Count); + state.Errors.Add(ERR.ELEMENT_NOT_IN_SEQUENCE(reader, state.Path.GetInstancePath(), reader.LocalName)); } + var name = reader.LocalName; - private static XHtml readXhtml(XmlReader reader) + while (reader.LocalName == name && reader.NodeType != XmlNodeType.EndElement) { - var xhtml = reader.ReadOuterXml(); - return new XHtml(xhtml); + var newEntry = readSingleValue(propValueMapping, propMapping, reader, state); + targetList.Add(newEntry); + state.Path.IncrementIndex(); } + } - //Will create a new list, or adds encountered values to an already existing list (and reports a user error). - private IList? addToList(Base target, ClassMapping propValueMapping, PropertyMapping propMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + private object? readSingleValue(ClassMapping propValueMapping, PropertyMapping propMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + { + if (propMapping.Choice == ChoiceType.ResourceChoice) { - // We know our POCOs will generate the correct, non-null list. - var currentList = (IList)propMapping.GetValue(target)!; - - readIntoList(currentList,propValueMapping, propMapping, reader, state); - return currentList; + return deserializeResourceContainer(reader, state); } - - //When done, the reader will be at the next token after the last element of the list or end of the file. - private void readIntoList(IList targetList, ClassMapping propValueMapping, PropertyMapping propMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + else if (propMapping.SerializationHint == Specification.XmlRepresentation.XHtml) { - //There was already a list created previously -> User error! - if (targetList.Count > 0) - { - state.Path.IncrementIndex(targetList.Count); - state.Errors.Add(ERR.ELEMENT_NOT_IN_SEQUENCE(reader, state.Path.GetInstancePath(), reader.LocalName)); - } - - var name = reader.LocalName; - - while (reader.LocalName == name && reader.NodeType != XmlNodeType.EndElement) - { - var newEntry = readSingleValue(propValueMapping, propMapping, reader, state); - targetList.Add(newEntry); - state.Path.IncrementIndex(); - } + return readXhtml(reader); } - - private object? readSingleValue(ClassMapping propValueMapping, PropertyMapping propMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + else { - if (propMapping.Choice == ChoiceType.ResourceChoice) - { - return deserializeResourceContainer(reader, state); - } - else if (propMapping.SerializationHint == Specification.XmlRepresentation.XHtml) - { - return readXhtml(reader); - } - else - { - var newDatatype = (Base)propValueMapping.Factory(); - deserializeElementInto(newDatatype, propValueMapping, reader, state); - return newDatatype; - } + var newDatatype = (Base)propValueMapping.Factory(); + DeserializeElementInto(newDatatype, propValueMapping, reader, state); + return newDatatype; } + } - private object? deserializeResourceContainer(XmlReader reader, FhirXmlPocoDeserializerState state) + private object? deserializeResourceContainer(XmlReader reader, FhirXmlPocoDeserializerState state) + { + var depth = reader.Depth; + // we are currently at the resource container (e.g. ) + if (reader.HasAttributes) { - var depth = reader.Depth; - // we are currently at the resource container (e.g. ) - if (reader.HasAttributes) - { - state.Errors.Add(ERR.NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER(reader, state.Path.GetInstancePath(), reader.LocalName)); - } - // let's move to the actual resource - reader.ReadToContent(state); - var result = DeserializeResourceInternal(reader, state); - // now we should be at the closing element of the resource container (e.g. ). We should check that and maybe fix that.) - if (reader.Depth != depth && reader.NodeType != XmlNodeType.EndElement) - { - state.Errors.Add(ERR.UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER(reader, state.Path.GetInstancePath(), reader.LocalName)); + state.Errors.Add(ERR.NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER(reader, state.Path.GetInstancePath(), reader.LocalName)); + } + // let's move to the actual resource + reader.ReadToContent(state); + var result = DeserializeResourceInternal(reader, state); + // now we should be at the closing element of the resource container (e.g. ). We should check that and maybe fix that.) + if (reader.Depth != depth && reader.NodeType != XmlNodeType.EndElement) + { + state.Errors.Add(ERR.UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER(reader, state.Path.GetInstancePath(), reader.LocalName)); - // skip until we're back at the closing of the - while (!(reader.Depth == depth && reader.NodeType == XmlNodeType.EndElement)) - { - reader.Read(); - } + // skip until we're back at the closing of the + while (!(reader.Depth == depth && reader.NodeType == XmlNodeType.EndElement)) + { + reader.Read(); } - - //we move out of the container to the next element. - reader.ReadToContent(state); - return result; } - private void readAttributes(Base target, ClassMapping propValueMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + //we move out of the container to the next element. + reader.ReadToContent(state); + return result; + } + + private static void readAttributes(Base target, ClassMapping propValueMapping, XmlReader reader, FhirXmlPocoDeserializerState state) + { + var elementName = reader.LocalName; + //move into first attribute + if (reader.MoveToFirstAttribute()) { - var elementName = reader.LocalName; - //move into first attribute - if (reader.MoveToFirstAttribute()) + try { - try + do { - do + if (reader.LocalName == "xmlns" || reader.Prefix == "xmlns") { - if (reader.LocalName == "xmlns" || reader.Prefix == "xmlns") - { - //Do nothing: checked before - } - else if (reader.LocalName == "schemaLocation" && reader.NamespaceURI == "http://www.w3.org/2001/XMLSchema-instance") - { - state.Errors.Add(ERR.SCHEMALOCATION_DISALLOWED(reader, state.Path.GetInstancePath())); - } - else + //Do nothing: checked before + } + else if (reader is { LocalName: "schemaLocation", NamespaceURI: "http://www.w3.org/2001/XMLSchema-instance" }) + { + state.Errors.Add(ERR.SCHEMALOCATION_DISALLOWED(reader, state.Path.GetInstancePath())); + } + else + { + var propMapping = propValueMapping.FindMappedElementByName(reader.LocalName); + if (propMapping is not null) { - var propMapping = propValueMapping.FindMappedElementByName(reader.LocalName); - if (propMapping is not null) + state.Path.EnterElement(propMapping.Name, !propMapping.IsCollection ? null : 0, propMapping.IsPrimitive); + try { - state.Path.EnterElement(propMapping.Name, !propMapping.IsCollection ? null : 0, propMapping.IsPrimitive); - try - { - readAttribute(target, propMapping!, elementName, reader, state); - } - finally - { - state.Path.ExitElement(); - } + readAttribute(target, propMapping!, elementName, reader, state); } - else + finally { - state.Errors.Add(ERR.UNKNOWN_ATTRIBUTE(reader, state.Path.GetInstancePath(), reader.LocalName)); + state.Path.ExitElement(); } } + else + { + state.Errors.Add(ERR.UNKNOWN_ATTRIBUTE(reader, state.Path.GetInstancePath(), reader.LocalName)); + } + } - } while (reader.MoveToNextAttribute()); - } - finally - { - //move reader back to element so it can continue later - reader.MoveToElement(); - } + } while (reader.MoveToNextAttribute()); } - } - - ///Parse current attribute value to set the value property of the target. - private void readAttribute(Base target, PropertyMapping propMapping, string elementName, XmlReader reader, FhirXmlPocoDeserializerState state) - { - if (!string.IsNullOrEmpty(reader.NamespaceURI) && reader.NamespaceURI != XmlNs.FHIR) + finally { - state.Errors.Add(ERR.INCORRECT_ATTRIBUTE_NAMESPACE(reader, state.Path.GetInstancePath(), reader.LocalName, elementName, reader.NamespaceURI)); + //move reader back to element so it can continue later + reader.MoveToElement(); } + } + } - int oldErrors = state.Errors.Count; - //parse current attribute to expected type - + ///Parse current attribute value to set the value property of the target. + private static void readAttribute(Base target, PropertyMapping propMapping, string elementName, XmlReader reader, FhirXmlPocoDeserializerState state) + { + if (!string.IsNullOrEmpty(reader.NamespaceURI) && reader.NamespaceURI != XmlNs.FHIR) + state.Errors.Add(ERR.INCORRECT_ATTRIBUTE_NAMESPACE(reader, state.Path.GetInstancePath(), reader.LocalName, elementName, reader.NamespaceURI)); - var (parsedValue, error) = ParsePrimitiveValue(reader, propMapping.ImplementingType, state.Path); + // "Implementers SHOULD trim leading and trailing whitespace before writing and SHOULD trim leading and + // trailing whitespace when reading attribute values (for XML schema conformance)" + string trimmedValue = reader.Value.TrimEnd().TrimStart(); - state.Errors.Add(error); + if (string.IsNullOrEmpty(trimmedValue)) + state.Errors.Add(ERR.ATTRIBUTE_HAS_EMPTY_VALUE(reader, state.Path.GetInstancePath())); - if (parsedValue != null) - { - if (Settings.Validator is not null && (Settings.ValidateOnFailedParse || oldErrors == state.Errors.Count)) - { - var (lineNumber, position) = reader.GenerateLineInfo(); - var name = reader.LocalName; + var parsedValue = parsePrimitiveValue(trimmedValue, propMapping.ImplementingType, state.Path); - var context = new PropertyDeserializationContext( - state.Path, - name, - lineNumber, position, - propMapping); + if (target is PrimitiveType primitive && propMapping.Name == "value") + { + primitive.ObjectValue = parsedValue; + } + else + { + // Handle atomic-types-as-primitives, Element.id, Extension.url etc. + propMapping.SetValue(target, parsedValue); + } + } - PocoDeserializationHelper.RunPropertyValidation(ref parsedValue, Settings.Validator, context, state.Errors); - } + private static object parsePrimitiveValue(string trimmedValue, Type implementingType, PathStack pathStack) + { + if (implementingType == typeof(FhirString)) + return new FhirString(trimmedValue); + if (implementingType == typeof(FhirUri)) + return new FhirUri(trimmedValue); - if (target is PrimitiveType primitive && propMapping.Name == "value") - { - primitive.ObjectValue = parsedValue; - } - else - { - propMapping.SetValue(target, parsedValue); - } - } + // bool, int and decimal are the only three types that are used in ObjectValue (and the json serialization) + if (implementingType == typeof(bool)) + { + return ElementModel.Types.Boolean.TryParse(trimmedValue, out var parsed) + ? parsed.Value : trimmedValue; } - - internal (object?, FhirXmlException?) ParsePrimitiveValue(XmlReader reader, Type implementingType, PathStack pathStack) + if (implementingType == typeof(int)) { - // "Implementers SHOULD trim leading and trailing whitespace before writing and SHOULD trim leading and - // trailing whitespace when reading attribute values (for XML schema conformance)" - string trimmedValue = reader.Value.TrimEnd().TrimStart(); + return ElementModel.Types.Integer.TryParse(trimmedValue, out var parsed) + ? parsed.Value : trimmedValue; + } + if (implementingType == typeof(decimal)) + { + return ElementModel.Types.Decimal.TryParse(trimmedValue, out var parsed) + ? parsed.Value : trimmedValue; + } - if (!string.IsNullOrEmpty(trimmedValue)) - { - if (implementingType == typeof(string)) - return (trimmedValue, null); - else if (implementingType == typeof(FhirString)) - return (new FhirString(trimmedValue), null); - else if(implementingType == typeof(FhirUri)) - return (new FhirUri(trimmedValue), null); - else if (implementingType == typeof(bool)) - { - return ElementModel.Types.Boolean.TryParse(trimmedValue, out var parsed) - ? (parsed?.Value, null) - : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(DateTimeOffset)) - { - return ElementModel.Types.DateTime.TryParse(trimmedValue, out var parsed) - ? (parsed.ToDateTimeOffset(TimeSpan.Zero), null) - : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(byte[])) - { - return !Settings.DisableBase64Decoding ? getByteArrayValue(reader, trimmedValue, pathStack) : ((object?, ERR?))(trimmedValue, null); - } - else if (implementingType == typeof(int)) - { - return ElementModel.Types.Integer.TryParse(trimmedValue, out var parsed) ? (parsed?.Value, null) : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(uint)) - { - return uint.TryParse(trimmedValue, out var parsed) ? (parsed, null) : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(long)) - { - return ElementModel.Types.Long.TryParse(trimmedValue, out var parsed) ? (parsed?.Value, null) : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(decimal)) - { - return ElementModel.Types.Decimal.TryParse(trimmedValue, out var parsed) ? (parsed?.Value, null) : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(double)) - { - return ElementModel.Types.Decimal.TryParse(trimmedValue, out var parsed) ? (parsed?.Value, null) : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(float)) - { - return ElementModel.Types.Decimal.TryParse(trimmedValue, out var parsed) ? (parsed?.Value, null) : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType == typeof(ulong)) - { - return ulong.TryParse(trimmedValue, out var parsed) ? (parsed, null) : (trimmedValue, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE(reader, pathStack.GetInstancePath(), trimmedValue, implementingType.Name)); - } - else if (implementingType.IsEnum) - { - return new(trimmedValue, null); - } - else - { - throw new ArgumentException($"Implementing type '{implementingType.Name}' is incorrect for value '{trimmedValue}'"); - } - } - else - { - return (trimmedValue, ERR.ATTRIBUTE_HAS_EMPTY_VALUE(reader, pathStack.GetInstancePath())); - } + return trimmedValue; + } - static (object, ERR?) getByteArrayValue(XmlReader reader, string trimmedValue, PathStack pathStack) - { - try - { - return (Convert.FromBase64String(trimmedValue), null); - } - catch (FormatException) - { - return (trimmedValue, ERR.INCORRECT_BASE64_DATA(reader, pathStack.GetInstancePath())); - } - } - } + /// + /// Returns the for the object to be deserialized using the root property. + /// + /// Assumes the reader is on the start of an object. + private static (ClassMapping?, FhirXmlException?) determineClassMappingFromInstance(XmlReader reader, ModelInspector inspector, PathStack path) + { + var resourceMapping = inspector.FindClassMapping(reader.LocalName); + return resourceMapping is not null ? + (new(resourceMapping, null)) : + (new(null, ERR.UNKNOWN_RESOURCE_TYPE(reader, path.GetInstancePath(), reader.LocalName))); + } + /// + /// Given a possibly suffixed property name (as encountered in the serialized form), lookup the + /// mapping for the property and the mapping for the value of the property. + /// + /// In case the name is a choice type, the type suffix will be used to determine the returned + /// , otherwise the is used. + /// + private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, FhirXmlException? error) tryGetMappedElementMetadata( + ModelInspector inspector, + ClassMapping parentMapping, + XmlReader reader, + PathStack path, + string propertyName) + { + var propertyMapping = parentMapping.FindMappedElementByName(propertyName) + ?? parentMapping.FindMappedElementByChoiceName(propertyName); - /// - /// Returns the for the object to be deserialized using the root property. - /// - /// Assumes the reader is on the start of an object. - internal static (ClassMapping?, FhirXmlException?) DetermineClassMappingFromInstance(XmlReader reader, ModelInspector inspector, PathStack path) + if (propertyMapping is null) { - var resourceMapping = inspector.FindClassMapping(reader.LocalName); - - return resourceMapping is not null ? - (new(resourceMapping, null)) : - (new(null, ERR.UNKNOWN_RESOURCE_TYPE(reader, path.GetInstancePath(), reader.LocalName))); + return (null, null, ERR.UNKNOWN_ELEMENT(reader, path.GetInstancePath(), propertyName)); } - /// - /// Given a possibly suffixed property name (as encountered in the serialized form), lookup the - /// mapping for the property and the mapping for the value of the property. - /// - /// In case the name is a choice type, the type suffix will be used to determine the returned - /// , otherwise the is used. - /// - private static (PropertyMapping? propMapping, ClassMapping? propValueMapping, FhirXmlException? error) tryGetMappedElementMetadata( - ModelInspector inspector, - ClassMapping parentMapping, - XmlReader reader, - PathStack path, - string propertyName) + (ClassMapping? propertyValueMapping, FhirXmlException? error) = propertyMapping.Choice switch { + ChoiceType.None or ChoiceType.ResourceChoice => + inspector.FindOrImportClassMapping(propertyMapping.GetInstantiableType()) is ClassMapping m + ? (m, null) + : throw new InvalidOperationException($"Encountered property type {propertyMapping.ImplementingType} for which no mapping was found in the model assemblies. " + reader.GenerateLocationMessage()), + ChoiceType.DatatypeChoice => getChoiceClassMapping(reader), + _ => throw new NotImplementedException("Unknown choice type in property mapping. " + reader.GenerateLocationMessage()) + }; - var propertyMapping = parentMapping.FindMappedElementByName(propertyName) - ?? parentMapping.FindMappedElementByChoiceName(propertyName); - - if (propertyMapping is null) - { - return (null, null, ERR.UNKNOWN_ELEMENT(reader, path.GetInstancePath(), propertyName)); - } - - (ClassMapping? propertyValueMapping, FhirXmlException? error) = propertyMapping.Choice switch - { - ChoiceType.None or ChoiceType.ResourceChoice => - inspector.FindOrImportClassMapping(propertyMapping.GetInstantiableType()) is ClassMapping m - ? (m, null) - : throw new InvalidOperationException($"Encountered property type {propertyMapping.ImplementingType} for which no mapping was found in the model assemblies. " + reader.GenerateLocationMessage()), - ChoiceType.DatatypeChoice => getChoiceClassMapping(reader), - _ => throw new NotImplementedException("Unknown choice type in property mapping. " + reader.GenerateLocationMessage()) - }; - - return (propertyMapping, propertyValueMapping, error); + return (propertyMapping, propertyValueMapping, error); - (ClassMapping?, FhirXmlException?) getChoiceClassMapping(XmlReader r) - { - string typeSuffix = propertyName.Substring(propertyMapping.Name.Length); + (ClassMapping?, FhirXmlException?) getChoiceClassMapping(XmlReader r) + { + string typeSuffix = propertyName[propertyMapping.Name.Length..]; - return string.IsNullOrEmpty(typeSuffix) - ? (null, ERR.CHOICE_ELEMENT_HAS_NO_TYPE(r, path.GetInstancePath(), propertyMapping.Name)) - : inspector.FindClassMapping(typeSuffix) is ClassMapping cm - ? (cm, null) - : (default, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); - } + return string.IsNullOrEmpty(typeSuffix) + ? (null, ERR.CHOICE_ELEMENT_HAS_NO_TYPE(r, path.GetInstancePath(), propertyMapping.Name)) + : inspector.FindClassMapping(typeSuffix) is { } cm + ? (cm, null) + : (null, ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE(r, path.GetInstancePath(), propertyMapping.Name, typeSuffix)); } } - - internal class FhirXmlPocoDeserializerState - { - public readonly ExceptionAggregator Errors = new(); - public readonly PathStack Path = new(); - } } -#nullable restore \ No newline at end of file +internal class FhirXmlPocoDeserializerState +{ + public readonly ExceptionAggregator Errors = new(); + public readonly PathStack Path = new(); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlSerializer.cs b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlSerializer.cs index 99ce18c8a3..2ca292d971 100644 --- a/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlSerializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/BaseFhirXmlSerializer.cs @@ -174,20 +174,13 @@ protected virtual void SerializePrimitiveValue(string elementName, object value, var literal = value switch { int i32 => XmlConvert.ToString(i32), - uint ui32 => XmlConvert.ToString(ui32), - long i64 => XmlConvert.ToString(i64), - ulong ui64 => XmlConvert.ToString(ui64), - float si => XmlConvert.ToString(si), - double dbl => XmlConvert.ToString(dbl), decimal dec => XmlConvert.ToString(dec), // A little note about trimming and whitespaces. The spec says: // "Implementers SHOULD trim leading and trailing whitespace before writing and SHOULD trim leading // and trailing whitespace when reading attribute values (for XML schema conformance)" string s => s.Trim(), bool b => XmlConvert.ToString(b), - DateTimeOffset dto => ElementModel.Types.DateTime.FormatDateTimeOffset(dto), - byte[] bytes => Convert.ToBase64String(bytes), - _ => throw new FormatException($"There is no known serialization for type {value.GetType()} into an Xml primitive property value.") + _ => PrimitiveTypeConverter.ConvertTo(value) }; writer.WriteAttributeString(elementName, ns: null, value: literal); diff --git a/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs b/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs index 636dee69ea..c30ebdb5ac 100644 --- a/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs +++ b/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs @@ -9,6 +9,7 @@ #nullable enable using Hl7.Fhir.Introspection; +using Hl7.Fhir.Model; using Hl7.Fhir.Utility; using Hl7.Fhir.Validation; using System; @@ -18,134 +19,125 @@ #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 +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 +/// information on and . +/// +public class DataAnnotationDeserialzationValidator : IDeserializationValidator { + public static readonly DataAnnotationDeserialzationValidator Default = new(); + /// - /// 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 - /// information on and . + /// 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 class DataAnnotationDeserialzationValidator : IDeserializationValidator + public NarrativeValidationKind NarrativeValidation { get; } = NarrativeValidationKind.None; + + public DataAnnotationDeserialzationValidator( + NarrativeValidationKind narrativeValidation = NarrativeValidationKind.None) { - 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; - } + NarrativeValidation = narrativeValidation; + } - /// - public void ValidateProperty(ref object? instance, in PropertyDeserializationContext context, out CodedValidationException[]? reportedErrors) - { - var validationContext = new ValidationContext(instance ?? new object()) - .SetValidateRecursively(false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. - .SetNarrativeValidationKind(NarrativeValidation) - .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) - .SetLocation(context.PathStack); + /// + 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) + .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) + .SetLocationProducer(context.PathStack.GetInstancePath); - reportedErrors = runAttributeValidation(instance, context.ElementMapping.ValidationAttributes, validationContext); - } + reportedErrors = runAttributeValidation(propertyValue, context.ElementMapping.ValidationAttributes, validationContext); + } - /// - public void ValidateInstance(object? instance, in InstanceDeserializationContext context, out CodedValidationException[]? reportedErrors) + /// + public virtual void ValidateInstance(Base instance, in InstanceDeserializationContext context, + out CodedValidationException[]? reportedErrors) + { + 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) + .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) + .SetLocationProducer(context.PathStack.GetInstancePath); + + IEnumerable? errors = null; + + // Make sure we detect missing values - go over all members that have cardinality constraints + // and invoke those if there is no value (if there was a value, ValidateProperty will have been + // called on it while deserializing the member). + foreach (var propMapping in context.InstanceMapping.PropertyMappings) { - var validationContext = new ValidationContext(instance ?? new object()) - .SetValidateRecursively(false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. - .SetNarrativeValidationKind(NarrativeValidation) - .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) - .SetLocation(context.PathStack); - - if (instance is null) + var cardinality = propMapping.ValidationAttributes.OfType().SingleOrDefault(); + if (cardinality is not null && cardinality.Min > 0) { - reportedErrors = null; - return; - } + // Note that some Value accessors (for Code.Value for example) can throw, but there are + // no Cardinality constraints on those, so we don't have to worry about that now. + var propValue = propMapping.GetValue(instance); - IEnumerable? errors = null; - - // Make sure we detect missing values - go over all members that have cardinality constraints - // and invoke those if there is no value (if there was a value, ValidateProperty will have been - // called on it while deserializing the member). - foreach (var propMapping in context.InstanceMapping.PropertyMappings) - { - var cardinality = propMapping.ValidationAttributes.OfType().SingleOrDefault(); - if (cardinality is not null && cardinality.Min > 0) + if (propValue is null || ReflectionHelper.IsRepeatingElement(propValue, out var list) && list.Count == 0) { - // Note that some Value accessors (for Code.Value for example) can throw, but there are - // no Cardinality constraints on those, so we don't have to worry about that now. - var propValue = propMapping.GetValue(instance); - - if (propValue is null || ReflectionHelper.IsRepeatingElement(propValue, out var list) && list.Count == 0) - { - // Add the name of the property to the path, so we can display the correct name of the element, - // even if it does not really contain any values. - var nestedContext = validationContext.IntoEmptyProperty(propMapping.Name); - - errors = add(errors, runAttributeValidation(propValue, new[] { cardinality }, nestedContext)); - } + // Add the name of the property to the path, so we can display the correct name of the element, + // even if it does not really contain any values. + var nestedContext = validationContext.IntoEmptyProperty(propMapping.Name); + + errors = add(errors, runAttributeValidation(propValue, [cardinality], nestedContext)); } } + } - // Validate the attributes on this instance itself - errors = add(errors, runAttributeValidation(instance, context.InstanceMapping.ValidationAttributes, validationContext)); + // Validate the attributes on this instance itself + errors = add(errors, runAttributeValidation(instance, context.InstanceMapping.ValidationAttributes, validationContext)); - // Now, just like Validator.Validate, run the IValidatableObject if applicable - if (instance is IValidatableObject ivo) + // Now, just like Validator.Validate, run the IValidatableObject if applicable + if (instance is IValidatableObject ivo) + { + var extraErrors = ivo.Validate(validationContext).ToList(); + if (extraErrors.Any(e => e != ValidationResult.Success)) { - var extraErrors = ivo.Validate(validationContext).ToList(); - if (extraErrors is not null && extraErrors.Any(e => e != ValidationResult.Success)) - { - var codedErrors = extraErrors.OfType().Select(cvr => cvr.ValidationException); - if (codedErrors.Count() != extraErrors.Count) - throw new InvalidOperationException($"IValidatableObject.Validates should return one or more {nameof(CodedValidationResult)}."); + var codedErrors = extraErrors.OfType().Select(cvr => cvr.ValidationException).ToList(); + if (codedErrors.Count != extraErrors.Count) + throw new InvalidOperationException($"IValidatableObject.Validates should return one or more {nameof(CodedValidationResult)}."); - errors = add(errors, codedErrors); - } + errors = add(errors, codedErrors); } - - reportedErrors = errors?.ToArray(); - return; } - private IEnumerable? add(IEnumerable? errors, IEnumerable? moreErrors) - { - return moreErrors is null ? - errors - : errors is not null ? errors.Concat(moreErrors) : moreErrors; - } + reportedErrors = errors?.ToArray(); + } - private CodedValidationException[]? runAttributeValidation( - object? candidateValue, - ValidationAttribute[] attributes, - ValidationContext validationContext) - { + private static IEnumerable? add(IEnumerable? errors, IEnumerable? moreErrors) + { + return moreErrors is null ? + errors + : errors is not null ? errors.Concat(moreErrors) : moreErrors; + } - // Avoid allocation of a list for every validation until we really have something to report. - IEnumerable? errors = null; + private static CodedValidationException[]? runAttributeValidation( + object? candidateValue, + ValidationAttribute[] attributes, + ValidationContext validationContext) + { - foreach (var va in attributes) - { - if (va.GetValidationResult(candidateValue, validationContext) is object vr) - { - if (vr is CodedValidationResult cvr) - errors = add(errors, new[] { cvr.ValidationException }); - else - throw new InvalidOperationException($"Validation attributes should return a {nameof(CodedValidationResult)}."); - } - } + // Avoid allocation of a list for every validation until we really have something to report. + IEnumerable? errors = null; - return errors?.ToArray(); + foreach (var va in attributes) + { + if (va.GetValidationResult(candidateValue, validationContext) is not object vr) continue; + if (vr is CodedValidationResult cvr) + errors = add(errors, [cvr.ValidationException]); + else + throw new InvalidOperationException($"Validation attributes should return a {nameof(CodedValidationResult)}."); } - } -} -#nullable restore \ No newline at end of file + return errors?.ToArray(); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs b/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs index 44f6f28816..1a49c95e2b 100644 --- a/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs +++ b/src/Hl7.Fhir.Base/Serialization/DeserializationContext.cs @@ -8,108 +8,105 @@ using Hl7.Fhir.Introspection; +using Hl7.Fhir.Model; #nullable enable -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + + +/// +/// Contains contextual information for the property that is currently being deserialized and is passed +/// to delegate methods implementing parts of user-definable deserialization and validation logic. +/// +public readonly struct PropertyDeserializationContext { - /// - /// Contains contextual information for the property that is currently being deserialized and is passed - /// to delegate methods implementing parts of user-definable deserialization and validation logic. - /// - public readonly struct PropertyDeserializationContext + internal PropertyDeserializationContext( + Base objectInstance, + PathStack path, + string propertyName, + long lineNumber, + long linePosition, + PropertyMapping propMapping) { - internal PropertyDeserializationContext( - PathStack path, - string propertyName, - long lineNumber, - long linePosition, - PropertyMapping propMapping) - { - _path = path; - PropertyName = propertyName; - LineNumber = lineNumber; - LinePosition = linePosition; - ElementMapping = propMapping; - } - - private readonly PathStack _path; - - internal PathStack PathStack { get { return _path; } } - - /// - /// The dotted path leading to this element from the root (has no indexers and includes the value virtual property on primitives) - /// - public string Path { get { return _path.GetPath(); } } - - /// - /// The dotted FhirPath path leading to this element from the root with indexers (and no value property on primitives). - /// - public string InstancePath { get { return _path.GetInstancePath(); } } - - /// - /// The property name for which an instance is currently being deserialized. - /// - public string PropertyName { get; } - - /// - /// The approximate line number in the source data that is being deserialized. - /// - public long LineNumber { get; } - - /// - /// The approximate line position in the source data that is being deserialized. - /// - public long LinePosition { get; } - - /// - /// The metadata for the element that is currently being deserialized. - /// - public PropertyMapping ElementMapping { get; } + PathStack = path; + ObjectInstance = objectInstance; + PropertyName = propertyName; + LineNumber = lineNumber; + LinePosition = linePosition; + ElementMapping = propMapping; } + internal PathStack PathStack { get; } + + /// + /// The dotted path leading to this element from the root (has no indexers and includes the value virtual property on primitives) + /// + public string Path => PathStack.GetPath(); + + /// + /// The POCO this property is an element of. + /// + public Base ObjectInstance { get; } + + /// + /// The property name for which an instance is currently being deserialized. + /// + public string PropertyName { get; } + + /// + /// The approximate line number in the source data that is being deserialized. + /// + public long LineNumber { get; } + + /// + /// The approximate line position in the source data that is being deserialized. + /// + public long LinePosition { get; } + /// - /// Contains contextual information for the instance that is currently being deserialized and is passed - /// to delegate methods implementing parts of user-definable deserialization and validation logic. + /// The metadata for the element that is currently being deserialized. /// - public readonly struct InstanceDeserializationContext + public PropertyMapping ElementMapping { get; } +} + +/// +/// Contains contextual information for the instance that is currently being deserialized and is passed +/// to delegate methods implementing parts of user-definable deserialization and validation logic. +/// +public readonly struct InstanceDeserializationContext +{ + internal InstanceDeserializationContext( + PathStack path, + long lineNumber, + long linePosition, + ClassMapping instanceMapping) { - internal InstanceDeserializationContext( - PathStack path, - long lineNumber, - long linePosition, - ClassMapping instanceMapping) - { - _path = path; - LineNumber = lineNumber; - LinePosition = linePosition; - InstanceMapping = instanceMapping; - } - - private readonly PathStack _path; - - internal PathStack PathStack { get { return _path; } } - - /// - /// The dotted FhirPath path leading to this element from the root. - /// - public string Path { get { return _path.GetInstancePath(); } } - - /// - /// The approximate line number in the source data that is being deserialized. - /// - public long LineNumber { get; } - - /// - /// The approximate line position in the source data that is being deserialized. - /// - public long LinePosition { get; } - - /// - /// The metadata for the type of which the current property is part of. - /// - public ClassMapping InstanceMapping { get; } + PathStack = path; + LineNumber = lineNumber; + LinePosition = linePosition; + InstanceMapping = instanceMapping; } -} -#nullable restore + internal PathStack PathStack { get; } + + /// + /// The dotted FhirPath path leading to this element from the root. + /// + public string Path => PathStack.GetPath(); + + /// + /// The approximate line number in the source data that is being deserialized. + /// + public long LineNumber { get; } + + /// + /// The approximate line position in the source data that is being deserialized. + /// + public long LinePosition { get; } + + /// + /// The metadata for the type of which the current property is part of. + /// + public ClassMapping InstanceMapping { get; } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/ExceptionAggregator.cs b/src/Hl7.Fhir.Base/Serialization/ExceptionAggregator.cs index c76ed65898..a97a6cb05b 100644 --- a/src/Hl7.Fhir.Base/Serialization/ExceptionAggregator.cs +++ b/src/Hl7.Fhir.Base/Serialization/ExceptionAggregator.cs @@ -21,7 +21,7 @@ namespace Hl7.Fhir.Serialization /// Is probably going to be used by the (future)XmlDynamicDeserializer too. internal class ExceptionAggregator : IEnumerable { - private List _aggregated = new(); + private readonly List _aggregated = []; public void Add(CodedException? e) { @@ -41,6 +41,13 @@ public void Add(IEnumerable? es) public IEnumerator GetEnumerator() => ((IEnumerable)_aggregated).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_aggregated).GetEnumerator(); + + public override string ToString() => _aggregated.Count switch + { + 0 => "No exceptions", + 1 => _aggregated[0].ToString(), + _ => $"Multiple exceptions ({_aggregated.Count})" + }; } diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs index 612fedfcd2..a05970679b 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverter.cs @@ -39,13 +39,13 @@ public FhirJsonConverterFactory( internal void SetEnforcedErrors(IEnumerable toEnforce) { Engine ??= createDefaultEngine(); - Engine.IgnoreFilter = Engine.IgnoreFilter.And(toEnforce.ToPredicate().Negate()); + Engine.IgnoreFilter = Engine.IgnoreFilter.And(toEnforce.IsInList().Negate()); } internal void SetIgnoredErrors(IEnumerable toIgnore) { Engine ??= createDefaultEngine(); - Engine.IgnoreFilter = Engine.IgnoreFilter.Or(toIgnore.ToPredicate()); + Engine.IgnoreFilter = Engine.IgnoreFilter.Or(toIgnore.IsInList()); } internal void SetMode(DeserializerModes mode) diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs index 7aa407a15c..2536d07928 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonConverterOptions.cs @@ -23,20 +23,7 @@ public record FhirJsonConverterOptions /// /// Specifies the filter to use for summary serialization. /// - public SerializationFilter? SummaryFilter { get; set; } = default; - - /// - /// If the caller will not access base64 data in the deserialized resources, base64 decoding - /// of values can be turned off to increase performance. - /// - /// The element's will - /// still contain the unparsed base64 data and will therefore be retained and round-tripped. - public bool DisableBase64Decoding { get; init; } = false; - - /// - /// If set, this delegate is called when the deserializer fails to parse a primitive json value. - /// - public PrimitiveParseHandler? OnPrimitiveParseFailed { get; init; } = null; + public SerializationFilter? SummaryFilter { get; set; } = null; /// /// If set, this validator is invoked before the value is set in the object under construction to validate diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs index 0db9cce645..04405e4347 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs @@ -17,196 +17,149 @@ #nullable enable -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// An error found during deserialization of Json data. These errors do not present issues +/// with the Json itself, but issues in the data with regards to the rules for FHIR Json format described +/// in http://hl7.org/fhir/json.html. +/// +public class FhirJsonException : ExtendedCodedException { + public const string EXPECTED_START_OF_OBJECT_CODE = "JSON101"; + public const string RESOURCETYPE_SHOULD_BE_STRING_CODE = "JSON102"; + public const string NO_RESOURCETYPE_PROPERTY_CODE = "JSON103"; + public const string EXPECTED_PRIMITIVE_NOT_OBJECT_CODE = "JSON104"; + public const string EXPECTED_PRIMITIVE_NOT_ARRAY_CODE = "JSON105"; + public const string EXPECTED_PRIMITIVE_NOT_NULL_CODE = "JSON109"; + public const string EXPECTED_START_OF_ARRAY_CODE = "JSON111"; + public const string USE_OF_UNDERSCORE_ILLEGAL_CODE = "JSON113"; + public const string CHOICE_ELEMENT_HAS_NO_TYPE_CODE = "JSON114"; + public const string CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE = "JSON115"; + public const string UNKNOWN_RESOURCE_TYPE_CODE = "JSON116"; + public const string RESOURCE_TYPE_NOT_A_RESOURCE_CODE = "JSON117"; + public const string UNKNOWN_PROPERTY_FOUND_CODE = "JSON118"; + public const string OBJECTS_CANNOT_BE_EMPTY_CODE = "JSON120"; + public const string ARRAYS_CANNOT_BE_EMPTY_CODE = "JSON121"; + public const string PRIMITIVE_ARRAYS_ONLY_NULL_CODE = "JSON125"; + public const string PROPERTY_MAY_NOT_BE_EMPTY_CODE = "JSON127"; + public const string DUPLICATE_ARRAY_CODE = "JSON128"; + public const string DUPLICATE_PROPERTY_CODE = "JSON129"; + + // ========================================== + // Unrecoverable Errors + // ========================================== + internal static FhirJsonException EXPECTED_START_OF_OBJECT(ref Utf8JsonReader reader, string instancePath, JsonTokenType value) => Initialize(ref reader, instancePath, EXPECTED_START_OF_OBJECT_CODE, $"Expected start of object, but found {value}.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException RESOURCETYPE_SHOULD_BE_STRING(ref Utf8JsonReader reader, string instancePath, JsonTokenType value) => Initialize(ref reader, instancePath, RESOURCETYPE_SHOULD_BE_STRING_CODE, $"Property 'resourceType' should be a string, but found {value}.", OO_Sev.Fatal, OO_Typ.Value); + internal static FhirJsonException NO_RESOURCETYPE_PROPERTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, NO_RESOURCETYPE_PROPERTY_CODE, "Resource has no 'resourceType' property.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException EXPECTED_PRIMITIVE_NOT_OBJECT(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_PRIMITIVE_NOT_OBJECT_CODE, "Expected a primitive value, not a json object.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException EXPECTED_PRIMITIVE_NOT_ARRAY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_PRIMITIVE_NOT_ARRAY_CODE, "Expected a primitive value, not the start of an array.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException CHOICE_ELEMENT_HAS_NO_TYPE(ref Utf8JsonReader reader, string instancePath, string propName) => Initialize(ref reader, instancePath, CHOICE_ELEMENT_HAS_NO_TYPE_CODE, $"Choice element '{propName}' is not suffixed with a type.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException CHOICE_ELEMENT_HAS_UNKOWN_TYPE(ref Utf8JsonReader reader, string instancePath, string value, string typeValue) => Initialize(ref reader, instancePath, CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE, $"Choice element '{value}' is suffixed with an unrecognized type '{typeValue}'.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException UNKNOWN_RESOURCE_TYPE(ref Utf8JsonReader reader, string instancePath, string resourceType) => Initialize(ref reader, instancePath, UNKNOWN_RESOURCE_TYPE_CODE, $"Unknown type '{resourceType}' found in 'resourceType' property.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException RESOURCE_TYPE_NOT_A_RESOURCE(ref Utf8JsonReader reader, string instancePath, string name) => Initialize(ref reader, instancePath, RESOURCE_TYPE_NOT_A_RESOURCE_CODE, $"Data type '{name}' in property 'resourceType' is not a type of resource.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirJsonException UNKNOWN_PROPERTY_FOUND(ref Utf8JsonReader reader, string instancePath, string propName) => Initialize(ref reader, instancePath, UNKNOWN_PROPERTY_FOUND_CODE, $"Encountered unrecognized element '{propName}'.", OO_Sev.Error, OO_Typ.Structure); // this could be ignored, so isn't fatal? + + internal static FhirJsonException DUPLICATE_PROPERTY(ref Utf8JsonReader reader, string instancePath, string propName) => Initialize(ref reader, instancePath, DUPLICATE_PROPERTY_CODE, $"Encountered duplicate property '{propName}'.", OO_Sev.Fatal, OO_Typ.Structure); + + // ========================================== + // Recoverable Errors + // ========================================== + + // The serialization contained a json null where it is not allowed, but a null does not contain data anyway. + internal static FhirJsonException EXPECTED_PRIMITIVE_NOT_NULL(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_PRIMITIVE_NOT_NULL_CODE, "Expected a primitive value, not a json null.", OO_Sev.Warning, OO_Typ.Value); + + // Encountered an empty property, which is illegal, but the empty text is maintained. + internal static FhirJsonException PROPERTY_MAY_NOT_BE_EMPTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, PROPERTY_MAY_NOT_BE_EMPTY_CODE, "Properties cannot be empty strings. Either they are absent, or they are present with at least one character of non-whitespace content.", OO_Sev.Warning, OO_Typ.Value); + + internal static FhirJsonException DUPLICATE_ARRAY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, DUPLICATE_ARRAY_CODE, $"Duplicate array detected.", OO_Sev.Warning, OO_Typ.Value); + + // The parser will turn a non-array value into an array with a single element, so no data is lost. + internal static FhirJsonException EXPECTED_START_OF_ARRAY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_START_OF_ARRAY_CODE, "Expected start of array.", OO_Sev.Error, OO_Typ.Structure); + + // We will just ignore the underscore and keep on parsing + internal static FhirJsonException USE_OF_UNDERSCORE_ILLEGAL(ref Utf8JsonReader reader, string instancePath, string propertyNameMapped, string propertyName) => Initialize(ref reader, instancePath, USE_OF_UNDERSCORE_ILLEGAL_CODE, $"Element '{propertyNameMapped}' is not a FHIR primitive, so it should not use an underscore in the '{propertyName}' property.", OO_Sev.Warning, OO_Typ.Structure); + + // Empty objects and arrays can be ignored without discarding data + internal static FhirJsonException OBJECTS_CANNOT_BE_EMPTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, OBJECTS_CANNOT_BE_EMPTY_CODE, "An object needs to have at least one property.", OO_Sev.Warning, OO_Typ.Structure); + internal static FhirJsonException ARRAYS_CANNOT_BE_EMPTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, ARRAYS_CANNOT_BE_EMPTY_CODE, "An array needs to have at least one element.", OO_Sev.Warning, OO_Typ.Structure); + + // This leaves the incorrect nulls in place, no change in data. + internal static FhirJsonException PRIMITIVE_ARRAYS_ONLY_NULL(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, PRIMITIVE_ARRAYS_ONLY_NULL_CODE, "Arrays need to have at least one non-null element.", OO_Sev.Warning, OO_Typ.Structure); + /// - /// An error found during deserialization of Json data. These errors do not present issues - /// with the Json itself, but issues in the data with regards to the rules for FHIR Json format described - /// in http://hl7.org/fhir/json.html. + /// 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. /// - public class FhirJsonException : ExtendedCodedException - { - // TODO: Document each of these errors, based on the text for the error. - public const string EXPECTED_START_OF_OBJECT_CODE = "JSON101"; - public const string RESOURCETYPE_SHOULD_BE_STRING_CODE = "JSON102"; - public const string NO_RESOURCETYPE_PROPERTY_CODE = "JSON103"; - public const string EXPECTED_PRIMITIVE_NOT_OBJECT_CODE = "JSON104"; - public const string EXPECTED_PRIMITIVE_NOT_ARRAY_CODE = "JSON105"; - public const string INCORRECT_BASE64_DATA_CODE = "JSON106"; - public const string STRING_ISNOTAN_INSTANT_CODE = "JSON107"; - public const string NUMBER_CANNOT_BE_PARSED_CODE = "JSON108"; - public const string EXPECTED_PRIMITIVE_NOT_NULL_CODE = "JSON109"; - public const string UNEXPECTED_JSON_TOKEN_CODE = "JSON110"; - public const string EXPECTED_START_OF_ARRAY_CODE = "JSON111"; - public const string USE_OF_UNDERSCORE_ILLEGAL_CODE = "JSON113"; - public const string CHOICE_ELEMENT_HAS_NO_TYPE_CODE = "JSON114"; - public const string CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE = "JSON115"; - public const string UNKNOWN_RESOURCE_TYPE_CODE = "JSON116"; - public const string RESOURCE_TYPE_NOT_A_RESOURCE_CODE = "JSON117"; - public const string UNKNOWN_PROPERTY_FOUND_CODE = "JSON118"; - - //[Obsolete("This issue is no longer raised as it is now allowed to use `resourceType` as the name of an element.")] - //public const string RESOURCETYPE_UNEXPECTED_CODE = "JSON119"; - public const string OBJECTS_CANNOT_BE_EMPTY_CODE = "JSON120"; - public const string ARRAYS_CANNOT_BE_EMPTY_CODE = "JSON121"; - public const string LONG_CANNOT_BE_PARSED_CODE = "JSON122"; - public const string LONG_INCORRECT_FORMAT_CODE = "JSON123"; - - //[Obsolete("According to the latest updates of the Json format, primitive arrays of different sizes are no longer considered an error.")] - //public const string PRIMITIVE_ARRAYS_INCOMPAT_SIZE_CODE = "JSON122"; - - public const string PRIMITIVE_ARRAYS_ONLY_NULL_CODE = "JSON125"; - - // Cannot happen anymore since 6.0 since the simple values Element.id and Extension.url are gone. - //public const string INCOMPATIBLE_SIMPLE_VALUE_CODE = "JSON126"; - public const string PROPERTY_MAY_NOT_BE_EMPTY_CODE = "JSON127"; - - public const string DUPLICATE_ARRAY_CODE = "JSON128"; - public const string DUPLICATE_PROPERTY_CODE = "JSON129"; - - // ========================================== - // Unrecoverable Errors - // ========================================== - internal static FhirJsonException EXPECTED_START_OF_OBJECT(ref Utf8JsonReader reader, string instancePath, JsonTokenType value) => Initialize(ref reader, instancePath, EXPECTED_START_OF_OBJECT_CODE, $"Expected start of object, but found {value}.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException RESOURCETYPE_SHOULD_BE_STRING(ref Utf8JsonReader reader, string instancePath, JsonTokenType value) => Initialize(ref reader, instancePath, RESOURCETYPE_SHOULD_BE_STRING_CODE, $"Property 'resourceType' should be a string, but found {value}.", OO_Sev.Fatal, OO_Typ.Value); - internal static FhirJsonException NO_RESOURCETYPE_PROPERTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, NO_RESOURCETYPE_PROPERTY_CODE, "Resource has no 'resourceType' property.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException EXPECTED_PRIMITIVE_NOT_OBJECT(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_PRIMITIVE_NOT_OBJECT_CODE, "Expected a primitive value, not a json object.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException EXPECTED_PRIMITIVE_NOT_ARRAY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_PRIMITIVE_NOT_ARRAY_CODE, "Expected a primitive value, not the start of an array.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException CHOICE_ELEMENT_HAS_NO_TYPE(ref Utf8JsonReader reader, string instancePath, string propName) => Initialize(ref reader, instancePath, CHOICE_ELEMENT_HAS_NO_TYPE_CODE, $"Choice element '{propName}' is not suffixed with a type.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException CHOICE_ELEMENT_HAS_UNKOWN_TYPE(ref Utf8JsonReader reader, string instancePath, string value, string typeValue) => Initialize(ref reader, instancePath, CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE, $"Choice element '{value}' is suffixed with an unrecognized type '{typeValue}'.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException UNKNOWN_RESOURCE_TYPE(ref Utf8JsonReader reader, string instancePath, string resourceType) => Initialize(ref reader, instancePath, UNKNOWN_RESOURCE_TYPE_CODE, $"Unknown type '{resourceType}' found in 'resourceType' property.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException RESOURCE_TYPE_NOT_A_RESOURCE(ref Utf8JsonReader reader, string instancePath, string name) => Initialize(ref reader, instancePath, RESOURCE_TYPE_NOT_A_RESOURCE_CODE, $"Data type '{name}' in property 'resourceType' is not a type of resource.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirJsonException UNKNOWN_PROPERTY_FOUND(ref Utf8JsonReader reader, string instancePath, string propName) => Initialize(ref reader, instancePath, UNKNOWN_PROPERTY_FOUND_CODE, $"Encountered unrecognized element '{propName}'.", OO_Sev.Error, OO_Typ.Structure); // this could be ignored, so isn't fatal? - //internal static FhirJsonException INCOMPATIBLE_SIMPLE_VALUE(ref Utf8JsonReader reader, string instancePath, string value, FhirJsonException? err) => Initialize(ref reader, instancePath, INCOMPATIBLE_SIMPLE_VALUE_CODE, $"Json primitive value does not match the expected type of the primitive property. Details: ({value})", OO_Sev.Fatal, OO_Typ.Value, err); - internal static FhirJsonException DUPLICATE_PROPERTY(ref Utf8JsonReader reader, string instancePath, string propName) => Initialize(ref reader, instancePath, DUPLICATE_PROPERTY_CODE, $"Encountered duplicate property '{propName}'.", OO_Sev.Fatal, OO_Typ.Structure); - - // ========================================== - // Recoverable Errors - // ========================================== - - // The serialization contained a json null where it is not allowed, but a null does not contain data anyway. - internal static FhirJsonException EXPECTED_PRIMITIVE_NOT_NULL(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_PRIMITIVE_NOT_NULL_CODE, "Expected a primitive value, not a json null.", OO_Sev.Error, OO_Typ.Value); - - // Encountered an empty property, which is illegal, but the empty text is maintained. - internal static FhirJsonException PROPERTY_MAY_NOT_BE_EMPTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, PROPERTY_MAY_NOT_BE_EMPTY_CODE, "Properties cannot be empty strings. Either they are absent, or they are present with at least one character of non-whitespace content.", OO_Sev.Error, OO_Typ.Value); - - // These errors signal parsing errors, but the original raw data is retained in the POCO so no data is lost. - internal static FhirJsonException INCORRECT_BASE64_DATA(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, INCORRECT_BASE64_DATA_CODE, "Encountered incorrectly encoded base64 data.", OO_Sev.Error, OO_Typ.Value); - internal static FhirJsonException STRING_ISNOTAN_INSTANT(ref Utf8JsonReader reader, string instancePath, string value) => Initialize(ref reader, instancePath, STRING_ISNOTAN_INSTANT_CODE, $"Literal string '{value}' cannot be parsed as an instant.", OO_Sev.Error, OO_Typ.Value); - internal static FhirJsonException NUMBER_CANNOT_BE_PARSED(ref Utf8JsonReader reader, string instancePath, string? value, string typeName) => Initialize(ref reader, instancePath, NUMBER_CANNOT_BE_PARSED_CODE, $"Json number '{value}' cannot be parsed as a {typeName}.", OO_Sev.Error, OO_Typ.Value); - internal static FhirJsonException UNEXPECTED_JSON_TOKEN(ref Utf8JsonReader reader, string instancePath, string expected, string actual, string? value) => Initialize(ref reader, instancePath, UNEXPECTED_JSON_TOKEN_CODE, $"Expecting a {expected}, but found a json {actual} with value '{value}'.", OO_Sev.Warning, OO_Typ.Value); - internal static FhirJsonException DUPLICATE_ARRAY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, DUPLICATE_ARRAY_CODE, $"Duplicate array detected.", OO_Sev.Warning, OO_Typ.Value); - - // In R5 Integer64 (long) are serialized as string. So we would expect a string during parsing. - internal static FhirJsonException LONG_CANNOT_BE_PARSED(ref Utf8JsonReader reader, string instancePath, string? s0, string typeName) => Initialize(ref reader, instancePath, LONG_CANNOT_BE_PARSED_CODE, $"Json string '{s0}' cannot be parsed as a {typeName}.", OO_Sev.Warning, OO_Typ.Value); - internal static FhirJsonException LONG_INCORRECT_FORMAT(ref Utf8JsonReader reader, string instancePath, string s0, string s1, string s2, string expectedType) => Initialize(ref reader, instancePath, LONG_INCORRECT_FORMAT_CODE, $"{s0} '{s1}' cannot be parsed as a {s2}, because it should be a {expectedType}.", OO_Sev.Warning, OO_Typ.Value); - - // The parser will turn a non-array value into an array with a single element, so no data is lost. - internal static FhirJsonException EXPECTED_START_OF_ARRAY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, EXPECTED_START_OF_ARRAY_CODE, "Expected start of array.", OO_Sev.Warning, OO_Typ.Structure); - - // We will just ignore the underscore and keep on parsing - internal static FhirJsonException USE_OF_UNDERSCORE_ILLEGAL(ref Utf8JsonReader reader, string instancePath, string propertyNameMapped, string propertyName) => Initialize(ref reader, instancePath, USE_OF_UNDERSCORE_ILLEGAL_CODE, $"Element '{propertyNameMapped}' is not a FHIR primitive, so it should not use an underscore in the '{propertyName}' property.", OO_Sev.Warning, OO_Typ.Structure); - - // The serialization contained a superfluous 'resourceType' property, but we have read all data anyway. - // Note, this is no longer considered an error, since there are Resources using an element named "resourceType" (Subscription.filterBy for example). - //[Obsolete("This issue is no longer raised as it is now allowed to use `resourceType` as the name of an element.")] - //internal static FhirJsonException RESOURCETYPE_UNEXPECTED(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, RESOURCETYPE_UNEXPECTED_CODE, "The 'resourceType' property should only be used in resources.", OO_Sev.Warning, OO_Typ.Structure); - - // Empty objects and arrays can be ignored without discarding data - internal static FhirJsonException OBJECTS_CANNOT_BE_EMPTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, OBJECTS_CANNOT_BE_EMPTY_CODE, "An object needs to have at least one property.", OO_Sev.Warning, OO_Typ.Structure); - internal static FhirJsonException ARRAYS_CANNOT_BE_EMPTY(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, ARRAYS_CANNOT_BE_EMPTY_CODE, "An array needs to have at least one element.", OO_Sev.Warning, OO_Typ.Structure); - - // Shortest array will be filled out with nulls - // [EK 20221027] The new R5 spec clarifies that this is actually correct behaviour, so this error is not used anymore. - //internal static FhirJsonException PRIMITIVE_ARRAYS_INCOMPAT_SIZE => Initialize(PRIMITIVE_ARRAYS_INCOMPAT_SIZE_CODE, "Primitive arrays split in two properties should have the same size."); - - // This leaves the incorrect nulls in place, no change in data. - internal static FhirJsonException PRIMITIVE_ARRAYS_ONLY_NULL(ref Utf8JsonReader reader, string instancePath) => Initialize(ref reader, instancePath, PRIMITIVE_ARRAYS_ONLY_NULL_CODE, "Arrays need to have at least one non-null element.", OO_Sev.Warning, OO_Typ.Structure); - - /// - /// 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. - /// #pragma warning disable CS0618 // Type or member is obsolete - internal static string[] RecoverableIssues = - [ - EXPECTED_PRIMITIVE_NOT_NULL_CODE, - INCORRECT_BASE64_DATA_CODE, - STRING_ISNOTAN_INSTANT_CODE, - NUMBER_CANNOT_BE_PARSED_CODE, - UNEXPECTED_JSON_TOKEN_CODE, - LONG_CANNOT_BE_PARSED_CODE, - LONG_INCORRECT_FORMAT_CODE, - EXPECTED_START_OF_ARRAY_CODE, - USE_OF_UNDERSCORE_ILLEGAL_CODE, - // RESOURCETYPE_UNEXPECTED_CODE, - OBJECTS_CANNOT_BE_EMPTY_CODE, - ARRAYS_CANNOT_BE_EMPTY_CODE, - // PRIMITIVE_ARRAYS_INCOMPAT_SIZE_CODE, - PRIMITIVE_ARRAYS_ONLY_NULL_CODE, - PROPERTY_MAY_NOT_BE_EMPTY_CODE, - DUPLICATE_ARRAY_CODE - ]; + internal static string[] RecoverableIssues = + [ + EXPECTED_PRIMITIVE_NOT_NULL_CODE, + EXPECTED_START_OF_ARRAY_CODE, + USE_OF_UNDERSCORE_ILLEGAL_CODE, + OBJECTS_CANNOT_BE_EMPTY_CODE, + ARRAYS_CANNOT_BE_EMPTY_CODE, + PRIMITIVE_ARRAYS_ONLY_NULL_CODE, + PROPERTY_MAY_NOT_BE_EMPTY_CODE, + DUPLICATE_ARRAY_CODE, + ]; #pragma warning restore CS0618 // Type or member is obsolete - /// - /// 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, 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 = - [ - 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) - { - // Nothing - } - - public FhirJsonException(string code, string message, Exception? innerException) - : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown, innerException) - { - // Nothing - } - - public FhirJsonException( - string errorCode, - string baseMessage, - string? instancePath, - long? lineNumber, - long? position, - OperationOutcome.IssueSeverity issueSeverity, - OperationOutcome.IssueType issueType, - Exception? innerException = null) : - base(errorCode, baseMessage, instancePath, lineNumber, position, issueSeverity, issueType, innerException) - { - // Nothing - } - - - internal static FhirJsonException Initialize(ref Utf8JsonReader reader, string instancePath, string code, string message, OperationOutcome.IssueSeverity issueSeverity, OperationOutcome.IssueType issueType, Exception? innerException = null) - { - var (lineNumber, position) = reader.GetLocation(); - - return new FhirJsonException( - code, - message, - instancePath, - lineNumber, - position, - issueSeverity, - issueType, - innerException); - } - - public FhirJsonException? CloneWith(string baseMessage, OO_Sev issueSeverity, OO_Typ issueType) => - new(ErrorCode, baseMessage, InstancePath, LineNumber, Position, - issueSeverity, issueType); + /// + /// 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, 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 = + [ + 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) + { + // Nothing + } + + public FhirJsonException(string code, string message, Exception? innerException) + : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown, innerException) + { + // Nothing + } + + public FhirJsonException( + string errorCode, + string baseMessage, + string? instancePath, + long? lineNumber, + long? position, + OperationOutcome.IssueSeverity issueSeverity, + OperationOutcome.IssueType issueType, + Exception? innerException = null) : + base(errorCode, baseMessage, instancePath, lineNumber, position, issueSeverity, issueType, innerException) + { + // Nothing + } + + + internal static FhirJsonException Initialize(ref Utf8JsonReader reader, string instancePath, string code, string message, OperationOutcome.IssueSeverity issueSeverity, OperationOutcome.IssueType issueType, Exception? innerException = null) + { + var (lineNumber, position) = reader.GetLocation(); + + return new FhirJsonException( + code, + message, + instancePath, + lineNumber, + position, + issueSeverity, + issueType, + innerException); } -} -#nullable restore \ No newline at end of file + public FhirJsonException? CloneWith(string baseMessage, OO_Sev issueSeverity, OO_Typ issueType) => + new(ErrorCode, baseMessage, InstancePath, LineNumber, Position, + issueSeverity, issueType); +} \ 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 3db148e673..2ee77bf15d 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirXmlException.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirXmlException.cs @@ -8,162 +8,152 @@ using OO_Sev = Hl7.Fhir.Model.OperationOutcome.IssueSeverity; using OO_Typ = Hl7.Fhir.Model.OperationOutcome.IssueType; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +public class FhirXmlException : ExtendedCodedException { - public class FhirXmlException : ExtendedCodedException + public const string EMPTY_ELEMENT_NAMESPACE_CODE = "XML101"; + public const string UNKNOWN_RESOURCE_TYPE_CODE = "XML102"; + public const string RESOURCE_TYPE_NOT_A_RESOURCE_CODE = "XML103"; + public const string UNKNOWN_ELEMENT_CODE = "XML104"; + public const string CHOICE_ELEMENT_HAS_NO_TYPE_CODE = "XML105"; + public const string CHOICE_ELEMENT_HAS_UNKNOWN_TYPE_CODE = "XML106"; + public const string INCORRECT_XHTML_NAMESPACE_CODE = "XML107"; + public const string UNKNOWN_ATTRIBUTE_CODE = "XML108"; + public const string ELEMENT_OUT_OF_ORDER_CODE = "XML109"; + public const string UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER_CODE = "XML110"; + public const string NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER_CODE = "XML111"; + public const string INCORRECT_ELEMENT_NAMESPACE_CODE = "XML112"; + public const string UNALLOWED_NODE_TYPE_CODE = "XML113"; + public const string INCORRECT_ATTRIBUTE_NAMESPACE_CODE = "XML114"; + public const string ATTRIBUTE_HAS_EMPTY_VALUE_CODE = "XML115"; + public const string ELEMENT_NOT_IN_SEQUENCE_CODE = "XML116"; + public const string SCHEMALOCATION_DISALLOWED_CODE = "XML117"; + public const string EXPECTED_OPENING_ELEMENT_CODE = "XML118"; + public const string ENCOUNTERED_DTD_REFERENCES_CODE = "XML119"; + public const string ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE = "XML120"; + public const string INVALID_DUPLICATE_PROPERTY_CODE = "XML121"; + + // ========================================== + // Unrecoverable Errors - when adding a new error, also add it to the appropriate error collections below. + // ========================================== + internal static FhirXmlException UNKNOWN_RESOURCE_TYPE(XmlReader reader, string instancePath, string typeName) => Initialize(reader, instancePath, UNKNOWN_RESOURCE_TYPE_CODE, $"Unknown type '{typeName}' found in root property.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException RESOURCE_TYPE_NOT_A_RESOURCE(XmlReader reader, string instancePath, string resourceType) => Initialize(reader, instancePath, RESOURCE_TYPE_NOT_A_RESOURCE_CODE, $"Data type '{resourceType}' in property 'resourceType' is not a type of resource.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException UNKNOWN_ELEMENT(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, UNKNOWN_ELEMENT_CODE, $"Encountered unrecognized element '{elementName}'.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException CHOICE_ELEMENT_HAS_NO_TYPE(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, CHOICE_ELEMENT_HAS_NO_TYPE_CODE, $"Choice element '{elementName}' is not suffixed with a type.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException CHOICE_ELEMENT_HAS_UNKOWN_TYPE(XmlReader reader, string instancePath, string elementName, string typeSuffix) => Initialize(reader, instancePath, CHOICE_ELEMENT_HAS_UNKNOWN_TYPE_CODE, $"Choice element '{elementName}' is suffixed with an unrecognized type '{typeSuffix}'.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException UNKNOWN_ATTRIBUTE(XmlReader reader, string instancePath, string s0) => Initialize(reader, instancePath, UNKNOWN_ATTRIBUTE_CODE, $"Encountered unrecognized attribute '{s0}'.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER(XmlReader reader, string instancePath, string s0) => Initialize(reader, instancePath, UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER_CODE, $"Encountered unallowed content '{s0}' in the resource container. Only a single resource is allowed.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER_CODE, $"Element '{elementName}' has a contained resource and therefore should not have attributes.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException UNALLOWED_NODE_TYPE(XmlReader reader, string instancePath, string s0) => Initialize(reader, instancePath, UNALLOWED_NODE_TYPE_CODE, $"Xml node of type '{s0}' is unexpected at this point", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException EXPECTED_OPENING_ELEMENT(XmlReader reader, string instancePath, string openElementName) => Initialize(reader, instancePath, EXPECTED_OPENING_ELEMENT_CODE, $"Expected opening element, but found {openElementName}.", OO_Sev.Fatal, OO_Typ.Structure); + internal static FhirXmlException INVALID_DUPLICATE_PROPERTY(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, INVALID_DUPLICATE_PROPERTY_CODE, $"Element '{elementName}' is not permitted to repeat", OO_Sev.Error, OO_Typ.Structure); + + // ========================================== + // Recoverable Errors - when adding a new error, also add it to the appropriate error collections below. + // ========================================== + // Although the namespace is not correct, we continue as if it was. + internal static FhirXmlException EMPTY_ELEMENT_NAMESPACE(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, EMPTY_ELEMENT_NAMESPACE_CODE, $"Element '{elementName}' has no namespace, expected the HL7 FHIR namespace ({XmlNs.FHIR})", OO_Sev.Error, OO_Typ.Structure); + internal static FhirXmlException INCORRECT_ELEMENT_NAMESPACE(XmlReader reader, string instancePath, string elementName, string s1) => Initialize(reader, instancePath, INCORRECT_ELEMENT_NAMESPACE_CODE, $"Element '{elementName}' uses the namespace '{s1}', which is not allowed.", OO_Sev.Error, OO_Typ.Structure); + internal static FhirXmlException INCORRECT_XHTML_NAMESPACE(XmlReader reader, string instancePath) => Initialize(reader, instancePath, INCORRECT_XHTML_NAMESPACE_CODE, $"Narrative has incorrect namespace. Namespace should be {XmlNs.XHTML}", OO_Sev.Error, OO_Typ.Structure); + internal static FhirXmlException INCORRECT_ATTRIBUTE_NAMESPACE(XmlReader reader, string instancePath, string localName, string elementName, string namespaceURI) => Initialize(reader, instancePath, INCORRECT_ATTRIBUTE_NAMESPACE_CODE, $"The attribute '{localName}' in element '{elementName}' uses the namespace '{namespaceURI}', which is not allowed.", OO_Sev.Error, OO_Typ.Structure); + + // These errors signal parsing errors, but the original raw data is retained in the POCO so no data is lost. + //internal static FhirXmlException VALUE_IS_NOT_OF_EXPECTED_TYPE(XmlReader reader, string instancePath, string trimmedValue, string typeName) => Initialize(reader, instancePath, VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, $"Literal string '{trimmedValue}' cannot be parsed as a '{typeName}'.", OO_Sev.Error, OO_Typ.Value); + + // An incorrect order does not mean we cannot parse the data safely + internal static FhirXmlException ELEMENT_OUT_OF_ORDER(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, ELEMENT_OUT_OF_ORDER_CODE, $"Element '{elementName}' is not in the correct order ", OO_Sev.Error, OO_Typ.Structure); + internal static FhirXmlException ELEMENT_NOT_IN_SEQUENCE(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, ELEMENT_NOT_IN_SEQUENCE_CODE, $"Element '{elementName}' was found multiple times, but not in sequence.", OO_Sev.Error, OO_Typ.Structure); + + // Empty values will result in nulls, but no data is lost. + internal static FhirXmlException ATTRIBUTE_HAS_EMPTY_VALUE(XmlReader reader, string instancePath) => Initialize(reader, instancePath, ATTRIBUTE_HAS_EMPTY_VALUE_CODE, "Attributes cannot be empty. Either they are absent, or they are present with at least one character of non - whitespace content", OO_Sev.Error, OO_Typ.Value); + internal static FhirXmlException ELEMENT_HAS_NO_VALUE_OR_CHILDREN(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE, $"Element '{elementName}' must have child elements and / or a value attribute", OO_Sev.Error, OO_Typ.Structure); + internal static FhirXmlException ELEMENT_HAS_NO_VALUE_OR_CHILDREN(string instancePath, int lineNumber, int position, string? locationMessage, string? localName) { - public const string EMPTY_ELEMENT_NAMESPACE_CODE = "XML101"; - public const string UNKNOWN_RESOURCE_TYPE_CODE = "XML102"; - public const string RESOURCE_TYPE_NOT_A_RESOURCE_CODE = "XML103"; - public const string UNKNOWN_ELEMENT_CODE = "XML104"; - public const string CHOICE_ELEMENT_HAS_NO_TYPE_CODE = "XML105"; - public const string CHOICE_ELEMENT_HAS_UNKNOWN_TYPE_CODE = "XML106"; - public const string INCORRECT_XHTML_NAMESPACE_CODE = "XML107"; - public const string UNKNOWN_ATTRIBUTE_CODE = "XML108"; - public const string ELEMENT_OUT_OF_ORDER_CODE = "XML109"; - public const string UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER_CODE = "XML110"; - public const string NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER_CODE = "XML111"; - public const string INCORRECT_ELEMENT_NAMESPACE_CODE = "XML112"; - public const string UNALLOWED_NODE_TYPE_CODE = "XML113"; - public const string INCORRECT_ATTRIBUTE_NAMESPACE_CODE = "XML114"; - public const string ATTRIBUTE_HAS_EMPTY_VALUE_CODE = "XML115"; - public const string ELEMENT_NOT_IN_SEQUENCE_CODE = "XML116"; - public const string SCHEMALOCATION_DISALLOWED_CODE = "XML117"; - public const string EXPECTED_OPENING_ELEMENT_CODE = "XML118"; - public const string ENCOUNTERED_DTD_REFERENCES_CODE = "XML119"; - public const string ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE = "XML120"; - public const string INVALID_DUPLICATE_PROPERTY_CODE = "XML121"; - - public const string INCORRECT_BASE64_DATA_CODE = "XML202"; - public const string VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE = "XML203"; - - // ========================================== - // Unrecoverable Errors - when adding a new error, also add it to the appropriate error collections below. - // ========================================== - internal static FhirXmlException UNKNOWN_RESOURCE_TYPE(XmlReader reader, string instancePath, string typeName) => Initialize(reader, instancePath, UNKNOWN_RESOURCE_TYPE_CODE, $"Unknown type '{typeName}' found in root property.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException RESOURCE_TYPE_NOT_A_RESOURCE(XmlReader reader, string instancePath, string resourceType) => Initialize(reader, instancePath, RESOURCE_TYPE_NOT_A_RESOURCE_CODE, $"Data type '{resourceType}' in property 'resourceType' is not a type of resource.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException UNKNOWN_ELEMENT(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, UNKNOWN_ELEMENT_CODE, $"Encountered unrecognized element '{elementName}'.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException CHOICE_ELEMENT_HAS_NO_TYPE(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, CHOICE_ELEMENT_HAS_NO_TYPE_CODE, $"Choice element '{elementName}' is not suffixed with a type.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException CHOICE_ELEMENT_HAS_UNKOWN_TYPE(XmlReader reader, string instancePath, string elementName, string typeSuffix) => Initialize(reader, instancePath, CHOICE_ELEMENT_HAS_UNKNOWN_TYPE_CODE, $"Choice element '{elementName}' is suffixed with an unrecognized type '{typeSuffix}'.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException UNKNOWN_ATTRIBUTE(XmlReader reader, string instancePath, string s0) => Initialize(reader, instancePath, UNKNOWN_ATTRIBUTE_CODE, $"Encountered unrecognized attribute '{s0}'.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER(XmlReader reader, string instancePath, string s0) => Initialize(reader, instancePath, UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER_CODE, $"Encountered unallowed content '{s0}' in the resource container. Only a single resource is allowed.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER_CODE, $"Element '{elementName}' has a contained resource and therefore should not have attributes.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException UNALLOWED_NODE_TYPE(XmlReader reader, string instancePath, string s0) => Initialize(reader, instancePath, UNALLOWED_NODE_TYPE_CODE, $"Xml node of type '{s0}' is unexpected at this point", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException EXPECTED_OPENING_ELEMENT(XmlReader reader, string instancePath, string openElementName) => Initialize(reader, instancePath, EXPECTED_OPENING_ELEMENT_CODE, $"Expected opening element, but found {openElementName}.", OO_Sev.Fatal, OO_Typ.Structure); - internal static FhirXmlException INVALID_DUPLICATE_PROPERTY(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, INVALID_DUPLICATE_PROPERTY_CODE, $"Element '{elementName}' is not permitted to repeat", OO_Sev.Error, OO_Typ.Structure); - - // ========================================== - // Recoverable Errors - when adding a new error, also add it to the appropriate error collections below. - // ========================================== - // Although the namespace is not correct, we continue as if it was. - internal static FhirXmlException EMPTY_ELEMENT_NAMESPACE(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, EMPTY_ELEMENT_NAMESPACE_CODE, $"Element '{elementName}' has no namespace, expected the HL7 FHIR namespace ({XmlNs.FHIR})", OO_Sev.Error, OO_Typ.Structure); - internal static FhirXmlException INCORRECT_ELEMENT_NAMESPACE(XmlReader reader, string instancePath, string elementName, string s1) => Initialize(reader, instancePath, INCORRECT_ELEMENT_NAMESPACE_CODE, $"Element '{elementName}' uses the namespace '{s1}', which is not allowed.", OO_Sev.Error, OO_Typ.Structure); - internal static FhirXmlException INCORRECT_XHTML_NAMESPACE(XmlReader reader, string instancePath) => Initialize(reader, instancePath, INCORRECT_XHTML_NAMESPACE_CODE, $"Narrative has incorrect namespace. Namespace should be {XmlNs.XHTML}", OO_Sev.Error, OO_Typ.Structure); - internal static FhirXmlException INCORRECT_ATTRIBUTE_NAMESPACE(XmlReader reader, string instancePath, string localName, string elementName, string namespaceURI) => Initialize(reader, instancePath, INCORRECT_ATTRIBUTE_NAMESPACE_CODE, $"The attribute '{localName}' in element '{elementName}' uses the namespace '{namespaceURI}', which is not allowed.", OO_Sev.Error, OO_Typ.Structure); - - // These errors signal parsing errors, but the original raw data is retained in the POCO so no data is lost. - internal static FhirXmlException INCORRECT_BASE64_DATA(XmlReader reader, string instancePath) => Initialize(reader, instancePath, INCORRECT_BASE64_DATA_CODE, "Encountered incorrectly encoded base64 data.", OO_Sev.Error, OO_Typ.Value); - internal static FhirXmlException VALUE_IS_NOT_OF_EXPECTED_TYPE(XmlReader reader, string instancePath, string trimmedValue, string typeName) => Initialize(reader, instancePath, VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, $"Literal string '{trimmedValue}' cannot be parsed as a '{typeName}'.", OO_Sev.Error, OO_Typ.Structure); - - // An incorrect order does not mean we cannot parse the data safely - internal static FhirXmlException ELEMENT_OUT_OF_ORDER(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, ELEMENT_OUT_OF_ORDER_CODE, $"Element '{elementName}' is not in the correct order ", OO_Sev.Error, OO_Typ.Structure); - internal static FhirXmlException ELEMENT_NOT_IN_SEQUENCE(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, ELEMENT_NOT_IN_SEQUENCE_CODE, $"Element '{elementName}' was found multiple times, but not in sequence.", OO_Sev.Error, OO_Typ.Structure); - - // Empty values will result in nulls, but no data is lost. - internal static FhirXmlException ATTRIBUTE_HAS_EMPTY_VALUE(XmlReader reader, string instancePath) => Initialize(reader, instancePath, ATTRIBUTE_HAS_EMPTY_VALUE_CODE, "Attributes cannot be empty. Either they are absent, or they are present with at least one character of non - whitespace content", OO_Sev.Error, OO_Typ.Structure); - internal static FhirXmlException ELEMENT_HAS_NO_VALUE_OR_CHILDREN(XmlReader reader, string instancePath, string elementName) => Initialize(reader, instancePath, ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE, $"Element '{elementName}' must have child elements and / or a value attribute", OO_Sev.Error, OO_Typ.Structure); - internal static FhirXmlException ELEMENT_HAS_NO_VALUE_OR_CHILDREN(string instancePath, int lineNumber, int position, string? locationMessage, string? localName) - { - var message = $"Element '{localName}' must have child elements and / or a value attribute"; - - return new FhirXmlException( - ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE, - message, instancePath, lineNumber, position, OO_Sev.Error, OO_Typ.Structure); - } - - // Xml paraphernalia that do not contain data so they can be safely skipped. - internal static FhirXmlException SCHEMALOCATION_DISALLOWED(XmlReader reader, string instancePath) => Initialize(reader, instancePath, SCHEMALOCATION_DISALLOWED_CODE, "The 'schemaLocation' attribute is disallowed.", OO_Sev.Warning, OO_Typ.Structure); - internal static FhirXmlException ENCOUNTERED_DTD_REFERENCES(XmlReader reader, string instancePath) => Initialize(reader, instancePath, ENCOUNTERED_DTD_REFERENCES_CODE, "There SHALL be no DTD references in FHIR resources (because of the XXE security exploit)", OO_Sev.Warning, OO_Typ.Structure); - - /// - /// 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 = - [ - EMPTY_ELEMENT_NAMESPACE_CODE, - INCORRECT_ELEMENT_NAMESPACE_CODE, - INCORRECT_XHTML_NAMESPACE_CODE, - INCORRECT_ATTRIBUTE_NAMESPACE_CODE, - INCORRECT_BASE64_DATA_CODE, - VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, - ELEMENT_OUT_OF_ORDER_CODE, - ELEMENT_NOT_IN_SEQUENCE_CODE, - ATTRIBUTE_HAS_EMPTY_VALUE_CODE, + var message = $"Element '{localName}' must have child elements and / or a value attribute"; + + return new FhirXmlException( ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE, - SCHEMALOCATION_DISALLOWED_CODE, - ENCOUNTERED_DTD_REFERENCES_CODE - ]; - - - /// - /// 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. - /// - internal static string[] BackwardsCompatibilityAllowedIssues = - [ - CodedValidationException.INVALID_CODED_VALUE_CODE, - UNKNOWN_ELEMENT_CODE, - CHOICE_ELEMENT_HAS_UNKNOWN_TYPE_CODE, - UNKNOWN_ATTRIBUTE_CODE - ]; - - public FhirXmlException(string code, string message) + message, instancePath, lineNumber, position, OO_Sev.Error, OO_Typ.Structure); + } + + // Xml paraphernalia that do not contain data so they can be safely skipped. + internal static FhirXmlException SCHEMALOCATION_DISALLOWED(XmlReader reader, string instancePath) => Initialize(reader, instancePath, SCHEMALOCATION_DISALLOWED_CODE, "The 'schemaLocation' attribute is disallowed.", OO_Sev.Warning, OO_Typ.Structure); + internal static FhirXmlException ENCOUNTERED_DTD_REFERENCES(XmlReader reader, string instancePath) => Initialize(reader, instancePath, ENCOUNTERED_DTD_REFERENCES_CODE, "There SHALL be no DTD references in FHIR resources (because of the XXE security exploit)", OO_Sev.Warning, OO_Typ.Structure); + + /// + /// 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 = + [ + EMPTY_ELEMENT_NAMESPACE_CODE, + INCORRECT_ELEMENT_NAMESPACE_CODE, + INCORRECT_XHTML_NAMESPACE_CODE, + INCORRECT_ATTRIBUTE_NAMESPACE_CODE, + ELEMENT_OUT_OF_ORDER_CODE, + ELEMENT_NOT_IN_SEQUENCE_CODE, + ATTRIBUTE_HAS_EMPTY_VALUE_CODE, + ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE, + SCHEMALOCATION_DISALLOWED_CODE, + ENCOUNTERED_DTD_REFERENCES_CODE, + ]; + + /// + /// 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. + /// + internal static string[] BackwardsCompatibilityAllowedIssues = + [ + CodedValidationException.INVALID_CODED_VALUE_CODE, + UNKNOWN_ELEMENT_CODE, + CHOICE_ELEMENT_HAS_UNKNOWN_TYPE_CODE, + UNKNOWN_ATTRIBUTE_CODE + ]; + + public FhirXmlException(string code, string message) : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown) - { - // Nothing - } - - public FhirXmlException(string code, string message, Exception? innerException) - : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown, innerException) - { - // Nothing - } - - public FhirXmlException( - string errorCode, - string baseMessage, - string? instancePath, - long? lineNumber, - long? position, - OperationOutcome.IssueSeverity issueSeverity, - OperationOutcome.IssueType issueType, - Exception? innerException = null) : - base(errorCode, baseMessage, instancePath, lineNumber, position, issueSeverity, issueType, innerException) - { - // Nothing - } - - internal static FhirXmlException Initialize(XmlReader reader, string instancePath, string code, string message, OperationOutcome.IssueSeverity issueSeverity, OperationOutcome.IssueType issueType, FhirXmlException? innerException = null) - { - var (lineNumber, position) = reader.GenerateLineInfo(); - - return new FhirXmlException( - code, - message, - instancePath, - lineNumber, - position, - issueSeverity, - issueType, - innerException); - } - - public FhirXmlException? CloneWith(string baseMessage, OO_Sev issueSeverity, OO_Typ issueType) => - new FhirXmlException(ErrorCode, baseMessage, InstancePath, LineNumber, Position, - issueSeverity, issueType); + { + // Nothing + } + + public FhirXmlException(string code, string message, Exception? innerException) + : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown, innerException) + { + // Nothing + } + + public FhirXmlException( + string errorCode, + string baseMessage, + string? instancePath, + long? lineNumber, + long? position, + OperationOutcome.IssueSeverity issueSeverity, + OperationOutcome.IssueType issueType, + Exception? innerException = null) : + base(errorCode, baseMessage, instancePath, lineNumber, position, issueSeverity, issueType, innerException) + { + // Nothing + } + + internal static FhirXmlException Initialize(XmlReader reader, string instancePath, string code, string message, OperationOutcome.IssueSeverity issueSeverity, OperationOutcome.IssueType issueType, FhirXmlException? innerException = null) + { + var (lineNumber, position) = reader.GenerateLineInfo(); + + return new FhirXmlException( + code, + message, + instancePath, + lineNumber, + position, + issueSeverity, + issueType, + innerException); } -} -#nullable restore + public FhirXmlException? CloneWith(string baseMessage, OO_Sev issueSeverity, OO_Typ issueType) => + new(ErrorCode, baseMessage, InstancePath, LineNumber, Position, + issueSeverity, issueType); +} \ 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 b854965e61..f0c5555628 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirXmlPocoDeserializerSettings.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirXmlPocoDeserializerSettings.cs @@ -6,14 +6,6 @@ namespace Hl7.Fhir.Serialization { public class FhirXmlPocoDeserializerSettings { - /// - /// If the caller will not access base64 data in the deserialized resources, base64 decoding - /// of values can be turned off to increase performance. - /// - /// The element's will - /// still contain the unparsed base64 data and will therefore be retained and round-tripped. - public bool DisableBase64Decoding { get; init; } = false; - /// /// 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. @@ -42,4 +34,4 @@ public class FhirXmlPocoDeserializerSettings } } -#nullable restore +#nullable restore \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/FilterPredicateExtensions.cs b/src/Hl7.Fhir.Base/Serialization/FilterPredicateExtensions.cs index edc043c910..45c7b28b98 100644 --- a/src/Hl7.Fhir.Base/Serialization/FilterPredicateExtensions.cs +++ b/src/Hl7.Fhir.Base/Serialization/FilterPredicateExtensions.cs @@ -2,6 +2,7 @@ using Hl7.Fhir.Serialization; using Hl7.Fhir.Utility; +using Hl7.Fhir.Validation; using System; using System.Collections.Generic; using System.Linq; @@ -10,15 +11,20 @@ namespace Hl7.Fhir.Serialization; internal static class FilterPredicateExtensions { - internal static Predicate IsRecoverableIssue => - FhirXmlException.RecoverableIssues.Concat(FhirJsonException.RecoverableIssues).ToPredicate(); + // 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 IsBackwardsCompatibilityIssue => - FhirXmlException.BackwardsCompatibilityAllowedIssues.Concat(FhirJsonException.BackwardsCompatibilityAllowedIssues).ToPredicate(); - - internal static Predicate ToPredicate(this IEnumerable ignoreList) => + 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; @@ -27,7 +33,4 @@ internal static Predicate Or(this Predicate a, P internal static Predicate Negate(this Predicate a) => ce => !a(ce); - -} - -#nullable restore \ No newline at end of file +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/IDeserializationValidator.cs b/src/Hl7.Fhir.Base/Serialization/IDeserializationValidator.cs index 6304a3f2f8..7feb9d4c6f 100644 --- a/src/Hl7.Fhir.Base/Serialization/IDeserializationValidator.cs +++ b/src/Hl7.Fhir.Base/Serialization/IDeserializationValidator.cs @@ -9,38 +9,38 @@ #nullable enable +using Hl7.Fhir.Model; using Hl7.Fhir.Validation; -namespace Hl7.Fhir.Serialization +namespace Hl7.Fhir.Serialization; + +/// +/// A validator that will be run to validate values while deserializing a POCO. +/// +public interface IDeserializationValidator { /// - /// A validator that will be run to validate values while deserializing a POCO. + /// Implements validation logic to be run on a property value just before that value is used + /// to initialize the property. /// - public interface IDeserializationValidator - { - /// - /// Implements validation logic to be run on a property value just before that value is used - /// to initialize the property. - /// - /// The instance to be validated. - /// The current context of deserialization, like the path and the type under deserialization. - /// null, zero or more validation errors which will be aggregated in the final result of deserialization. - void ValidateProperty( - ref object? instance, - in PropertyDeserializationContext context, - out CodedValidationException[]? reportedErrors); - - /// - /// Implements validation logic to be run on a deserialized instance. - /// - /// The instance to be validated - /// The current context of deserialization, like the path and the type under deserialization. - /// null, zero or more validation errors which will be aggregated in the final result of deserialization. - void ValidateInstance( - object? instance, - in InstanceDeserializationContext context, - out CodedValidationException[]? reportedErrors); - } -} + /// The value for the property (can be Base or a List of Base). + /// The current context of deserialization, like the path and the type under deserialization. + /// null, zero or more validation errors which will be aggregated in the final result of deserialization. + /// This validation will not be called on Primitive's Value/ObjectValue properties, which is done during + /// instead. + void ValidateProperty( + object? propertyValue, + in PropertyDeserializationContext context, + out CodedValidationException[]? reportedErrors); -#nullable restore + /// + /// Implements validation logic to be run on a deserialized instance. + /// + /// The instance to be validated + /// The current context of deserialization, like the path and the type under deserialization. + /// null, zero or more validation errors which will be aggregated in the final result of deserialization. + void ValidateInstance( + Base instance, + in InstanceDeserializationContext context, + out CodedValidationException[]? reportedErrors); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/OperationOutcomeExtensions.cs b/src/Hl7.Fhir.Base/Serialization/OperationOutcomeExtensions.cs index 8d76bfee92..1046f37c28 100644 --- a/src/Hl7.Fhir.Base/Serialization/OperationOutcomeExtensions.cs +++ b/src/Hl7.Fhir.Base/Serialization/OperationOutcomeExtensions.cs @@ -56,84 +56,40 @@ public static Model.OperationOutcome.IssueComponent ToIssue(this ExtendedCodedEx case CodedValidationException.INCORRECT_CARDINALITY_MAX_CODE: shortDisplay = "Exceeded max values"; break; case CodedValidationException.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE: shortDisplay = "Cannot be null"; break; case CodedValidationException.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE: shortDisplay = "Mandatory field cannot be null"; break; - case CodedValidationException.CODE_LITERAL_INVALID_CODE: shortDisplay = "Invalid code"; break; - case CodedValidationException.DATE_LITERAL_INVALID_CODE: shortDisplay = "Invalid date"; break; - case CodedValidationException.DATETIME_LITERAL_INVALID_CODE: shortDisplay = "Invalid datetime"; break; - case CodedValidationException.ID_LITERAL_INVALID_CODE: shortDisplay = "Invalid id"; break; - case CodedValidationException.OID_LITERAL_INVALID_CODE: shortDisplay = "Invalid oid"; break; - case CodedValidationException.TIME_LITERAL_INVALID_CODE: shortDisplay = "Invalid time"; break; - case CodedValidationException.URI_LITERAL_INVALID_CODE: shortDisplay = "Invalid uri"; break; - case CodedValidationException.UUID_LITERAL_INVALID_CODE: shortDisplay = "Invalid uuid"; break; + case CodedValidationException.LITERAL_INVALID_CODE: shortDisplay = "Invalid literal"; break; case CodedValidationException.NARRATIVE_XML_IS_MALFORMED_CODE: shortDisplay = "Malformed narrative"; break; case CodedValidationException.NARRATIVE_XML_IS_INVALID_CODE: shortDisplay = "Invalid narrative"; break; case CodedValidationException.INVALID_CODED_VALUE_CODE: shortDisplay = "Invalid code"; break; case CodedValidationException.CONTAINED_RESOURCES_CANNOT_BE_NESTED_CODE: shortDisplay = "Contained resources cannot be nested"; break; + case CodedValidationException.INVALID_BASE64_VALUE_CODE: shortDisplay = "Invalid Base64 data"; break; case FhirJsonException.EXPECTED_START_OF_OBJECT_CODE: shortDisplay = "Expected Object"; break; - // case FhirJsonException.RESOURCETYPE_SHOULD_BE_STRING_CODE: shortDisplay = "JSON102"; break; case FhirJsonException.NO_RESOURCETYPE_PROPERTY_CODE: shortDisplay = "Missing ResourceType"; break; case FhirJsonException.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE: shortDisplay = "Expected Object"; break; case FhirJsonException.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE: shortDisplay = "Expected Primitive"; break; - case FhirJsonException.INCORRECT_BASE64_DATA_CODE: shortDisplay = "Invalid Base64 data"; break; - // case FhirJsonException.STRING_ISNOTAN_INSTANT_CODE: shortDisplay = "JSON107"; break; - case FhirJsonException.NUMBER_CANNOT_BE_PARSED_CODE: shortDisplay = "Invalid number"; break; - // case FhirJsonException.EXPECTED_PRIMITIVE_NOT_NULL_CODE: shortDisplay = "JSON109"; break; - case FhirJsonException.UNEXPECTED_JSON_TOKEN_CODE: shortDisplay = "Invalid datatype"; break; - // case FhirJsonException.EXPECTED_START_OF_ARRAY_CODE: shortDisplay = "JSON111"; break; - // case FhirJsonException.USE_OF_UNDERSCORE_ILLEGAL_CODE: shortDisplay = "JSON113"; break; - // case FhirJsonException.CHOICE_ELEMENT_HAS_NO_TYPE_CODE: shortDisplay = "JSON114"; break; - // case FhirJsonException.CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE: shortDisplay = "JSON115"; break; case FhirJsonException.UNKNOWN_RESOURCE_TYPE_CODE: shortDisplay = "Invalid ResourceType"; break; - // case FhirJsonException.RESOURCE_TYPE_NOT_A_RESOURCE_CODE: shortDisplay = "JSON117"; break; case FhirJsonException.UNKNOWN_PROPERTY_FOUND_CODE: shortDisplay = "Unknown element"; break; - // case FhirJsonException.RESOURCETYPE_UNEXPECTED_CODE: shortDisplay = "JSON119"; break; - // case FhirJsonException.OBJECTS_CANNOT_BE_EMPTY_CODE: shortDisplay = "JSON120"; break; - // case FhirJsonException.ARRAYS_CANNOT_BE_EMPTY_CODE: shortDisplay = "JSON121"; break; - // case FhirJsonException.LONG_CANNOT_BE_PARSED_CODE: shortDisplay = "JSON122"; break; - // case FhirJsonException.LONG_INCORRECT_FORMAT_CODE: shortDisplay = "JSON123"; break; - - // case FhirJsonException.PRIMITIVE_ARRAYS_ONLY_NULL_CODE: shortDisplay = "JSON125"; break; - // case FhirJsonException.INCOMPATIBLE_SIMPLE_VALUE_CODE: shortDisplay = "JSON126"; break; - - case FhirXmlException.EMPTY_ELEMENT_NAMESPACE_CODE: shortDisplay = "Empty Element Namespace"; break; case FhirXmlException.UNKNOWN_RESOURCE_TYPE_CODE: shortDisplay = "Invalid ResourceType"; break; - // case FhirXmlException.RESOURCE_TYPE_NOT_A_RESOURCE_CODE: shortDisplay = "XML103"; break; case FhirXmlException.UNKNOWN_ELEMENT_CODE: shortDisplay = "Unknown element"; break; - // case FhirXmlException.CHOICE_ELEMENT_HAS_NO_TYPE_CODE: shortDisplay = "XML105"; break; - // case FhirXmlException.CHOICE_ELEMENT_HAS_UNKNOWN_TYPE_CODE: shortDisplay = "XML106"; break; - // case FhirXmlException.INCORRECT_XHTML_NAMESPACE_CODE: shortDisplay = "XML107"; break; - // case FhirXmlException.UNKNOWN_ATTRIBUTE_CODE: shortDisplay = "XML108"; break; case FhirXmlException.ELEMENT_OUT_OF_ORDER_CODE: shortDisplay = "XML element out of order"; break; - // case FhirXmlException.UNALLOWED_ELEMENT_IN_RESOURCE_CONTAINER_CODE: shortDisplay = "XML110"; break; - // case FhirXmlException.NO_ATTRIBUTES_ALLOWED_ON_RESOURCE_CONTAINER_CODE: shortDisplay = "XML111"; break; case FhirXmlException.INCORRECT_ELEMENT_NAMESPACE_CODE: shortDisplay = "Invalid element namespace"; break; - // case FhirXmlException.UNALLOWED_NODE_TYPE_CODE: shortDisplay = "XML113"; break; case FhirXmlException.INCORRECT_ATTRIBUTE_NAMESPACE_CODE: shortDisplay = "Invalid attribute namespace"; break; - // case FhirXmlException.ATTRIBUTE_HAS_EMPTY_VALUE_CODE: shortDisplay = "XML115"; break; - // case FhirXmlException.ELEMENT_NOT_IN_SEQUENCE_CODE: shortDisplay = "XML116"; break; - // case FhirXmlException.SCHEMALOCATION_DISALLOWED_CODE: shortDisplay = "XML117"; break; - // case FhirXmlException.EXPECTED_OPENING_ELEMENT_CODE: shortDisplay = "XML118"; break; - // case FhirXmlException.ENCOUNTERED_DTP_REFERENCES_CODE: shortDisplay = "XML119"; break; - // case FhirXmlException.ELEMENT_HAS_NO_VALUE_OR_CHILDREN_CODE: shortDisplay = "XML120"; break; - - case FhirXmlException.INCORRECT_BASE64_DATA_CODE: shortDisplay = "Invalid Base64 data"; break; - case FhirXmlException.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE: shortDisplay = "Invalid datatype"; break; } - var result = new Model.OperationOutcome.IssueComponent() + var result = new OperationOutcome.IssueComponent() { Severity = me.IssueSeverity, Code = me.IssueType, - Details = new Model.CodeableConcept(ValidationErrorMessageCodeSystem, me.ErrorCode, shortDisplay, me.BaseErrorMessage) + Details = new CodeableConcept(ValidationErrorMessageCodeSystem, me.ErrorCode, shortDisplay, me.BaseErrorMessage) }; if (me.LineNumber.HasValue && me.Position.HasValue) - result.Location = new[] { $"line {me.LineNumber}, position {me.Position}" }; + result.Location = [$"line {me.LineNumber}, position {me.Position}"]; if (!string.IsNullOrEmpty(me.InstancePath)) - result.Expression = new[] { me.InstancePath }; + result.Expression = [me.InstancePath]; return result; } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Serialization/PathStack.cs b/src/Hl7.Fhir.Base/Serialization/PathStack.cs index a512b90ba8..747c858a48 100644 --- a/src/Hl7.Fhir.Base/Serialization/PathStack.cs +++ b/src/Hl7.Fhir.Base/Serialization/PathStack.cs @@ -123,7 +123,7 @@ public string GetPath() if (_tops.Count == 0) return "$this"; var index = _path.LastIndexOf(RESOURCEPREFIX); - return index != -1 ? _path.Substring(index + 1) : _path; + return index != -1 ? _path[(index + 1)..] : _path; } /// @@ -133,6 +133,11 @@ public string GetInstancePath() { return String.Join(".", _paths.Reverse().Where(v => !string.IsNullOrEmpty(v))); } + + /// + /// Returns the last part of the path (most often the current property name) + /// + public string GetLastPart() => _paths.Peek(); } } diff --git a/src/Hl7.Fhir.Base/Serialization/PocoDeserializationHelper.cs b/src/Hl7.Fhir.Base/Serialization/PocoDeserializationHelper.cs index 5a839ea764..1e6b4fe81c 100644 --- a/src/Hl7.Fhir.Base/Serialization/PocoDeserializationHelper.cs +++ b/src/Hl7.Fhir.Base/Serialization/PocoDeserializationHelper.cs @@ -1,23 +1,20 @@ #nullable enable -namespace Hl7.Fhir.Serialization +using Hl7.Fhir.Model; + +namespace Hl7.Fhir.Serialization; + +internal static class PocoDeserializationHelper { - internal static class PocoDeserializationHelper + internal static void RunPropertyValidation(object? propertyValue, IDeserializationValidator validator, PropertyDeserializationContext context, ExceptionAggregator aggregator) { - internal static void RunPropertyValidation(ref object? instance, IDeserializationValidator validator, PropertyDeserializationContext context, ExceptionAggregator aggregator) - { - validator.ValidateProperty(ref instance, context, out var errors); - aggregator.Add(errors); - return; - } - - internal static void RunInstanceValidation(object? instance, IDeserializationValidator validator, InstanceDeserializationContext context, ExceptionAggregator aggregator) - { - validator.ValidateInstance(instance, context, out var errors); - aggregator.Add(errors); - return; - } + validator.ValidateProperty(propertyValue, context, out var errors); + aggregator.Add(errors); } -} -#nullable restore \ No newline at end of file + internal static void RunInstanceValidation(Base instance, IDeserializationValidator validator, InstanceDeserializationContext context, ExceptionAggregator aggregator) + { + validator.ValidateInstance(instance, context, out var errors); + aggregator.Add(errors); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Utility/ExtendedCodedException.cs b/src/Hl7.Fhir.Base/Utility/ExtendedCodedException.cs index 35766abfe7..5295723e69 100644 --- a/src/Hl7.Fhir.Base/Utility/ExtendedCodedException.cs +++ b/src/Hl7.Fhir.Base/Utility/ExtendedCodedException.cs @@ -43,11 +43,14 @@ public ExtendedCodedException( private static string formatLocationMessage(string baseMessage, string? instancePath, long? lineNumber, long? position) { - string location = $"At line {lineNumber}, position {position}"; + string? location = null; + + if(lineNumber is not null && position is not null) + location = $" At line {lineNumber}, position {position}"; if (!string.IsNullOrEmpty(instancePath)) - location = $"At {instancePath}, line {lineNumber}, position {position}"; - var messageWithLocation = $"{baseMessage} {location}"; - return messageWithLocation; + location = $" At {instancePath}, line {lineNumber}, position {position}"; + + return location is not null ? $"{baseMessage}{location}." : baseMessage; } /// @@ -89,4 +92,4 @@ private static string formatLocationMessage(string baseMessage, string? instance } } -#nullable restore +#nullable restore \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Utility/SerializationUtil.cs b/src/Hl7.Fhir.Base/Utility/SerializationUtil.cs index 8b31d9c50c..f4de032900 100644 --- a/src/Hl7.Fhir.Base/Utility/SerializationUtil.cs +++ b/src/Hl7.Fhir.Base/Utility/SerializationUtil.cs @@ -16,7 +16,6 @@ using System.IO; using System.Linq; using System.Text; -using System.Text.Encodings.Web; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -114,19 +113,24 @@ public static async Task JObjectFromReaderAsync(JsonReader reader) } } + /// + /// Reads a string of Xml and returns a XDocument. + /// + /// Thrown when the Xml parser detects an error. + /// Microsoft's Xml parsing infrastructure normally throws XmlExceptions when it encounters invalid Xml, + /// but in this method specifically, we catch those exceptions and rethrow them as FormatExceptions. In retrospect, + /// this is a potential source of confusion, but would be a breaking change to fix. public static XDocument XDocumentFromXmlText(string xml, bool ignoreComments = true) { - using (var reader = XmlReaderFromXmlText(xml)) - { - return xDocumentFromReaderInternal(reader); - } + using var reader = XmlReaderFromXmlText(xml, ignoreComments); + return xDocumentFromReaderInternal(reader); } public static XmlReader XmlReaderFromXmlText(string xml, bool ignoreComments = true) - => WrapXmlReader(XmlReader.Create(new StringReader(SerializationUtil.SanitizeXml(xml))), ignoreComments); + => WrapXmlReader(XmlReader.Create(new StringReader(SanitizeXml(xml))), ignoreComments); public static Task XmlReaderFromXmlTextAsync(string xml, bool ignoreComments = true) - => Task.FromResult(WrapXmlReader(XmlReader.Create(new StringReader(SerializationUtil.SanitizeXml(xml))), ignoreComments, async: true)); + => Task.FromResult(WrapXmlReader(XmlReader.Create(new StringReader(SanitizeXml(xml))), ignoreComments, async: true)); public static JsonReader JsonReaderFromJsonText(string json) => jsonReaderFromTextReader(new StringReader(json)); @@ -147,7 +151,6 @@ private static JsonReader jsonReaderFromTextReader(TextReader input) }; } - public static XmlReader WrapXmlReader(XmlReader xmlReader, bool ignoreComments = true, bool async = false) { var settings = new XmlReaderSettings @@ -165,46 +168,38 @@ public static XmlReader WrapXmlReader(XmlReader xmlReader, bool ignoreComments = public static byte[] WriteXmlToBytes(Action serializer, bool pretty = false) { // [WMR 20160421] Explicit disposal - using (MemoryStream stream = new MemoryStream()) + using var stream = new MemoryStream(); + var settings = new XmlWriterSettings { - XmlWriterSettings settings = new XmlWriterSettings - { - Encoding = new UTF8Encoding(false), - OmitXmlDeclaration = true, - NewLineHandling = NewLineHandling.Entitize, - Indent = pretty - }; + Encoding = new UTF8Encoding(false), + OmitXmlDeclaration = true, + NewLineHandling = NewLineHandling.Entitize, + Indent = pretty + }; - using (XmlWriter xw = XmlWriter.Create(stream, settings)) - { - serializer(xw); - xw.Flush(); - return stream.ToArray(); - } - } + using XmlWriter xw = XmlWriter.Create(stream, settings); + serializer(xw); + xw.Flush(); + return stream.ToArray(); } public static async Task WriteXmlToBytesAsync(Func serializer, bool pretty = false) { // [WMR 20160421] Explicit disposal - using (MemoryStream stream = new MemoryStream()) + using var stream = new MemoryStream(); + var settings = new XmlWriterSettings { - XmlWriterSettings settings = new XmlWriterSettings - { - Encoding = new UTF8Encoding(false), - OmitXmlDeclaration = true, - NewLineHandling = NewLineHandling.Entitize, - Async = true, - Indent = pretty - }; - - using (XmlWriter xw = XmlWriter.Create(stream, settings)) - { - await serializer(xw).ConfigureAwait(false); - await xw.FlushAsync().ConfigureAwait(false); - return stream.ToArray(); - } - } + Encoding = new UTF8Encoding(false), + OmitXmlDeclaration = true, + NewLineHandling = NewLineHandling.Entitize, + Async = true, + Indent = pretty + }; + + using var xw = XmlWriter.Create(stream, settings); + await serializer(xw).ConfigureAwait(false); + await xw.FlushAsync().ConfigureAwait(false); + return stream.ToArray(); } public static string WriteXmlToString(Action serializer, bool pretty = false) @@ -423,20 +418,20 @@ public static string SanitizeXml(string xml) var matches = RE.Matches(xml); if (matches.Count > 0) { - StringBuilder sbRE = new StringBuilder(); + var sbRe = new StringBuilder(); int currentPosition = 0; foreach (Match m in matches) { if (xr.TryGetValue(m.Value, out string? value)) { - sbRE.Append(xml.Substring(currentPosition, m.Index - currentPosition)); - sbRE.Append(value); + sbRe.Append(xml.Substring(currentPosition, m.Index - currentPosition)); + sbRe.Append(value); currentPosition = m.Index + m.Length; } // System.Diagnostics.Trace.WriteLine(String.Format("{0} - {1}: {2}", m.Index, m.Length, m.Value)); } - sbRE.Append(xml.Substring(currentPosition)); - resultRe = sbRE.ToString(); + sbRe.Append(xml.Substring(currentPosition)); + resultRe = sbRe.ToString(); } else { @@ -446,30 +441,16 @@ public static string SanitizeXml(string xml) return resultRe; } - public static string[] RunFhirXhtmlSchemaValidation(string xmlText) - { - try - { - var doc = SerializationUtil.XDocumentFromXmlText(xmlText); - return RunFhirXhtmlSchemaValidation(doc); - } - catch (FormatException fe) - { - return new[] { fe.Message }; - } - } - public static string[] RunFhirXhtmlSchemaValidation(XDocument doc) { - var result = new List(); - if (!doc.Root!.AtXhtmlDiv()) - return new[] { $"Root element of XHTML is not a
from the XHTML namespace ({XmlNs.XHTML})." }; + return [$"Root element of XHTML is not a
from the XHTML namespace ({XmlNs.XHTML})."]; if (!hasContent(doc.Root!)) - return new[] { $"The narrative SHALL have some non-whitespace content." }; + return [$"The narrative SHALL have some non-whitespace content."]; - doc.Validate(BASEFHIRSCHEMAS.CompiledSchemas, (s, a) => result.Add(a.Message)); + var result = new List(); + doc.Validate(BASEFHIRSCHEMAS.CompiledSchemas, (_, a) => result.Add(a.Message)); return result.ToArray(); // content consist of xml elements with non-whitespace content (text or an image) @@ -477,7 +458,6 @@ static bool hasContent(XElement el) => el.DescendantsAndSelf().Any(e => !string.IsNullOrWhiteSpace(e.Value) || e.Name.LocalName == "img"); } - private static readonly Regex RE = new("(&[a-zA-Z0-9]+;)", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static Dictionary? _xmlReplacements; private static Dictionary getXmlReplacements() @@ -754,7 +734,7 @@ static void addSubsetted(Base instance, bool atRoot) ///
internal static Base MakeSubsettedClone(Base source) { - var clone = (Base)source.DeepCopy(); + var clone = source.DeepCopy(); MarkSubsetted(clone); return clone; } diff --git a/src/Hl7.Fhir.Base/Validation/CodePatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/CodePatternAttribute.cs index b5a9a65ec0..29d9a51950 100644 --- a/src/Hl7.Fhir.Base/Validation/CodePatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/CodePatternAttribute.cs @@ -13,24 +13,15 @@ #nullable enable -namespace Hl7.Fhir.Validation -{ - /// - /// Validates a code value against the FHIR rules for code. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class CodePatternAttribute : ValidationAttribute - { - /// - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when Code.IsValidValue(s) => ValidationResult.Success, - string s => COVE.CODE_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(CodePatternAttribute)} attributes can only be applied to string properties.") - }; - } -} +namespace Hl7.Fhir.Validation; -#nullable restore \ No newline at end of file +/// +/// Validates a code value against the FHIR rules for code. +/// +[AttributeUsage(AttributeTargets.Property)] +public class CodePatternAttribute : ValidationAttribute +{ + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => + new Code { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); +} \ 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 2262ef3b1d..bc2fe59429 100644 --- a/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs +++ b/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs @@ -7,7 +7,9 @@ */ using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; using Hl7.Fhir.Utility; +using System; using System.ComponentModel.DataAnnotations; using COVE = Hl7.Fhir.Validation.CodedValidationException; using OO_Sev = Hl7.Fhir.Model.OperationOutcome.IssueSeverity; @@ -15,107 +17,112 @@ #nullable enable -namespace Hl7.Fhir.Validation +namespace Hl7.Fhir.Validation; + +/// +/// An error found during validation of POCO's using the validators. +/// +public class CodedValidationException : ExtendedCodedException { - /// - /// An error found during validation of POCO's using the validators. - /// - public class CodedValidationException : ExtendedCodedException - { - public const string CHOICE_TYPE_NOT_ALLOWED_CODE = "PVAL101"; - public const string INCORRECT_CARDINALITY_MIN_CODE = "PVAL102"; - public const string INCORRECT_CARDINALITY_MAX_CODE = "PVAL103"; - public const string REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE = "PVAL104"; - public const string MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE = "PVAL105"; - public const string CODE_LITERAL_INVALID_CODE = "PVAL106"; - public const string DATE_LITERAL_INVALID_CODE = "PVAL107"; - public const string DATETIME_LITERAL_INVALID_CODE = "PVAL108"; - public const string ID_LITERAL_INVALID_CODE = "PVAL109"; - public const string OID_LITERAL_INVALID_CODE = "PVAL110"; - public const string TIME_LITERAL_INVALID_CODE = "PVAL111"; - public const string URI_LITERAL_INVALID_CODE = "PVAL112"; - public const string UUID_LITERAL_INVALID_CODE = "PVAL113"; - public const string NARRATIVE_XML_IS_MALFORMED_CODE = "PVAL114"; - public const string NARRATIVE_XML_IS_INVALID_CODE = "PVAL115"; - public const string INVALID_CODED_VALUE_CODE = "PVAL116"; - public const string CONTAINED_RESOURCE_CANNOT_HAVE_NARRATIVE_CODE = "PVAL117"; // This was removed in R4 and is no longer validated - public const string CONTAINED_RESOURCES_CANNOT_BE_NESTED_CODE = "PVAL118"; - public const string INVALID_STRING_LENGTH_CODE = "PVAL119"; + public const string CHOICE_TYPE_NOT_ALLOWED_CODE = "PVAL101"; + public const string INCORRECT_CARDINALITY_MIN_CODE = "PVAL102"; + public const string INCORRECT_CARDINALITY_MAX_CODE = "PVAL103"; + public const string REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE = "PVAL104"; + public const string MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE = "PVAL105"; + public const string NARRATIVE_XML_IS_MALFORMED_CODE = "PVAL114"; + public const string NARRATIVE_XML_IS_INVALID_CODE = "PVAL115"; + public const string INVALID_CODED_VALUE_CODE = "PVAL116"; + public const string CONTAINED_RESOURCES_CANNOT_BE_NESTED_CODE = "PVAL118"; + public const string INVALID_STRING_LENGTH_CODE = "PVAL119"; + public const string INVALID_BASE64_VALUE_CODE = "PVAL120"; + public const string INCORRECT_LITERAL_VALUE_TYPE_CODE = "PVAL123"; + public const string LITERAL_INVALID_CODE = "PVAL124"; + public const string POSITIVE_INT_MUST_BE_POSITIVE_CODE = "PVAL125"; + public const string UNSIGNED_INT_MUST_NOT_BE_NEGATIVE_CODE = "PVAL126"; - 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); - internal static COVE REPEATING_ELEMENT_CANNOT_CONTAIN_NULL(ValidationContext context) => Initialize(context, REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE, "Repeating elements should not contain a null value.", OO_Sev.Error, OO_Typ.Structure); - internal static COVE MANDATORY_ELEMENT_CANNOT_BE_NULL(ValidationContext context, string? MemberName, int Min) => Initialize(context, MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, $"Element '{MemberName}' with minimum cardinality {Min} cannot be null.", OO_Sev.Error, OO_Typ.Required); - internal static COVE CODE_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, CODE_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for a code.", OO_Sev.Error, OO_Typ.Value); - internal static COVE DATE_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, DATE_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for a date.", OO_Sev.Error, OO_Typ.Value); - internal static COVE DATETIME_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, DATETIME_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for a dateTime.", OO_Sev.Error, OO_Typ.Value); - internal static COVE ID_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, ID_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for an id.", OO_Sev.Error, OO_Typ.Value); - internal static COVE NARRATIVE_XML_IS_MALFORMED(ValidationContext context, string? value) => Initialize(context, NARRATIVE_XML_IS_MALFORMED_CODE, $"Value is not well-formatted Xml: {value}", OO_Sev.Error, OO_Typ.Structure); - internal static COVE NARRATIVE_XML_IS_INVALID(ValidationContext context, string value) => Initialize(context, NARRATIVE_XML_IS_INVALID_CODE, $"Value is not well-formed Xml adhering to the FHIR schema for Narrative: {value}", OO_Sev.Error, OO_Typ.Structure); - internal static COVE OID_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, OID_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for an oid.", OO_Sev.Error, OO_Typ.Value); - internal static COVE TIME_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, TIME_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for a time.", OO_Sev.Error, OO_Typ.Value); - internal static COVE URI_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, URI_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for an uri.", OO_Sev.Error, OO_Typ.Value); - internal static COVE UUID_LITERAL_INVALID(ValidationContext context, string s) => Initialize(context, UUID_LITERAL_INVALID_CODE, $"'{s}' is not a correct literal for a uuid.", OO_Sev.Error, OO_Typ.Value); - internal static COVE INVALID_CODED_VALUE(ValidationContext context, object? value, string name) => Initialize(context, INVALID_CODED_VALUE_CODE, $"Value '{value}' is not a correct code for valueset '{name}'.", OO_Sev.Error, OO_Typ.CodeInvalid); - // internal static COVE CONTAINED_RESOURCE_CANNOT_HAVE_NARRATIVE(ValidationContext context) => Initialize(context, CONTAINED_RESOURCE_CANNOT_HAVE_NARRATIVE_CODE, "Resource has contained resources with narrative, which is not allowed.", OO_Sev.Error, OO_Typ.Structure); - internal static COVE CONTAINED_RESOURCES_CANNOT_BE_NESTED(ValidationContext context) => Initialize(context, CONTAINED_RESOURCES_CANNOT_BE_NESTED_CODE, "It is not allowed for a resource to contain resources which themselves contain resources.", OO_Sev.Error, OO_Typ.Structure); - internal static COVE INVALID_STRING_LENGTH(ValidationContext context, string name, string value) => Initialize(context, INVALID_STRING_LENGTH_CODE, (value.Length > 0 ? $"String {name} exceeds maximum length of 1MB." : $"String {name} is empty"), OO_Sev.Error, OO_Typ.Value); + 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); + internal static COVE REPEATING_ELEMENT_CANNOT_CONTAIN_NULL(ValidationContext context) => Initialize(context, REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE, "Repeating elements should not contain a null value.", OO_Sev.Error, OO_Typ.Structure); + internal static COVE MANDATORY_ELEMENT_CANNOT_BE_NULL(ValidationContext context, string? memberName, int Min) => Initialize(context, MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, $"Element '{memberName}' with minimum cardinality {Min} cannot be null.", OO_Sev.Error, OO_Typ.Required); + internal static COVE NARRATIVE_XML_IS_MALFORMED(ValidationContext context, string? value) => Initialize(context, NARRATIVE_XML_IS_MALFORMED_CODE, $"Value is not well-formatted Xml: {value}", OO_Sev.Error, OO_Typ.Structure); + internal static COVE NARRATIVE_XML_IS_INVALID(ValidationContext context, string value) => Initialize(context, NARRATIVE_XML_IS_INVALID_CODE, $"Value is not well-formed Xml adhering to the FHIR schema for Narrative: {value}", OO_Sev.Error, OO_Typ.Structure); + internal static COVE INVALID_CODED_VALUE(ValidationContext? context, object? value, string name) => Initialize(context, INVALID_CODED_VALUE_CODE, $"Value '{value}' is not a correct code for valueset '{name}'.", OO_Sev.Error, OO_Typ.CodeInvalid); + internal static COVE CONTAINED_RESOURCES_CANNOT_BE_NESTED(ValidationContext context) => Initialize(context, CONTAINED_RESOURCES_CANNOT_BE_NESTED_CODE, "It is not allowed for a resource to contain resources which themselves contain resources.", OO_Sev.Error, OO_Typ.Structure); + internal static COVE INVALID_STRING_LENGTH(ValidationContext context, string name, string value) => Initialize(context, INVALID_STRING_LENGTH_CODE, (value.Length > 0 ? $"String {name} exceeds maximum length of 1MB." : $"String {name} is empty"), OO_Sev.Error, OO_Typ.Value); + internal static COVE INVALID_BASE64_VALUE(ValidationContext? context, object? value) => Initialize(context, INVALID_BASE64_VALUE_CODE, $"Value '{value}' is not parseable as Base64 data.", OO_Sev.Error, OO_Typ.Value); + internal static COVE INCORRECT_LITERAL_VALUE_TYPE(ValidationContext? context, object? value, string fhirTypeName) => + Initialize(context, INCORRECT_LITERAL_VALUE_TYPE_CODE, $"{niceValue(value)} is not the right type of literal for a {fhirTypeName}.", OO_Sev.Error, OO_Typ.Value); + internal static COVE LITERAL_INVALID(ValidationContext? context, object? value, string fhirTypeName) => + Initialize(context, LITERAL_INVALID_CODE, $"{niceValue(value)} is not a correct literal for a {fhirTypeName}.", OO_Sev.Error, OO_Typ.Value); + internal static COVE POSITIVE_INT_MUST_BE_POSITIVE(ValidationContext? context, int value) => + Initialize(context, POSITIVE_INT_MUST_BE_POSITIVE_CODE, $"Value {value} is not positive, which is required for a PositiveInt.", OO_Sev.Error, OO_Typ.Value); + internal static COVE UNSIGNED_INT_MUST_NOT_BE_NEGATIVE(ValidationContext? context, int value) => + Initialize(context, UNSIGNED_INT_MUST_NOT_BE_NEGATIVE_CODE, $"Value {value} is negative, which is not allowed for an UnsignedInt.", OO_Sev.Error, OO_Typ.Value); - public CodedValidationException(string code, string message) - : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown) - { - // Nothing - } - public CodedValidationException( - string errorCode, - string baseMessage, - string? instancePath, - long? lineNumber, - long? position, - OperationOutcome.IssueSeverity issueSeverity, - OperationOutcome.IssueType issueType) : - base(errorCode, baseMessage, instancePath, lineNumber, position, issueSeverity, issueType) + private static string niceValue(object? v) + { + return v switch { - // Nothing - } + null => "null", + string s => $"string '{s}'", + int i => $"integer {i}", + decimal d => $"decimal {d}", + bool b => $"boolean {b}", + _ => $"value '{PrimitiveTypeConverter.ConvertTo(v)}' of type '{v.GetType()}'" + }; + } - internal static CodedValidationException Initialize(ValidationContext context, string code, string message, OperationOutcome.IssueSeverity issueSeverity, OperationOutcome.IssueType issueType) - { - var path = context.GetLocation() as string; + public CodedValidationException(string code, string message) + : base(code, message, null, null, null, OO_Sev.Error, OO_Typ.Unknown) + { + // Nothing + } - if (path is not null) + public CodedValidationException( + string errorCode, + string baseMessage, + string? instancePath, + long? lineNumber, + long? position, + OperationOutcome.IssueSeverity issueSeverity, + OperationOutcome.IssueType issueType) : + base(errorCode, baseMessage, instancePath, lineNumber, position, issueSeverity, issueType) + { + // Nothing + } + + internal static CodedValidationException Initialize(ValidationContext? context, string code, string message, OperationOutcome.IssueSeverity issueSeverity, OperationOutcome.IssueType issueType) + { + var path = context?.GetLocation(); + + if (path is not null) + { + // Bit of a hack. The location returned by GetLocation() will be different depending on + // whether this validation is run within the deserializer or the DataAnnotations.Validator. + // In the latter case, the MemberName will be set, and GetLocation() + // will return the parent, so we need to add the MemberName. + if (context?.MemberName is not null) { - // Bit of a hack. The location returned by GetLocation() will be different depending on - // whether this validation is run within the deserializer or the DataAnnotations.Validator. - // In the latter case, the MemberName will be set, and GetLocation() - // will return the parent, so we need to add the MemberName. - if (context.MemberName is not null) - { - path = $"{path}.{context.MemberName}"; - } + path = $"{path}.{context.MemberName}"; } + } - var pi = context.GetPositionInfo() as IPositionInfo; - - var codedException = new CodedValidationException( - code, - message, - path, - pi?.LineNumber, - pi?.LinePosition, issueSeverity, issueType); + var pi = context?.GetPositionInfo(); - return codedException; - } + var codedException = new CodedValidationException( + code, + message, + path, + pi?.LineNumber, + pi?.LinePosition, issueSeverity, issueType); - internal CodedValidationResult AsResult(ValidationContext context) - { - return context.MemberName is string mn - ? new(this, memberNames: new[] { mn }) - : new(this); - } + return codedException; } -} -#nullable restore + internal CodedValidationResult AsResult(ValidationContext? context) => + context?.MemberName is { } mn + ? new CodedValidationResult(this, memberNames: [mn]) + : new CodedValidationResult(this); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/DatePatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/DatePatternAttribute.cs index 141ff38e51..6c800e507a 100644 --- a/src/Hl7.Fhir.Base/Validation/DatePatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/DatePatternAttribute.cs @@ -13,23 +13,15 @@ #nullable enable -namespace Hl7.Fhir.Validation +namespace Hl7.Fhir.Validation; + +/// +/// Validates a date value against the FHIR rules for date. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class DatePatternAttribute : ValidationAttribute { - /// - /// Validates a date value against the FHIR rules for date. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class DatePatternAttribute : ValidationAttribute - { - /// - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when Date.IsValidValue(s) => ValidationResult.Success, - string s => COVE.DATE_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(DatePatternAttribute)} attributes can only be applied to string properties.") - }; - } -} -#nullable restore \ No newline at end of file + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => + new Date { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/DateTimePatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/DateTimePatternAttribute.cs index 8f13de3cd7..aee05f3a06 100644 --- a/src/Hl7.Fhir.Base/Validation/DateTimePatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/DateTimePatternAttribute.cs @@ -13,24 +13,15 @@ #nullable enable -namespace Hl7.Fhir.Validation -{ - /// - /// Validates a datetime value against the FHIR rules for datetime. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class DateTimePatternAttribute : ValidationAttribute - { - /// - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when FhirDateTime.IsValidValue(s) => ValidationResult.Success, - string s => COVE.DATETIME_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(DateTimePatternAttribute)} attributes can only be applied to string properties.") - }; - } -} +namespace Hl7.Fhir.Validation; -#nullable restore \ No newline at end of file +/// +/// Validates a datetime value against the FHIR rules for datetime. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class DateTimePatternAttribute : ValidationAttribute +{ + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => + new FhirDateTime { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/DotNetAttributeValidation.cs b/src/Hl7.Fhir.Base/Validation/DotNetAttributeValidation.cs index 56719b9296..5f9d963db0 100644 --- a/src/Hl7.Fhir.Base/Validation/DotNetAttributeValidation.cs +++ b/src/Hl7.Fhir.Base/Validation/DotNetAttributeValidation.cs @@ -12,99 +12,86 @@ #nullable enable -namespace Hl7.Fhir.Validation +namespace Hl7.Fhir.Validation; + +/// +/// Utility methods for invoking .NET's -based validation mechanism. +/// +public static class DotNetAttributeValidation { /// - /// Utility methods for invoking .NET's -based validation mechanism. + /// Validate and object and its members against any s present. + /// Will throw when a validation error is encountered. /// - public static class DotNetAttributeValidation + /// The POCO to validate + /// Whether to validate the object recursively, by also validating the contents of each property of the object. + /// The kind of narrative validation to perform when validating . + public static void Validate( + this Base poco, + bool recurse = false, + NarrativeValidationKind narrativeValidation = NarrativeValidationKind.FhirXhtml) { - /// - /// Validate and object and its members against any s present. - /// Will throw when a validation error is encountered. - /// - /// The object to validate - /// Whether to validate the object recursively, by also validating the contents of each property of the object. - /// The kind of narrative validation to perform when validating . - public static void Validate( - this Base value, - bool recurse = false, - NarrativeValidationKind narrativeValidation = NarrativeValidationKind.FhirXhtml) => - Validate((object)value, recurse, narrativeValidation); - - /// - internal static void Validate(object value, bool recurse = false, NarrativeValidationKind narrativeValidation = NarrativeValidationKind.FhirXhtml) - { - var validationContext = buildContext(recurse, narrativeValidation, value); - Validator.ValidateObject(value, validationContext, true); - } - - /// - /// Validate an object and its members against any s present. - /// - /// If is null, no errors will be returned. - public static bool TryValidate(this Base value, ICollection? validationResults = null, bool recurse = false, NarrativeValidationKind narrativeValidation = NarrativeValidationKind.FhirXhtml) - => TryValidate((object)value, validationResults, recurse, narrativeValidation); - - /// - internal static bool TryValidate(object value, ICollection? validationResults = null, bool recurse = false, NarrativeValidationKind narrativeValidation = NarrativeValidationKind.FhirXhtml) - { - var validationContext = buildContext(recurse, narrativeValidation, value); - - // Validate the object, also calling the validators on each child property. - var results = validationResults ?? new List(); - return Validator.TryValidateObject((object)value, validationContext, results, validateAllProperties: true); - } + var validationContext = buildContext(poco, recurse, narrativeValidation); + Validator.ValidateObject(poco, validationContext, true); + } - /// - internal static bool TryValidate(object value, ValidationContext context, ICollection? validationResults) - { - // Validate the object, also calling the validators on each child property. - var results = validationResults ?? new List(); - return Validator.TryValidateObject(value, context, results, validateAllProperties: true); - } + /// + /// Validate an object and its members against any s present. + /// + /// The POCO to validate + /// Whether to validate the object recursively, by also validating the contents of each property of the object. + /// The kind of narrative validation to perform when validating . + /// A collection to which any validation errors will be added. + /// If is null, no errors will be returned. + public static bool TryValidate(this Base poco, ICollection? validationResults = null, bool recurse = false, NarrativeValidationKind narrativeValidation = NarrativeValidationKind.FhirXhtml) + { + var validationContext = buildContext(poco, recurse, narrativeValidation); - internal static ValidationContext IntoPath(this ValidationContext ctx, object instance, string nestedElementName) - { - var location = ctx.GetLocation(); + // Validate the object, also calling the validators on each child property. + var results = validationResults ?? []; + return Validator.TryValidateObject(poco, validationContext, results, validateAllProperties: true); + } - var newContext = new ValidationContext(instance, ctx.Items); + internal static ValidationContext IntoPath(this ValidationContext ctx, Base poco, string nestedElementName) + { + // TODO: this will still trigger production of the location, so wasting time. + // need to defer this here too, but have to check first what that means for the PathStack, + // which keeps being updated in the face of delayed validations. + var location = ctx.GetLocation(); - if (location is not null) - newContext.SetLocation($"{location}.{nestedElementName}"); - else - newContext.SetLocation(nestedElementName); + var newContext = new ValidationContext(poco, ctx.Items); - return newContext; - } + if (location is not null) + newContext.SetLocationProducer(() => $"{location}.{nestedElementName}"); + else + newContext.SetLocationProducer(() => nestedElementName); - /// - /// This is very similar to IntoPath except that it doesn't walk into the actual property - /// as the property does not exist, but we do need to set the membername so that the error message - /// is created correctly identifies the location in the message (in the context of the location) - /// - /// - /// - /// - internal static ValidationContext IntoEmptyProperty(this ValidationContext ctx, string elementName) - { - var newContext = new ValidationContext(ctx.ObjectInstance, ctx.Items); - newContext.MemberName = elementName; - return newContext; - } + return newContext; + } - private static ValidationContext buildContext(bool recurse, NarrativeValidationKind kind, object instance) + /// + /// This is very similar to IntoPath except that it doesn't walk into the actual property + /// as the property does not exist, but we do need to set the membername so that the error message + /// is created correctly identifies the location in the message (in the context of the location) + /// + /// + /// + /// + internal static ValidationContext IntoEmptyProperty(this ValidationContext ctx, string elementName) + { + return new ValidationContext(ctx.ObjectInstance, ctx.Items) { - ValidationContext newContext = new(instance); - - newContext.SetValidateRecursively(recurse); - newContext.SetNarrativeValidationKind(kind); - newContext.SetLocation(instance.GetType().Name); - return newContext; - } - + MemberName = elementName + }; } -} + private static ValidationContext buildContext(Base instance, bool recurse, NarrativeValidationKind kind) + { + var newContext = new ValidationContext(instance); -#nullable restore \ No newline at end of file + newContext.SetValidateRecursively(recurse); + newContext.SetNarrativeValidationKind(kind); + newContext.SetLocationProducer(() => instance.GetType().Name); + return newContext; + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/IdPatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/IdPatternAttribute.cs index db813fa758..5bb93bb0f8 100644 --- a/src/Hl7.Fhir.Base/Validation/IdPatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/IdPatternAttribute.cs @@ -13,24 +13,15 @@ #nullable enable -namespace Hl7.Fhir.Validation -{ - /// - /// Validates an id value against the FHIR rules for id. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class IdPatternAttribute : ValidationAttribute - { - /// - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when Id.IsValidValue(s) => ValidationResult.Success, - string s => COVE.ID_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(IdPatternAttribute)} attributes can only be applied to string properties.") - }; - } -} +namespace Hl7.Fhir.Validation; -#nullable restore \ No newline at end of file +/// +/// Validates an id value against the FHIR rules for id. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class IdPatternAttribute : ValidationAttribute +{ + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => + new Id { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/NarrativeXhtmlPatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/NarrativeXhtmlPatternAttribute.cs index 800ca323dc..b7b214d8e4 100644 --- a/src/Hl7.Fhir.Base/Validation/NarrativeXhtmlPatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/NarrativeXhtmlPatternAttribute.cs @@ -7,66 +7,28 @@ */ using Hl7.Fhir.Model; -using Hl7.Fhir.Utility; using System; using System.ComponentModel.DataAnnotations; -using System.Linq; -using COVE = Hl7.Fhir.Validation.CodedValidationException; #nullable enable -namespace Hl7.Fhir.Validation +namespace Hl7.Fhir.Validation; + +/// +/// Validates an xhtml value against the FHIR rules for xhtml. +/// +[AttributeUsage(AttributeTargets.Property)] +public class NarrativeXhtmlPatternAttribute : ValidationAttribute { - /// - /// Validates an xhtml value against the FHIR rules for xhtml. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class NarrativeXhtmlPatternAttribute : ValidationAttribute + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { - /// - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - IsValid(value, validationContext.GetNarrativeValidationKind(), validationContext); - - /// - /// Validates whether the value is a string of well-formatted Xml. - /// - public ValidationResult? IsValid(object? value, NarrativeValidationKind kind, ValidationContext context) - { - if (value is null) return ValidationResult.Success; - - if (value is string xml) - { - return kind switch - { - NarrativeValidationKind.None => ValidationResult.Success, - NarrativeValidationKind.Xml => XHtml.IsValidXml(xml, out var error) - ? ValidationResult.Success - : COVE.NARRATIVE_XML_IS_MALFORMED(context, error).AsResult(context), - NarrativeValidationKind.FhirXhtml => runValidateXhtmlSchema(xml, context), - _ => throw new NotSupportedException($"Encountered unknown narrative validation kind {kind}.") - }; - } - else - throw new ArgumentException($"{nameof(NarrativeXhtmlPatternAttribute)} attributes can only be applied to string properties."); - } - - private static ValidationResult runValidateXhtmlSchema(string text, ValidationContext context) + return value switch { - try - { - var doc = SerializationUtil.XDocumentFromXmlText(text); - var errors = SerializationUtil.RunFhirXhtmlSchemaValidation(doc); - - return errors.Any() ? COVE.NARRATIVE_XML_IS_INVALID(context, string.Join(", ", errors)).AsResult(context) : ValidationResult.Success!; - } - catch (FormatException fe) - { - return COVE.NARRATIVE_XML_IS_MALFORMED(context, fe.Message).AsResult(context); - } - } + null => ValidationResult.Success, + string xml => XHtml.ValidateXmlLiteral(xml, validationContext)?.AsResult(validationContext), + _ => throw new ArgumentException( + $"{nameof(NarrativeXhtmlPatternAttribute)} attributes can only be applied to string properties.") + }; } -} - - - -#nullable restore \ No newline at end of file +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/OidPatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/OidPatternAttribute.cs index f4e85a2708..3bf07f594e 100644 --- a/src/Hl7.Fhir.Base/Validation/OidPatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/OidPatternAttribute.cs @@ -13,24 +13,15 @@ #nullable enable -namespace Hl7.Fhir.Validation -{ - /// - /// Validates an Oid value against the FHIR rules for oid. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class OidPatternAttribute : ValidationAttribute - { - /// - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when Oid.IsValidValue(s) => ValidationResult.Success, - string s => COVE.OID_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(OidPatternAttribute)} attributes can only be applied to string properties.") - }; - } -} +namespace Hl7.Fhir.Validation; -#nullable restore \ No newline at end of file +/// +/// Validates an Oid value against the FHIR rules for oid. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class OidPatternAttribute : ValidationAttribute +{ + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => + new Oid { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/StringPatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/StringPatternAttribute.cs index 8f2ea1de61..abc2ccc1b0 100644 --- a/src/Hl7.Fhir.Base/Validation/StringPatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/StringPatternAttribute.cs @@ -10,7 +10,7 @@ namespace Hl7.Fhir.Validation; /// /// Validates an Uri value against the FHIR rules for Uri. /// -[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +[AttributeUsage(AttributeTargets.Property)] public class StringPatternAttribute : ValidationAttribute { protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => @@ -19,6 +19,6 @@ public class StringPatternAttribute : ValidationAttribute null => ValidationResult.Success, string s when FhirString.IsValidValue(s) => ValidationResult.Success, string s => COVE.INVALID_STRING_LENGTH(validationContext, validationContext.MemberName!, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(StringPatternAttribute)} attributes can only be applied to string properties.") + _ => ValidationResult.Success // Will happen during deserialization calls, where the raw value is fed to the attribute validation logic. }; } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/TimePatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/TimePatternAttribute.cs index a481b39374..dfaf27690f 100644 --- a/src/Hl7.Fhir.Base/Validation/TimePatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/TimePatternAttribute.cs @@ -13,24 +13,15 @@ #nullable enable -namespace Hl7.Fhir.Validation -{ - /// - /// Validates a datetime value against the FHIR rules for time. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class TimePatternAttribute : ValidationAttribute - { - /// - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when Time.IsValidValue(s) => ValidationResult.Success, - string s => COVE.TIME_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(TimePatternAttribute)} attributes can only be applied to string properties.") - }; - } -} +namespace Hl7.Fhir.Validation; -#nullable restore \ No newline at end of file +/// +/// Validates a datetime value against the FHIR rules for time. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class TimePatternAttribute : ValidationAttribute +{ + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => + new Time { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/UriPatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/UriPatternAttribute.cs index 934168d285..657293c6fd 100644 --- a/src/Hl7.Fhir.Base/Validation/UriPatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/UriPatternAttribute.cs @@ -13,23 +13,14 @@ #nullable enable -namespace Hl7.Fhir.Validation -{ - /// - /// Validates an Uri value against the FHIR rules for Uri. - /// - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public class UriPatternAttribute : ValidationAttribute - { - protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when FhirUri.IsValidValue(s) => ValidationResult.Success, - string s => COVE.URI_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(UriPatternAttribute)} attributes can only be applied to string properties.") - }; - } -} +namespace Hl7.Fhir.Validation; -#nullable restore +/// +/// Validates an Uri value against the FHIR rules for Uri. +/// +[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] +public class UriPatternAttribute : ValidationAttribute +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => + new FhirUri { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Validation/UuidPatternAttribute.cs b/src/Hl7.Fhir.Base/Validation/UuidPatternAttribute.cs index 459ec39c28..ad4e47e310 100644 --- a/src/Hl7.Fhir.Base/Validation/UuidPatternAttribute.cs +++ b/src/Hl7.Fhir.Base/Validation/UuidPatternAttribute.cs @@ -23,13 +23,7 @@ namespace Hl7.Fhir.Validation public class UuidPatternAttribute : ValidationAttribute { protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => - value switch - { - null => ValidationResult.Success, - string s when Uuid.IsValidValue(s) => ValidationResult.Success, - string s => COVE.UUID_LITERAL_INVALID(validationContext, s).AsResult(validationContext), - _ => throw new ArgumentException($"{nameof(UuidPatternAttribute)} attributes can only be applied to string properties.") - }; + new Uuid { ObjectValue = value }.ValidateObjectValue(validationContext)?.AsResult(validationContext); } } diff --git a/src/Hl7.Fhir.Base/Validation/ValidationContextExtensions.cs b/src/Hl7.Fhir.Base/Validation/ValidationContextExtensions.cs index 61c8dd59ab..565078cf00 100644 --- a/src/Hl7.Fhir.Base/Validation/ValidationContextExtensions.cs +++ b/src/Hl7.Fhir.Base/Validation/ValidationContextExtensions.cs @@ -7,106 +7,89 @@ */ using Hl7.Fhir.Utility; +using System; using System.ComponentModel.DataAnnotations; #nullable enable -namespace Hl7.Fhir.Validation +namespace Hl7.Fhir.Validation; + +/// +/// Extension methods on to support POCO validation. +/// +public static class ValidationContextExtensions { + private const string RECURSE_ITEM_KEY = "__dotnetapi_recurse__"; + private const string NARRATIVE_VALIDATION_KIND_ITEM_KEY = "__dotnetapi_narrative_validation_kind__"; + private const string POSITIONINFO_ITEM_KEY = "__dotnetapi_positioninfo__"; + private const string LOCATION_ITEM_KEY = "__dotnetapi_location__"; + /// - /// Extension methods on to support POCO validation. + /// Alters the ValidationContext to indicate that validation should or should not recurse into nested objects + /// (i.e. validate members of the validated objects complex members recursively) /// - public static class ValidationContextExtensions + public static ValidationContext SetValidateRecursively(this ValidationContext ctx, bool recursively) { - private const string RECURSE_ITEM_KEY = "__dotnetapi_recurse__"; - private const string NARRATIVE_VALIDATION_KIND_ITEM_KEY = "__dotnetapi_narrative_validation_kind__"; - private const string POSITIONINFO_ITEM_KEY = "__dotnetapi_positioninfo__"; - private const string LOCATION_ITEM_KEY = "__dotnetapi_location__"; - - /// - /// Alters the ValidationContext to indicate that validation should or should not recurse into nested objects - /// (i.e. validate members of the validated objects complex members recursively) - /// - public static ValidationContext SetValidateRecursively(this ValidationContext ctx, bool recursively) - { - ctx.Items[RECURSE_ITEM_KEY] = recursively; - return ctx; - } - - /// - /// Gets the indication from the ValidationContext whether validation should recurse into nested objects - /// - /// - /// - public static bool ValidateRecursively(this ValidationContext ctx) => - ctx.Items.TryGetValue(RECURSE_ITEM_KEY, out var result) && result is bool b && b; + ctx.Items[RECURSE_ITEM_KEY] = recursively; + return ctx; + } - /// - /// Alters the ValidationContext to indicate the kind of narrative validation the - /// should perform. - /// - public static ValidationContext SetNarrativeValidationKind(this ValidationContext ctx, NarrativeValidationKind kind) - { - ctx.Items[NARRATIVE_VALIDATION_KIND_ITEM_KEY] = kind; - return ctx; - } + /// + /// Gets the indication from the ValidationContext whether validation should recurse into nested objects + /// + /// + /// + public static bool ValidateRecursively(this ValidationContext ctx) => + ctx.Items.TryGetValue(RECURSE_ITEM_KEY, out var result) && result is bool b && b; - /// - /// Gets the kind of narrative validation the should perform - /// from the ValidationContext. - /// - public static NarrativeValidationKind GetNarrativeValidationKind(this ValidationContext ctx) => - ctx.Items.TryGetValue(NARRATIVE_VALIDATION_KIND_ITEM_KEY, out var result) && result is NarrativeValidationKind k ? - k : NarrativeValidationKind.FhirXhtml; + /// + /// Alters the ValidationContext to indicate the kind of narrative validation the + /// should perform. + /// + public static ValidationContext SetNarrativeValidationKind(this ValidationContext ctx, NarrativeValidationKind kind) + { + ctx.Items[NARRATIVE_VALIDATION_KIND_ITEM_KEY] = kind; + return ctx; + } - /// - /// Alters the ValidationContext to include line/position information for the validation errors to use. - /// - public static ValidationContext SetPositionInfo(this ValidationContext ctx, IPositionInfo position) - { - ctx.Items[POSITIONINFO_ITEM_KEY] = position; - return ctx; - } + /// + /// Gets the kind of narrative validation the should perform + /// from the ValidationContext. + /// + public static NarrativeValidationKind GetNarrativeValidationKind(this ValidationContext ctx) => + ctx.Items.TryGetValue(NARRATIVE_VALIDATION_KIND_ITEM_KEY, out var result) && result is NarrativeValidationKind k ? + k : NarrativeValidationKind.FhirXhtml; - /// - /// Gets the position information for the validation errors from the ValidationContext. - /// - public static IPositionInfo? GetPositionInfo(this ValidationContext ctx) => - ctx.Items.TryGetValue(POSITIONINFO_ITEM_KEY, out var result) && result is IPositionInfo pi ? - pi : null; + /// + /// Alters the ValidationContext to include line/position information for the validation errors to use. + /// + public static ValidationContext SetPositionInfo(this ValidationContext ctx, IPositionInfo position) + { + ctx.Items[POSITIONINFO_ITEM_KEY] = position; + return ctx; + } - /// - /// Alters the ValidationContext to include the human-readable location for the validation errors to use. - /// - public static ValidationContext SetLocation(this ValidationContext ctx, string location) - { - ctx.Items[LOCATION_ITEM_KEY] = location; - return ctx; - } + /// + /// Gets the position information for the validation errors from the ValidationContext. + /// + public static IPositionInfo? GetPositionInfo(this ValidationContext ctx) => + ctx.Items.TryGetValue(POSITIONINFO_ITEM_KEY, out var result) && result is IPositionInfo pi ? + pi : null; - /// - /// Alters the ValidationContext to include the human-readable location for the validation errors to use. - /// - internal static ValidationContext SetLocation(this ValidationContext ctx, Hl7.Fhir.Serialization.PathStack location) - { - ctx.Items[LOCATION_ITEM_KEY] = location; - return ctx; - } - /// - /// Gets the human-readable location for the validation errors from the ValidationContext. - /// - public static string? GetLocation(this ValidationContext ctx) - { - if (!ctx.Items.TryGetValue(LOCATION_ITEM_KEY, out var result)) - return null; - if (result is Hl7.Fhir.Serialization.PathStack path) - return path.GetInstancePath(); - if (result is string loc) - return loc; - return null; - } + /// + /// Alters the ValidationContext to include the human-readable location for the validation errors to use. + /// + public static ValidationContext SetLocationProducer(this ValidationContext ctx, Func locationProducer) + { + ctx.Items[LOCATION_ITEM_KEY] = locationProducer; + return ctx; } -} -#nullable restore \ No newline at end of file + /// + /// Gets the human-readable location for the validation errors from the ValidationContext. + /// + public static string? GetLocation(this ValidationContext ctx) => + ctx.Items.TryGetValue(LOCATION_ITEM_KEY, out var result) && result is Func locProducer ? + locProducer() : null; +} \ No newline at end of file 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/Rest/FhirClientTests.cs b/src/Hl7.Fhir.STU3.Tests/Rest/FhirClientTests.cs index 4e29bf10f8..fdeb96f0c3 100644 --- a/src/Hl7.Fhir.STU3.Tests/Rest/FhirClientTests.cs +++ b/src/Hl7.Fhir.STU3.Tests/Rest/FhirClientTests.cs @@ -61,7 +61,6 @@ public static void ClassInitialize(TestContext testContext) | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls13; - CreateItems().WaitNoResult(); } diff --git a/src/Hl7.Fhir.STU3.Tests/Serialization/ResourceParsingTests.cs b/src/Hl7.Fhir.STU3.Tests/Serialization/ResourceParsingTests.cs index 66aaedfe12..5f3674285d 100644 --- a/src/Hl7.Fhir.STU3.Tests/Serialization/ResourceParsingTests.cs +++ b/src/Hl7.Fhir.STU3.Tests/Serialization/ResourceParsingTests.cs @@ -10,6 +10,7 @@ using Hl7.Fhir.ElementModel; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; +using Hl7.Fhir.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; @@ -234,7 +235,7 @@ public async Tasks.Task AcceptUnknownEnums() try { pser.Settings.AllowUnrecognizedEnums = false; - p = await pser.ParseAsync(xml2); + await pser.ParseAsync(xml2); Assert.Fail(); } catch (FormatException) @@ -245,7 +246,7 @@ public async Tasks.Task AcceptUnknownEnums() // Now, allow unknown enums and check support pser.Settings.AllowUnrecognizedEnums = true; p = await pser.ParseAsync(xml2); - Assert.ThrowsException(() => p.Gender); + Assert.ThrowsException(() => p.Gender); Assert.AreEqual("superman", p.GenderElement.ObjectValue); } 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.Serialization.Shared.Tests/RoundTripAttachments.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/RoundTripAttachments.cs index 23a919aefc..9ddf5e6ab6 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/RoundTripAttachments.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/RoundTripAttachments.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Text.Json; using ERR = Hl7.Fhir.Serialization.FhirJsonException; +using COVE = Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Serialization.Tests; @@ -19,11 +20,11 @@ public class RoundTripAttachments private static IEnumerable attachmentSource() { yield return ["{\"size\":\"12\", \"title\": \"Correct Attachment\"}", 12L, null!]; - yield return ["{\"size\":12, \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_INCORRECT_FORMAT_CODE + yield return ["{\"size\":12, \"title\": \"An incorrect Attachment\"}", null!, COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE ]; - yield return ["{\"size\":25.345, \"title\": \"An incorrect Attachment\"}", null!, ERR.NUMBER_CANNOT_BE_PARSED_CODE + yield return ["{\"size\":25.345, \"title\": \"An incorrect Attachment\"}", null!, COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE ]; - yield return ["{\"size\":\"12.345\", \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_CANNOT_BE_PARSED_CODE + yield return ["{\"size\":\"12.345\", \"title\": \"An incorrect Attachment\"}", null!, COVE.LITERAL_INVALID_CODE ]; } #else @@ -34,16 +35,11 @@ private static IEnumerable attachmentSource() yield return ["{\"size\":12, \"title\": \"Correct Attachment\"}", 12, null!]; yield return [ - "{\"size\":12.345, \"title\": \"An incorrect Attachment\"}", null!, ERR.NUMBER_CANNOT_BE_PARSED_CODE + "{\"size\":12.345, \"title\": \"An incorrect Attachment\"}", null!, COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE ]; yield return [ - "{\"size\":\"12\", \"title\": \"An incorrect Attachment\"}", null!, ERR.UNEXPECTED_JSON_TOKEN_CODE - ]; - yield return - [ - "{\"size\":\"12.345\", \"title\": \"An incorrect Attachment\"}", null!, - ERR.UNEXPECTED_JSON_TOKEN_CODE + "{\"size\":\"12\", \"title\": \"An incorrect Attachment\"}", null!, COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE ]; } #endif diff --git a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs index 45c576241b..ff7e7f6568 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersJsonPoco.cs @@ -1,10 +1,11 @@ using Hl7.Fhir.Model; using Hl7.Fhir.Utility; +using Hl7.Fhir.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Serialization.Tests { @@ -16,25 +17,6 @@ private static T serializeResource(string json) { var settings = new FhirJsonConverterOptions() { - OnPrimitiveParseFailed = (ref Utf8JsonReader reader, - Type targetType, - object originalValue, - FhirJsonException originalException) => - { - System.Diagnostics.Trace.WriteLine($"Primitive Parse Failed: {originalValue} {originalException.Message}"); - - // retry the conversion ourselves - try - { - var convertedValue = PrimitiveTypeConverter.ConvertTo(originalValue, targetType); - return (convertedValue, originalException.CloneWith(originalException.BaseErrorMessage, OperationOutcome.IssueSeverity.Warning, originalException.IssueType)); - } - catch (Exception ex) - { - System.Diagnostics.Trace.WriteLine($" => {ex.Message}"); - return (null, originalException.CloneWith(originalException.BaseErrorMessage, OperationOutcome.IssueSeverity.Fatal, originalException.IssueType)); - } - }, // Validator = null ValidateOnFailedParse = true }; @@ -75,7 +57,7 @@ public void JsonInvalidEnumerationValue() Assert.AreEqual("Patient.gender", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("PVAL116", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INVALID_CODED_VALUE_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -107,15 +89,15 @@ public void JsonInvalidEmptyObservation() Assert.AreEqual("Observation", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON120", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(FhirJsonException.OBJECTS_CANNOT_BE_EMPTY_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual("Observation.status", oc.Issue[1].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("PVAL105", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[1].Details.Coding[0].Code); Assert.AreEqual("Observation.code", oc.Issue[2].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("PVAL105", oc.Issue[2].Details.Coding[0].Code); + Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[2].Details.Coding[0].Code); Assert.AreEqual(3, oc.Issue.Count); } @@ -163,20 +145,20 @@ public void JsonInvalidPatientContainedInObservation() Assert.AreEqual("Observation.contained[0].birthDate", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("PVAL107", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual("Observation.contained[0].gender", oc.Issue[1].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("PVAL116", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.INVALID_CODED_VALUE_CODE, oc.Issue[1].Details.Coding[0].Code); Assert.AreEqual("Observation.status", oc.Issue[2].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("PVAL105", oc.Issue[2].Details.Coding[0].Code); + Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[2].Details.Coding[0].Code); Assert.IsTrue(oc.Issue[2].Details.Text.Contains("status")); Assert.AreEqual("Observation.code", oc.Issue[3].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[3].Severity); - Assert.AreEqual("PVAL105", oc.Issue[3].Details.Coding[0].Code); + Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[3].Details.Coding[0].Code); Assert.IsTrue(oc.Issue[3].Details.Text.Contains("code")); Assert.AreEqual(4, oc.Issue.Count); @@ -214,7 +196,7 @@ public void JsonInvalidDateValue() Assert.AreEqual("Patient.birthDate", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("PVAL107", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -252,7 +234,7 @@ public void JsonInvalidDateValueWithTime() Assert.AreEqual("Patient.birthDate", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("PVAL107", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -295,11 +277,11 @@ public void JsonInvalidPropertyDetected() Assert.AreEqual("Patient.name[1]", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("JSON118", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(FhirJsonException.UNKNOWN_PROPERTY_FOUND_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual("Patient", oc.Issue[1].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("JSON118", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(FhirJsonException.UNKNOWN_PROPERTY_FOUND_CODE, oc.Issue[1].Details.Coding[0].Code); Assert.AreEqual(2, oc.Issue.Count); } @@ -398,12 +380,12 @@ public void JsonInvalidDecimalValue() DebugDump.OutputJson(ex.PartialResult); Assert.AreEqual("Observation.component[5].value.value", oc.Issue[0].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON110", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); Assert.AreEqual("Observation.component[6].value.value", oc.Issue[1].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[1].Severity); - Assert.AreEqual("JSON110", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); Assert.AreEqual(2, oc.Issue.Count); } @@ -443,8 +425,8 @@ public void JsonInvalidIntegerValue() DebugDump.OutputJson(ex.PartialResult); Assert.AreEqual("Parameters.parameter[1].value", oc.Issue[0].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON110", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); Assert.AreEqual(1, oc.Issue.Count); } @@ -484,8 +466,8 @@ public void JsonInvalidBooleanValue() DebugDump.OutputJson(ex.PartialResult); Assert.AreEqual("Parameters.parameter[1].value", oc.Issue[0].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON110", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); Assert.AreEqual(1, oc.Issue.Count); } @@ -495,36 +477,36 @@ public void JsonInvalidBooleanValue() public void JsonMixedInvalidParseIssues() { // string containing a FHIR Patient with name John Doe, 17 Jan 1970, an invalid gender and an invalid date of birth - string rawData = """ - { - "resourceType": "Observation", - "id": "decimal", - "status": "glarb", - "code": { - "text": "Decimal Testing Observation" - }, - "component": [ - { - "code": { - "text": "Component" - }, - "valueQuantity": { - "value": "1.00000000000000000e-24", - "unit": "g" - } - }, - { - "code": { - "text": "Component" - }, - "valueQuantity": { - "value": "-1.00000000000000000e245", - "unit": "g" - } - } - ] - } - """; + const string rawData = """ + { + "resourceType": "Observation", + "id": "decimal", + "status": "glarb", + "code": { + "text": "Decimal Testing Observation" + }, + "component": [ + { + "code": { + "text": "Component" + }, + "valueQuantity": { + "value": "1.00000000000000000e-24", + "unit": "g" + } + }, + { + "code": { + "text": "Component" + }, + "valueQuantity": { + "value": "-1.00000000000000000e245", + "unit": "g" + } + } + ] + } + """; try { @@ -540,16 +522,16 @@ public void JsonMixedInvalidParseIssues() DebugDump.OutputJson(ex.PartialResult); Assert.AreEqual("Observation.component[0].value.value", oc.Issue[0].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON110", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); Assert.AreEqual("Observation.component[1].value.value", oc.Issue[1].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[1].Severity); - Assert.AreEqual("JSON110", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); Assert.AreEqual("Observation.status", oc.Issue[2].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("PVAL116", oc.Issue[2].Details.Coding[0].Code); + Assert.AreEqual(COVE.INVALID_CODED_VALUE_CODE, oc.Issue[2].Details.Coding[0].Code); Assert.AreEqual(3, oc.Issue.Count); } @@ -591,8 +573,8 @@ public void JsonInvalidElementId() DebugDump.OutputJson(ex.PartialResult); Assert.AreEqual("Patient.birthDate.id", oc.Issue[0].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON110", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); Assert.AreEqual(1, oc.Issue.Count); } @@ -632,8 +614,8 @@ public void JsonInvalidExtensionNonArray() DebugDump.OutputJson(ex.PartialResult); Assert.AreEqual("Patient.birthDate.extension[0]", oc.Issue[0].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON111", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); + Assert.AreEqual(FhirJsonException.EXPECTED_START_OF_ARRAY_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -674,7 +656,7 @@ public void JsonInvalidExtensionNonObjectInArray() Assert.AreEqual("Patient.birthDate.extension[0]", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[0].Severity); - Assert.AreEqual("JSON101", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(FhirJsonException.EXPECTED_START_OF_OBJECT_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -720,7 +702,7 @@ public void JsonInvalidDuplicateArray() Assert.AreEqual("Patient.name[2]", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON128", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(FhirJsonException.DUPLICATE_ARRAY_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -761,7 +743,7 @@ public void JsonInvalidDuplicateArrayExtension() Assert.AreEqual("Patient.name[0].given[2]", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON128", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(FhirJsonException.DUPLICATE_ARRAY_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(3, oc.Issue.Count); } @@ -799,17 +781,17 @@ public void JsonInvalidElementIdArrayPath() DebugDump.OutputXml(oc); DebugDump.OutputJson(ex.PartialResult); - Assert.AreEqual(OperationOutcome.IssueSeverity.Warning, oc.Issue[0].Severity); - Assert.AreEqual("JSON110", oc.Issue[0].Details.Coding[0].Code); - Assert.AreEqual("Patient.name[0].given[1].id", oc.Issue[0].Expression.First()); + Assert.AreEqual(FhirJsonException.EXPECTED_START_OF_OBJECT_CODE, oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[0].Severity); + Assert.AreEqual("Patient.name[0].given[1].extension[1]", oc.Issue[0].Expression.First()); - Assert.AreEqual(OperationOutcome.IssueSeverity.Fatal, oc.Issue[1].Severity); - Assert.AreEqual("JSON101", oc.Issue[1].Details.Coding[0].Code); - Assert.AreEqual("Patient.name[0].given[1].extension[1]", oc.Issue[1].Expression.First()); + Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); + Assert.AreEqual("Patient.name[0].given[1].extension[2].url", oc.Issue[1].Expression.First()); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[2].Details.Coding[0].Code); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("PVAL105", oc.Issue[2].Details.Coding[0].Code); - Assert.AreEqual("Patient.name[0].given[1].extension[2].url", oc.Issue[2].Expression.First()); + Assert.AreEqual("Patient.name[0].given[1].id", oc.Issue[2].Expression.First()); Assert.AreEqual(3, oc.Issue.Count); } @@ -1056,15 +1038,15 @@ public void JsonInvalidBundledResources() Assert.AreEqual("Bundle.entry[0].resource.gender", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("PVAL116", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INVALID_CODED_VALUE_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual("Bundle.entry[1].resource.birthDate", oc.Issue[1].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("PVAL107", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[1].Details.Coding[0].Code); Assert.AreEqual("Bundle.entry[2].resource.birthDate", oc.Issue[2].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("PVAL107", oc.Issue[2].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[2].Details.Coding[0].Code); Assert.AreEqual(12, oc.Issue.Count); } diff --git a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs index 193748382f..65d0ed2963 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/SerializationExcexptionHandlersXmlPoco.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Serialization.Tests { @@ -154,15 +155,15 @@ public void XmlInvalidPatientContainedInObservation() Assert.AreEqual("Observation.contained[0].active", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("XML203", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual("Observation.contained[0].gender", oc.Issue[1].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("PVAL116", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.INVALID_CODED_VALUE_CODE, oc.Issue[1].Details.Coding[0].Code); Assert.AreEqual("Observation.status", oc.Issue[2].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("PVAL105", oc.Issue[2].Details.Coding[0].Code); + Assert.AreEqual(COVE.MANDATORY_ELEMENT_CANNOT_BE_NULL_CODE, oc.Issue[2].Details.Coding[0].Code); Assert.IsTrue(oc.Issue[2].Details.Text.Contains("status")); Assert.AreEqual("Observation.code", oc.Issue[3].Expression.First()); @@ -205,7 +206,7 @@ public void XMLInvalidDateValue() Assert.AreEqual("Patient.birthDate", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("PVAL107", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -240,11 +241,11 @@ public void XMLInvalidBooleanValue() Assert.AreEqual("Patient.active", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("XML203", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual("Patient.birthDate", oc.Issue[1].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("PVAL107", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[1].Details.Coding[0].Code); Assert.AreEqual(2, oc.Issue.Count); } @@ -279,7 +280,7 @@ public void XMLInvalidDateValueWithTime() Assert.AreEqual("Patient.birthDate", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("PVAL107", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -470,7 +471,7 @@ public void XMLInvalidDecimalValue() Assert.AreEqual("Observation.component[2].value.value", oc.Issue[0].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[0].Severity); - Assert.AreEqual("XML203", oc.Issue[0].Details.Coding[0].Code); + Assert.AreEqual(COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, oc.Issue[0].Details.Coding[0].Code); Assert.AreEqual(1, oc.Issue.Count); } @@ -790,18 +791,18 @@ public void XmlInvalidBundledResources() Assert.AreEqual("Bundle.entry[1].resource.birthDate", oc.Issue[1].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[1].Severity); - Assert.AreEqual("PVAL107", oc.Issue[1].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE , oc.Issue[1].Details.Coding[0].Code); Assert.AreEqual("Bundle.entry[2].resource.birthDate", oc.Issue[2].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[2].Severity); - Assert.AreEqual("PVAL107", oc.Issue[2].Details.Coding[0].Code); + Assert.AreEqual(COVE.LITERAL_INVALID_CODE, oc.Issue[2].Details.Coding[0].Code); Assert.AreEqual("Bundle.entry[7].resource.status", oc.Issue[3].Expression.First()); Assert.AreEqual(OperationOutcome.IssueSeverity.Error, oc.Issue[3].Severity); - Assert.AreEqual("PVAL116", oc.Issue[3].Details.Coding[0].Code); + Assert.AreEqual(COVE.INVALID_CODED_VALUE_CODE, oc.Issue[3].Details.Coding[0].Code); Assert.AreEqual(4, oc.Issue.Count); } } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Shared.Tests/Rest/FhirClientTests.cs b/src/Hl7.Fhir.Shared.Tests/Rest/FhirClientTests.cs index abf6b91286..8616777383 100644 --- a/src/Hl7.Fhir.Shared.Tests/Rest/FhirClientTests.cs +++ b/src/Hl7.Fhir.Shared.Tests/Rest/FhirClientTests.cs @@ -73,10 +73,9 @@ public static void ClassInitialize(TestContext testContext) // Ignore SSL certificate errors ServicePointManager.ServerCertificateValidationCallback += (a, b, c, d) => true; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls - | SecurityProtocolType.Tls12 - | SecurityProtocolType.Tls11 - | SecurityProtocolType.Tls13; - + | SecurityProtocolType.Tls12 + | SecurityProtocolType.Tls11 + | SecurityProtocolType.Tls13; CreateItems(); } diff --git a/src/Hl7.Fhir.Shared.Tests/Serialization/ResourceParsingTests.cs b/src/Hl7.Fhir.Shared.Tests/Serialization/ResourceParsingTests.cs index 1656d7e39d..3a3c5d282d 100644 --- a/src/Hl7.Fhir.Shared.Tests/Serialization/ResourceParsingTests.cs +++ b/src/Hl7.Fhir.Shared.Tests/Serialization/ResourceParsingTests.cs @@ -6,9 +6,11 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +using FluentAssertions; using Hl7.Fhir.ElementModel; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; +using Hl7.Fhir.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; @@ -24,7 +26,7 @@ public partial class ResourceParsingTests [TestMethod] public void ConfigureFailOnUnknownMember() { - var xml = ""; + const string xml = ""; var parser = new FhirXmlParser(); parser.Settings.AllowUnrecognizedEnums = true; parser.Settings.ExceptionHandler = (object source, Utility.ExceptionNotification args) => @@ -39,7 +41,7 @@ public void ConfigureFailOnUnknownMember() try { - var r2 = parser.Parse(xml); + parser.Parse(xml); Assert.Fail("Should have failed on unknown member"); } catch (StructuralTypeException ste) @@ -233,7 +235,7 @@ public async Tasks.Task AcceptUnknownEnums() try { pser.Settings.AllowUnrecognizedEnums = false; - p = await pser.ParseAsync(xml2); + await pser.ParseAsync(xml2); Assert.Fail(); } catch (FormatException) @@ -244,7 +246,7 @@ public async Tasks.Task AcceptUnknownEnums() // Now, allow unknown enums and check support pser.Settings.AllowUnrecognizedEnums = true; p = await pser.ParseAsync(xml2); - Assert.ThrowsException(() => p.Gender); + Assert.ThrowsException(() => p.Gender); Assert.AreEqual("superman", p.GenderElement.ObjectValue); } diff --git a/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonPocoSerializer.cs b/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonPocoSerializer.cs index 4ddab46380..b9643d5990 100644 --- a/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonPocoSerializer.cs +++ b/src/Hl7.Fhir.Shims.STU3AndUp/Serialization/FhirJsonPocoSerializer.cs @@ -5,5 +5,5 @@ namespace Hl7.Fhir.Serialization; -[Obsolete("This class has been replaced by the equivalent FhirXmlSerializer class.")] +[Obsolete("This class has been replaced by the equivalent FhirJsonSerializer class.")] public class FhirJsonPocoSerializer() : BaseFhirJsonPocoSerializer(ModelInfo.ModelInspector); \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/Base64Tests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/Base64Tests.cs new file mode 100644 index 0000000000..548887ce5f --- /dev/null +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/Base64Tests.cs @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014, 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 + */ + +using FluentAssertions; +using Hl7.Fhir.Model; +using Hl7.Fhir.Validation; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Text; + +namespace Hl7.Fhir.Tests.Model; + +[TestClass] +public class Base64Tests +{ + [TestMethod] + public void SetValueUpdatesRawValue() + { + var c = new Base64Binary(); + Assert.IsNull(c.ObjectValue); + Assert.IsNull(c.Value); + + var bytes = "Hi!"u8.ToArray(); + c.Value = bytes; + c.ObjectValue.Should().Be("SGkh"); + + c.Value = null; + c.ObjectValue.Should().BeNull(); + } + + + [TestMethod] + public void SetRawValueUpdatesValue() + { + var c = new Base64Binary { ObjectValue = "SGkh" }; + Encoding.UTF8.GetString(c.Value).Should().Be("Hi!"); + + // Value gets recomputed when we change it. + // Since base64binary will only keep the parsed value or the original value, + // try to see if both remain in sync. + c.ObjectValue = "dGhlcmU="; + Encoding.UTF8.GetString(c.Value).Should().Be("there"); + c.ObjectValue.Should().Be("dGhlcmU="); + Encoding.UTF8.GetString(c.Value).Should().Be("there"); + + c.ObjectValue = null; + c.Value.Should().BeNull(); + + c.ObjectValue = "Hoi"; + Assert.ThrowsException(() => c.Value); + c.HasValidValue().Should().BeFalse(); + + c.ObjectValue = 314; + Assert.ThrowsException(() => c.Value); + c.HasValidValue().Should().BeFalse(); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/CodeEnumTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/CodeEnumTests.cs index c2a2ad94dc..c09ed30b43 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/Model/CodeEnumTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/CodeEnumTests.cs @@ -6,67 +6,72 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +using FluentAssertions; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using COVE=Hl7.Fhir.Validation.CodedValidationException; -namespace Hl7.Fhir.Tests.Model +namespace Hl7.Fhir.Tests.Model; + +[TestClass] +public class CodeEnumTests { - [TestClass] - public class CodeEnumTests + [TestMethod] + public void SetValueUpdatesRawValue() { - [TestMethod] - public void SetValueUpdatesRawValue() - { - var c = new Code(); - Assert.IsNull(c.ObjectValue); - Assert.IsNull(c.Value); + var c = new Code(); + Assert.IsNull(c.ObjectValue); + Assert.IsNull(c.Value); + + c = new Code(AdministrativeGender.Female); + Assert.AreEqual("female", c.ObjectValue); + Assert.AreEqual(AdministrativeGender.Female, c.Value); - c = new Code(AdministrativeGender.Female); - Assert.AreEqual("female", c.ObjectValue); - Assert.AreEqual(AdministrativeGender.Female, c.Value); + c.Value = AdministrativeGender.Unknown; + Assert.AreEqual("unknown", c.ObjectValue); + Assert.AreEqual(AdministrativeGender.Unknown, c.Value); + } - c.Value = AdministrativeGender.Unknown; - Assert.AreEqual("unknown", c.ObjectValue); - Assert.AreEqual(AdministrativeGender.Unknown, c.Value); - } + [TestMethod] + public void SetRawValueUpdatesValue() + { + var c = new Code(AdministrativeGender.Female) { ObjectValue = "male" }; + Assert.AreEqual(AdministrativeGender.Male, c.Value); - [TestMethod] - public void SetRawValueUpdatesValue() - { - var c = new Code(AdministrativeGender.Female); - c.ObjectValue = "male"; - Assert.AreEqual(AdministrativeGender.Male, c.Value); + c.ObjectValue = "other"; + Assert.AreEqual(AdministrativeGender.Other, c.Value); - c.ObjectValue = "maleX"; - Assert.ThrowsException(() => c.Value); + c.ObjectValue = null; + Assert.IsNull(c.Value); - c.Value = AdministrativeGender.Other; - Assert.AreEqual("other", c.ObjectValue); + c.ObjectValue = "maleX"; + Assert.ThrowsException(() => c.Value); + c.HasValidValue().Should().BeFalse(); - c.ObjectValue = null; - Assert.IsNull(c.Value); - } + c.ObjectValue = 314; + Assert.ThrowsException(() => c.Value).Message.Should().Contain("integer 314 is not the right type of literal for a code."); + c.HasValidValue().Should().BeFalse(); + } - [TestMethod] - public void TestToSystemCode() - { - var c = new Code(AdministrativeGender.Female).ToSystemCode(); + [TestMethod] + public void TestToSystemCode() + { + var c = new Code(AdministrativeGender.Female).ToSystemCode(); - Assert.AreEqual("female", c.Value); - Assert.AreEqual("http://hl7.org/fhir/administrative-gender", c.System); + Assert.AreEqual("female", c.Value); + Assert.AreEqual("http://hl7.org/fhir/administrative-gender", c.System); - c = new Code(TestEnum.IHaveNoSystem).ToSystemCode(); - Assert.AreEqual("IHaveNoSystem", c.Value); - Assert.IsNull(c.System); - } + c = new Code(TestEnum.IHaveNoSystem).ToSystemCode(); + Assert.AreEqual("IHaveNoSystem", c.Value); + Assert.IsNull(c.System); + } - [FhirEnumeration("TestEnum")] - private enum TestEnum - { - IHaveNoSystem = 4 - } + [FhirEnumeration("TestEnum")] + private enum TestEnum + { + IHaveNoSystem = 4 } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTests.cs index dd08e48c37..7ac5d1d3b3 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTests.cs @@ -8,6 +8,7 @@ using FluentAssertions; using Hl7.Fhir.Model; +using Hl7.Fhir.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -90,9 +91,9 @@ public void ToDateTimeOffsetThrowsInvalidFormat() { var dft = new Date("T45:45:56"); - Assert.ThrowsException(() => dft.ToDateTimeOffset()); + Assert.ThrowsException(() => dft.ToDateTimeOffset()); - dft.TryToDateTimeOffset(out var _).Should().BeFalse(); + dft.TryToDateTimeOffset(out _).Should().BeFalse(); } [TestMethod] @@ -109,7 +110,7 @@ public void CanConvertToDateTime() dt.Precision.Should().Be(ElementModel.Types.DateTimePrecision.Month); dft = new Date(null); - dft.TryToSystemDate(out dt).Should().BeTrue(); + dft.TryToSystemDate(out dt).Should().BeFalse(); dt.Should().BeNull(); } } diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTimeTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTimeTests.cs index c9dcd6c843..6be579de21 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTimeTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/DateTimeTests.cs @@ -8,6 +8,7 @@ using FluentAssertions; using Hl7.Fhir.Model; +using Hl7.Fhir.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -114,7 +115,7 @@ public void ToDateTimeOffsetThrowsInvalidFormat() { var dft = new FhirDateTime("T45:45:56"); - Assert.ThrowsException(() => dft.ToDateTimeOffset(TimeSpan.Zero)); + Assert.ThrowsException(() => dft.ToDateTimeOffset(TimeSpan.Zero)); dft.TryToDateTimeOffset(out var _).Should().BeFalse(); } @@ -134,16 +135,16 @@ public void TimeZoneHandlingWithTZ() } [TestMethod] - public void TimeZoneHandlingNoTZ() + public void TimeZoneHandlingPartial() { - var dft = new FhirDateTime("2023-07-11T13:00:00"); + var dft = new FhirDateTime("2023-07-11"); - // Assume UTC, so +2 is 15:00 - dft.ToDateTimeOffset(TimeSpan.FromHours(2)).Hour.Should().Be(15); + // Assume UTC, so +2 is midnight + 2 + dft.ToDateTimeOffset(TimeSpan.FromHours(2)).Hour.Should().Be(2); dft.TryToDateTimeOffset(out _).Should().BeFalse(); // only works with a TZ present. dft.TryToDateTimeOffset(TimeSpan.FromHours(1), out var dto).Should().BeTrue(); - dto.Hour.Should().Be(13); // unchanged + dto.Hour.Should().Be(0); // unchanged dto.Offset.Hours.Should().Be(1); } @@ -166,7 +167,7 @@ public void CanConvertToDateTime() dft.TryToSystemDateTime(out dt).Should().BeFalse(); dft = new FhirDateTime(null); - dft.TryToSystemDateTime(out dt).Should().BeTrue(); + dft.TryToSystemDateTime(out dt).Should().BeFalse(); dt.Should().BeNull(); } } diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/InstantTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/InstantTests.cs new file mode 100644 index 0000000000..bd089020de --- /dev/null +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/InstantTests.cs @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2014, 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 + */ + +using FluentAssertions; +using Hl7.Fhir.Model; +using Hl7.Fhir.Validation; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; + +namespace Hl7.Fhir.Tests.Model; + +[TestClass] +public class InstantTests +{ + [TestMethod] + public void SetValueUpdatesRawValue() + { + var c = new Instant(); + Assert.IsNull(c.ObjectValue); + Assert.IsNull(c.Value); + + c = new Instant(DateTimeOffset.UnixEpoch); + Assert.AreEqual(ElementModel.Types.DateTime.FormatDateTimeOffset(DateTimeOffset.UnixEpoch), c.ObjectValue); + Assert.AreEqual(DateTimeOffset.UnixEpoch, c.Value); + + var now = DateTimeOffset.Now; + c.Value = now; + Assert.AreEqual(ElementModel.Types.DateTime.FormatDateTimeOffset(now), c.ObjectValue); + Assert.AreEqual(now, c.Value); + } + + + [TestMethod] + public void SetRawValueUpdatesValue() + { + var c = new Instant { ObjectValue = ElementModel.Types.DateTime.FormatDateTimeOffset(DateTimeOffset.UnixEpoch) }; + Assert.AreEqual(DateTimeOffset.UnixEpoch, c.Value); + + c.ObjectValue = null; + Assert.IsNull(c.Value); + + c.ObjectValue = "nonsense"; + Assert.ThrowsException(() => c.Value); + c.HasValidValue().Should().BeFalse(); + + c.ObjectValue = 314; + Assert.ThrowsException(() => c.Value); + c.HasValidValue().Should().BeFalse(); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/Integer64Tests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/Integer64Tests.cs new file mode 100644 index 0000000000..5711fe68e9 --- /dev/null +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/Integer64Tests.cs @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014, 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 + */ + +using FluentAssertions; +using Hl7.Fhir.Model; +using Hl7.Fhir.Validation; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; + +namespace Hl7.Fhir.Tests.Model; + +[TestClass] +public class Integer64Tests +{ + [TestMethod] + public void SetValueUpdatesRawValue() + { + var c = new Integer64(); + Assert.IsNull(c.ObjectValue); + Assert.IsNull(c.Value); + + c = new Integer64(3); + Assert.AreEqual("3", c.ObjectValue); + Assert.AreEqual(3, c.Value); + + c.Value = 5; + Assert.AreEqual("5", c.ObjectValue); + Assert.AreEqual(5, c.Value); + } + + + [TestMethod] + public void SetRawValueUpdatesValue() + { + var c = new Integer64 { ObjectValue = "7" }; + Assert.AreEqual(7, c.Value); + + c.ObjectValue = null; + Assert.IsNull(c.Value); + + c.ObjectValue = "nonsense"; + Assert.ThrowsException(() => c.Value); + c.HasValidValue().Should().BeFalse(); + + c.ObjectValue = 314; + Assert.ThrowsException(() => c.Value); + c.HasValidValue().Should().BeFalse(); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/TimeTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/TimeTests.cs index 53fdce0d09..92aa4477b0 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/Model/TimeTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/TimeTests.cs @@ -108,8 +108,8 @@ public void CanConvertToTime() dt.Precision.Should().Be(ElementModel.Types.DateTimePrecision.Fraction); dft = new Time(null); - dft.TryToSystemTime(out dt).Should().BeTrue(); + dft.TryToSystemTime(out dt).Should().BeFalse(); dt.Should().BeNull(); } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Model/TypedElementToPocoTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/Model/TypedElementToPocoTests.cs index f85645df7a..4d225c8475 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/Model/TypedElementToPocoTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/Model/TypedElementToPocoTests.cs @@ -48,7 +48,7 @@ public static IEnumerable PrimitiveTestData [ [typeof(FhirString), "hi!", null], [typeof(Integer), 42, null], - [typeof(Integer64), 42L, null], + [typeof(Integer64), 42L, "42"], [typeof(FhirBoolean), true, null], [typeof(FhirDecimal), 3.14m, null], [typeof(DynamicPrimitive), 3.14, null], diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs index b193e4c796..bc77853ebe 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs @@ -10,7 +10,6 @@ using System.Buffers; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -21,1165 +20,980 @@ #nullable enable -namespace Hl7.Fhir.Support.Poco.Tests +namespace Hl7.Fhir.Support.Poco.Tests; + +[TestClass] +public class FhirJsonDeserializationTests { - [TestClass] - public class FhirJsonDeserializationTests + private static BaseFhirJsonPocoDeserializer getTestDeserializer(FhirJsonConverterOptions settings) => + new(typeof(Patient).Assembly, settings); + + + [TestMethod] + public void PrimitiveValueCannotBeComplex() + { + ParsePrimitiveValue(new { bla = 4 }, typeof(FhirBoolean), ERR.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE); + } + + [DataTestMethod] + [DataRow("OperationOutcome", null)] + [DataRow("OperationOutcomeX", ERR.UNKNOWN_RESOURCE_TYPE_CODE)] + [DataRow("Meta", null)] + [DataRow(4, ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE)] + [DataRow(null, ERR.NO_RESOURCETYPE_PROPERTY_CODE)] + public void DeriveClassMapping(object? typename, string? errorcode) { - [DataTestMethod] - [DataRow(null, null, typeof(decimal), null, ERR.EXPECTED_PRIMITIVE_NOT_NULL_CODE)] - [DataRow(new[] { 1, 2 }, null, typeof(decimal), null, ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE)] - [DataRow("hi!", "hi!", typeof(string), null, null)] - [DataRow("SGkh", null, typeof(byte[]), null, null)] - [DataRow("hi!", null, typeof(byte[]), null, ERR.INCORRECT_BASE64_DATA_CODE)] - [DataRow("hi!", null, typeof(DateTimeOffset), null, ERR.STRING_ISNOTAN_INSTANT_CODE)] - [DataRow("2007-02-03", null, typeof(DateTimeOffset), null, null)] - [DataRow("enumvalue", null, typeof(UriFormat), null, COVE.INVALID_CODED_VALUE_CODE)] - [DataRow(true, "true", typeof(Enum), null, ERR.UNEXPECTED_JSON_TOKEN_CODE)] - [DataRow("hi!", "hi!", typeof(int), null, ERR.UNEXPECTED_JSON_TOKEN_CODE)] - [DataRow("", "", typeof(string), null, ERR.PROPERTY_MAY_NOT_BE_EMPTY_CODE)] - [DataRow(3, 3, typeof(decimal), null, null)] - [DataRow(3, 3, typeof(uint), null, null)] - [DataRow(3L, 3L, typeof(long), typeof(Integer64), ERR.LONG_INCORRECT_FORMAT_CODE)] - [DataRow(3L, 3L, typeof(long), typeof(UnsignedInt), null)] - [DataRow(3, 3, typeof(ulong), null, null)] - [DataRow(3.14, 3.14, typeof(decimal), null, null)] - [DataRow(3.14, "3.14", typeof(int), null, ERR.NUMBER_CANNOT_BE_PARSED_CODE)] - [DataRow(3.14, "3.14", typeof(uint), null, ERR.NUMBER_CANNOT_BE_PARSED_CODE)] - [DataRow(3.14, "3.14", typeof(long), null, ERR.NUMBER_CANNOT_BE_PARSED_CODE)] - [DataRow(-3, "-3", typeof(ulong), null, ERR.NUMBER_CANNOT_BE_PARSED_CODE)] - [DataRow(long.MaxValue, long.MaxValue, typeof(decimal), null, null)] - [DataRow(5, 5, typeof(float), null, null)] - [DataRow(6.14, 6.14, typeof(double), null, null)] - [DataRow(314, 314, typeof(int), null, null)] - [DataRow(314, 314, typeof(decimal), null, null)] - [DataRow(3.14, "3.14", typeof(bool), null, ERR.UNEXPECTED_JSON_TOKEN_CODE)] - [DataRow(true, true, typeof(bool), null, null)] - [DataRow(true, "true", typeof(string), null, ERR.UNEXPECTED_JSON_TOKEN_CODE)] - public void TryDeserializePrimitiveValue(object input, object expectedResult, Type expectedImplementingType, - Type? fhirType, string code) + var (result, error) = test(typename); + if (errorcode is null) + error.Should().BeNull(); + else + error?.ErrorCode.Should().Be(errorcode); + + if (errorcode is null) + result!.Name.Should().Be((string?)typename); + + static (ClassMapping?, FhirJsonException?) test(object? typename) { - var reader = constructReader(input); + var inspector = ModelInspector.ForAssembly(typeof(Resource).Assembly); + + var jsonBytes = typename != null + ? JsonSerializer.SerializeToUtf8Bytes(new { resourceType = typename }) + : JsonSerializer.SerializeToUtf8Bytes(new { resourceTypeX = "wrong" }); + var reader = new Utf8JsonReader(jsonBytes); reader.Read(); - var deserializer = getTestDeserializer(new()); var ps = new PathStack(); ps.EnterElement("Patient", 0, false); - var (result, error) = - deserializer.DeserializePrimitiveValue(ref reader, expectedImplementingType, fhirType, ps); - - if (code is not null) - error?.ErrorCode.Should().Be(code); - else - error.Should().BeNull(); - - if (expectedImplementingType == typeof(byte[])) - { - if (error is null) - Convert.ToBase64String((byte[])result!).Should().Be((string)input); - else - result.Should().Be(input); - } - else if (expectedImplementingType == typeof(DateTimeOffset)) - { - if (error is null) - result.Should().BeOfType().Which.ToFhirDate().Should().Be((string)input); - else - result.Should().Be(input); - } - else if (code == ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE || - code == ERR.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE) -#pragma warning disable CS0642 // Possible mistaken empty statement - ; // nothing to check -#pragma warning restore CS0642 // Possible mistaken empty statement - else - { - if (error is null) - result.Should().Be(input); - else - result.Should().Be(expectedResult); - } + return BaseFhirJsonPocoDeserializer.DetermineClassMappingFromInstance(ref reader, inspector, ps); } + } - private static BaseFhirJsonPocoDeserializer getTestDeserializer(FhirJsonConverterOptions settings) => - new(typeof(Patient).Assembly, settings); + [DataTestMethod] + [DataRow(null, typeof(FhirString), ERR.EXPECTED_PRIMITIVE_NOT_NULL_CODE)] + [DataRow(new[] { 1, 2 }, typeof(FhirString), ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE)] + [DataRow("SGkh", typeof(FhirString), null, "SGkh")] + [DataRow(4, typeof(FhirString), COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, 4)] + [DataRow("SGkh", typeof(Base64Binary), null, "SGkh")] + [DataRow("hi!", typeof(Base64Binary), COVE.INVALID_BASE64_VALUE_CODE, "hi!")] + [DataRow(4, typeof(Base64Binary), COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, 4)] + [DataRow("2007-04", typeof(FhirDateTime), null, "2007-04")] + [DataRow("", typeof(FhirDateTime), ERR.PROPERTY_MAY_NOT_BE_EMPTY_CODE, null)] + [DataRow("2007-", typeof(FhirDateTime), COVE.LITERAL_INVALID_CODE, "2007-")] + [DataRow(true, typeof(FhirDateTime), COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, true)] + [DataRow("female", typeof(Code), null, "female")] + [DataRow("is-a", typeof(Code), null, "is-a")] + [DataRow("wrong", typeof(Code), COVE.INVALID_CODED_VALUE_CODE, + "wrong")] // just sets ObjectValue, POCO validation handles enum checks + [DataRow(true, typeof(Code), COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, true)] + [DataRow("hi!", typeof(Instant), COVE.LITERAL_INVALID_CODE)] + [DataRow("2007-02-03T12:00:00Z", typeof(Instant), null, "2007-02-03T12:00:00Z")] + [DataRow(3, typeof(FhirDecimal), null, 3)] + [DataRow(3.14, typeof(FhirDecimal), null, 3.14)] + [DataRow(3L, typeof(Integer64), COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE)] + [DataRow("hoi", typeof(Integer64), COVE.LITERAL_INVALID_CODE)] + [DataRow("3", typeof(Integer64), null, "3")] + [DataRow(314, typeof(Integer), null, 314)] + [DataRow(3.14, typeof(FhirBoolean), COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE)] + [DataRow(true, typeof(FhirBoolean), null, true)] + public void ParsePrimitiveValue(object value, Type targetType, string? errorcode, + object? expectedObjectValue = null) + { + var state = new FhirJsonPocoDeserializerState(); - [TestMethod] - public void TestCustomRecovery() + PrimitiveType test() { - var (result, error) = test(1); - result.Should().Be(false); + var inspector = ModelInspector.ForAssembly(typeof(Patient).Assembly); + var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly); + var mapping = inspector.ImportType(targetType)!; - (result, error) = test(11); - result.Should().Be(true); + var reader = constructReader(value); + reader.Read(); - (result, error) = test(21); - result.Should().Be("21"); - error?.ErrorCode.Should().Be(ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); + return deserializer.DeserializeFhirPrimitive(null, "dummy", mapping, null!, ref reader, null, state); + } - (result, error) = test(31); - result.Should().BeNull(); - error?.ErrorCode.Should().Be(ERR.UNEXPECTED_JSON_TOKEN_CODE); + var result = test(); - try - { - _ = test(41); - Assert.Fail(); - } - catch (InvalidOperationException) - { - } + if (state.Errors.HasExceptions) + { + if (errorcode is not null) + state.Errors.Should().OnlyContain(ce => ce.ErrorCode == errorcode); + else + errorcode.Should().BeNull(because: state.Errors.ToString()); + } + else + { + errorcode.Should().BeNull(because: state.Errors.ToString()); + } - static (object?, FhirJsonException?) correctIntToBool(ref Utf8JsonReader reader, - Type targetType, - object? originalValue, - FhirJsonException originalException) - { - return reader.GetInt32() switch - { - < 10 => (false, null), - < 20 => (true, null), - < 30 => (originalValue, ERR.ARRAYS_CANNOT_BE_EMPTY(ref reader, "Patient")), - < 40 => (null, originalException), - _ => throw new InvalidOperationException("Something") - }; - } + if (expectedObjectValue is not null) + result.ObjectValue.Should().BeEquivalentTo(expectedObjectValue); + } - static (object?, FhirJsonException?) test(int number) - { - var reader = constructReader(number); - reader.Read(); - var deserializer = getTestDeserializer(new() { OnPrimitiveParseFailed = correctIntToBool }); - var ps = new PathStack(); - ps.EnterElement("Patient", 0, false); - return deserializer.DeserializePrimitiveValue(ref reader, typeof(bool), null, ps); - } - } + private static (Base?, IReadOnlyCollection) deserializeComplex(Type objectType, + object testObject, out Utf8JsonReader readerState, + FhirJsonConverterOptions settings) + { + // For the tests, enable full XHML validation so we can test it when necessary. + var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly, settings); + Utf8JsonReader reader = constructReader(testObject); + reader.Read(); - [TestMethod] - public void PrimitiveValueCannotBeComplex() + try { - TryDeserializePrimitiveValue(new { bla = 4 }, null!, typeof(int), null, - ERR.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE); - TryDeserializePrimitiveValue(double.MaxValue, double.MaxValue.ToString(CultureInfo.InvariantCulture), - typeof(decimal), null, FhirJsonException.NUMBER_CANNOT_BE_PARSED_CODE); - TryDeserializePrimitiveValue(long.MaxValue, long.MaxValue.ToString(), typeof(uint), null, - ERR.NUMBER_CANNOT_BE_PARSED_CODE); - TryDeserializePrimitiveValue(long.MaxValue, long.MaxValue.ToString(), typeof(int), null, - ERR.NUMBER_CANNOT_BE_PARSED_CODE); - TryDeserializePrimitiveValue(double.MaxValue, double.MaxValue.ToString(CultureInfo.InvariantCulture), - typeof(float), null, FhirJsonException.NUMBER_CANNOT_BE_PARSED_CODE); - } + var result = objectType.IsAssignableTo(typeof(Resource)) + ? deserializer.DeserializeResource(ref reader) + : deserializer.DeserializeObject(objectType, ref reader); - [DataTestMethod] - [DataRow("OperationOutcome", null)] - [DataRow("OperationOutcomeX", ERR.UNKNOWN_RESOURCE_TYPE_CODE)] - [DataRow("Meta", null)] - [DataRow(4, ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE)] - [DataRow(null, ERR.NO_RESOURCETYPE_PROPERTY_CODE)] - public void DeriveClassMapping(object typename, string errorcode) + readerState = reader; // copy + return (result, Array.Empty()); + } + catch (DeserializationFailedException dfe) { - var (result, error) = test(typename); - if (errorcode is null) - error.Should().BeNull(); - else - error?.ErrorCode.Should().Be(errorcode); + readerState = reader; + return (dfe.PartialResult, dfe.Exceptions); + } + } - if (errorcode is null) - result!.Name.Should().Be((string)typename); + private static Utf8JsonReader constructReader(object testObject) + { + var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(testObject); + var reader = new Utf8JsonReader(jsonBytes); + return reader; + } - static (ClassMapping?, FhirJsonException?) test(object typename) - { - var inspector = ModelInspector.ForAssembly(typeof(Resource).Assembly); + private static void assertErrors(IEnumerable actual, string[] expected) + { + if (expected.Length == 0 && !actual.Any()) + return; + + string why = + $"Should be the same: actual [{string.Join(",", actual.Select(a => a.ErrorCode))}] and expected [{string.Join(";", expected)}]"; + Console.WriteLine("Messages: " + string.Join(", ", actual.Select(a => a.Message))); + actual.Count().Should().Be(expected.Length, because: why); + _ = actual.Zip(expected, (a, e) => a.ErrorCode.Should().Be(e, because: why)).ToList(); + Console.WriteLine($"Found {string.Join(", ", actual.Select(a => a.Message))}"); + } - var jsonBytes = typename != null - ? JsonSerializer.SerializeToUtf8Bytes(new { resourceType = typename }) - : JsonSerializer.SerializeToUtf8Bytes(new { resourceTypeX = "wrong" }); - var reader = new Utf8JsonReader(jsonBytes); - reader.Read(); + [TestMethod] + [DynamicData(nameof(TestDeserializeResourceData))] + [DynamicData(nameof(TestDeserializeNestedResource))] + public void TestDeserializeResource(object testObject, JsonTokenType tokenAfterParsing, params string[] errors) + { + var reader = constructReader(testObject); + reader.Read(); + + var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly); + var state = new FhirJsonPocoDeserializerState(); + _ = deserializer.DeserializeResourceInternal(ref reader, state, stayOnLastToken: false); + assertErrors(state.Errors, errors); + reader.TokenType.Should().Be(tokenAfterParsing); + } - var ps = new PathStack(); - ps.EnterElement("Patient", 0, false); - return BaseFhirJsonPocoDeserializer.DetermineClassMappingFromInstance(ref reader, inspector, ps); - } + public static IEnumerable TestDeserializeResourceData + { + get + { + yield return [5, JsonTokenType.Number, ERR.EXPECTED_START_OF_OBJECT_CODE]; + yield return [new { }, JsonTokenType.EndObject, ERR.NO_RESOURCETYPE_PROPERTY_CODE]; + yield return + [ + new { resourceType = 4, crap = 4 }, JsonTokenType.EndObject, + ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE + ]; + yield return + [ + new { resourceType = "Doesnotexist", crap = 5 }, JsonTokenType.EndObject, + ERR.UNKNOWN_RESOURCE_TYPE_CODE + ]; + yield return + [ + new { resourceType = nameof(OperationOutcome), crap = 5 }, JsonTokenType.EndObject, + ERR.UNKNOWN_PROPERTY_FOUND_CODE + ]; + yield return + [ + new { resourceType = nameof(Meta) }, JsonTokenType.EndObject, ERR.OBJECTS_CANNOT_BE_EMPTY_CODE, + ERR.RESOURCE_TYPE_NOT_A_RESOURCE_CODE + ]; + yield return + [ + new { resourceType = "Patient", deceasedDateTime = "2022-05" }, JsonTokenType.EndObject + ]; + yield return + [ + new + { + resourceType = "Patient", + deceasedDateTime = "2022-05", + _deceasedDateTime = + new { extension = new object[] { new { url = "test", valueString = "Smile" } } } + }, + JsonTokenType.EndObject + ]; } + } - [DataTestMethod] - [DataRow(null, typeof(FhirString), ERR.EXPECTED_PRIMITIVE_NOT_NULL_CODE)] - [DataRow(new[] { 1, 2 }, typeof(FhirString), ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE)] - [DataRow("SGkh", typeof(FhirString), null, "SGkh")] - [DataRow("SGkh", typeof(Base64Binary), null, new byte[] { 72, 105, 33 })] - [DataRow("hi!", typeof(Base64Binary), ERR.INCORRECT_BASE64_DATA_CODE, "hi!")] - [DataRow(4, typeof(Base64Binary), ERR.UNEXPECTED_JSON_TOKEN_CODE, "4")] - [DataRow("2007-04", typeof(FhirDateTime), null, "2007-04")] - [DataRow("", typeof(FhirDateTime), ERR.PROPERTY_MAY_NOT_BE_EMPTY_CODE, null)] - [DataRow("2007-", typeof(FhirDateTime), COVE.DATETIME_LITERAL_INVALID_CODE, "2007-")] - [DataRow(4.45, typeof(FhirDateTime), ERR.UNEXPECTED_JSON_TOKEN_CODE, "4.45")] - [DataRow("female", typeof(Code), null, "female")] - [DataRow("is-a", typeof(Code), null, "is-a")] - [DataRow("wrong", typeof(Code), COVE.INVALID_CODED_VALUE_CODE, - "wrong")] // just sets ObjectValue, POCO validation handles enum checks - [DataRow(true, typeof(Code), ERR.UNEXPECTED_JSON_TOKEN_CODE, "true")] - [DataRow("hi!", typeof(Instant), ERR.STRING_ISNOTAN_INSTANT_CODE)] - [DataRow("2007-02-03", typeof(Instant), null, 2007)] - public void ParsePrimitiveValue(object value, Type targetType, string errorcode, - object? expectedObjectValue = null) + public static IEnumerable TestDeserializeNestedResource + { + get { - var state = new FhirJsonPocoDeserializerState(); + yield return + [ + new + { + resourceType = "Parameters", + parameter = new[] + { + new { name = "a", resource = new { resourceType = "Patient", active = true } } + } + }, + JsonTokenType.EndObject + ]; + } + } - PrimitiveType test() + [TestMethod] + [DynamicData(nameof(TestPrimitiveArrayData), DynamicDataSourceType.Method)] + [DynamicData(nameof(CatchesIncorrectlyStructuredComplexData), DynamicDataSourceType.Method)] + [DynamicData(nameof(TestNormalArrayData), DynamicDataSourceType.Method)] + [DynamicData(nameof(TestPrimitiveData), DynamicDataSourceType.Method)] + [DynamicData(nameof(TestValidatePrimitiveData), DynamicDataSourceType.Method)] + public void TestData(Type t, object testObject, JsonTokenType token, Action? verify, + params string[] expectedErrors) + { + // Enable full narrative validation so we can test for it + var (result, errors) = deserializeComplex(t, testObject, out var readerState, + new FhirJsonConverterOptions { - var inspector = ModelInspector.ForAssembly(typeof(Patient).Assembly); - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly); - var mapping = inspector.ImportType(targetType)!; + Validator = new DataAnnotationDeserialzationValidator( + narrativeValidation: NarrativeValidationKind.FhirXhtml) + }); + + assertErrors(errors, expectedErrors); + readerState.TokenType.Should().Be(token); + result.Should().BeOfType(t); + verify?.Invoke(result); + } - var reader = constructReader(value); - reader.Read(); + private static object?[] data(object data, Action verifier, params object[] args) => + new[] { typeof(T), data, JsonTokenType.EndObject, verifier }.Concat(args).ToArray(); - return deserializer.DeserializeFhirPrimitive(null, "dummy", mapping, null!, ref reader, null, state); - } + private static object?[] data(object data, JsonTokenType token, params object[] args) => + new[] { typeof(T), data, token, null }.Concat(args).ToArray(); - var result = test(); + private static object?[] data(object data, params object[] args) => + new[] { typeof(T), data, JsonTokenType.EndObject, null }.Concat(args).ToArray(); - state.Errors.HasExceptions.Should().Be(errorcode is not null); - if (state.Errors.HasExceptions) - { - if (errorcode is not null) - state.Errors.Should().OnlyContain(ce => ce.ErrorCode == errorcode); - else - throw state.Errors.Single(); - } + public static IEnumerable CatchesIncorrectlyStructuredComplexData() + { + yield return + [ + typeof(Extension), 5, JsonTokenType.Number, null, + ERR.EXPECTED_START_OF_OBJECT_CODE + ]; + yield return data(5, JsonTokenType.Number, ERR.EXPECTED_START_OF_OBJECT_CODE); + yield return data(new[] { 2, 3 }, JsonTokenType.EndArray, ERR.EXPECTED_START_OF_OBJECT_CODE); + yield return data(new { }, ERR.OBJECTS_CANNOT_BE_EMPTY_CODE); + yield return data(new { }, ERR.OBJECTS_CANNOT_BE_EMPTY_CODE); + yield return data(new { unknown = "test" }, ERR.UNKNOWN_PROPERTY_FOUND_CODE); + yield return data(new { url = "test" }); + yield return data(new { _url = "test" }, ERR.EXPECTED_START_OF_OBJECT_CODE); + yield return data(new { unknown = "test", url = "test" }, + ERR.UNKNOWN_PROPERTY_FOUND_CODE); + yield return data(new { value = "no type suffix" }, ERR.CHOICE_ELEMENT_HAS_NO_TYPE_CODE); + yield return data(new { valueUnknown = "incorrect type suffix" }, + ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE); + yield return data(new { valueBoolean = true, url = "http://something.nl" }, + JsonTokenType.EndObject); + yield return data(new { valueUnknown = "incorrect type suffix", unknown = "unknown" }, + ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE, ERR.UNKNOWN_PROPERTY_FOUND_CODE); + } - if (expectedObjectValue is not null) - { - if (targetType != typeof(Instant)) - result.ObjectValue.Should().BeEquivalentTo(expectedObjectValue); - else - result.ObjectValue.Should().BeOfType().Which.Year.Should() - .Be((int)expectedObjectValue!); - } + public static IEnumerable TestNormalArrayData() + { + yield return data(new { name = "Ewout", telecom = 4 }, checkName, + ERR.EXPECTED_START_OF_ARRAY_CODE, ERR.EXPECTED_START_OF_OBJECT_CODE); + yield return data(new { name = "Ewout", telecom = Array.Empty() }, checkName, + ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); + yield return data( + new { name = "Ewout", telecom = new object[] { new { system = "phone" }, new { systemX = "b" } } }, + checkData, ERR.UNKNOWN_PROPERTY_FOUND_CODE); + yield return data( + new { name = "Ewout", _telecom = new object[] { new { system = "phone" }, new { systemX = "b" } } }, + checkData, ERR.USE_OF_UNDERSCORE_ILLEGAL_CODE, ERR.UNKNOWN_PROPERTY_FOUND_CODE); + yield return data(new { name = new[] { "Ewout" } }, ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE); + + static void checkName(object parsed) => + parsed.Should().BeOfType().Which.Name.Should().Be("Ewout"); + + static void checkData(object parsedObject) + { + checkName(parsedObject); + + var parsed = parsedObject.Should().BeOfType().Subject; + parsed.Telecom.Count.Should().Be(2); + parsed.Telecom[0].System.Should().Be(ContactPoint.ContactPointSystem.Phone); + parsed.Telecom[1].EnumerateElements().Count().Should().Be(0); } + } - private static (Base?, IReadOnlyCollection) deserializeComplex(Type objectType, - object testObject, out Utf8JsonReader readerState, - FhirJsonConverterOptions settings) - { - // For the tests, enable full XHML validation so we can test it when necessary. - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly, settings); - Utf8JsonReader reader = constructReader(testObject); - reader.Read(); + public static IEnumerable TestPrimitiveData() + { + yield return data(new { name = new[] { "Ewout" } }, ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE); + yield return data(new { name = new { dummy = "Ewout" } }, + ERR.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE); + yield return data(new { _name = new[] { "Ewout" } }, ERR.EXPECTED_START_OF_OBJECT_CODE); + yield return data(new { _name = "Ewout" }, ERR.EXPECTED_START_OF_OBJECT_CODE); + yield return data(new { name = "Ewout" }, checkName); + yield return data(new { _name = new { id = "12345" } }, checkId); + yield return data(new { _name = new { id = true } }, COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE); + yield return data(new { name = "Ewout", _name = new { id = "12345" } }, checkAll); - try - { - var result = objectType.IsAssignableTo(typeof(Resource)) - ? deserializer.DeserializeResource(ref reader) - : deserializer.DeserializeObject(objectType, ref reader); + static void checkName(object parsed) => parsed.Should().BeOfType().Which.NameElement.Value + .Should().Be("Ewout"); - readerState = reader; // copy - return (result, Array.Empty()); - } - catch (DeserializationFailedException dfe) - { - readerState = reader; - return (dfe.PartialResult, dfe.Exceptions); - } - } + static void checkId(object parsed) => parsed.Should().BeOfType().Which.NameElement.ElementId + .Should().Be("12345"); - private static Utf8JsonReader constructReader(object testObject) + static void checkAll(object parsed) { - var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(testObject); - var reader = new Utf8JsonReader(jsonBytes); - return reader; + checkName(parsed); + checkId(parsed); } + } - private static void assertErrors(IEnumerable actual, string[] expected) + public static IEnumerable TestValidatePrimitiveData() + { + yield return data(new { - if (expected.Length == 0 && !actual.Any()) - return; - - string why = - $"Should be the same: actual [{string.Join(",", actual.Select(a => a.ErrorCode))}] and expected [{string.Join(";", expected)}]"; - Console.WriteLine("Messages: " + string.Join(", ", actual.Select(a => a.Message))); - actual.Count().Should().Be(expected.Length, because: why); - _ = actual.Zip(expected, (a, e) => a.ErrorCode.Should().Be(e, because: why)).ToList(); - Console.WriteLine($"Found {string.Join(", ", actual.Select(a => a.Message))}"); - } + div = "

correct

", status = "additional" + }); + yield return data(new { div = "this isn't xml" }, COVE.NARRATIVE_XML_IS_MALFORMED_CODE); + yield return data(new { div = "" }, COVE.NARRATIVE_XML_IS_INVALID_CODE); - [TestMethod] - [DynamicData(nameof(TestDeserializeResourceData))] - [DynamicData(nameof(TestDeserializeNestedResource))] - public void TestDeserializeResource(object testObject, JsonTokenType tokenAfterParsing, params string[] errors) - { - var reader = constructReader(testObject); - reader.Read(); + yield return data(new { url = "urn:oid:1.3.6.1.4.1.343" }); + // yield return data(new { url = "urn:oid:1" }, COVE.URI_LITERAL_INVALID_CODE); + // This is an URL in our datamodel now, since that's what it is in R4 and later. + } - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Patient).Assembly); - var state = new FhirJsonPocoDeserializerState(); - _ = deserializer.DeserializeResourceInternal(ref reader, state, stayOnLastToken: false); - assertErrors(state.Errors, errors); - reader.TokenType.Should().Be(tokenAfterParsing); + public static IEnumerable TestPrimitiveArrayData() + { + yield return data
(new { line = "hi!" }, ERR.EXPECTED_START_OF_ARRAY_CODE); + yield return data
(new { line = Array.Empty() }, ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); + yield return data
(new { line = Array.Empty(), _line = Array.Empty() }, + ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); + yield return data
(new { line = Array.Empty(), _line = new string?[] { null } }, + ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); + yield return data
(new { line = new string?[] { null }, _line = new[] { new { id = "1" } } }, + ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); + yield return data
(new { line = new[] { "Ewout" }, _line = new string?[] { null } }, + ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); + yield return data
(new { line = new string?[] { null }, _line = new string?[] { null } }, + ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); + yield return data
(new { line = new string?[] { null }, _line = new string?[] { null, null } }, + ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); + yield return data
(new { line = new string?[] { null, null }, _line = new string?[] { null } }, + ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); + yield return data
(new { line = new[] { "Ewout", "Wouter" } }, checkName); + yield return data
( + new { line = new[] { "Ewout", "Wouter" }, _line = new[] { new { id = "1" } } }, checkId1AndName); + yield return data
( + new { line = new[] { "Ewout", "Wouter" }, _line = new[] { new { id = "1" }, null } }, checkId1AndName); + yield return data
( + new { line = new[] { "Ewout", "Wouter" }, _line = new[] { new { id = "1" }, new { id = "2" } } }, + checkAll); + yield return data
(new + { + line = new[] { "Ewout", null }, _line = new[] { null, new { id = "2" } } + }); + yield return data
( + new { line = new[] { "Ewout", null }, _line = new[] { new { id = "1" }, null } }, checkId1, + COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE); + yield return data
(new { _line = new[] { new { id = "1" }, null } }, checkId1, + COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE); + yield return data
(new { _line = new[] { new { id = "1" }, new { id = "2" } } }, checkIds); + + static void checkName(object parsed) => parsed.Should().BeOfType
().Which.Line.Should() + .BeEquivalentTo("Ewout", "Wouter"); + + static void checkIds(object parsed) => + parsed.Should().BeOfType
().Which.LineElement.Select(le => le?.ElementId).Should() + .BeEquivalentTo("1", "2"); + + static void checkId1(object parsed) => + parsed.Should().BeOfType
().Which.LineElement.Select(le => le?.ElementId).Should() + .BeEquivalentTo("1", null); + + static void checkId1AndName(object parsed) + { + checkName(parsed); + checkId1(parsed); + } + + static void checkAll(object parsed) + { + checkName(parsed); + checkIds(parsed); } + } - public static IEnumerable TestDeserializeResourceData - { - get - { - yield return new object[] { 5, JsonTokenType.Number, ERR.EXPECTED_START_OF_OBJECT_CODE }; - yield return new object[] { new { }, JsonTokenType.EndObject, ERR.NO_RESOURCETYPE_PROPERTY_CODE }; - yield return new object[] - { - new { resourceType = 4, crap = 4 }, JsonTokenType.EndObject, - ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE - }; - yield return new object[] - { - new { resourceType = "Doesnotexist", crap = 5 }, JsonTokenType.EndObject, - ERR.UNKNOWN_RESOURCE_TYPE_CODE - }; - yield return new object[] - { - new { resourceType = nameof(OperationOutcome), crap = 5 }, JsonTokenType.EndObject, - ERR.UNKNOWN_PROPERTY_FOUND_CODE - }; - yield return new object[] - { - new { resourceType = nameof(Meta) }, JsonTokenType.EndObject, ERR.OBJECTS_CANNOT_BE_EMPTY_CODE, - ERR.RESOURCE_TYPE_NOT_A_RESOURCE_CODE - }; - yield return new object[] - { - new { resourceType = "Patient", deceasedDateTime = "2022-05" }, JsonTokenType.EndObject - }; - yield return new object[] - { - new - { - resourceType = "Patient", - deceasedDateTime = "2022-05", - _deceasedDateTime = - new { extension = new object[] { new { url = "test", valueString = "Smile" } } } - }, - JsonTokenType.EndObject - }; - } - } + [TestMethod] + public void TestParseResourcePublicMethod() + { + var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Resource).Assembly); + var reader = constructReader( + new { resourceType = "Parameters", parameter = new[] { new { name = "a" } } }); - public static IEnumerable TestDeserializeNestedResource + deserializer.DeserializeResource(ref reader).Should().NotBeNull(); + + reader = constructReader( + new { resourceType = "ParametersX", }); + + try { - get - { - yield return new object[] - { - new - { - resourceType = "Parameters", - parameter = new[] - { - new { name = "a", resource = new { resourceType = "Patient", active = true } } - } - }, - JsonTokenType.EndObject - }; - } + deserializer.DeserializeResource(ref reader); + Assert.Fail(); } - - [TestMethod] - [DynamicData(nameof(TestPrimitiveArrayData), DynamicDataSourceType.Method)] - [DynamicData(nameof(CatchesIncorrectlyStructuredComplexData), DynamicDataSourceType.Method)] - [DynamicData(nameof(TestNormalArrayData), DynamicDataSourceType.Method)] - [DynamicData(nameof(TestPrimitiveData), DynamicDataSourceType.Method)] - [DynamicData(nameof(TestValidatePrimitiveData), DynamicDataSourceType.Method)] - public void TestData(Type t, object testObject, JsonTokenType token, Action? verify, - params string[] expectedErrors) + catch (DeserializationFailedException) { - // Enable full narrative validation so we can test for it - var (result, errors) = deserializeComplex(t, testObject, out var readerState, - new() - { - Validator = new DataAnnotationDeserialzationValidator( - narrativeValidation: Validation.NarrativeValidationKind.FhirXhtml) - }); - - assertErrors(errors, expectedErrors); - readerState.TokenType.Should().Be(token); - var cdResult = result.Should().BeOfType(t); - verify?.Invoke(result); + // ok! } + } - private static object?[] data(object data, Action verifier, params object[] args) => - new[] { typeof(T), data, JsonTokenType.EndObject, verifier }.Concat(args).ToArray(); - - private static object?[] data(object data, JsonTokenType token, params object[] args) => - new[] { typeof(T), data, token, default(Action) }.Concat(args).ToArray(); + [TestMethod] + public void TestParseObjectPublicMethod() + { + var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Resource).Assembly); + var reader = constructReader( + new { name = "Ewout" }); - private static object?[] data(object data, params object[] args) => - new[] { typeof(T), data, JsonTokenType.EndObject, null }.Concat(args).ToArray(); + deserializer.DeserializeObject(ref reader).Should().NotBeNull(); + reader = constructReader( + new { nameX = "Ewout", }); - public static IEnumerable CatchesIncorrectlyStructuredComplexData() + try { - yield return new object?[] - { - typeof(Extension), 5, JsonTokenType.Number, default(Action), - ERR.EXPECTED_START_OF_OBJECT_CODE - }; - yield return data(5, JsonTokenType.Number, ERR.EXPECTED_START_OF_OBJECT_CODE); - yield return data(new[] { 2, 3 }, JsonTokenType.EndArray, ERR.EXPECTED_START_OF_OBJECT_CODE); - yield return data(new { }, ERR.OBJECTS_CANNOT_BE_EMPTY_CODE); - yield return data(new { }, ERR.OBJECTS_CANNOT_BE_EMPTY_CODE); - yield return data(new { unknown = "test" }, ERR.UNKNOWN_PROPERTY_FOUND_CODE); - yield return data(new { url = "test" }); - yield return data(new { _url = "test" }, ERR.EXPECTED_START_OF_OBJECT_CODE); - yield return data(new { unknown = "test", url = "test" }, - ERR.UNKNOWN_PROPERTY_FOUND_CODE); - yield return data(new { value = "no type suffix" }, ERR.CHOICE_ELEMENT_HAS_NO_TYPE_CODE); - yield return data(new { valueUnknown = "incorrect type suffix" }, - ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE); - yield return data(new { valueBoolean = true, url = "http://something.nl" }, - JsonTokenType.EndObject); - yield return data(new { valueUnknown = "incorrect type suffix", unknown = "unknown" }, - ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE, ERR.UNKNOWN_PROPERTY_FOUND_CODE); + deserializer.DeserializeObject(ref reader); + Assert.Fail(); } - - public static IEnumerable TestNormalArrayData() + catch (DeserializationFailedException) { - yield return data(new { name = "Ewout", telecom = 4 }, checkName, - ERR.EXPECTED_START_OF_ARRAY_CODE, ERR.EXPECTED_START_OF_OBJECT_CODE); - yield return data(new { name = "Ewout", telecom = Array.Empty() }, checkName, - ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); - yield return data( - new { name = "Ewout", telecom = new object[] { new { system = "phone" }, new { systemX = "b" } } }, - checkData, ERR.UNKNOWN_PROPERTY_FOUND_CODE); - yield return data( - new { name = "Ewout", _telecom = new object[] { new { system = "phone" }, new { systemX = "b" } } }, - checkData, ERR.USE_OF_UNDERSCORE_ILLEGAL_CODE, ERR.UNKNOWN_PROPERTY_FOUND_CODE); - yield return data(new { name = new[] { "Ewout" } }, ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE); - - static void checkName(object parsed) => - parsed.Should().BeOfType().Which.Name.Should().Be("Ewout"); - - static void checkData(object parsedObject) - { - checkName(parsedObject); - - var parsed = parsedObject.Should().BeOfType().Subject; - parsed.Telecom.Count.Should().Be(2); - parsed.Telecom[0].System.Should().Be(ContactPoint.ContactPointSystem.Phone); - parsed.Telecom[1].EnumerateElements().Count().Should().Be(0); - } + // ok! } - public static IEnumerable TestPrimitiveData() + try { - yield return data(new { name = new[] { "Ewout" } }, ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE); - yield return data(new { name = new { dummy = "Ewout" } }, - ERR.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE); - yield return data(new { _name = new[] { "Ewout" } }, ERR.EXPECTED_START_OF_OBJECT_CODE); - yield return data(new { _name = "Ewout" }, ERR.EXPECTED_START_OF_OBJECT_CODE); - yield return data(new { name = "Ewout" }, checkName); - yield return data(new { _name = new { id = "12345" } }, checkId); - yield return data(new { _name = new { id = true } }, ERR.UNEXPECTED_JSON_TOKEN_CODE); - yield return data(new { name = "Ewout", _name = new { id = "12345" } }, checkAll); - - static void checkName(object parsed) => parsed.Should().BeOfType().Which.NameElement.Value - .Should().Be("Ewout"); - - static void checkId(object parsed) => parsed.Should().BeOfType().Which.NameElement.ElementId - .Should().Be("12345"); - - static void checkAll(object parsed) - { - checkName(parsed); - checkId(parsed); - } + deserializer.DeserializeObject(typeof(FhirJsonDeserializationTests), ref reader); + Assert.Fail(); } - - public static IEnumerable TestValidatePrimitiveData() + catch (ArgumentException) { - yield return data(new - { - div = "

correct

", status = "additional" - }); - yield return data(new { div = "this isn't xml" }, COVE.NARRATIVE_XML_IS_MALFORMED_CODE); - yield return data(new { div = "" }, COVE.NARRATIVE_XML_IS_INVALID_CODE); - - yield return data(new { url = "urn:oid:1.3.6.1.4.1.343" }); - // yield return data(new { url = "urn:oid:1" }, COVE.URI_LITERAL_INVALID_CODE); - // This is an URL in our datamodel now, since that's what it is in R4 and later. + // ok! } + } - public static IEnumerable TestPrimitiveArrayData() - { - yield return data
(new { line = "hi!" }, ERR.EXPECTED_START_OF_ARRAY_CODE); - yield return data
(new { line = Array.Empty() }, ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); - yield return data
(new { line = Array.Empty(), _line = Array.Empty() }, - ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); - yield return data
(new { line = Array.Empty(), _line = new string?[] { null } }, - ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); - yield return data
(new { line = new string?[] { null }, _line = new[] { new { id = "1" } } }, - ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); - yield return data
(new { line = new[] { "Ewout" }, _line = new string?[] { null } }, - ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); - yield return data
(new { line = new string?[] { null }, _line = new string?[] { null } }, - ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); - yield return data
(new { line = new string?[] { null }, _line = new string?[] { null, null } }, - ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); - yield return data
(new { line = new string?[] { null, null }, _line = new string?[] { null } }, - ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE); - yield return data
(new { line = new[] { "Ewout", "Wouter" } }, checkName); - yield return data
( - new { line = new[] { "Ewout", "Wouter" }, _line = new[] { new { id = "1" } } }, checkId1AndName); - yield return data
( - new { line = new[] { "Ewout", "Wouter" }, _line = new[] { new { id = "1" }, null } }, checkId1AndName); - yield return data
( - new { line = new[] { "Ewout", "Wouter" }, _line = new[] { new { id = "1" }, new { id = "2" } } }, - checkAll); - yield return data
(new - { - line = new[] { "Ewout", null }, _line = new[] { null, new { id = "2" } } - }); - yield return data
( - new { line = new[] { "Ewout", null }, _line = new[] { new { id = "1" }, null } }, checkId1, - COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE); - yield return data
(new { _line = new[] { new { id = "1" }, null } }, checkId1, - COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE); - yield return data
(new { _line = new[] { new { id = "1" }, new { id = "2" } } }, checkIds); + [TestMethod] + public void TestRecovery() + { + var filename = Path.Combine("TestData", "fp-test-patient-errors.json"); + var jsonInput = File.ReadAllText(filename); + + var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); + + try + { + var actual = JsonSerializer.Deserialize(jsonInput, options); + Assert.Fail("Should have encountered errors."); + } + catch (DeserializationFailedException dfe) + { + Console.WriteLine(dfe.Message); + var recoveredActual = JsonSerializer.Serialize(dfe.PartialResult, options); + Console.WriteLine(recoveredActual); + + assertErrors(dfe.Exceptions, [ + COVE.LITERAL_INVALID_CODE, + ERR.UNKNOWN_PROPERTY_FOUND_CODE, // resourceType at the non-root level + ERR.UNKNOWN_RESOURCE_TYPE_CODE, ERR.RESOURCE_TYPE_NOT_A_RESOURCE_CODE, + ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE, ERR.NO_RESOURCETYPE_PROPERTY_CODE, + COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, ERR.EXPECTED_START_OF_ARRAY_CODE, + ERR.UNKNOWN_PROPERTY_FOUND_CODE, // mother is not a property of HumanName + ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE, // family is not an array, + ERR.EXPECTED_PRIMITIVE_NOT_NULL_CODE, // telecom use cannot be null + ERR.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE, // address.use is not an object + COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE, // address.line should not have a null at the same position in both arrays + COVE.INVALID_CODED_VALUE_CODE, // status 'generatedY' + ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, // Questionnaire._subjectType cannot be just null + COVE.CHOICE_TYPE_NOT_ALLOWED_CODE, // incorrect use of valueBoolean in option. + ERR.EXPECTED_START_OF_OBJECT_CODE, // item.code is a complex object, not a boolean + COVE.LITERAL_INVALID_CODE, // incorrect oid + COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE, // given cannot be a single array with just a null + COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, // telecom.rank should be a number, not a boolean + ERR.EXPECTED_START_OF_OBJECT_CODE, // extension._url is an object (although not applicable) + COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, // gender.extension.valueCode should be a string, not a number + ERR.CHOICE_ELEMENT_HAS_NO_TYPE_CODE, // extension.value is incorrect + ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE, // extension.valueSuperDecimal is incorrect + COVE.INVALID_BASE64_VALUE_CODE, ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, ERR.PROPERTY_MAY_NOT_BE_EMPTY_CODE, + ERR.OBJECTS_CANNOT_BE_EMPTY_CODE, + COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, // deceasedBoolean should be a boolean not a string + COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, // multipleBirthInteger should not be a float (3.14) + ]); + + var recoveredFilename = Path.Combine("TestData", "fp-test-patient-errors-recovered.json"); + var recoveredExpected = File.ReadAllText(recoveredFilename); + + List errors = new(); + JsonAssert.AreSame("fp-test-patient-json-errors/recovery", recoveredExpected, recoveredActual, errors); + errors.Should().BeEmpty(); + } + } - static void checkName(object parsed) => parsed.Should().BeOfType
().Which.Line.Should() - .BeEquivalentTo("Ewout", "Wouter"); + [TestMethod] + public void TestBase64Parsing() + { + var attachment = deserializeAttachment(new()); - static void checkIds(object parsed) => - parsed.Should().BeOfType
().Which.LineElement.Select(le => le?.ElementId).Should() - .BeEquivalentTo("1", "2"); + // After parsing, the ObjectValue is supposed to be the base64 string + attachment.DataElement.ObjectValue.Should().BeOfType().And.Subject.Should().Be("SGkh"); - static void checkId1(object parsed) => - parsed.Should().BeOfType
().Which.LineElement.Select(le => le?.ElementId).Should() - .BeEquivalentTo("1", null); + // Getting the Value should decode and return a byte[] + Encoding.UTF8.GetString(attachment.Data).Should().Be("Hi!"); - static void checkId1AndName(object parsed) - { - checkName(parsed); - checkId1(parsed); - } + static Attachment deserializeAttachment(FhirJsonConverterOptions settings) + { + var (attachment, errors) = + deserializeComplex(typeof(Attachment), new { data = "SGkh" }, out _, settings); + errors.Any().Should().BeFalse(); - static void checkAll(object parsed) - { - checkName(parsed); - checkIds(parsed); - } + return (Attachment)attachment!; } + } - [TestMethod] - public void TestParseResourcePublicMethod() - { - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Resource).Assembly); - var reader = constructReader( - new { resourceType = "Parameters", parameter = new[] { new { name = "a" } } }); - - deserializer.DeserializeResource(ref reader).Should().NotBeNull(); + internal class CustomComplexValidator : DataAnnotationDeserialzationValidator + { + //public object? DateTimeSeenByObjectValueValidator; + public FhirDateTime? DateTimeSeenByInstanceValidator; + public FhirDateTime? DateTimeSeenByPropertyValidator; - reader = constructReader( - new { resourceType = "ParametersX", }); + // public override void ValidateObjectValue(ref object? value, in ObjectValueDeserializationContext context, + // out COVE[]? reportedErrors) + // { + // DateTimeSeenByObjectValueValidator = value; + // + // // Now change it, to whether the next step picks it up. + // value = "1972-30-11T12:00:00Z"; + // + // base.ValidateObjectValue(ref value, context, out reportedErrors); + // } - try - { - deserializer.DeserializeResource(ref reader); - Assert.Fail(); - } - catch (DeserializationFailedException) + public override void ValidateInstance(Base instance, in InstanceDeserializationContext context, + out COVE[]? reportedErrors) + { + if (instance is FhirDateTime fdt) { - // ok! + DateTimeSeenByInstanceValidator = fdt; + fdt.ObjectValue = "1972-11-30T12:00:00Z"; } + + base.ValidateInstance(instance, context, out reportedErrors); } - [TestMethod] - public void TestParseObjectPublicMethod() + public override void ValidateProperty(object? propertyValue, in PropertyDeserializationContext context, + out COVE[]? reportedErrors) { - var deserializer = new BaseFhirJsonPocoDeserializer(typeof(Resource).Assembly); - var reader = constructReader( - new { name = "Ewout" }); - - deserializer.DeserializeObject(ref reader).Should().NotBeNull(); + base.ValidateProperty(propertyValue, context, out reportedErrors); - reader = constructReader( - new { nameX = "Ewout", }); - - try - { - deserializer.DeserializeObject(ref reader); - Assert.Fail(); - } - catch (DeserializationFailedException) + if (context.Path == "Patient.deceased") { - // ok! - } + var fdt = propertyValue.Should().BeOfType().Subject; - try - { - deserializer.DeserializeObject(typeof(FhirJsonDeserializationTests), ref reader); - Assert.Fail(); - } - catch (ArgumentException) - { - // ok! + // Take note of what we got. + DateTimeSeenByPropertyValidator = fdt; + + var validationContext = new ValidationContext(context.ObjectInstance) + .SetValidateRecursively( + false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. + .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) + .SetLocationProducer(context.PathStack.GetInstancePath); + reportedErrors = [..reportedErrors ?? [], COVE.LITERAL_INVALID(validationContext, "Nothing wrong, really", "DateTime")]; } } - [TestMethod] - public void TestRecovery() - { - var filename = Path.Combine("TestData", "fp-test-patient-errors.json"); - var jsonInput = File.ReadAllText(filename); - var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - try - { - var actual = JsonSerializer.Deserialize(jsonInput, options); - Assert.Fail("Should have encountered errors."); - } - catch (DeserializationFailedException dfe) - { - Console.WriteLine(dfe.Message); - var recoveredActual = JsonSerializer.Serialize(dfe.PartialResult, options); - Console.WriteLine(recoveredActual); - - assertErrors(dfe.Exceptions, [ - ERR.STRING_ISNOTAN_INSTANT_CODE, - ERR.UNKNOWN_PROPERTY_FOUND_CODE, // resourceType at the non-root level - ERR.UNKNOWN_RESOURCE_TYPE_CODE, ERR.RESOURCE_TYPE_NOT_A_RESOURCE_CODE, - ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE, ERR.NO_RESOURCETYPE_PROPERTY_CODE, - ERR.UNEXPECTED_JSON_TOKEN_CODE, ERR.EXPECTED_START_OF_ARRAY_CODE, - ERR.UNKNOWN_PROPERTY_FOUND_CODE, // mother is not a property of HumanName - ERR.EXPECTED_PRIMITIVE_NOT_ARRAY_CODE, // family is not an array, - ERR.EXPECTED_PRIMITIVE_NOT_NULL_CODE, // telecom use cannot be null - ERR.EXPECTED_PRIMITIVE_NOT_OBJECT_CODE, // address.use is not an object - COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE, // address.line should not have a null at the same position in both arrays - COVE.INVALID_CODED_VALUE_CODE, // status 'generatedY' - ERR.PRIMITIVE_ARRAYS_ONLY_NULL_CODE, // Questionnaire._subjectType cannot be just null - COVE.CHOICE_TYPE_NOT_ALLOWED_CODE, // incorrect use of valueBoolean in option. - ERR.EXPECTED_START_OF_OBJECT_CODE, // item.code is a complex object, not a boolean - COVE.URI_LITERAL_INVALID_CODE, // incorrect oid - COVE.REPEATING_ELEMENT_CANNOT_CONTAIN_NULL_CODE, // given cannot be a single array with just a null - ERR.UNEXPECTED_JSON_TOKEN_CODE, // telecom.rank should be a number, not a boolean - ERR.EXPECTED_START_OF_OBJECT_CODE, // extension._url is an object (although not applicable) - ERR.UNEXPECTED_JSON_TOKEN_CODE, // gender.extension.valueCode should be a string, not a number - ERR.CHOICE_ELEMENT_HAS_NO_TYPE_CODE, // extension.value is incorrect - ERR.CHOICE_ELEMENT_HAS_UNKOWN_TYPE_CODE, // extension.valueSuperDecimal is incorrect - ERR.UNEXPECTED_JSON_TOKEN_CODE, // deceasedBoolean should be a boolean not a string - ERR.NUMBER_CANNOT_BE_PARSED_CODE, // multipleBirthInteger should not be a float (3.14) - ERR.INCORRECT_BASE64_DATA_CODE, ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, ERR.PROPERTY_MAY_NOT_BE_EMPTY_CODE, - ERR.OBJECTS_CANNOT_BE_EMPTY_CODE - ]); - - var recoveredFilename = Path.Combine("TestData", "fp-test-patient-errors-recovered.json"); - var recoveredExpected = File.ReadAllText(recoveredFilename); - - List errors = new(); - JsonAssert.AreSame("fp-test-patient-json-errors/recovery", recoveredExpected, recoveredActual, errors); - errors.Should().BeEmpty(); - } - } + } - [TestMethod] - public void TestDisableBase64Parsing() - { - var attachment = deserializeAttachment(new()); - Encoding.ASCII.GetString(attachment.Data).Should().Be("Hi!"); + [TestMethod] + public void TestValidatorIsCalledDuringDeserialization() + { + var validator = new CustomComplexValidator(); - attachment = deserializeAttachment(new() { DisableBase64Decoding = true }); - attachment.DataElement.ObjectValue.Should().BeOfType().And.Subject.Should().Be("SGkh"); + var (_, errors) = deserializeComplex(typeof(Patient), + new { resourceType = "Patient", deceasedDateTime = "2070-01-01T12:01:02Z" }, + out _, new FhirJsonConverterOptions { Validator = validator }); - static Attachment deserializeAttachment(FhirJsonConverterOptions settings) - { - var (attachment, errors) = - deserializeComplex(typeof(Attachment), new { data = "SGkh" }, out _, settings); - errors.Any().Should().BeFalse(); + errors.Should().HaveCount(1); + errors.Single().Should().BeOfType().Which.ErrorCode.Should().Be(COVE.LITERAL_INVALID_CODE); + validator.DateTimeSeenByInstanceValidator?.Value.Should().Be("1972-11-30T12:00:00Z"); + } - return (Attachment)attachment!; - } - } + private class MixedClass + { + public Patient? FhirPatient { get; init; } - internal class CustomComplexValidator : IDeserializationValidator - { - public void ValidateInstance(object? instance, in InstanceDeserializationContext context, - out COVE[]? reportedErrors) - { - reportedErrors = null; - } + public string? HandledByTextJson { get; init; } - public void ValidateProperty(ref object? instance, in PropertyDeserializationContext context, - out CodedValidationException[]? reportedErrors) - { - reportedErrors = null; + // This only works well when we construct deserializers using the ConverterFactory method + // from System.Text.Json + public List? FhirIdentifier { get; init; } + } - if (instance is not FhirDateTime f) return; - if (context.Path != "Patient.deceased") return; - context.ElementMapping.DeclaringClass.Name.Should().Be("Patient"); - context.PropertyName.Should().Be("deceasedDateTime"); - context.ElementMapping.Name.Should().Be("deceased"); + [TestMethod] + public void CanParseIsolatedDataType() + { + var reader = constructReader(new { system = "http://nu.nl", value = "bla" }); - // Invalid value, but since this value has already been validated during - // deserialization of the FhirDateTime, validation will not be triggered! - if (f.Value.EndsWith("Z")) f.Value = f.Value.TrimEnd('Z') + "+00:00"; + var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - var validationContext = new ValidationContext(instance ?? new object()) - .SetValidateRecursively( - false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. - .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) - .SetLocation(context.PathStack); + var identifier = JsonSerializer.Deserialize(ref reader, options)!; + identifier.Should().BeOfType(); + identifier.System.Should().Be("http://nu.nl"); + } - reportedErrors = new[] { COVE.DATETIME_LITERAL_INVALID(validationContext, context.PropertyName) }; - } - } + [TestMethod] + public void CanParseMixedClass() + { + var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - internal class CustomPropertyValueValidator : IDeserializationValidator + var mc = new MixedClass { - public void ValidateInstance(object? instance, in InstanceDeserializationContext context, - out COVE[]? reportedErrors) - { - reportedErrors = null; - } + FhirIdentifier = new() { new Identifier("http://nu.nl", "bla") }, + HandledByTextJson = "Hi!", + FhirPatient = new() { Active = true } + }; - public void ValidateProperty(ref object? instance, in PropertyDeserializationContext context, - out CodedValidationException[]? reportedErrors) - { - reportedErrors = null; + var json = JsonSerializer.Serialize(mc, options); - if (instance is not String f) return; - if (context.Path != "Patient.deceased.value") return; + var mc2 = JsonSerializer.Deserialize(json, options)!; - context.ElementMapping.DeclaringClass.Name.Should().Be("dateTime"); - context.PropertyName.Should().Be("value"); - context.ElementMapping.Name.Should().Be("value"); + mc2.Should().BeOfType(); + mc2.FhirIdentifier!.Single().System.Should().Be("http://nu.nl"); + mc2.HandledByTextJson.Should().Be("Hi!"); + mc2.FhirPatient?.Active.Should().Be(true); + } - // Invalid value, but since this value has already been validated during - // deserialization of the FhirDateTime, validation will not be triggered! - if (f.EndsWith("Z")) - { - instance = f.TrimEnd('Z') + "+00:00"; - } + [TestMethod] + public void ReportsMissingMandatoryElements() + { + var (codesystem, errors) = deserializeComplex(typeof(CodeSystem), + new { resourceType = "CodeSystem", content = "example" }, out _, new()); - var validationContext = new ValidationContext(instance ?? new object()) - .SetValidateRecursively( - false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. - .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) - .SetLocation(context.PathStack); - reportedErrors = new[] { COVE.DATETIME_LITERAL_INVALID(validationContext, f) }; - } - } + // should contain error that mandatory item "status" is missing. + errors.Should().ContainSingle(ce => ce.ErrorCode == "PVAL105"); + } - internal class CustomDataTypeValidator : IDeserializationValidator - { - public void ValidateInstance(object? instance, in InstanceDeserializationContext context, - out COVE[]? reportedErrors) - { - var validationContext = new ValidationContext(instance ?? new object()) - .SetValidateRecursively( - false) // Don't go deeper - we've already validated the children because we're parsing bottom-up. - .SetPositionInfo(new PositionInfo((int)context.LineNumber, (int)context.LinePosition)) - .SetLocation(context.PathStack); - if (context.InstanceMapping.Name == "dateTime") + private static IEnumerable getDuplicatePropertyTests() + { + (string, string[])[] duplicatePropertiesJson = + [ + (""" + { + "resourceType" : "Patient", + "extension" : [{ + "url" : "http://nu.nl", + "url" : "http://nu.nl" + }] + } + """, [ERR.DUPLICATE_PROPERTY_CODE]), + (""" + { + "resourceType" : "Patient", + "active" : true, + "active" : false + } + """, [ERR.DUPLICATE_PROPERTY_CODE]), + (""" + { + "resourceType" : "Patient", + "active" : true, + "_active" : { "id" : "1234" }, + "_active" : { "id" : "5678" } + } + """, [ERR.DUPLICATE_PROPERTY_CODE, ERR.DUPLICATE_PROPERTY_CODE]), + ( + """ { - var dt = instance.Should().BeOfType().Subject; - - if (dt.Value.EndsWith("Z")) dt.Value = dt.Value.TrimEnd('Z') + "+00:00"; - reportedErrors = new[] { COVE.DATETIME_LITERAL_INVALID(validationContext, dt.Value) }; + "resourceType" : "Patient", + "_active" : { "id" : "1234" }, + "_active" : { "id" : "5678" } } - else + """, [ERR.DUPLICATE_PROPERTY_CODE, ERR.DUPLICATE_PROPERTY_CODE]), + ( + """ { - reportedErrors = null; + "resourceType" : "Patient", + "_active" : { "id" : "1234" }, + "_active" : { "extension" : [{ "url" : "http://nu.nl" }] } } - } + """, [ERR.DUPLICATE_PROPERTY_CODE]), + (""" + { + "resourceType" : "OperationOutcome", + "issue" : [{ + "severity" : "error", + "code" : "code-invalid", + "expression" : ["Patient.gender"], + "_expression" : [{ "id" : "1234" }], + "_expression" : [{ "id" : "3456" }] + }] + } + """, [ERR.DUPLICATE_ARRAY_CODE]), + (""" + { + "resourceType" : "OperationOutcome", + "issue" : [{ + "severity" : "error", + "code" : "code-invalid", + "expression" : ["Patient.gender"], + "expression" : ["Patient.administrativeGender"], + "_expression" : [{ "id" : "3456" }] + }] + } + """, [ERR.DUPLICATE_ARRAY_CODE]), + (""" + { + "resourceType" : "Patient", + "identifier" : + [{ + "use" : "usual", + "system" : "urn:oid:2.16.840.1.113883.2.4.6.3", + "value" : "738472983" + }], + "identifier" : + [{ + "use" : "usual", + "system" : "urn:oid:2.16.840.1.113883.2.4.6.3", + "value" : "738472983" + }] + } + """, [ERR.DUPLICATE_ARRAY_CODE]), + (""" + { + "resourceType" : "Patient", + "managingOrganization" : + { + "reference" : "Organization/f001", + "display" : "Burgers University Medical Centre" + }, + "managingOrganization" : + { + "reference" : "Organization/f002", + "display" : "Burgers Zoo" + } + } + """, [ERR.DUPLICATE_PROPERTY_CODE]) + ]; - public void ValidateProperty(ref object? instance, in PropertyDeserializationContext context, - out CodedValidationException[]? reportedErrors) - { - reportedErrors = null; - } - } + return duplicatePropertiesJson.Select(testCase => (object[])( [testCase.Item1, testCase.Item2])); + } - [TestMethod] - public void TestUpdatePrimitiveValue() - { - //test(new CustomComplexValidator()); - //test(new CustomDataTypeValidator()); - test(new CustomPropertyValueValidator()); - static void test(IDeserializationValidator validator) - { - var (result, errors) = deserializeComplex(typeof(Patient), - new { resourceType = "Patient", deceasedDateTime = "2070-01-01T12:01:02Z" }, - out _, new() { Validator = validator }); - - errors.Any().Should().BeTrue(); - errors.Should().AllBeOfType() - .And.ContainSingle(e => ((COVE)e).ErrorCode == COVE.DATETIME_LITERAL_INVALID_CODE); - result.Should().BeOfType() - .Which.Deceased.Should().BeOfType() - .Which.Value.Should().EndWith("+00:00"); - } - } + [DataTestMethod] + [DynamicData(nameof(getDuplicatePropertyTests), DynamicDataSourceType.Method)] + public void TestDuplicateProperties(string testJson, string[] expectedErrs) + { + var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - private class MixedClass + try { - public Patient? FhirPatient { get; set; } - - public string? HandledByTextJson { get; set; } - - // This only works well when we construct deserializers using the ConverterFactory method - // from System.Text.Json - public List? FhirIdentifier { get; set; } + _ = JsonSerializer.Deserialize(testJson, options); + Assert.Fail("Should have encountered errors."); } - - - [TestMethod] - public void CanParseIsolatedDataType() + catch (DeserializationFailedException dfe) { - var reader = constructReader(new { system = "http://nu.nl", value = "bla" }); - - var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - - var identifier = JsonSerializer.Deserialize(ref reader, options)!; - identifier.Should().BeOfType(); - identifier.System.Should().Be("http://nu.nl"); + assertErrors(dfe.Exceptions, expectedErrs); } + } - [TestMethod] - public void CanParseMixedClass() - { - var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - - var mc = new MixedClass - { - FhirIdentifier = new() { new Identifier("http://nu.nl", "bla") }, - HandledByTextJson = "Hi!", - FhirPatient = new() { Active = true } - }; + [TestMethod] + public void TestDuplicateChoiceTypeEntries() + { + var scenario = """ + { + "resourceType": "Patient", + "deceasedBoolean": true, + "deceasedDateTime": "2022-01-01T12:00:00Z" + } + """; - var json = JsonSerializer.Serialize(mc, options); + string expected = ERR.DUPLICATE_PROPERTY_CODE; - var mc2 = JsonSerializer.Deserialize(json, options)!; + var jsonSerializerOptions = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - mc2.Should().BeOfType(); - mc2.FhirIdentifier!.Single().System.Should().Be("http://nu.nl"); - mc2.HandledByTextJson.Should().Be("Hi!"); - mc2.FhirPatient?.Active.Should().Be(true); + try + { + _ = JsonSerializer.Deserialize(scenario, jsonSerializerOptions); + Assert.Fail("Should have encountered errors."); } - - [TestMethod] - public void ReportsMissingMandatoryElements() + catch (DeserializationFailedException dfe) { - var (codesystem, errors) = deserializeComplex(typeof(CodeSystem), - new { resourceType = "CodeSystem", content = "example" }, out _, new()); - - // should contain error that mandatory item "status" is missing. - errors.Should().ContainSingle(ce => ce.ErrorCode == "PVAL105"); + assertErrors(dfe.Exceptions, [expected]); } + } + [TestMethod] + public void TestBackboneElementEmptyStack() + { + var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - private static IEnumerable getDuplicatePropertyTests() + var bundleEntryComponent = new Parameters.ParameterComponent() { - (string, string[])[] duplicatePropertiesJson = - [ - (""" - { - "resourceType" : "Patient", - "extension" : [{ - "url" : "http://nu.nl", - "url" : "http://nu.nl" - }] - } - """, [ERR.DUPLICATE_PROPERTY_CODE]), - (""" - { - "resourceType" : "Patient", - "active" : true, - "active" : false - } - """, [ERR.DUPLICATE_PROPERTY_CODE]), - (""" - { - "resourceType" : "Patient", - "active" : true, - "_active" : { "id" : "1234" }, - "_active" : { "id" : "5678" } - } - """, [ERR.DUPLICATE_PROPERTY_CODE, ERR.DUPLICATE_PROPERTY_CODE]), - ( - """ - { - "resourceType" : "Patient", - "_active" : { "id" : "1234" }, - "_active" : { "id" : "5678" } - } - """, [ERR.DUPLICATE_PROPERTY_CODE, ERR.DUPLICATE_PROPERTY_CODE]), - ( - """ - { - "resourceType" : "Patient", - "_active" : { "id" : "1234" }, - "_active" : { "extension" : [{ "url" : "http://nu.nl" }] } - } - """, [ERR.DUPLICATE_PROPERTY_CODE]), - (""" - { - "resourceType" : "OperationOutcome", - "issue" : [{ - "severity" : "error", - "code" : "code-invalid", - "expression" : ["Patient.gender"], - "_expression" : [{ "id" : "1234" }], - "_expression" : [{ "id" : "3456" }] - }] - } - """, [ERR.DUPLICATE_ARRAY_CODE]), - (""" - { - "resourceType" : "OperationOutcome", - "issue" : [{ - "severity" : "error", - "code" : "code-invalid", - "expression" : ["Patient.gender"], - "expression" : ["Patient.administrativeGender"], - "_expression" : [{ "id" : "3456" }] - }] - } - """, [ERR.DUPLICATE_ARRAY_CODE]), - (""" - { - "resourceType" : "Patient", - "identifier" : - [{ - "use" : "usual", - "system" : "urn:oid:2.16.840.1.113883.2.4.6.3", - "value" : "738472983" - }], - "identifier" : - [{ - "use" : "usual", - "system" : "urn:oid:2.16.840.1.113883.2.4.6.3", - "value" : "738472983" - }] - } - """, [ERR.DUPLICATE_ARRAY_CODE]), - (""" - { - "resourceType" : "Patient", - "managingOrganization" : - { - "reference" : "Organization/f001", - "display" : "Burgers University Medical Centre" - }, - "managingOrganization" : - { - "reference" : "Organization/f002", - "display" : "Burgers Zoo" - } - } - """, [ERR.DUPLICATE_PROPERTY_CODE]) - ]; + Name = "name", + Resource = new Patient{Gender = AdministrativeGender.Female} + }; - return duplicatePropertiesJson.Select(testCase => (object[])( [testCase.Item1, testCase.Item2])); - } + var jsonString = JsonSerializer.Serialize(bundleEntryComponent, options); + var seq = new ReadOnlySequence(Encoding.UTF8.GetBytes(jsonString)); - [DataTestMethod] - [DynamicData(nameof(getDuplicatePropertyTests), DynamicDataSourceType.Method)] - public void TestDuplicateProperties(string testJson, string[] expectedErrs) - { - var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); + var newJsonReader = new Utf8JsonReader(seq, true, default); - try - { - _ = JsonSerializer.Deserialize(testJson, options); - Assert.Fail("Should have encountered errors."); - } - catch (DeserializationFailedException dfe) - { - assertErrors(dfe.Exceptions, expectedErrs); - } - } + // System.InvalidOperationException: 'Stack empty.' thrown when attempting to deserialize + var result = JsonSerializer.Deserialize(ref newJsonReader, options); + } + + private static IEnumerable getExtensionOptionsAndExpectedErrors() + { + yield return + [ + new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) + .UsingMode(DeserializerModes.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))) + ]; + yield return + [ + new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) + .UsingMode(DeserializerModes.BackwardsCompatible), + new Predicate>(errs => !errs.Any(e => FilterPredicateExtensions.IsBackwardsCompatibilityIssue(e))) + ]; + yield return + [ + new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) + .Ignoring([COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE]), + new Predicate>(errs => errs.All(e => e.ErrorCode != COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE)) + ]; + yield return + [ + new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly).UsingMode(DeserializerModes.Ostrich) + .Enforcing([ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, COVE.LITERAL_INVALID_CODE]), + new Predicate>(errs => + { + IEnumerable codedExceptions = errs as CodedException[] ?? errs.ToArray(); + return codedExceptions.Any() && codedExceptions.All(e => + e.ErrorCode is ERR.ARRAYS_CANNOT_BE_EMPTY_CODE or COVE.LITERAL_INVALID_CODE); + }) + ]; + } + + [DataTestMethod] + [DynamicData(nameof(getExtensionOptionsAndExpectedErrors), DynamicDataSourceType.Method)] + public void TestExtensionMethods(JsonSerializerOptions options, Predicate> shouldHold) + { + string testJson = File.ReadAllText(Path.Combine("TestData", "fp-test-patient-errors.json")); - [TestMethod] - public void TestDuplicateChoiceTypeEntries() + try { - var scenario = """ - { - "resourceType": "Patient", - "deceasedBoolean": true, - "deceasedDateTime": "2022" - } - """; - - string expected = ERR.DUPLICATE_PROPERTY_CODE; - - var jsonSerializerOptions = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - - try - { - _ = JsonSerializer.Deserialize(scenario, jsonSerializerOptions); - Assert.Fail("Should have encountered errors."); - } - catch (DeserializationFailedException dfe) - { - assertErrors(dfe.Exceptions, [expected]); - } + _ = JsonSerializer.Deserialize(testJson, options); + throw new DeserializationFailedException(null, []); } - - [TestMethod] - public void TestBackboneElementEmptyStack() + catch (DeserializationFailedException dfe) { - var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly); - - var bundleEntryComponent = new Parameters.ParameterComponent() - { - Name = "name", - Resource = new Patient{Gender = AdministrativeGender.Female} - }; - - var jsonString = JsonSerializer.Serialize(bundleEntryComponent, options); + shouldHold(dfe.Exceptions).Should().BeTrue(); + } + } - var seq = new ReadOnlySequence(Encoding.UTF8.GetBytes(jsonString)); + private static Predicate getPredicateFromOptions(JsonSerializerOptions options) + { + var factory = options.Converters.FindCustomConverter() as FhirJsonConverterFactory ?? throw new InvalidOperationException(); + return factory.Engine!.IgnoreFilter; + } - var newJsonReader = new Utf8JsonReader(seq, true, default); + private static IEnumerable getIgnoreEnforceTests() + { + yield return + [ + getPredicateFromOptions(new JsonSerializerOptions() + .ForFhir(typeof(Patient).Assembly) + .Ignoring([COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE]) + .Ignoring([ERR.ARRAYS_CANNOT_BE_EMPTY_CODE]) + .Ignoring([COVE.INVALID_BASE64_VALUE_CODE])), + new Predicate(ce => + ce.ErrorCode is COVE.INVALID_BASE64_VALUE_CODE or ERR.ARRAYS_CANNOT_BE_EMPTY_CODE + or COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE) + ]; + yield return + [ + getPredicateFromOptions(new JsonSerializerOptions() + .ForFhir(typeof(Patient).Assembly) + .Ignoring([COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE]) + .Enforcing([COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE])), + new Predicate(_ => false) + ]; + yield return + [ + getPredicateFromOptions(new JsonSerializerOptions() + .ForFhir(typeof(Patient).Assembly) + .Ignoring([COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE]) + .Enforcing([COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE]) + .Ignoring([COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE])), + new Predicate(ce => ce.ErrorCode == COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE) + ]; + } - // System.InvalidOperationException: 'Stack empty.' thrown when attempting to deserialize - var result = JsonSerializer.Deserialize(ref newJsonReader, options); - } + private static IEnumerable getErrorsList() + { + var testDeserializerOptions = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) + .UsingMode(DeserializerModes.Strict); + string testJson = File.ReadAllText(Path.Combine("TestData", "fp-test-patient-errors.json")); - private static IEnumerable getExtensionOptionsAndExpectedErrors() + try { - yield return - [ - new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .UsingMode(DeserializerModes.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))) - ]; - yield return - [ - new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .UsingMode(DeserializerModes.BackwardsCompatible), - new Predicate>(errs => !errs.Any(e => FilterPredicateExtensions.IsBackwardsCompatibilityIssue(e))) - ]; - yield return - [ - new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .Ignoring([ERR.UNEXPECTED_JSON_TOKEN_CODE]), - new Predicate>(errs => errs.All(e => e.ErrorCode != ERR.UNEXPECTED_JSON_TOKEN_CODE)) - ]; - yield return - [ - new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly).UsingMode(DeserializerModes.Ostrich) - .Enforcing([ERR.ARRAYS_CANNOT_BE_EMPTY_CODE, ERR.LONG_CANNOT_BE_PARSED_CODE]), - new Predicate>(errs => - { - IEnumerable codedExceptions = errs as CodedException[] ?? errs.ToArray(); - return codedExceptions.Any() && codedExceptions.All(e => - e.ErrorCode is ERR.ARRAYS_CANNOT_BE_EMPTY_CODE or ERR.LONG_CANNOT_BE_PARSED_CODE); - }) - ]; + _ = JsonSerializer.Deserialize(testJson, testDeserializerOptions); } - - [DataTestMethod] - [DynamicData(nameof(getExtensionOptionsAndExpectedErrors), DynamicDataSourceType.Method)] - public void TestExtensionMethods(JsonSerializerOptions options, Predicate> shouldHold) + catch (DeserializationFailedException dfe) { - string testJson = File.ReadAllText(Path.Combine("TestData", "fp-test-patient-errors.json")); - - try - { - _ = JsonSerializer.Deserialize(testJson, options); - throw new DeserializationFailedException(null, []); - } - catch (DeserializationFailedException dfe) - { - shouldHold(dfe.Exceptions).Should().BeTrue(); - } + return dfe.Exceptions; } - private static Predicate getPredicateFromOptions(JsonSerializerOptions options) - { - var factory = options.Converters.FindCustomConverter() as FhirJsonConverterFactory ?? throw new InvalidOperationException(); - return factory.Engine!.IgnoreFilter; - } + throw new InvalidOperationException("Should have encountered errors"); + } - private static IEnumerable getIgnoreEnforceTests() - { - yield return - [ - getPredicateFromOptions(new JsonSerializerOptions() - .ForFhir(typeof(Patient).Assembly) - .Ignoring([ERR.UNEXPECTED_JSON_TOKEN_CODE]) - .Ignoring([ERR.ARRAYS_CANNOT_BE_EMPTY_CODE]) - .Ignoring([ERR.INCORRECT_BASE64_DATA_CODE])), - new Predicate(ce => - ce.ErrorCode is (ERR.INCORRECT_BASE64_DATA_CODE or ERR.ARRAYS_CANNOT_BE_EMPTY_CODE - or ERR.UNEXPECTED_JSON_TOKEN_CODE)) - ]; - yield return - [ - getPredicateFromOptions(new JsonSerializerOptions() - .ForFhir(typeof(Patient).Assembly) - .Ignoring([ERR.UNEXPECTED_JSON_TOKEN_CODE]) - .Enforcing([ERR.UNEXPECTED_JSON_TOKEN_CODE])), - new Predicate(_ => false) - ]; - yield return - [ - getPredicateFromOptions(new JsonSerializerOptions() - .ForFhir(typeof(Patient).Assembly) - .Ignoring([ERR.UNEXPECTED_JSON_TOKEN_CODE]) - .Enforcing([ERR.UNEXPECTED_JSON_TOKEN_CODE]) - .Ignoring([ERR.UNEXPECTED_JSON_TOKEN_CODE])), - new Predicate(ce => ce.ErrorCode == ERR.UNEXPECTED_JSON_TOKEN_CODE) - ]; - } - private static IEnumerable getErrorsList() - { - var testDeserializerOptions = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly) - .UsingMode(DeserializerModes.Strict); - string testJson = File.ReadAllText(Path.Combine("TestData", "fp-test-patient-errors.json")); - - try - { - _ = JsonSerializer.Deserialize(testJson, testDeserializerOptions); - } - catch (DeserializationFailedException dfe) - { - return dfe.Exceptions; - } + [DataTestMethod] + [DynamicData(nameof(getIgnoreEnforceTests), DynamicDataSourceType.Method)] + public void TestIgnoreEnforcePrevalence(Predicate actual, Predicate expected) + { + var errors = getErrorsList(); - throw new InvalidOperationException("Should have encountered errors"); - } - - - [DataTestMethod] - [DynamicData(nameof(getIgnoreEnforceTests), DynamicDataSourceType.Method)] - public void TestIgnoreEnforcePrevalence(Predicate actual, Predicate expected) - { - var errors = getErrorsList(); + foreach (var err in errors) (actual(err) == expected(err)).Should().BeTrue(); // test if predicates are equivalent + } - foreach (var err in errors) (actual(err) == expected(err)).Should().BeTrue(); // test if predicates are equivalent - } - - - [TestMethod] - public void TestInvalidCustomization() - { - var shouldThrow = () => (_ = new JsonSerializerOptions().UsingMode(DeserializerModes.Ostrich)); - shouldThrow.Should().Throw("Expected error trying to set the mode of a non-existent converter"); - } + + [TestMethod] + public void TestInvalidCustomization() + { + var shouldThrow = () => (_ = new JsonSerializerOptions().UsingMode(DeserializerModes.Ostrich)); + shouldThrow.Should().Throw("Expected error trying to set the mode of a non-existent converter"); } -} -#nullable restore \ No newline at end of file +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonSerializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonSerializationTests.cs index 192c49e5fd..26a9036677 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonSerializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonSerializationTests.cs @@ -22,19 +22,8 @@ public class FhirJsonSerializationTests var filename = Path.Combine("TestData", "json-edge-cases.json"); var expected = File.ReadAllText(filename); - try - { - var parsed = JsonSerializer.Deserialize(expected, BaseOptions); - return (parsed, expected); - } - catch (DeserializationFailedException dfe) - { - if (dfe.Exceptions.All(e => e.ErrorCode == CodedValidationException.CONTAINED_RESOURCE_CANNOT_HAVE_NARRATIVE_CODE)) - return (dfe.PartialResult as Patient, expected); - else - throw; - } - + var parsed = JsonSerializer.Deserialize(expected, BaseOptions); + return (parsed, expected); } [TestMethod] diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirSerializationEngineTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirSerializationEngineTests.cs index ed8b419e41..9b456bb83d 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirSerializationEngineTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirSerializationEngineTests.cs @@ -7,85 +7,83 @@ using Hl7.Fhir.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Runtime.CompilerServices; -using System.Xml.Linq; -namespace Hl7.Fhir.Support.Poco.Tests +namespace Hl7.Fhir.Support.Poco.Tests; + +[TestClass] +public class FhirSerializationEngineTests { + private static readonly ModelInspector TESTINSPECTOR = ModelInspector.ForType(typeof(Patient)); - [TestClass] - public class FhirSerializationEngineTests - { - private static readonly ModelInspector TESTINSPECTOR = ModelInspector.ForType(typeof(Patient)); + private const string CORRECTXML = """"""; + private const string PERMISSIVEXML = """"""; + private const string BWCOMPATIBLEXML = """"""; + private const string EM_WRONGXML = """"""; - private const string CORRECTXML = """"""; - private const string PERMISSIVEXML = """"""; - private const string BWCOMPATIBLEXML = """"""; - private const string WRONGXML = """"""; - - private const string CORRECTJSON = """{ "resourceType": "Patient", "active": true }"""; - private const string PERMISSIVEJSON = """{ "resourceType": "Patient", "gender": "" }"""; - private const string BWCOMPATIBLEJSON = """{ "resourceType": "Patient", "activex": "true" }"""; - private const string WRONGJSON = """{ "resourceType": "Patient", "multipleBirthString": "crazy" }"""; + private const string CORRECTJSON = """{ "resourceType": "Patient", "active": true }"""; + private const string PERMISSIVEJSON = """{ "resourceType": "Patient", "gender": "" }"""; + private const string BWCOMPATIBLEJSON = """{ "resourceType": "Patient", "activex": "true" }"""; + private const string EM_WRONGJSON = """{ "resourceType": "Patient", "deceasedImaginary": "2i" }"""; - private const string EM_UNKNOWN_ELEMENT = "*Encountered unknown element 'activex'*"; - private const string EM_INCORRECT_CHOICE = "*suffixed with unexpected type 'string'*"; - private const string EM_EMPTY_VALUE = "*'gender' has an empty*value*"; + private const string EM_UNKNOWN_ELEMENT = "*Encountered unknown element 'activex'*"; + private const string EM_INCORRECT_CHOICE = "*Choice element 'deceasedImaginary' is suffixed with unexpected type 'Imaginary'*"; + private const string EM_EMPTY_VALUE = "*'gender' has an empty*value*"; - [TestMethod] - [DataRow(CORRECTXML, null, null, null)] - [DataRow(PERMISSIVEXML, null, EM_EMPTY_VALUE, EM_EMPTY_VALUE)] - [DataRow(BWCOMPATIBLEXML, EM_UNKNOWN_ELEMENT, EM_UNKNOWN_ELEMENT, null)] - [DataRow(WRONGXML, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE)] - [DataRow(CORRECTJSON, null, null, null)] - [DataRow(PERMISSIVEJSON, null, EM_EMPTY_VALUE, EM_EMPTY_VALUE)] - [DataRow(BWCOMPATIBLEJSON, EM_UNKNOWN_ELEMENT, EM_UNKNOWN_ELEMENT, null)] - [DataRow(WRONGJSON, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE)] - public void TestParsingEM(string data, string? permissive, string? strict, string? bw) - { - test(data, FhirSerializationEngineFactory.Legacy.Permissive(TESTINSPECTOR), permissive); - test(data, FhirSerializationEngineFactory.Legacy.Strict(TESTINSPECTOR), strict); - test(data, FhirSerializationEngineFactory.Legacy.BackwardsCompatible(TESTINSPECTOR), bw); - test(data, FhirSerializationEngineFactory.Legacy.Ostrich(TESTINSPECTOR), null); // there should never be an error ;-) - } + [TestMethod] + [DataRow(CORRECTXML, null, null, null, DisplayName = "Correct XML")] + [DataRow(PERMISSIVEXML, null, EM_EMPTY_VALUE, EM_EMPTY_VALUE, DisplayName = "Permissive XML")] + [DataRow(BWCOMPATIBLEXML, EM_UNKNOWN_ELEMENT, EM_UNKNOWN_ELEMENT, null, DisplayName = "Backwards-compatible XML")] + [DataRow(EM_WRONGXML, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE, DisplayName = "Wrong XML")] + [DataRow(CORRECTJSON, null, null, null, DisplayName = "Correct JSON")] + [DataRow(PERMISSIVEJSON, null, EM_EMPTY_VALUE, EM_EMPTY_VALUE, DisplayName = "Permissive JSON")] + [DataRow(BWCOMPATIBLEJSON, EM_UNKNOWN_ELEMENT, EM_UNKNOWN_ELEMENT, null, DisplayName = "Backwards-compatible JSON")] + [DataRow(EM_WRONGJSON, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE, EM_INCORRECT_CHOICE, DisplayName = "Wrong JSON")] + public void TestParsingEM(string data, string? permissive, string? strict, string? bw) + { + test(data, FhirSerializationEngineFactory.Legacy.Permissive(TESTINSPECTOR), permissive); + test(data, FhirSerializationEngineFactory.Legacy.Strict(TESTINSPECTOR), strict); + test(data, FhirSerializationEngineFactory.Legacy.BackwardsCompatible(TESTINSPECTOR), bw); + test(data, FhirSerializationEngineFactory.Legacy.Ostrich(TESTINSPECTOR), null); // there should never be an error ;-) + } - private const string POCO_UNKNOWN_ELEMENT = "*Encountered unrecognized * 'activex'*"; - private const string POCO_INCORRECT_CHOICE = "*Value is of type 'string', which is not an allowed choice*"; - private const string POCO_EMPTY_VALUE = "* cannot be empty*. Either they are absent*"; + private const string POCO_UNKNOWN_ELEMENT = "*Encountered unrecognized * 'activex'*"; + private const string POCO_INCORRECT_REPEAT = "*Expected a primitive value, not the start of an array.*"; + private const string POCO_EXPECTED_OBJECT = "*Expected start of object, but found Number*"; + private const string POCO_EMPTY_VALUE = "* cannot be empty*. Either they are absent*"; + private const string POCO_WRONGXML = """{ "resourceType": "Patient", "contact": 5 }"""; + private const string POCO_WRONGJSON = """{ "resourceType": "Patient", "active": [true,false] }"""; - [TestMethod] - [DataRow(CORRECTXML, null, null, null)] - [DataRow(PERMISSIVEXML, null, POCO_EMPTY_VALUE, POCO_EMPTY_VALUE)] - [DataRow(BWCOMPATIBLEXML, POCO_UNKNOWN_ELEMENT, POCO_UNKNOWN_ELEMENT, null)] - [DataRow(WRONGXML, POCO_INCORRECT_CHOICE, POCO_INCORRECT_CHOICE, POCO_INCORRECT_CHOICE)] - [DataRow(CORRECTJSON, null, null, null)] - [DataRow(PERMISSIVEJSON, null, POCO_EMPTY_VALUE, POCO_EMPTY_VALUE)] - [DataRow(BWCOMPATIBLEJSON, POCO_UNKNOWN_ELEMENT, POCO_UNKNOWN_ELEMENT, null)] - [DataRow(WRONGJSON, POCO_INCORRECT_CHOICE, POCO_INCORRECT_CHOICE, POCO_INCORRECT_CHOICE)] - public void TestParsingPoco(string data, string? permissive, string? strict, string? bw) - { - test(data, FhirSerializationEngineFactory.Recoverable(TESTINSPECTOR), permissive); - test(data, FhirSerializationEngineFactory.Strict(TESTINSPECTOR), strict); - test(data, FhirSerializationEngineFactory.BackwardsCompatible(TESTINSPECTOR), bw); - test(data, FhirSerializationEngineFactory.Ostrich(TESTINSPECTOR), null); // there should never be an error ;-) - // TODO add custom test - } + [TestMethod] + [DataRow(CORRECTXML, null, null, null, DisplayName = "Correct XML")] + [DataRow(PERMISSIVEXML, null, POCO_EMPTY_VALUE, POCO_EMPTY_VALUE, DisplayName = "Permissive XML")] + [DataRow(BWCOMPATIBLEXML, POCO_UNKNOWN_ELEMENT, POCO_UNKNOWN_ELEMENT, null, DisplayName = "Backwards-compatible XML")] + [DataRow(POCO_WRONGXML, POCO_EXPECTED_OBJECT, POCO_EXPECTED_OBJECT, POCO_EXPECTED_OBJECT, DisplayName = "Wrong XML")] + [DataRow(CORRECTJSON, null, null, null, DisplayName = "Correct JSON")] + [DataRow(PERMISSIVEJSON, null, POCO_EMPTY_VALUE, POCO_EMPTY_VALUE, DisplayName = "Permissive JSON")] + [DataRow(BWCOMPATIBLEJSON, POCO_UNKNOWN_ELEMENT, POCO_UNKNOWN_ELEMENT, null, DisplayName = "Backwards-compatible JSON")] + [DataRow(POCO_WRONGJSON, POCO_INCORRECT_REPEAT, POCO_INCORRECT_REPEAT, POCO_INCORRECT_REPEAT, DisplayName = "Wrong JSON")] + public void TestParsingPoco(string data, string? permissive, string? strict, string? bw) + { + test(data, FhirSerializationEngineFactory.Recoverable(TESTINSPECTOR), permissive); + test(data, FhirSerializationEngineFactory.Strict(TESTINSPECTOR), strict); + test(data, FhirSerializationEngineFactory.BackwardsCompatible(TESTINSPECTOR), bw); + test(data, FhirSerializationEngineFactory.Ostrich(TESTINSPECTOR), null); // there should never be an error ;-) + // TODO add custom test + } - private static void test(string data, IFhirSerializationEngine engine, string? error, [CallerArgumentExpression(nameof(engine))] string? engineName = default) + private static void test(string data, IFhirSerializationEngine engine, string? error, [CallerArgumentExpression(nameof(engine))] string? engineName = default) + { + try { - try - { - _ = SerializationUtil.ProbeIsXml(data) ? engine.DeserializeFromXml(data) : engine.DeserializeFromJson(data); + _ = SerializationUtil.ProbeIsXml(data) ? engine.DeserializeFromXml(data) : engine.DeserializeFromJson(data); - if (error is not null) - Assert.Fail($"Engine {engineName} should have thrown message with {error}"); - } - catch (DeserializationFailedException dfe) - { - error.Should().NotBeNull($"Did not expect exception {dfe.Message} for engine {engineName}."); - dfe.Message.Should().Match(error, because: $"that should be the message for engine {engineName}"); - } + if (error is not null) + Assert.Fail($"Engine {engineName} should have thrown message with {error}"); + } + catch (DeserializationFailedException dfe) + { + error.Should().NotBeNull($"Did not expect exception {dfe.Message} for engine {engineName}."); + dfe.Message.Should().Match(error, because: $"that should be the message for engine {engineName}"); } } -} - -#nullable restore \ No newline at end of file +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs index 0b85345b4c..8254837560 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirXmlDeserializationTests.cs @@ -1,15 +1,14 @@ using FluentAssertions; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; -using Hl7.Fhir.Utility; using Hl7.Fhir.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.IO; +using System.Linq; using System.Xml; -using Date = Hl7.Fhir.ElementModel.Types.Date; -using DateTime = Hl7.Fhir.ElementModel.Types.DateTime; using ERR = Hl7.Fhir.Serialization.FhirXmlException; +using COVE=Hl7.Fhir.Validation.CodedValidationException; namespace Hl7.Fhir.Support.Poco.Tests { @@ -35,49 +34,44 @@ public void TryDeserializePrimitives(string xmlPrimitive, Type expectedFhirType, } [DataTestMethod] - [DataRow("", typeof(bool), true, null, DisplayName = "XmlBool1")] - [DataRow("", typeof(bool), "1", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlBool2")] - [DataRow("", typeof(bool), "treu", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlBool3")] - [DataRow("", typeof(DateTimeOffset), "2000-01-01", null, DisplayName = "XmlInstant1")] - [DataRow("", typeof(DateTimeOffset), "foo", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlInstant2")] - [DataRow("", typeof(byte[]), "foo", ERR.INCORRECT_BASE64_DATA_CODE, DisplayName = "XmlByteArray")] - [DataRow("", typeof(int), 1, null, DisplayName = "XmlInteger1")] - [DataRow("", typeof(int), "1.1", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlInteger2")] - [DataRow("", typeof(long), 1, null, DisplayName = "XmlLong1")] - [DataRow("", typeof(long), "1.1", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlLong2")] - [DataRow("", typeof(uint), 1, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlUint1")] - [DataRow("", typeof(uint), "-1", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlUint2")] - [DataRow("", typeof(decimal), 3.14, null, DisplayName = "XmlDecimal1")] - [DataRow("", typeof(decimal), 3.14e2, null, DisplayName = "XmlDecimal1")] - [DataRow("", typeof(decimal), "3.14e500", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlDecimal2")] - [DataRow("", typeof(double), 3.14, null, DisplayName = "XmlDouble1")] - [DataRow("", typeof(ulong), 1, ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlUlong1")] - [DataRow("", typeof(ulong), "-1", ERR.VALUE_IS_NOT_OF_EXPECTED_TYPE_CODE, DisplayName = "XmlUlong2")] - [DataRow("", typeof(float), 1, null, DisplayName = "XmlFloat1")] - public void TryDeserializePrimitiveValue(string xmlPrimitive, Type implementingType, object expectedValue, string expectedErrorCode) + [DataRow("", typeof(FhirBoolean), true, null, DisplayName = "XmlBool1")] + [DataRow("", typeof(FhirBoolean), "1", COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, DisplayName = "XmlBool2")] + [DataRow("", typeof(FhirBoolean), "treu", COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, DisplayName = "XmlBool3")] + [DataRow("", typeof(Instant), "2000-01-01T12:00:00Z", null, DisplayName = "XmlInstant1")] + [DataRow("", typeof(Instant), "foo", COVE.LITERAL_INVALID_CODE, DisplayName = "XmlInstant2")] + [DataRow("", typeof(Base64Binary), "foo", COVE.INVALID_BASE64_VALUE_CODE, DisplayName = "XmlByteArray")] + [DataRow("", typeof(Integer), 1, null, DisplayName = "XmlInteger1")] + [DataRow("", typeof(Integer), "1.1", COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, DisplayName = "XmlInteger2")] + [DataRow("", typeof(Integer64), "1", null, DisplayName = "XmlLong1")] + [DataRow("", typeof(Integer64), "1.1", COVE.LITERAL_INVALID_CODE, DisplayName = "XmlLong2")] + [DataRow("", typeof(FhirDecimal), 3.14, null, DisplayName = "XmlDecimal1")] + [DataRow("", typeof(FhirDecimal), 3.14e2, null, DisplayName = "XmlDecimal1")] + [DataRow("", typeof(FhirDecimal), "3.14e500", COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE, DisplayName = "XmlDecimal2")] + + public void TryDeserializePrimitiveValue(string xmlPrimitive, Type fhirTargetType, object expectedValue, string expectedErrorCode) { var reader = constructReader(xmlPrimitive); reader.MoveToContent(); - reader.MoveToFirstAttribute(); + //reader.MoveToFirstAttribute(); - var deserializer = getTestDeserializer(new()); - var ps = new PathStack(); - ps.EnterElement("Patient", 0, false); - var (value, error) = deserializer.ParsePrimitiveValue(reader, implementingType, ps); + var deserializer = getTestDeserializer(new FhirXmlPocoDeserializerSettings()); + var classMapping = ModelInfo.ModelInspector.ImportType(fhirTargetType)!; + var target = (PrimitiveType)classMapping.Factory()!; + var state = new FhirXmlPocoDeserializerState(); + deserializer.DeserializeElementInto(target, classMapping, reader, state); - error?.ErrorCode.Should().Be(expectedErrorCode); + state.Errors.Should().HaveCount(expectedErrorCode == null ? 0 : 1); - if (implementingType == typeof(DateTimeOffset) && expectedErrorCode is null) + if(state.Errors.Count > 0) { - value.Should().BeOfType().Which.ToFhirDate().Should().Be((string)expectedValue); + state.Errors.First().ErrorCode.Should().Be(expectedErrorCode); } else { - value.Should().Be(expectedValue); + target.ObjectValue.Should().Be(expectedValue); } } - [TestMethod] public void TryDeserializeResourceSinglePrimitive() { @@ -234,7 +228,7 @@ public void TryDeserializeExtensions() var resource = deserializer.DeserializeResource(reader); resource.Should().BeOfType(); - resource.As().Active.Value.Should().Be(true); + resource.As().Active!.Should().Be(true); resource.As().Extension.Should().HaveCount(1); resource.As().Extension[0].Url.Should().Be("http://fire.ly/fhir/StructureDefinition/extension-test"); resource.As().Extension[0].Value.As().Value.Should().Be("foo"); @@ -477,30 +471,21 @@ public void TryDeserializeDatatypeWithId() } [TestMethod] - public void TestCustomValidators() + public void TestValidatorIsCalledDuringDeserialization() { - test(new FhirJsonDeserializationTests.CustomComplexValidator()); - test(new FhirJsonDeserializationTests.CustomDataTypeValidator()); - test(new FhirJsonDeserializationTests.CustomPropertyValueValidator()); + var validator = new FhirJsonDeserializationTests.CustomComplexValidator(); - static void test(IDeserializationValidator validator) - { - var xml = ""; - var reader = constructReader(xml); - reader.Read(); - - var serializer = getTestDeserializer(new FhirXmlPocoDeserializerSettings { Validator = validator }); - var state = new FhirXmlPocoDeserializerState(); + const string xml = ""; + var reader = constructReader(xml); + reader.Read(); - var result = serializer.DeserializeResourceInternal(reader, state); + var serializer = getTestDeserializer(new FhirXmlPocoDeserializerSettings { Validator = validator }); + serializer.TryDeserializeResource(reader, out _, out var issues); - state.Errors.HasExceptions.Should().BeTrue(); - state.Errors.Should().AllBeOfType() - .And.ContainSingle(e => ((CodedValidationException)e).ErrorCode == CodedValidationException.DATETIME_LITERAL_INVALID_CODE); - result.Should().BeOfType() - .Which.Deceased.Should().BeOfType() - .Which.Value.Should().EndWith("+00:00"); - } + var errors = issues.ToList(); + errors.Should().HaveCount(1); + errors.Single().Should().BeOfType().Which.ErrorCode.Should().Be(CodedValidationException.LITERAL_INVALID_CODE); + validator.DateTimeSeenByInstanceValidator?.Value.Should().Be("1972-11-30T12:00:00Z"); } [TestMethod] diff --git a/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/IsValidValueTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/IsValidValueTests.cs index b129efb974..a2c16ee70a 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/IsValidValueTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/IsValidValueTests.cs @@ -26,9 +26,6 @@ public void TestValidPrimitives() t(Date.IsValidValue("2020-02-03")); f(Date.IsValidValue("2020-02-03+03:00")); - t(FhirBoolean.IsValidValue("true")); - f(FhirBoolean.IsValidValue("True")); - t(FhirDateTime.IsValidValue("2020")); f(FhirDateTime.IsValidValue("2020+01:00")); t(FhirDateTime.IsValidValue("2020-01-01T14+01:00")); @@ -51,14 +48,6 @@ public void TestValidPrimitives() // Integer's test are exactly covered by System's Integer.TryParse // Integer64's test are exactly covered by System's Integer.TryParse - t(PositiveInt.IsValidValue("4")); - f(PositiveInt.IsValidValue("0")); - f(PositiveInt.IsValidValue("-1")); - - t(UnsignedInt.IsValidValue("4")); - t(UnsignedInt.IsValidValue("0")); - f(UnsignedInt.IsValidValue("-1")); - t(Time.IsValidValue("12:03:04")); f(Time.IsValidValue("12:03:04-01:00")); } @@ -72,14 +61,14 @@ public void TestValidXhtmlXhml() t(XHtml.IsValidXml("hi!", out _)); f(XHtml.IsValidXml("hi!", out _)); f(XHtml.IsValidXml("hi!", out _)); - f(XHtml.IsValidNarrativeXhtml("hi!", out _)); - f(XHtml.IsValidNarrativeXhtml("hi!", out _)); - t(XHtml.IsValidNarrativeXhtml("
some text
", out _)); - t(XHtml.IsValidNarrativeXhtml("
some text
", out _)); - f(XHtml.IsValidNarrativeXhtml("hi!")); - f(XHtml.IsValidNarrativeXhtml("hi!")); - t(XHtml.IsValidNarrativeXhtml("
some text
")); - t(XHtml.IsValidNarrativeXhtml("
some text
")); + f(XHtml.IsValidNarrativeXhtml("hi!", out _, out _)); + f(XHtml.IsValidNarrativeXhtml("hi!", out _, out _)); + t(XHtml.IsValidNarrativeXhtml("
some text
", out _, out _)); + t(XHtml.IsValidNarrativeXhtml("
some text
", out _, out _)); + f(XHtml.IsValidNarrativeXhtml("hi!", out _, out _)); + f(XHtml.IsValidNarrativeXhtml("hi!", out _, out _)); + t(XHtml.IsValidNarrativeXhtml("
some text
", out _, out _)); + t(XHtml.IsValidNarrativeXhtml("
some text
", out _, out _)); } public static IEnumerable ValidUris => cases( @@ -130,4 +119,4 @@ private static IEnumerable cases(params string[] tests) => public void TestInvalidCanonical(string test) => Canonical.IsValidValue(test).Should().BeFalse(); } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/ValidatableObjectTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/ValidatableObjectTests.cs index 06f2a0b87a..029d4e0342 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/ValidatableObjectTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/PocoValidationTests/ValidatableObjectTests.cs @@ -39,11 +39,11 @@ public void TestCodeOfT() c.ObjectValue = "wrong"; assertValid(c, errorCode: COVE.INVALID_CODED_VALUE_CODE); - Assert.ThrowsException(() => c.Value); + Assert.ThrowsException(() => c.Value); c.ObjectValue = 4; - assertValid(c, errorCode: COVE.INVALID_CODED_VALUE_CODE); - Assert.ThrowsException(() => c.Value); + assertValid(c, errorCode: COVE.INCORRECT_LITERAL_VALUE_TYPE_CODE); + Assert.ThrowsException(() => c.Value); } diff --git a/src/Hl7.Fhir.Support.Poco.Tests/TestData/fp-test-patient-errors-recovered.json b/src/Hl7.Fhir.Support.Poco.Tests/TestData/fp-test-patient-errors-recovered.json index 2f10d1a61a..33dac7fd11 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/TestData/fp-test-patient-errors-recovered.json +++ b/src/Hl7.Fhir.Support.Poco.Tests/TestData/fp-test-patient-errors-recovered.json @@ -16,7 +16,7 @@ "id": "1", "identifier": [ { - "id": "true", + "id": true, "system": "http://hl7.org/fhir/sid/us-ssn", "value": "444222222" } @@ -223,7 +223,7 @@ "system": "phone", "value": "555-555-2003", "use": "work", - "rank": "true" + "rank": true } ], "gender": "male", @@ -234,7 +234,7 @@ }, { "url": "http://example.org/StructureDefinition/extension-with-number-as-code", - "valueCode": "28" + "valueCode": 28 }, { "url": "http://example.org/StructureDefinition/extension-without-type" }, { "url": "http://example.org/StructureDefinition/extension-with-unknown-type" } @@ -250,7 +250,7 @@ ] }, "deceasedBoolean": "false", - "multipleBirthInteger": "3.14", + "multipleBirthInteger": 3.14, "photo": [ { "contentType": "image/gif", diff --git a/src/Hl7.FhirPath.Tests/Tests/CastTests.cs b/src/Hl7.FhirPath.Tests/Tests/CastTests.cs index e27c60fcbb..98dfc11ce4 100644 --- a/src/Hl7.FhirPath.Tests/Tests/CastTests.cs +++ b/src/Hl7.FhirPath.Tests/Tests/CastTests.cs @@ -37,7 +37,7 @@ public void TestUnbox() Assert.AreEqual(complex, Typecasts.UnboxTo(singleC, typeof(PocoNode))); Assert.AreEqual(4L, Typecasts.UnboxTo(singleV, typeof(long))); - Assert.AreEqual(4L, Typecasts.UnboxTo(PocoNode.ForPrimitive(4L), typeof(long))); + Assert.AreEqual(4L, Typecasts.UnboxTo(PocoNode.ForPrimitive("4"), typeof(long))); Assert.AreEqual(complex, Typecasts.UnboxTo(complex, typeof(PocoNode))); Assert.IsNull(Typecasts.UnboxTo(null, typeof(string)));