Skip to content

Commit

Permalink
Improved delayed parsing for base64 and code<t>
Browse files Browse the repository at this point in the history
  • Loading branch information
ewoutkramer committed Feb 7, 2025
1 parent 87295e4 commit 190faac
Show file tree
Hide file tree
Showing 28 changed files with 368 additions and 285 deletions.
75 changes: 73 additions & 2 deletions src/Hl7.Fhir.Base/Model/Base64Binary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ POSSIBILITY OF SUCH DAMAGE.
*/

using Hl7.Fhir.ElementModel.Types;
using Hl7.Fhir.Validation;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using P=Hl7.Fhir.ElementModel.Types;

#nullable enable
Expand All @@ -38,20 +42,87 @@ namespace Hl7.Fhir.Model;

public partial class Base64Binary
{
/// <summary>
/// Constructs a Base64Binary instance from a string of base64-encoded data.
/// </summary>
public static Base64Binary FromBase64String(string base64Data) =>
new(Convert.FromBase64String(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));

[NonSerialized] // To prevent binary serialization from serializing this field
private byte[]? _parsedValue = null;

protected override void OnObjectValueChanged()
{
_parsedValue = null;
base.OnObjectValueChanged();
}

public partial byte[]? Value
{
get
{
if (_parsedValue is null && ObjectValue is not null)
{
if(ObjectValue is string base64data)
_parsedValue = Convert.FromBase64String(base64data);
else
{
throw new InvalidCastException($"Cannot convert value '{ObjectValue}' of type {ObjectValue.GetType()} to a byte array.");
}
}

return _parsedValue;
}

set
{
ObjectValue = value is null ? null : Convert.ToBase64String(value);
OnPropertyChanged("Value");
}
}

public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var baseResults = base.Validate(validationContext);

if (HasValidValue())
return baseResults;

var result = CodedValidationException.INVALID_BASE64_VALUE(validationContext, ObjectValue).AsResult(validationContext);
return baseResults.Append(result);
}

public bool HasValidValue()
{
try
{
// There is no TryDecode(), so this is all we can do.
_ = Value;
return true;
}
catch
{
return false;
}
}

/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
public static bool IsValidValue(string value)
{
try
{
_ = Convert.FromBase64String(value);
var b64 = FromBase64String(value);
_ = b64.Value; // triggers b64 decoding
return true;
}
catch
Expand Down
27 changes: 23 additions & 4 deletions src/Hl7.Fhir.Base/Model/CodeOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,32 @@ public Code(T? value)
Value = value;
}

[NonSerialized] // To prevent binary serialization from serializing this field
private T? _parsedValue = null;

protected override void OnObjectValueChanged()
{
_parsedValue = null;
base.OnObjectValueChanged();
}

// 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 (_parsedValue is null && ObjectValue is not null)
{
if(TryParseObjectValue(out var parsed))
_parsedValue = parsed;
else
throw new InvalidCastException($"Cannot convert value '{ObjectValue}' of type {ObjectValue.GetType()} to an enum of type {typeof(T)}.");
}

return _parsedValue;
}
set
{
ObjectValue = value?.GetLiteral();
Expand All @@ -92,7 +110,8 @@ internal bool TryParseObjectValue(out T? value)
value = parsed;
return true;
}
else return ObjectValue is null;

return ObjectValue is null;
}

/// <inheritdoc />
Expand Down
10 changes: 3 additions & 7 deletions src/Hl7.Fhir.Base/Model/Date.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ 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))
if (Value is null || !(P.Date.TryParse(Value, out _parsedValue) && !_parsedValue!.HasOffset))
_parsedValue = INVALID_VALUE;
}

Expand Down Expand Up @@ -124,11 +124,7 @@ protected override void OnObjectValueChanged()
/// <returns>A DateTimeOffset filled out to midnight, january 1 (UTC) in case of a partial date.</returns>
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);
Expand All @@ -140,7 +136,7 @@ public DateTimeOffset ToDateTimeOffset()
/// <returns>True if the value of the Fhir Date is not null and can be parsed as a DateTimeOffset, false otherwise.</returns>
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;
Expand Down
20 changes: 7 additions & 13 deletions src/Hl7.Fhir.Base/Model/FhirDateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public bool TryToSystemDateTime([NotNullWhen(true)] out P.DateTime? dateTime)
{
if (_parsedValue is null)
{
if (Value is not null && !P.DateTime.TryParse(Value, out _parsedValue))
if (Value is null || !P.DateTime.TryParse(Value, out _parsedValue))
_parsedValue = INVALID_VALUE;
}

Expand All @@ -111,11 +111,9 @@ public bool TryToSystemDateTime([NotNullWhen(true)] out P.DateTime? dateTime)
dateTime = null;
return false;
}
else
{
dateTime = _parsedValue!;
return true;
}

