Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
190faac
Improved delayed parsing for base64 and code<t>
ewoutkramer Feb 7, 2025
1219776
Merge remote-tracking branch 'origin/develop-6.0' into 2781-work-on-o…
ewoutkramer Feb 10, 2025
62318db
Merge branch 'develop-6.0' into 2781-work-on-objectvalue
ewoutkramer Feb 10, 2025
a990ebe
WIP
ewoutkramer Feb 12, 2025
bb8c2be
Merge remote-tracking branch 'origin/2781-work-on-objectvalue' into 2…
ewoutkramer Feb 12, 2025
2802494
Started to introduce explicit ObjectValue validation step in DotNetAt…
ewoutkramer Feb 14, 2025
7b3ab35
Restructured validation in all primitive types to make them more cons…
ewoutkramer Feb 15, 2025
03dd822
Fixed unit tests
ewoutkramer Feb 17, 2025
37d2a84
Upgrade to .NET 9 so we can build C# 13.
ewoutkramer Feb 17, 2025
61a1f4c
Try again.
ewoutkramer Feb 18, 2025
ac72f85
Fixed a bug. Tried to update to 9.0 again.
ewoutkramer Feb 18, 2025
ba458c1
Ok, back to C# 12.
ewoutkramer Feb 18, 2025
785b3dd
Ok, again.
ewoutkramer Feb 18, 2025
0c97288
Ok, if that's all....
ewoutkramer Feb 18, 2025
ee7fea5
Last few minor features implemented.
ewoutkramer Feb 19, 2025
f56492b
Merge branch 'develop-6.0' into 2781-work-on-objectvalue
ewoutkramer Feb 25, 2025
8a80651
Merge remote-tracking branch 'origin/spike/remove-iscopednode' into 2…
ewoutkramer Feb 25, 2025
94e89b6
And retry with new compat suppressions.
ewoutkramer Feb 25, 2025
c96ed23
Merge branch 'develop-6.0' into 2781-work-on-objectvalue
ewoutkramer Feb 26, 2025
ad3f0ff
Better documentation of the side-effect of ValidateObjectValue.
ewoutkramer Feb 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
504 changes: 504 additions & 0 deletions src/Hl7.Fhir.Base/CompatibilitySuppressions.xml

Large diffs are not rendered by default.

18 changes: 7 additions & 11 deletions src/Hl7.Fhir.Base/ElementModel/NewPocoBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}'.");
}
}

/// <summary>
/// Convert the value of a typed element to a value that can be set on a POCO property.
/// </summary>
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
Expand Down
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/ElementModel/PocoElementNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ internal object ToITypedElementValue()
Integer64 fint64 => fint64.Value,
PositiveInt pint => pint.Value,
UnsignedInt unsint => unsint.Value,
Base64Binary { Value: { } b64 } => PrimitiveTypeConverter.ConvertTo<string>(b64),
Base64Binary { ObjectValue: { } b64 } => b64,
Comment thread
ewoutkramer marked this conversation as resolved.
PrimitiveType prim => prim.ObjectValue,
_ => null
};
Expand Down
7 changes: 6 additions & 1 deletion src/Hl7.Fhir.Base/ElementModel/Types/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public static DateTime FromDateTimeOffset(DateTimeOffset dto, DateTimePrecision

public Date TruncateToDate() => Date.FromDateTimeOffset(_value, Precision > DateTimePrecision.Day ? DateTimePrecision.Day : Precision, HasOffset);

/// <summary>
/// Whether this DateTime represents a precise instant in time, according to FHIR rules.
/// </summary>
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;
Expand All @@ -79,7 +84,7 @@ public static DateTime FromDateTimeOffset(DateTimeOffset dto, DateTimePrecision
/// <summary>
/// Whether the time specifies an offset to UTC
/// </summary>
public bool HasOffset { get; private set; }
public bool HasOffset { get; }

/// <summary>
/// If this instance was constructed using Parse(), this is the original
Expand Down
4 changes: 2 additions & 2 deletions src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ public static SymbolTable AddFhirExtensions(this SymbolTable t)
/// </summary>
/// <param name="focus"></param>
/// <returns></returns>
public static bool HtmlChecks(this IScopedNode focus)
public static bool HtmlChecks(this IScopedNode? focus)
{
if (focus?.Value is null) return false;

// Perform the checking of the content for valid html content
return XHtml.IsValidNarrativeXhtml(focus.Value.ToString()!);
return XHtml.IsValidNarrativeXhtml(focus.Value.ToString()!, out _, out _);
}

public static IEnumerable<Base?> ToFhirValues(this IEnumerable<IScopedNode> results)
Expand Down
16 changes: 10 additions & 6 deletions src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ POSSIBILITY OF SUCH DAMAGE.

*/

using Hl7.Fhir.Model;
using Hl7.Fhir.Specification;
using Hl7.Fhir.Utility;
using Hl7.Fhir.Validation;
Expand Down Expand Up @@ -63,7 +64,7 @@ public FhirElementAttribute(string name, ChoiceType choice, XmlRepresentation re
/// <summary>
/// The name of the element in FHIR this property represents.
/// </summary>
public string Name { get; private set; }
public string Name { get; }

/// <summary>
/// The element represents the primitive `value` attribute/property in the FHIR serialization
Expand Down Expand Up @@ -123,10 +124,13 @@ public FhirElementAttribute(string name, ChoiceType choice, XmlRepresentation re
return result.FirstOrDefault();
}

private void validateElement(object value, ValidationContext validationContext, List<ValidationResult> result)
private void validateElement(object value, ValidationContext validationContext, List<ValidationResult> 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
var nestedContext = validationContext.IntoPath(b, validationContext.MemberName ?? Name);
_ = Validator.TryValidateObject(b, nestedContext, results, validateAllProperties: true);
}
}
5 changes: 1 addition & 4 deletions src/Hl7.Fhir.Base/Model/Base.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// FHIR Type Name
Expand All @@ -56,8 +55,6 @@ public abstract partial class Base : IAnnotatable,
protected Dictionary<string, object> Overflow =>
LazyInitializer.EnsureInitialized(ref _overflow, () => new Dictionary<string, object>())!;

public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext) => [];

#region << Annotations >>

[NonSerialized] private AnnotationList? _annotations = null;
Expand Down
103 changes: 90 additions & 13 deletions src/Hl7.Fhir.Base/Model/Base64Binary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,28 +43,100 @@ 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)
Comment thread
ewoutkramer marked this conversation as resolved.
throw error;

