diff --git a/src/Benchmarks/Benchmarks.csproj b/src/Benchmarks/Benchmarks.csproj index d9a65e755b..118d35af9a 100644 --- a/src/Benchmarks/Benchmarks.csproj +++ b/src/Benchmarks/Benchmarks.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Hl7.Fhir.Base/Introspection/DeclaredTypeAttribute.cs b/src/Hl7.Fhir.Base/Introspection/DeclaredTypeAttribute.cs index ccc3aaebe5..3fa1c0a6ad 100644 --- a/src/Hl7.Fhir.Base/Introspection/DeclaredTypeAttribute.cs +++ b/src/Hl7.Fhir.Base/Introspection/DeclaredTypeAttribute.cs @@ -16,7 +16,7 @@ namespace Hl7.Fhir.Introspection /// in the constructor to this attribute. /// [CLSCompliant(false)] - [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)] public class DeclaredTypeAttribute : VersionedAttribute { public DeclaredTypeAttribute() diff --git a/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs b/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs index 7a49a53e15..78793fc5f3 100644 --- a/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs +++ b/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs @@ -22,7 +22,7 @@ namespace Hl7.Fhir.Introspection /// /// A cache of FHIR type mappings found on .NET classes. /// - /// POCO's in the "common" assemblies + /// POCO's in the "base" assembly /// can reflect the definition of multiple releases of FHIR using /// attributes. A will always capture the metadata for one such /// which is passed to it in the constructor. @@ -41,8 +41,8 @@ public class ModelInspector : IStructureDefinitionSummaryProvider, IModelInfo /// Calling this function repeatedly for the same type will return the same ClassMapping. /// /// If the type given is FHIR Release specific, the returned mapping will contain - /// metadata for that release only. If the type is from the common assembly, it will contain - /// metadata for that type from the most recent release of the common assembly. + /// metadata for that release only. If the type is from the base assembly, it will contain + /// metadata for that type from the most recent release of the base assembly. public static ClassMapping? GetClassMappingForType(Type t) => ForAssembly(t.GetTypeInfo().Assembly).FindOrImportClassMapping(t); @@ -52,8 +52,8 @@ public class ModelInspector : IStructureDefinitionSummaryProvider, IModelInfo /// the same assembly will return the same inspector. /// /// If the assembly given is FHIR Release specific, the returned inspector will contain - /// metadata for that release only. If the assembly is the common assembly, it will contain - /// metadata for the most recent release for those common classes. + /// metadata for that release only. If the assembly is the base assembly, it will contain + /// metadata for the most recent release for those base classes. public static ModelInspector ForAssembly(Assembly a) { return _inspectedAssemblies.GetOrAdd(a.FullName ?? throw Error.ArgumentNull(nameof(a.FullName)), _ => configureInspector(a)); @@ -67,10 +67,10 @@ static ModelInspector configureInspector(Assembly a) var newInspector = new ModelInspector(modelAssemblyAttr.Since); newInspector.Import(a); - // Make sure we always include the common types too. - var commonAssembly = typeof(Resource).GetTypeInfo().Assembly; - if (a.FullName != commonAssembly.FullName) - newInspector.Import(commonAssembly); + // Make sure we always include the types from the base assembly too. + var baseAssembly = typeof(Resource).GetTypeInfo().Assembly; + if (a.FullName != baseAssembly.FullName) + newInspector.Import(baseAssembly); // And finally, the System/CQL primitive types foreach (var cqlType in getCqlTypes()) @@ -104,9 +104,9 @@ static IEnumerable getCqlTypes() /// /// Returns a fully configured with the - /// FHIR metadata contents of the common assembly + /// FHIR metadata contents of the base assembly /// - public static ModelInspector Common => ForType(typeof(ModelInspector)); + public static ModelInspector Base => ForType(typeof(ModelInspector)); /// /// Constructs a ModelInspector that will reflect the FHIR metadata for the given FHIR release diff --git a/src/Hl7.Fhir.Base/Model/Generated/Attachment.cs b/src/Hl7.Fhir.Base/Model/Generated/Attachment.cs index 92b05fe6f3..817624abbc 100644 --- a/src/Hl7.Fhir.Base/Model/Generated/Attachment.cs +++ b/src/Hl7.Fhir.Base/Model/Generated/Attachment.cs @@ -183,6 +183,8 @@ public string Url /// Number of bytes of content (if url provided) /// [FhirElement("size", InSummary=true, Order=70)] + [DeclaredType(Type = typeof(UnsignedInt), Since = FhirRelease.STU3)] + [DeclaredType(Type = typeof(Integer64), Since = FhirRelease.R5)] [DataMember] public Hl7.Fhir.Model.Integer64 SizeElement { diff --git a/src/Hl7.Fhir.Base/Model/Markdown.cs b/src/Hl7.Fhir.Base/Model/Markdown.cs index a6f6755e5a..b44b8de229 100644 --- a/src/Hl7.Fhir.Base/Model/Markdown.cs +++ b/src/Hl7.Fhir.Base/Model/Markdown.cs @@ -40,8 +40,8 @@ public partial class Markdown /// public static bool IsValidValue(string value) => FhirString.IsValidValue(value); - public static implicit operator string(Markdown md) => md.Value; - public static implicit operator Markdown(string s) => new(s); + public static implicit operator string?(Markdown? md) => md?.Value; + public static implicit operator Markdown?(string? s) => s is not null ? new(s) : null; } diff --git a/src/Hl7.Fhir.Base/Properties/AssemblyInfo.cs b/src/Hl7.Fhir.Base/Properties/AssemblyInfo.cs index d8deb1b4f9..744c396e65 100644 --- a/src/Hl7.Fhir.Base/Properties/AssemblyInfo.cs +++ b/src/Hl7.Fhir.Base/Properties/AssemblyInfo.cs @@ -2,7 +2,7 @@ using System; using System.Runtime.CompilerServices; -[assembly: FhirModelAssembly] +[assembly: FhirModelAssembly(Since = Hl7.Fhir.Specification.FhirRelease.STU3)] // The following GUID is for the ID of the typelib if this project is exposed to COM diff --git a/src/Hl7.Fhir.Base/Rest/TransactionBuilder.cs b/src/Hl7.Fhir.Base/Rest/TransactionBuilder.cs index a3a78088a7..3f6f3a2045 100644 --- a/src/Hl7.Fhir.Base/Rest/TransactionBuilder.cs +++ b/src/Hl7.Fhir.Base/Rest/TransactionBuilder.cs @@ -269,7 +269,7 @@ private string paramValueToString(Parameters.ParameterComponent parameter) case CodeableConcept codeableConcept: return codeableConcept.ToToken(); default: - if (ModelInspector.Common.IsPrimitive(parameter.Value.GetType())) + if (ModelInspector.Base.IsPrimitive(parameter.Value.GetType())) { return parameter.Value.ToString(); } diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonBuilder.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonBuilder.cs index 6d3b0a27b0..c5ac6003fc 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonBuilder.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonBuilder.cs @@ -103,7 +103,7 @@ private JObject buildInternal(ITypedElement source) object value = node.Definition != null ? node.Value : details?.OriginalValue ?? node.Value; var objectInShadow = node.InstanceType != null ? primitiveTypes.Contains(node.InstanceType) : details?.UsesShadow ?? false; - JToken first = value != null ? buildValue(value) : null; + JToken first = value != null ? buildValue(value, node.InstanceType) : null; JObject second = buildChildren(node); // If this is a complex type with a value (should not occur) @@ -200,25 +200,12 @@ private void addChildren(ITypedElement node, JObject parent) } } - private JValue buildValue(object value) + private JValue buildValue(object value, string requiredType = null) => value switch { - switch (value) - { - case bool b: - case decimal d: - case Int32 i32: - case Int16 i16: - case ulong ul: - case double db: - case BigInteger bi: - case float f: - return new JValue(value); - case string s: - return new JValue(s.Trim()); - case long l: - default: - return new JValue(PrimitiveTypeConverter.ConvertTo(value)); - } - } + bool or decimal or Int32 or Int16 or ulong or double or BigInteger or float => new JValue(value), + string s => new JValue(s.Trim()), + long l when requiredType is "integer" or "unsignedInt" or "positiveInt" => new JValue(l), + _ => new JValue(PrimitiveTypeConverter.ConvertTo(value)), + }; } } diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs index 17ccdae6fe..e6401629d2 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonException.cs @@ -11,7 +11,6 @@ using Hl7.Fhir.Utility; using System; using System.Globalization; -using System.Linq; using System.Text.Json; #nullable enable @@ -46,6 +45,8 @@ public class FhirJsonException : CodedException 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"; @@ -81,6 +82,10 @@ public class FhirJsonException : CodedException internal static readonly FhirJsonException NUMBER_CANNOT_BE_PARSED = new(NUMBER_CANNOT_BE_PARSED_CODE, "Json number '{0}' cannot be parsed as a {1}."); internal static readonly FhirJsonException UNEXPECTED_JSON_TOKEN = new(UNEXPECTED_JSON_TOKEN_CODE, "Expecting a {0}, but found a json {1} with value '{2}'."); + // In R5 Integer64 (long) are serialized as string. So we would expect a string during parsing. + internal static readonly FhirJsonException LONG_CANNOT_BE_PARSED = new(LONG_CANNOT_BE_PARSED_CODE, "Json string '{0}' cannot be parsed as a {1}."); + internal static readonly FhirJsonException LONG_INCORRECT_FORMAT = new(LONG_INCORRECT_FORMAT_CODE, "{0} '{1}' cannot be parsed as a {2}, because it should be a {3}."); + // The parser will turn a non-array value into an array with a single element, so no data is lost. internal static readonly FhirJsonException EXPECTED_START_OF_ARRAY = new(EXPECTED_START_OF_ARRAY_CODE, "Expected start of array."); diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoDeserializer.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoDeserializer.cs index c998c8c839..81676c04f6 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoDeserializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoDeserializer.cs @@ -13,6 +13,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Text.Json; using ERR = Hl7.Fhir.Serialization.FhirJsonException; @@ -344,11 +345,13 @@ FhirJsonPocoDeserializerState state // (one with, and one without the '_') var existingValue = propertyMapping.GetValue(target); + var fhirType = 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, ref reader, delayedValidations, state) : - DeserializeFhirPrimitive(existingValue as PrimitiveType, propertyName, propertyValueMapping, ref reader, delayedValidations, state); + deserializeFhirPrimitiveList((IList)existingValue!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state) : + DeserializeFhirPrimitive(existingValue as PrimitiveType, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state); } else { @@ -358,8 +361,8 @@ FhirJsonPocoDeserializerState state // Note that repeating simple elements (like Extension.url) do not currently exist in the FHIR serialization result = propertyMapping.IsCollection - ? deserializeNormalList(propertyValueMapping, ref reader, state) - : deserializeSingleValue(ref reader, propertyValueMapping, state); + ? deserializeNormalList(propertyValueMapping, ref reader, propertyMapping, state) + : deserializeSingleValue(ref reader, propertyValueMapping, propertyMapping, state); } // Only do validation when no parse errors were encountered, otherwise we'll just @@ -397,6 +400,7 @@ FhirJsonPocoDeserializerState state private IList? deserializeNormalList( ClassMapping propertyValueMapping, ref Utf8JsonReader reader, + PropertyMapping propertyMapping, FhirJsonPocoDeserializerState state) { // Create a list of the type of this property's value. @@ -424,7 +428,7 @@ FhirJsonPocoDeserializerState state // to simply create a list by Adding(). Not the fastest approach :-( while (reader.TokenType != JsonTokenType.EndArray) { - var result = deserializeSingleValue(ref reader, propertyValueMapping, state); + var result = deserializeSingleValue(ref reader, propertyValueMapping, propertyMapping, state); listInstance.Add(result); if (oneshot) break; @@ -462,6 +466,7 @@ public void Run() IList existingList, string propertyName, ClassMapping propertyValueMapping, + Type? fhirType, ref Utf8JsonReader reader, DelayedValidations delayedValidations, FhirJsonPocoDeserializerState state @@ -508,7 +513,7 @@ FhirJsonPocoDeserializerState state { existingList[elementIndex] ??= propertyValueMapping.Factory(); onlyNulls = false; - _ = DeserializeFhirPrimitive((PrimitiveType)existingList[elementIndex]!, propertyName, propertyValueMapping, ref reader, delayedValidations, state); + _ = DeserializeFhirPrimitive((PrimitiveType)existingList[elementIndex]!, propertyName, propertyValueMapping, fhirType, ref reader, delayedValidations, state); } elementIndex += 1; @@ -538,6 +543,7 @@ internal PrimitiveType DeserializeFhirPrimitive( PrimitiveType? existingPrimitive, string propertyName, ClassMapping propertyValueMapping, + Type? fhirType, ref Utf8JsonReader reader, DelayedValidations? delayedValidations, FhirJsonPocoDeserializerState state @@ -558,7 +564,7 @@ FhirJsonPocoDeserializerState state try { - var (result, error) = DeserializePrimitiveValue(ref reader, primitiveValueProperty.ImplementingType); + var (result, error) = DeserializePrimitiveValue(ref reader, primitiveValueProperty.ImplementingType, fhirType); // Only do validation when no parse errors were encountered, otherwise we'll just // produce spurious messages. @@ -609,7 +615,7 @@ FhirJsonPocoDeserializerState state /// 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) + private object? deserializeSingleValue(ref Utf8JsonReader reader, ClassMapping propertyValueMapping, PropertyMapping propertyMapping, FhirJsonPocoDeserializerState state) { // Resources if (propertyValueMapping.IsResource) @@ -622,7 +628,7 @@ FhirJsonPocoDeserializerState state // needs to handle PrimitiveType.ObjectValue & dual properties. else if (propertyValueMapping.IsPrimitive) { - var (result, error) = DeserializePrimitiveValue(ref reader, propertyValueMapping.NativeType); + var (result, error) = DeserializePrimitiveValue(ref reader, propertyValueMapping.NativeType, propertyMapping.FhirType.FirstOrDefault()); if (error is not null && result is not null) { @@ -654,7 +660,7 @@ FhirJsonPocoDeserializerState state /// 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 requiredType) + internal (object?, FhirJsonException?) DeserializePrimitiveValue(ref Utf8JsonReader reader, Type implementingType, Type? fhirType) { // Check for unexpected non-value types. if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) @@ -670,16 +676,17 @@ FhirJsonPocoDeserializerState state (object? partial, FhirJsonException? error) result = reader.TokenType switch { JsonTokenType.Null => new(null, ERR.EXPECTED_PRIMITIVE_NOT_NULL.With(ref reader)), - JsonTokenType.String when requiredType == typeof(string) => new(reader.GetString(), null), - JsonTokenType.String when requiredType == typeof(byte[]) => + JsonTokenType.String when implementingType == typeof(string) => new(reader.GetString(), null), + JsonTokenType.String when implementingType == typeof(byte[]) => !Settings.DisableBase64Decoding ? readBase64(ref reader) : new(reader.GetString(), null), - JsonTokenType.String when requiredType == typeof(DateTimeOffset) => readDateTimeOffset(ref reader), - JsonTokenType.String when requiredType.IsEnum => new(reader.GetString(), null), + JsonTokenType.String when implementingType == typeof(DateTimeOffset) => readDateTimeOffset(ref reader), + JsonTokenType.String when implementingType.IsEnum => new(reader.GetString(), null), + JsonTokenType.String when implementingType == typeof(long) => readLong(ref reader, fhirType), //JsonTokenType.String when requiredType.IsEnum => readEnum(ref reader, requiredType), - JsonTokenType.String => unexpectedToken(ref reader, reader.GetString(), requiredType.Name, "string"), - JsonTokenType.Number => tryGetMatchingNumber(ref reader, requiredType), - JsonTokenType.True or JsonTokenType.False when requiredType == typeof(bool) => new(reader.GetBoolean(), null), - JsonTokenType.True or JsonTokenType.False => unexpectedToken(ref reader, reader.GetRawText(), requiredType.Name, "boolean"), + JsonTokenType.String => unexpectedToken(ref reader, reader.GetString(), implementingType.Name, "string"), + JsonTokenType.Number => tryGetMatchingNumber(ref reader, implementingType, fhirType), + JsonTokenType.True or JsonTokenType.False when implementingType == typeof(bool) => new(reader.GetBoolean(), null), + JsonTokenType.True or JsonTokenType.False => unexpectedToken(ref reader, reader.GetRawText(), implementingType.Name, "boolean"), _ => // This would be an internal logic error, since our callers should have made sure we're @@ -692,7 +699,7 @@ FhirJsonPocoDeserializerState state // 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, requiredType, result.partial, result.error); + result = Settings.OnPrimitiveParseFailed(ref reader, implementingType, result.partial, result.error); // Read past the value reader.Read(); @@ -713,6 +720,26 @@ FhirJsonPocoDeserializerState state new(contents, ERR.STRING_ISNOTAN_INSTANT.With(ref reader, contents)); } + static (object?, FhirJsonException?) readLong(ref Utf8JsonReader reader, Type? fhirType) + { + // convert string in json to a long. + var contents = reader.GetString()!; + + return long.TryParse(contents, out var parsed) switch + { + true when isInteger64() => new(parsed, null), + true => new(parsed, ERR.LONG_INCORRECT_FORMAT.With(ref reader, "Json string", contents, typeName(), "Json number")), + false when isInteger64() => new(contents, ERR.LONG_CANNOT_BE_PARSED.With(ref reader, contents, nameof(Integer64))), + false => new(contents, ERR.LONG_INCORRECT_FORMAT.With(ref reader, "Json string", contents, typeName(), "Json number")) + }; + + string typeName() + => fhirType?.Name ?? string.Empty; + + bool isInteger64() + => fhirType == typeof(Integer64); + } + // Validation is now done using POCO validation, so have removed it here. // Keep code around in case I make my mind up before publication. //static (object?, FhirJsonException?) readEnum(ref Utf8JsonReader reader, Type enumType) @@ -733,7 +760,7 @@ private static (object?, FhirJsonException) unexpectedToken(ref Utf8JsonReader r /// 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 requiredType) + private static (object?, FhirJsonException?) tryGetMatchingNumber(ref Utf8JsonReader reader, Type implementingType, Type? fhirType) { if (reader.TokenType != JsonTokenType.Number) throw new InvalidOperationException($"Cannot read a numeric when reader is on a {reader.TokenType}. " + @@ -742,35 +769,37 @@ private static (object?, FhirJsonException?) tryGetMatchingNumber(ref Utf8JsonRe object? value = null; bool success; - if (requiredType == typeof(decimal)) + if (implementingType == typeof(decimal)) success = reader.TryGetDecimal(out decimal dec) && (value = dec) is { }; - else if (requiredType == typeof(int)) + else if (implementingType == typeof(int)) success = reader.TryGetInt32(out int i32) && (value = i32) is { }; - else if (requiredType == typeof(uint)) + else if (implementingType == typeof(uint)) success = reader.TryGetUInt32(out uint ui32) && (value = ui32) is { }; - else if (requiredType == typeof(long)) + else if (implementingType == typeof(long)) success = reader.TryGetInt64(out long i64) && (value = i64) is { }; - else if (requiredType == typeof(ulong)) + else if (implementingType == typeof(ulong)) success = reader.TryGetUInt64(out ulong ui64) && (value = ui64) is { }; - else if (requiredType == typeof(float)) + else if (implementingType == typeof(float)) success = reader.TryGetSingle(out float si) && si.IsNormal() && (value = si) is { }; - else if (requiredType == typeof(double)) + else if (implementingType == typeof(double)) success = reader.TryGetDouble(out double dbl) && dbl.IsNormal() && (value = dbl) is { }; else { var rawValue = reader.GetRawText(); - return unexpectedToken(ref reader, rawValue, requiredType.Name, "number"); + return unexpectedToken(ref reader, rawValue, implementingType.Name, "number"); } // We expected a number, we found a json number, but they don't match (e.g. precision etc) if (success) { - return new(value, null); + return implementingType == typeof(long) && fhirType == typeof(Integer64) + ? new(value, ERR.LONG_INCORRECT_FORMAT.With(ref reader, "Json number", reader.GetRawText(), nameof(Integer64), "Json string")) + : new(value, null); } else { var rawValue = reader.GetRawText(); - return new(rawValue, ERR.NUMBER_CANNOT_BE_PARSED.With(ref reader, rawValue, requiredType.Name)); + return new(rawValue, ERR.NUMBER_CANNOT_BE_PARSED.With(ref reader, rawValue, implementingType.Name)); } } diff --git a/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoSerializer.cs b/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoSerializer.cs index f56d28d40d..445aecb302 100644 --- a/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoSerializer.cs +++ b/src/Hl7.Fhir.Base/Serialization/FhirJsonPocoSerializer.cs @@ -19,7 +19,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Threading; namespace Hl7.Fhir.Serialization { @@ -98,11 +97,12 @@ private void serializeInternal( var propertyName = propertyMapping?.Choice == ChoiceType.DatatypeChoice ? addSuffixToElementName(member.Key, member.Value) : member.Key; + var requiredType = propertyMapping?.FhirType.FirstOrDefault(); if (member.Value is PrimitiveType pt) - serializeFhirPrimitive(propertyName, pt, writer); + serializeFhirPrimitive(propertyName, pt, writer, requiredType); else if (member.Value is IReadOnlyCollection pts) - serializeFhirPrimitiveList(propertyName, pts, writer); + serializeFhirPrimitiveList(propertyName, pts, writer, requiredType); else { writer.WritePropertyName(propertyName); @@ -112,12 +112,12 @@ private void serializeInternal( writer.WriteStartArray(); foreach (var value in coll) - serializeMemberValue(value, writer); + serializeMemberValue(value, writer, requiredType); writer.WriteEndArray(); } else - serializeMemberValue(member.Value, writer); + serializeMemberValue(member.Value, writer, requiredType); } filter?.LeaveMember(member.Key, member.Value, propertyMapping); @@ -140,12 +140,12 @@ private static string addSuffixToElementName(string elementName, object elementV } - private void serializeMemberValue(object value, Utf8JsonWriter writer) + private void serializeMemberValue(object value, Utf8JsonWriter writer, Type? requiredType = null) { if (value is IReadOnlyDictionary complex) serializeInternal(complex, writer, skipValue: false); else - SerializePrimitiveValue(value, writer); + SerializePrimitiveValue(value, writer, requiredType); } /// @@ -157,7 +157,8 @@ private void serializeMemberValue(object value, Utf8JsonWriter writer) private void serializeFhirPrimitiveList( string elementName, IReadOnlyCollection values, - Utf8JsonWriter writer) + Utf8JsonWriter writer, + Type? requiredType = null) { if (values is null) throw new ArgumentNullException(nameof(values)); @@ -181,7 +182,7 @@ private void serializeFhirPrimitiveList( writeStartArray(elementName, numNullsMissed, writer); } - SerializePrimitiveValue(value!.ObjectValue, writer); + SerializePrimitiveValue(value!.ObjectValue, writer, requiredType); } else { @@ -239,7 +240,7 @@ private static void writeStartArray(string propName, int numNulls, Utf8JsonWrite /// /// FHIR primitives are handled separately here since they may require /// serialization into two Json properties called "elementName" and "_elementName". - private void serializeFhirPrimitive(string elementName, PrimitiveType value, Utf8JsonWriter writer) + private void serializeFhirPrimitive(string elementName, PrimitiveType value, Utf8JsonWriter writer, Type? requiredType = null) { if (value is null) throw new ArgumentNullException(nameof(value)); @@ -247,7 +248,7 @@ private void serializeFhirPrimitive(string elementName, PrimitiveType value, Utf { // Write a property with 'elementName' writer.WritePropertyName(elementName); - SerializePrimitiveValue(value.ObjectValue, writer); + SerializePrimitiveValue(value.ObjectValue, writer, requiredType); } if (value.HasElements) @@ -271,13 +272,22 @@ private void serializeFhirPrimitive(string elementName, PrimitiveType value, Utf /// to be written that fit in .NET's type, which may be less /// precision than required by the FHIR specification (http://hl7.org/fhir/json.html#primitive). /// - protected virtual void SerializePrimitiveValue(object value, Utf8JsonWriter writer) + protected virtual void SerializePrimitiveValue(object value, Utf8JsonWriter writer, Type? requiredType) { switch (value) { case int i32: writer.WriteNumberValue(i32); break; case uint ui32: writer.WriteNumberValue(ui32); break; - case long i64: writer.WriteNumberValue(i64); 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; diff --git a/src/Hl7.Fhir.Conformance/Properties/AssemblyInfo.cs b/src/Hl7.Fhir.Conformance/Properties/AssemblyInfo.cs index b025c81117..7715f0bfb5 100644 --- a/src/Hl7.Fhir.Conformance/Properties/AssemblyInfo.cs +++ b/src/Hl7.Fhir.Conformance/Properties/AssemblyInfo.cs @@ -5,7 +5,7 @@ // This assembly is not fully CLSCompliant, but this triggers compiler warnings to avoid the issue as described here, when mixing C# and VB // https://msdn.microsoft.com/en-us/library/ms235408(v=vs.90).aspx [assembly: CLSCompliant(true)] -[assembly: FhirModelAssembly] +[assembly: FhirModelAssembly(Since = Hl7.Fhir.Specification.FhirRelease.R4)] #if DEBUG [assembly: InternalsVisibleTo("Hl7.Fhir.R4")] diff --git a/src/Hl7.Fhir.Conformance/Specification/Navigation/ElementDefinitionNavigationFunctions.cs b/src/Hl7.Fhir.Conformance/Specification/Navigation/ElementDefinitionNavigationFunctions.cs index 5c0cd96f59..6a198b142b 100644 --- a/src/Hl7.Fhir.Conformance/Specification/Navigation/ElementDefinitionNavigationFunctions.cs +++ b/src/Hl7.Fhir.Conformance/Specification/Navigation/ElementDefinitionNavigationFunctions.cs @@ -118,7 +118,7 @@ public static string ParseTypeFromRenamedElement(string rename, string choiceNam // Primitive types start with a lower-case character var altTypeName = Utility.StringExtensions.Uncapitalize(typeName); - if (ModelInspector.Common.IsPrimitive(altTypeName)) { return altTypeName; } + if (ModelInspector.Base.IsPrimitive(altTypeName)) { return altTypeName; } return typeName; } diff --git a/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs b/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs index 0b1d440a5a..077e67f33d 100644 --- a/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs +++ b/src/Hl7.Fhir.Conformance/Specification/Snapshot/ElementDefnMerger.cs @@ -173,7 +173,7 @@ void merge(ElementDefinition snap, ElementDefinition diff, bool mergeElementId, snap.Binding = mergeBinding(snap.Binding, diff.Binding); // [MV 20220803] Remove Binding when the element has no bindable type - if (snap.Binding is not null && !snap.Type.Any(t => ModelInspector.Common.IsBindable(t.Code))) + if (snap.Binding is not null && !snap.Type.Any(t => ModelInspector.Base.IsBindable(t.Code))) { snap.Binding = null; } diff --git a/src/Hl7.Fhir.ElementModel.STU3.Tests/TypedElementToSourceNodeAdapterTests.cs b/src/Hl7.Fhir.ElementModel.STU3.Tests/TypedElementToSourceNodeAdapterTests.cs index 650dd02b62..4976ecde33 100644 --- a/src/Hl7.Fhir.ElementModel.STU3.Tests/TypedElementToSourceNodeAdapterTests.cs +++ b/src/Hl7.Fhir.ElementModel.STU3.Tests/TypedElementToSourceNodeAdapterTests.cs @@ -28,7 +28,7 @@ public void AnnotationsTest() var result2 = sourceNode.Annotation(); Assert.IsNotNull(result2); Assert.AreEqual("TypedElementToSourceNodeAdapter", result2.GetType().Name); // I use the classname here, because PocoElementNode is internal in Hl7.Fhir.Core - Assert.AreSame(sourceNode, result2); + Assert.AreSame(sourceNode, result2); } [TestMethod] @@ -72,7 +72,7 @@ public async Tasks.Task SourceNodeFromElementNodeReturnsResourceTypeSupplier() Assert.IsNotNull(result); Assert.AreEqual(typeof(TypedElementToSourceNodeAdapter), result.GetType()); Assert.AreEqual("Patient", adapter.GetResourceTypeIndicator()); - Assert.AreSame(adapter, result); + Assert.AreSame(adapter, result); } } } diff --git a/src/Hl7.Fhir.ElementModel.Shared.Tests/TypedElementToSourceNodeAdapterTests.cs b/src/Hl7.Fhir.ElementModel.Shared.Tests/TypedElementToSourceNodeAdapterTests.cs index 650dd02b62..4976ecde33 100644 --- a/src/Hl7.Fhir.ElementModel.Shared.Tests/TypedElementToSourceNodeAdapterTests.cs +++ b/src/Hl7.Fhir.ElementModel.Shared.Tests/TypedElementToSourceNodeAdapterTests.cs @@ -28,7 +28,7 @@ public void AnnotationsTest() var result2 = sourceNode.Annotation(); Assert.IsNotNull(result2); Assert.AreEqual("TypedElementToSourceNodeAdapter", result2.GetType().Name); // I use the classname here, because PocoElementNode is internal in Hl7.Fhir.Core - Assert.AreSame(sourceNode, result2); + Assert.AreSame(sourceNode, result2); } [TestMethod] @@ -72,7 +72,7 @@ public async Tasks.Task SourceNodeFromElementNodeReturnsResourceTypeSupplier() Assert.IsNotNull(result); Assert.AreEqual(typeof(TypedElementToSourceNodeAdapter), result.GetType()); Assert.AreEqual("Patient", adapter.GetResourceTypeIndicator()); - Assert.AreSame(adapter, result); + Assert.AreSame(adapter, result); } } } diff --git a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripNewSerializers.cs b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripNewSerializers.cs index 0742c8f126..713756d846 100644 --- a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripNewSerializers.cs +++ b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripNewSerializers.cs @@ -1,11 +1,16 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +#nullable enable +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; using System.Text.Json; +using ERR = Hl7.Fhir.Serialization.FhirJsonException; namespace Hl7.Fhir.Serialization.Tests { [TestClass] public partial class RoundTripNewSerializers { + private readonly string _attachmentJson = "{\"size\":12}"; + [DynamicData(nameof(prepareExampleZipFilesXml), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayNames))] [DataTestMethod] [TestCategory("LongRunner")] @@ -21,5 +26,14 @@ public void FullRoundtripOfAllExamplesJsonNewSerializer(string file, string base { doRoundTrip(baseTestPath, file, xmlSerializer, xmlDeserializer, jsonOptions); } + + private static IEnumerable attachmentSource() + { + yield return new object[] { "{\"size\":12, \"title\": \"Correct Attachment\"}", 12L, null! }; + yield return new object[] { "{\"size\":12.345, \"title\": \"An incorrect Attachment\"}", null!, ERR.NUMBER_CANNOT_BE_PARSED_CODE }; + yield return new object[] { "{\"size\":\"12\", \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_INCORRECT_FORMAT_CODE }; + yield return new object[] { "{\"size\":\"12.345\", \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_INCORRECT_FORMAT_CODE }; + } } -} \ No newline at end of file +} +#nullable restore \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripTest.cs b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripTest.cs index 9c24254604..ba233a9e1e 100644 --- a/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripTest.cs +++ b/src/Hl7.Fhir.Serialization.R4.Tests/RoundtripTest.cs @@ -16,6 +16,8 @@ namespace Hl7.Fhir.Serialization.Tests [TestClass] public partial class RoundtripTest { + private readonly string _attachmentJson = "{\"size\":12}"; + [TestMethod] [TestCategory("LongRunner")] public void FullRoundtripOfAllExamplesXmlPoco() diff --git a/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripNewSerializers.cs b/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripNewSerializers.cs index 0742c8f126..c268a89607 100644 --- a/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripNewSerializers.cs +++ b/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripNewSerializers.cs @@ -1,11 +1,15 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; using System.Text.Json; +using ERR = Hl7.Fhir.Serialization.FhirJsonException; namespace Hl7.Fhir.Serialization.Tests { [TestClass] public partial class RoundTripNewSerializers { + private readonly string _attachmentJson = "{\"size\":12}"; + [DynamicData(nameof(prepareExampleZipFilesXml), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayNames))] [DataTestMethod] [TestCategory("LongRunner")] @@ -21,5 +25,14 @@ public void FullRoundtripOfAllExamplesJsonNewSerializer(string file, string base { doRoundTrip(baseTestPath, file, xmlSerializer, xmlDeserializer, jsonOptions); } + + private static IEnumerable attachmentSource() + { + yield return new object[] { "{\"size\":12, \"title\": \"Correct Attachment\"}", 12L, null! }; + yield return new object[] { "{\"size\":12.345, \"title\": \"An incorrect Attachment\"}", null!, ERR.NUMBER_CANNOT_BE_PARSED_CODE }; + yield return new object[] { "{\"size\":\"12\", \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_INCORRECT_FORMAT_CODE }; + yield return new object[] { "{\"size\":\"12.345\", \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_INCORRECT_FORMAT_CODE }; + } + } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripTest.cs b/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripTest.cs index 9c24254604..ba233a9e1e 100644 --- a/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripTest.cs +++ b/src/Hl7.Fhir.Serialization.R4B.Tests/RoundtripTest.cs @@ -16,6 +16,8 @@ namespace Hl7.Fhir.Serialization.Tests [TestClass] public partial class RoundtripTest { + private readonly string _attachmentJson = "{\"size\":12}"; + [TestMethod] [TestCategory("LongRunner")] public void FullRoundtripOfAllExamplesXmlPoco() diff --git a/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripNewSerializers.cs b/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripNewSerializers.cs index ab548a8740..12b746d688 100644 --- a/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripNewSerializers.cs +++ b/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripNewSerializers.cs @@ -1,15 +1,20 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +#nullable enable +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; using System.Text.Json; +using ERR = Hl7.Fhir.Serialization.FhirJsonException; namespace Hl7.Fhir.Serialization.Tests { [TestClass] - [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public partial class RoundTripNewSerializers { + private readonly string _attachmentJson = "{\"size\":\"12\"}"; + [DynamicData(nameof(prepareExampleZipFilesXml), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayNames))] [DataTestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesXmlNewSerializer(string file, string baseTestPath, FhirXmlPocoSerializer xmlSerializer, FhirXmlPocoDeserializer xmlDeserializer, JsonSerializerOptions jsonOptions) { doRoundTrip(baseTestPath, file, xmlSerializer, xmlDeserializer, jsonOptions); @@ -18,9 +23,19 @@ public void FullRoundtripOfAllExamplesXmlNewSerializer(string file, string baseT [DynamicData(nameof(prepareExampleZipFilesJson), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayNames))] [DataTestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesJsonNewSerializer(string file, string baseTestPath, FhirXmlPocoSerializer xmlSerializer, FhirXmlPocoDeserializer xmlDeserializer, JsonSerializerOptions jsonOptions) { doRoundTrip(baseTestPath, file, xmlSerializer, xmlDeserializer, jsonOptions); } + + private static IEnumerable attachmentSource() + { + yield return new object[] { "{\"size\":\"12\", \"title\": \"Correct Attachment\"}", 12L, null! }; + yield return new object[] { "{\"size\":12, \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_INCORRECT_FORMAT_CODE }; + yield return new object[] { "{\"size\":25.345, \"title\": \"An incorrect Attachment\"}", null!, ERR.NUMBER_CANNOT_BE_PARSED_CODE }; + yield return new object[] { "{\"size\":\"12.345\", \"title\": \"An incorrect Attachment\"}", null!, ERR.LONG_CANNOT_BE_PARSED_CODE }; + } } -} \ No newline at end of file +} +#nullable restore \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripTest.cs b/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripTest.cs index 5f7824544d..490260f5ea 100644 --- a/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripTest.cs +++ b/src/Hl7.Fhir.Serialization.R5.Tests/RoundtripTest.cs @@ -14,11 +14,13 @@ namespace Hl7.Fhir.Serialization.Tests { [TestClass] - [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public partial class RoundtripTest { + private readonly string _attachmentJson = "{\"size\":\"12\"}"; + [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesXmlPoco() { FullRoundtripOfAllExamples("examples.zip", "FHIRRoundTripTestXml", @@ -27,6 +29,7 @@ public void FullRoundtripOfAllExamplesXmlPoco() [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public async Tasks.Task FullRoundtripOfAllExamplesXmlPocoAsync() { await FullRoundtripOfAllExamplesAsync("examples.zip", "FHIRRoundTripTestXml", @@ -35,6 +38,7 @@ await FullRoundtripOfAllExamplesAsync("examples.zip", "FHIRRoundTripTestXml", [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesJsonPoco() { FullRoundtripOfAllExamples("examples-json.zip", "FHIRRoundTripTestJson", @@ -43,6 +47,7 @@ public void FullRoundtripOfAllExamplesJsonPoco() [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public async Tasks.Task FullRoundtripOfAllExamplesJsonPocoAsync() { await FullRoundtripOfAllExamplesAsync("examples-json.zip", "FHIRRoundTripTestJson", @@ -51,6 +56,7 @@ await FullRoundtripOfAllExamplesAsync("examples-json.zip", "FHIRRoundTripTestJso [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesXmlNavPocoProvider() { FullRoundtripOfAllExamples("examples.zip", "FHIRRoundTripTestXml", @@ -59,6 +65,7 @@ public void FullRoundtripOfAllExamplesXmlNavPocoProvider() [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public async Tasks.Task FullRoundtripOfAllExamplesXmlNavPocoProviderAsync() { await FullRoundtripOfAllExamplesAsync("examples.zip", "FHIRRoundTripTestXml", @@ -67,6 +74,7 @@ await FullRoundtripOfAllExamplesAsync("examples.zip", "FHIRRoundTripTestXml", [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesJsonNavPocoProvider() { FullRoundtripOfAllExamples("examples-json.zip", "FHIRRoundTripTestJson", @@ -75,6 +83,7 @@ public void FullRoundtripOfAllExamplesJsonNavPocoProvider() [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public async Tasks.Task FullRoundtripOfAllExamplesJsonNavPocoProviderAsync() { await FullRoundtripOfAllExamplesAsync("examples-json.zip", "FHIRRoundTripTestJson", @@ -83,6 +92,7 @@ await FullRoundtripOfAllExamplesAsync("examples-json.zip", "FHIRRoundTripTestJso [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesXmlNavSdProvider() { var source = new CachedResolver(ZipSource.CreateValidationSource()); @@ -92,6 +102,7 @@ public void FullRoundtripOfAllExamplesXmlNavSdProvider() [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public async Tasks.Task FullRoundtripOfAllExamplesXmlNavSdProviderAsync() { var source = new CachedResolver(ZipSource.CreateValidationSource()); @@ -101,6 +112,7 @@ await FullRoundtripOfAllExamplesAsync("examples.zip", "FHIRRoundTripTestXml", [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public void FullRoundtripOfAllExamplesJsonNavSdProvider() { var source = new CachedResolver(ZipSource.CreateValidationSource()); @@ -110,6 +122,7 @@ public void FullRoundtripOfAllExamplesJsonNavSdProvider() [TestMethod] [TestCategory("LongRunner")] + [Ignore("Because of incorrect example files in R5 (5.0.0-snapshot3).")] public async Tasks.Task FullRoundtripOfAllExamplesJsonNavSdProviderAsync() { var source = new CachedResolver(ZipSource.CreateValidationSource()); diff --git a/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripNewSerializers.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripNewSerializers.cs index 52b26e39d2..95d80fc0b7 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripNewSerializers.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripNewSerializers.cs @@ -1,4 +1,6 @@ -using Hl7.Fhir.Model; +#nullable enable +using FluentAssertions; +using Hl7.Fhir.Model; using Hl7.Fhir.Tests; using Hl7.Fhir.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -63,8 +65,6 @@ private static bool skipFile(string file) { if (file.Contains("notification-") || file.Contains("subscriptionstatus-")) return true; // These are Subscription resources that have invalid data in R5. - if (file.Contains("integer64.profile.json") || file.Contains("documentreference-example.json") || file.Contains("documentmanifest-fm-attachment.json") || file.Contains("communication-example-fm-solicited-attachment.json") || file.Contains("communication-example-fm-attachment.json")) - return true; // Are examples that have quotes around integers in R5 if (file.Contains("examplescenario-example")) return true; // this resource has a property name resourceType (which is reserved in the .net json serializer) if (file.Contains("json-edge-cases")) @@ -166,7 +166,7 @@ private static void convertResource(string inputFile, string outputFile, FhirXml } catch (DeserializationFailedException e) { - resource = (Resource)e.PartialResult; + resource = (Resource)e.PartialResult!; } var r2 = resource.DeepCopy(); @@ -181,21 +181,21 @@ private static void convertResource(string inputFile, string outputFile, FhirXml else { var json = File.ReadAllText(inputFile); - Resource resource; + Resource? resource; try { resource = JsonSerializer.Deserialize(json, options); } catch (DeserializationFailedException e) { - resource = (Resource)e.PartialResult; + resource = (Resource)e.PartialResult!; } var sb = new StringBuilder(); using (var w = XmlWriter.Create(sb)) { - xmlSerializer.Serialize(resource, w); + xmlSerializer.Serialize(resource!, w); } File.WriteAllText(outputFile, sb.ToString()); @@ -223,5 +223,35 @@ private static void compareFile(string expectedFile, string actualFile, List(_attachmentJson, options); + attachment.Should().BeOfType().Subject.Size.Should().Be(12L); + var json = JsonSerializer.Serialize(attachment, options); + json.Should().Be(_attachmentJson); + } + + [DataTestMethod] + [DynamicData(nameof(attachmentSource), DynamicDataSourceType.Method)] + public void ParseAttachment(string input, long? expectedAttachmentSize, string? errorCode) + { + var options = new JsonSerializerOptions().ForFhir(ModelInfo.ModelInspector); + if (errorCode is not null) + { + Action action = () => JsonSerializer.Deserialize(input, options); + + action.Should().Throw().Which.Exceptions.Should().OnlyContain(e => e.ErrorCode == errorCode); + } + else + { + var attachment = JsonSerializer.Deserialize(input, options); + attachment.Should().NotBeNull(); + attachment!.Size.Should().Be(expectedAttachmentSize!.Value); + } + } } -} \ No newline at end of file +} +#nullable restore \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripTest.cs b/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripTest.cs index 9e4d35e78e..6cb65aabb1 100644 --- a/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripTest.cs +++ b/src/Hl7.Fhir.Serialization.Shared.Tests/RoundtripTest.cs @@ -6,6 +6,7 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +using FluentAssertions; using Hl7.Fhir.Model; using Hl7.Fhir.Specification; using Hl7.Fhir.Tests; @@ -56,7 +57,16 @@ public static async Tasks.Task FullRoundtripOfAllExamplesAsync(string zipname, s await doRoundTripAsync(examples, baseTestPath, usingPoco, provider); } - + [TestMethod] + public void RoundTripAttachmentWithSize() + { + var parser = new FhirJsonParser(new ParserSettings() { PermissiveParsing = false }); + var attachment = parser.Parse(_attachmentJson); + attachment.Size.Should().Be(12L); + var serializer = new FhirJsonSerializer(); + var result = serializer.SerializeToString(attachment); + result.Should().Be(_attachmentJson); + } //[TestMethod] //public void CompareIntermediate2Xml() diff --git a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs index 3749bc2e70..d4387a1e3b 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs +++ b/src/Hl7.Fhir.Support.Poco.Tests/NewPocoSerializers/FhirJsonDeserializationTests.cs @@ -8,6 +8,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -22,70 +23,64 @@ namespace Hl7.Fhir.Support.Poco.Tests [TestClass] public class FhirJsonDeserializationTests { - //TODO: Now that we have constants for all of these errors, we could replace the string here. - private const string NUMBER_CANNOT_BE_PARSED = "JSON108"; - [DataTestMethod] - [DataRow(null, typeof(decimal), "JSON109")] - [DataRow(new[] { 1, 2 }, typeof(decimal), "JSON105")] - - [DataRow("hi!", typeof(string), null)] - [DataRow("SGkh", typeof(byte[]), null)] - [DataRow("hi!", typeof(byte[]), "JSON106")] - [DataRow("hi!", typeof(DateTimeOffset), "JSON107")] - [DataRow("2007-02-03", typeof(DateTimeOffset), null)] - [DataRow("enumvalue", typeof(UriFormat), COVE.INVALID_CODED_VALUE_CODE)] - [DataRow(true, typeof(Enum), "JSON110")] - [DataRow("hi!", typeof(int), "JSON110")] - - [DataRow(3, typeof(decimal), null)] - [DataRow(3, typeof(uint), null)] - [DataRow(3, typeof(long), null)] - [DataRow(3, typeof(ulong), null)] - [DataRow(3.14, typeof(decimal), null)] - [DataRow(double.MaxValue, typeof(decimal), NUMBER_CANNOT_BE_PARSED)] - [DataRow(3.14, typeof(int), NUMBER_CANNOT_BE_PARSED)] - [DataRow(3.14, typeof(uint), NUMBER_CANNOT_BE_PARSED)] - [DataRow(3.14, typeof(long), NUMBER_CANNOT_BE_PARSED)] - [DataRow(-3, typeof(ulong), NUMBER_CANNOT_BE_PARSED)] - [DataRow(long.MaxValue, typeof(uint), NUMBER_CANNOT_BE_PARSED)] - [DataRow(long.MaxValue, typeof(int), NUMBER_CANNOT_BE_PARSED)] - [DataRow(long.MaxValue, typeof(decimal), null)] - [DataRow(5, typeof(float), null)] - [DataRow(double.MaxValue, typeof(float), NUMBER_CANNOT_BE_PARSED)] - [DataRow(6.14, typeof(double), null)] - [DataRow(314, typeof(int), null)] - [DataRow(314, typeof(decimal), null)] - [DataRow(3.14, typeof(bool), "JSON110")] - - [DataRow(true, typeof(bool), null)] - [DataRow(true, typeof(string), "JSON110")] - public void TryDeserializePrimitiveValue(object data, Type expected, string code) + [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(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 reader = constructReader(data); + var reader = constructReader(input); reader.Read(); var deserializer = getTestDeserializer(new()); - var (result, error) = deserializer.DeserializePrimitiveValue(ref reader, expected); + var (result, error) = deserializer.DeserializePrimitiveValue(ref reader, expectedImplementingType, fhirType); if (code is not null) error?.ErrorCode.Should().Be(code); else error.Should().BeNull(); - if (expected == typeof(byte[])) + if (expectedImplementingType == typeof(byte[])) { if (error is null) - Convert.ToBase64String((byte[])result!).Should().Be((string)data); + Convert.ToBase64String((byte[])result!).Should().Be((string)input); else - result.Should().Be(data); + result.Should().Be(input); } - else if (expected == typeof(DateTimeOffset)) + else if (expectedImplementingType == typeof(DateTimeOffset)) { if (error is null) - result.Should().BeOfType().Which.ToFhirDate().Should().Be((string)data); + result.Should().BeOfType().Which.ToFhirDate().Should().Be((string)input); else - result.Should().Be(data); + result.Should().Be(input); } else if (code == ERR.EXPECTED_PRIMITIVE_NOT_ARRAY.ErrorCode || code == ERR.EXPECTED_PRIMITIVE_NOT_OBJECT.ErrorCode) @@ -95,9 +90,9 @@ public void TryDeserializePrimitiveValue(object data, Type expected, string code else { if (error is null) - result.Should().Be(data); + result.Should().Be(input); else - result.Should().Be(data is not null ? PrimitiveTypeConverter.ConvertTo(data) : null); + result.Should().Be(expectedResult); } } @@ -115,11 +110,11 @@ public void TestCustomRecovery() (result, error) = test(21); result.Should().Be("21"); - error?.ErrorCode.Should().Be(FhirJsonException.ARRAYS_CANNOT_BE_EMPTY_CODE); + error?.ErrorCode.Should().Be(ERR.ARRAYS_CANNOT_BE_EMPTY_CODE); (result, error) = test(31); result.Should().BeNull(); - error?.ErrorCode.Should().Be(FhirJsonException.UNEXPECTED_JSON_TOKEN_CODE); + error?.ErrorCode.Should().Be(ERR.UNEXPECTED_JSON_TOKEN_CODE); try { @@ -149,22 +144,26 @@ public void TestCustomRecovery() { var reader = constructReader(number); reader.Read(); var deserializer = getTestDeserializer(new() { OnPrimitiveParseFailed = correctIntToBool }); - return deserializer.DeserializePrimitiveValue(ref reader, typeof(bool)); + return deserializer.DeserializePrimitiveValue(ref reader, typeof(bool), null); } } [TestMethod] public void PrimitiveValueCannotBeComplex() { - TryDeserializePrimitiveValue(new { bla = 4 }, typeof(int), ERR.EXPECTED_PRIMITIVE_NOT_OBJECT.ErrorCode); + TryDeserializePrimitiveValue(new { bla = 4 }, null!, typeof(int), null, ERR.EXPECTED_PRIMITIVE_NOT_OBJECT.ErrorCode); + 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); } [DataTestMethod] [DataRow("OperationOutcome", null)] - [DataRow("OperationOutcomeX", "JSON116")] + [DataRow("OperationOutcomeX", ERR.UNKNOWN_RESOURCE_TYPE_CODE)] [DataRow("Meta", null)] - [DataRow(4, "JSON102")] - [DataRow(null, "JSON103")] + [DataRow(4, ERR.RESOURCETYPE_SHOULD_BE_STRING_CODE)] + [DataRow(null, ERR.NO_RESOURCETYPE_PROPERTY_CODE)] public void DeriveClassMapping(object typename, string errorcode) { var (result, error) = test(typename); @@ -191,24 +190,24 @@ public void DeriveClassMapping(object typename, string errorcode) } [DataTestMethod] - [DataRow(null, typeof(FhirString), "JSON109")] - [DataRow(new[] { 1, 2 }, typeof(FhirString), "JSON105")] + [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), "JSON106", "hi!")] - [DataRow(4, typeof(Base64Binary), "JSON110", "4")] + [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("2007-", typeof(FhirDateTime), COVE.DATETIME_LITERAL_INVALID_CODE, "2007-")] - [DataRow(4.45, typeof(FhirDateTime), "JSON110", "4.45")] + [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), "JSON107")] + [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) @@ -224,7 +223,7 @@ PrimitiveType test() var reader = constructReader(value); reader.Read(); - return deserializer.DeserializeFhirPrimitive(null, "dummy", mapping, ref reader, null, state); + return deserializer.DeserializeFhirPrimitive(null, "dummy", mapping, null!, ref reader, null, state); } var result = test(); diff --git a/src/Hl7.Fhir.Support.Tests/FhirPath/FhirPathTests.cs b/src/Hl7.Fhir.Support.Tests/FhirPath/FhirPathTests.cs index e9a4c272b1..4d8139cedb 100644 --- a/src/Hl7.Fhir.Support.Tests/FhirPath/FhirPathTests.cs +++ b/src/Hl7.Fhir.Support.Tests/FhirPath/FhirPathTests.cs @@ -77,8 +77,8 @@ public static IEnumerable GetTypedElements() yield return new object[] { sourceNode.ToTypedElement(ModelInspector.ForAssembly(typeof(Resource).Assembly)), "sourceNode to TypedElement" }; - var poco = sourceNode.ToPoco(ModelInspector.Common); - yield return new object[] { poco.ToTypedElement(ModelInspector.Common), "poco to TypedElement" }; + var poco = sourceNode.ToPoco(ModelInspector.Base); + yield return new object[] { poco.ToTypedElement(ModelInspector.Base), "poco to TypedElement" }; } } diff --git a/src/Hl7.Fhir.Support.Tests/Serialization/CommonTypeParsingTest.cs b/src/Hl7.Fhir.Support.Tests/Serialization/CommonTypeParsingTest.cs index 5384284e3e..bdcfda1bc1 100644 --- a/src/Hl7.Fhir.Support.Tests/Serialization/CommonTypeParsingTest.cs +++ b/src/Hl7.Fhir.Support.Tests/Serialization/CommonTypeParsingTest.cs @@ -20,10 +20,10 @@ public class CommonTypeParsingTest public void CanConvertPocoToTypedElement() { Coding c = new Coding("http://nu.nl", "bla"); - var te = c.ToTypedElement(ModelInspector.Common); + var te = c.ToTypedElement(ModelInspector.Base); Assert.AreEqual("Coding", te.InstanceType); - Coding c2 = te.ToPoco(ModelInspector.Common); + Coding c2 = te.ToPoco(ModelInspector.Base); Assert.AreEqual(c.Code, c2.Code); Assert.AreEqual(c.System, c2.System); @@ -33,10 +33,10 @@ public void CanConvertPocoToTypedElement() public void CanConvertPocoToSourceNode() { Coding c = new Coding("http://nu.nl", "bla"); - var sn = c.ToSourceNode(ModelInspector.Common, "kode"); + var sn = c.ToSourceNode(ModelInspector.Base, "kode"); Assert.AreEqual("kode", sn.Name); - Coding c2 = sn.ToPoco(ModelInspector.Common); + Coding c2 = sn.ToPoco(ModelInspector.Base); Assert.AreEqual(c.Code, c2.Code); Assert.AreEqual(c.System, c2.System); diff --git a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs index d301937915..cf5ea7e564 100644 --- a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs +++ b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs @@ -558,7 +558,7 @@ public void SingleScalarTest() public void ContextNestingLevelTest() { Coding c = new("http://nu.nl", "nl"); - var te = c.ToTypedElement(ModelInspector.Common); + var te = c.ToTypedElement(ModelInspector.Base); Assert.IsTrue(te.IsBoolean($"system.endsWith(code)", true)); Assert.IsTrue(te.IsBoolean($"system.endsWith(%context.code)", true)); Assert.IsTrue(te.IsBoolean($"system.endsWith('nl')", true)); diff --git a/src/firely-net-sdk-tests.props b/src/firely-net-sdk-tests.props index b31712495a..f23d3023da 100644 --- a/src/firely-net-sdk-tests.props +++ b/src/firely-net-sdk-tests.props @@ -19,14 +19,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/firely-net-sdk.props b/src/firely-net-sdk.props index 2128119ab7..66d77d6ea9 100644 --- a/src/firely-net-sdk.props +++ b/src/firely-net-sdk.props @@ -10,7 +10,7 @@ beta2 Firely (info@fire.ly) and contributors Firely (https://fire.ly) - Copyright 2013-2022 Firely. Contains materials (C) HL7 International + Copyright 2013-2023 Firely. Contains materials (C) HL7 International https://github.com/FirelyTeam/firely-net-sdk https://github.com/FirelyTeam/firely-net-sdk git