dateTime = _parsedValue!;
return true;

bool hasInvalidParsedValue() => ReferenceEquals(_parsedValue, INVALID_VALUE);
}
Expand Down Expand Up @@ -154,11 +152,7 @@ protected override void OnObjectValueChanged()
/// effect on this, this merely converts the given Fhir datetime to the desired timezone</returns>
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);
Expand All @@ -171,7 +165,7 @@ public DateTimeOffset ToDateTimeOffset(TimeSpan zone)
/// specified timezone, false otherwise.</returns>
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;
Expand All @@ -189,7 +183,7 @@ public bool TryToDateTimeOffset(out DateTimeOffset dto)
/// <returns>True if the value of the FhirDateTime is not null and can be parsed as a DateTimeOffset, false otherwise.</returns>
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;
Expand Down
6 changes: 1 addition & 5 deletions src/Hl7.Fhir.Base/Model/Generated/Base64Binary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,7 @@ public Base64Binary(): this((byte[])null) {}
[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"); }
}
public partial byte[] Value { get; set; }

protected internal override Base DeepCopyInternal()
{
Expand Down
14 changes: 1 addition & 13 deletions src/Hl7.Fhir.Base/Model/Generated/Integer64.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,7 @@ public Integer64(): this((long?)null) {}
[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"); }
}
public partial long? Value { get; set; }

protected internal override Base DeepCopyInternal()
{
Expand Down
3 changes: 2 additions & 1 deletion src/Hl7.Fhir.Base/Model/Generated/XHtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ protected internal override Base DeepCopyInternal()
CopyToInternal(instance);
return instance;
}

}

}

// end of file
// end of file
14 changes: 14 additions & 0 deletions src/Hl7.Fhir.Base/Model/Integer64.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ namespace Hl7.Fhir.Model;

public partial class Integer64
{
public partial long? Value
{
get
{
return ObjectValue switch
{
null => null,
long l => l,
_ => Convert.ToInt64(ObjectValue)
};
}
set { ObjectValue = value; OnPropertyChanged("Value"); }
}

/// <summary>
/// Checks whether the given literal is correctly formatted.
/// </summary>
Expand Down
20 changes: 8 additions & 12 deletions src/Hl7.Fhir.Base/Model/Time.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public bool TryToSystemTime([NotNullWhen(true)] out P.Time? time)
{
if (_parsedValue is null)
{
if (Value is not null && !P.Time.TryParse(Value, out _parsedValue))
if (Value is null || !P.Time.TryParse(Value, out _parsedValue))
_parsedValue = INVALID_VALUE;
}

Expand All @@ -81,11 +81,9 @@ public bool TryToSystemTime([NotNullWhen(true)] out P.Time? time)
time = null;
return false;
}
else
{
time = _parsedValue!;
return true;
}

time = _parsedValue!;
return true;

bool hasInvalidParsedValue() => ReferenceEquals(_parsedValue, INVALID_VALUE);
}
Expand Down Expand Up @@ -129,16 +127,14 @@ public TimeSpan ToTimeSpan() =>
/// <returns>True if the value of the Fhir Time is not null and can be parsed as a Time without an offset, false otherwise.</returns>
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;
}

dto = TimeSpan.Zero;
return false;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,7 @@ FhirJsonPocoDeserializerState state
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(byte[]) => 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),
Expand Down Expand Up @@ -749,11 +748,6 @@ FhirJsonPocoDeserializerState state

return result;

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

static (object?, FhirJsonException?) readDateTimeOffset(ref Utf8JsonReader reader, PathStack pathStack)
{
var contents = reader.GetString()!;
Expand Down
Loading

0 comments on commit 190faac

Please sign in to comment.