public static Base64Binary FromText(string text) =>
new(System.Text.Encoding.UTF8.GetBytes(text));
return _parsedValue;
}

/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
public static bool IsValidValue(string value)
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;

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;
}
}

/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
public static bool IsValidValue(string value) => doParse(value) is not null;


/// <summary>
/// Constructs a Base64Binary instance from a string of base64-encoded data.
/// </summary>
public static Base64Binary FromBase64String(string base64Data) =>
new() { ObjectValue = base64Data };

/// <summary>
/// Constructs a Base64Binary instance from a string of human-readable text.
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static Base64Binary FromText(string text) =>
new(System.Text.Encoding.UTF8.GetBytes(text));


/// <summary>
/// Converts this binary to a Base64-encoded <see cref="P.String" />.
/// </summary>
Expand All @@ -69,7 +146,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;
}
35 changes: 23 additions & 12 deletions src/Hl7.Fhir.Base/Model/Canonical.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ POSSIBILITY OF SUCH DAMAGE.
using P=Hl7.Fhir.ElementModel.Types;
using Hl7.Fhir.Utility;
using System;
using System.ComponentModel.DataAnnotations;
using COVE=Hl7.Fhir.Validation.CodedValidationException;

#nullable enable

Expand All @@ -46,20 +48,19 @@ public Canonical(Uri uri) : this(uri.OriginalString)
// nothing
}

/// <summary>
/// Constructs a canonical from its components.
/// </summary>
public Canonical(string? uri, string? version, string? fragment = null)
{
if ((uri is not null) && uri.IndexOfAny(['|', '#']) != -1)
throw Error.Argument(nameof(uri), "cannot contain version/fragment data");

if ((version is not null) && version.IndexOfAny(['|', '#']) != -1)
throw Error.Argument(nameof(version), "cannot contain version/fragment data");
/// <summary>
/// Constructs a canonical from its components.
/// </summary>
public Canonical(string? uri, string? version, string? fragment = null)
{
if ((uri is not null) && uri.IndexOfAny(['|', '#']) != -1)
throw Error.Argument(nameof(uri), "cannot contain version/fragment data");

if ((fragment is not null) && fragment.IndexOfAny(['|', '#']) != -1)
throw Error.Argument(nameof(fragment), "already contains version/fragment data");
if ((version is not null) && version.IndexOfAny(['|', '#']) != -1)
throw Error.Argument(nameof(version), "cannot contain version/fragment data");

if ((fragment is not null) && fragment.IndexOfAny(['|', '#']) != -1)
throw Error.Argument(nameof(fragment), "already contains version/fragment data");

Value = uri +
(version is not null ? "|" + version : null) +
Expand Down Expand Up @@ -88,6 +89,16 @@ public void Deconstruct(out string? uri, out string? version, out string? fragme
/// <param name="value"></param>
public static implicit operator string?(Canonical? value) => value?.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)
};


/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
Expand Down
13 changes: 12 additions & 1 deletion src/Hl7.Fhir.Base/Model/Code.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -54,6 +55,16 @@ Value is not null
? new P.Code(system: null, code: Value, display: null, version: null)
: null;

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)
};

/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
Expand Down
Loading