Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2910 Make Base implement IScopedNode - phase 1 #2913

Open
wants to merge 30 commits into
base: develop-6.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
30fa951
Folded PocoElementNode into Resource
ewoutkramer Sep 23, 2024
d9bcf37
Merge branch 'develop-6.0' into spike/make-resource-implement-itypede…
ewoutkramer Oct 14, 2024
061a0fd
WIP voor Kas
ewoutkramer Oct 14, 2024
2bc3152
wip
Kasdejong Oct 14, 2024
a35a365
Removed ElementNode from Base
Kasdejong Oct 15, 2024
6847fc2
Removed ElementNode from Base
Kasdejong Oct 15, 2024
13f4286
Merge remote-tracking branch 'origin/spike/make-resource-implement-is…
Kasdejong Oct 16, 2024
80ec690
small oversight here :)
Kasdejong Oct 16, 2024
a6e1eb5
trying this to fix the pipeline
Kasdejong Oct 16, 2024
e7e3dab
disable package validation
Kasdejong Oct 16, 2024
ac73de8
Fixed serializer again?
Kasdejong Oct 16, 2024
ce5b1e8
runs locally, let's see if it works on the pipeline?
Kasdejong Oct 16, 2024
716f620
runs locally, let's see if it works on the pipeline?
Kasdejong Oct 16, 2024
35379b4
Merge remote-tracking branch 'origin/spike/make-resource-implement-is…
Kasdejong Oct 16, 2024
c61fd46
why is this needed???????
Kasdejong Oct 16, 2024
209e0be
Removed IBaseElementNavigator
Kasdejong Oct 16, 2024
41e2ec5
Merge branch 'develop-6.0' into spike/remove-IBaseElementNavigator
Kasdejong Oct 16, 2024
95cdbe8
defined IScopedNode (for now) and made base implement it.
Kasdejong Oct 16, 2024
8454cbd
Merge branch 'spike/define-Iscopednode' into spike/remove-IBaseElemen…
Kasdejong Oct 16, 2024
a75232a
Merge branch 'develop-6.0' into spike/make-resource-implement-iscoped…
Kasdejong Oct 16, 2024
06470cb
Revert "defined IScopedNode (for now) and made base implement it."
Kasdejong Oct 16, 2024
451de22
removed pragmas
Kasdejong Oct 16, 2024
e8556e2
applied PR requested changes
Kasdejong Oct 17, 2024
6142626
Merge pull request #2920 from FirelyTeam/spike/remove-IBaseElementNav…
ewoutkramer Oct 17, 2024
ab9f46b
updated compatibility suppressions
Kasdejong Oct 18, 2024
10d91b3
Merge branch 'develop-6.0' into spike/make-resource-implement-iscoped…
Kasdejong Oct 21, 2024
48b7f7b
removed unused imports after merge
Kasdejong Oct 21, 2024
f3f50fc
applied PR requested changes
Kasdejong Oct 21, 2024
c5dec88
applied PR requested changes
Kasdejong Oct 21, 2024
34c4d0c
Merge remote-tracking branch 'origin/spike/make-resource-implement-is…
Kasdejong Oct 21, 2024
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
65 changes: 22 additions & 43 deletions src/Hl7.Fhir.Base/ElementModel/PocoElementNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using P = Hl7.Fhir.ElementModel.Types;

namespace Hl7.Fhir.ElementModel
Expand All @@ -32,7 +31,7 @@ internal PocoElementNode(ModelInspector inspector, Base root, string rootName =
{
Current = root;
_inspector = inspector;
_myClassMapping = _inspector.FindOrImportClassMapping(root.GetType());
_myClassMapping = _inspector.FindOrImportClassMapping(root.GetType())!;

InstanceType = ((IStructureDefinitionSummary)_myClassMapping).TypeName;
Definition = ElementDefinitionSummary.ForRoot(_myClassMapping, rootName ?? root.TypeName);
Expand All @@ -54,7 +53,7 @@ private PocoElementNode(ModelInspector inspector, Base instance, PocoElementNode
InstanceType = ((IStructureDefinitionSummary)_myClassMapping).TypeName;
Definition = definition ?? throw Error.ArgumentNull(nameof(definition));

ExceptionHandler = parent.ExceptionHandler;
// ExceptionHandler = parent.ExceptionHandler;
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
Location = location;
ShortPath = shortPath;
}
Expand All @@ -70,7 +69,7 @@ private Type determineInstanceType(PropertyMapping definition)
{
"url" => typeof(FhirUri),
"id" => typeof(FhirString),
"div" => typeof(XHtml),
//"div" => typeof(XHtml),
_ => throw new NotSupportedException(
$"Encountered unexpected primitive type {Name} in backward compat behaviour for PocoElementNode.InstanceType.")
};
Expand Down Expand Up @@ -205,39 +204,20 @@ internal object ToITypedElementValue()
{
try
{
switch (Current)
return Current switch
{
case Hl7.Fhir.Model.Instant ins when ins.Value.HasValue:
return P.DateTime.FromDateTimeOffset(ins.Value.Value);
case Hl7.Fhir.Model.Time time when time.Value is { }:
return P.Time.Parse(time.Value);
case Hl7.Fhir.Model.Date dt when dt.Value is { }:
return P.Date.Parse(dt.Value);
case FhirDateTime fdt when fdt.Value is { }:
return P.DateTime.Parse(fdt.Value);
case Hl7.Fhir.Model.Integer fint:
if (!fint.Value.HasValue)
return null;
return (int)fint.Value;
case Hl7.Fhir.Model.Integer64 fint64:
if (!fint64.Value.HasValue)
return null;
return (long)fint64.Value;
case Hl7.Fhir.Model.PositiveInt pint:
if (!pint.Value.HasValue)
return null;
return (int)pint.Value;
case Hl7.Fhir.Model.UnsignedInt unsint:
if (!unsint.Value.HasValue)
return null;
return (int)unsint.Value;
case Hl7.Fhir.Model.Base64Binary b64:
return b64.Value != null ? PrimitiveTypeConverter.ConvertTo<string>(b64.Value) : null;
case PrimitiveType prim:
return prim.ObjectValue;
default:
return null;
}
Instant { Value: { } ins } => P.DateTime.FromDateTimeOffset(ins),
Time { Value: { } time } => P.Time.Parse(time),
Date { Value: { } dt } => P.Date.Parse(dt),
FhirDateTime { Value: { } fdt } => P.DateTime.Parse(fdt),
Integer fint => fint.Value,
Integer64 fint64 => fint64.Value,
PositiveInt pint => pint.Value,
UnsignedInt unsint => unsint.Value,
Base64Binary { Value: { } b64 } => PrimitiveTypeConverter.ConvertTo<string>(b64),
PrimitiveType prim => prim.ObjectValue,
_ => null
};
}
catch (FormatException)
{
Expand All @@ -247,25 +227,24 @@ internal object ToITypedElementValue()
}


public string InstanceType { get; private set; }
public string InstanceType { get; }

public string Location { get; private set; }
public string Location { get; }

public string ResourceType => Current is Resource ? InstanceType : null;

public IEnumerable<object> Annotations(Type type)
{
if (type == typeof(PocoElementNode) || type == typeof(ITypedElement) || type == typeof(IShortPathGenerator))
return new[] { this };
return [this];
else if (type == typeof(IFhirValueProvider))
return new[] { this };
return [this];
else if (type == typeof(IResourceTypeSupplier))
return new[] { this };
return [this];
else if (FhirValue is IAnnotated ia)
return ia.Annotations(type);

else
return Enumerable.Empty<object>();
return [];
}
}
}
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/ElementModel/TypedElementExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,4 @@ public static bool Matches<T>(this T value, T pattern) where T : IBaseElementNav
}
}
}
#nullable restore
#nullable restore
1 change: 1 addition & 0 deletions src/Hl7.Fhir.Base/FhirPath/CompiledExpression.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable enable

using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
using Hl7.FhirPath.Functions;
using System.Collections.Generic;
using System.Linq;
Expand Down
1 change: 1 addition & 0 deletions src/Hl7.Fhir.Base/FhirPath/Expressions/Closure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@


using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
Expand Down
1 change: 1 addition & 0 deletions src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
using Hl7.FhirPath.Expressions;
using Hl7.FhirPath.Parser;
using Hl7.FhirPath.Sprache;
Expand Down
177 changes: 177 additions & 0 deletions src/Hl7.Fhir.Base/Model/Base.TypedElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#if true

#nullable enable

using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Introspection;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Specification;
using Hl7.Fhir.Utility;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using P=Hl7.Fhir.ElementModel.Types;

namespace Hl7.Fhir.Model;


/// <summary>
/// An element within a tree of typed FHIR data with also a parent element.
/// </summary>
/// <remarks>
/// This interface represents FHIR data as a tree of elements, including type information either present in
/// the instance or derived from fully aware of the FHIR definitions and types
/// </remarks>
#pragma warning disable CS0618 // Type or member is obsolete
public interface IScopedNode : ITypedElement, IShortPathGenerator
#pragma warning restore CS0618 // Type or member is obsolete
{
/// <summary>
/// The parent node of this node, or null if this is the root node.
/// </summary>
IScopedNode? Parent { get; }

// /// <summary>
// /// An indication of the location of this node within the data represented by the <c>ITypedElement</c>.
// /// </summary>
// /// <remarks>The format of the location is the dotted name of the property, including indices to make
// /// sure repeated occurrences of an element can be distinguished. It needs to be sufficiently precise to aid
// /// the user in locating issues in the data.</remarks>
// string Location { get; }
}

internal record ScopeInformation(IScopedNode? Parent, string Name, int? Index);


public abstract partial class Base : IScopedNode,
IFhirValueProvider, IResourceTypeSupplier
{
[NonSerialized]
private ScopeInformation? _scopeInfo;

private ScopeInformation ScopeInfo
{
get => LazyInitializer.EnsureInitialized(ref _scopeInfo, () => BuildRoot())!;
set => _scopeInfo = value;
}

internal ScopeInformation BuildRoot(string? rootName = null) => new(null, rootName ?? TypeName, null);

internal Base WithScopeInfo(ScopeInformation info)
{
this.ScopeInfo = info;
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
return this;
}

IEnumerable<ITypedElement> IBaseElementNavigator<ITypedElement>.Children(string? name) =>
this.GetElementPairs()
.Where(ep => (name == null || name == ep.Key))
.SelectMany<KeyValuePair<string, object>, Base>(ep =>
(ep.Key, ep.Value) switch
{
(_, Base b) => (IEnumerable<Base>)[b.WithScopeInfo(new ScopeInformation(this, ep.Key, null))],
(_, IEnumerable<Base> list) => list.Select((item, idx) => item.WithScopeInfo(new ScopeInformation(this, ep.Key, idx))),
("url", string s) when this is Extension => [new FhirUri(s).WithScopeInfo(new ScopeInformation(this, ep.Key, null))],
("id", string s) when this is Element => [new FhirString(s).WithScopeInfo(new ScopeInformation(this, ep.Key, null))],
("value", _) => [],
_ => throw new InvalidOperationException("Unexpected system primitive in child list")
}
);

IScopedNode? IScopedNode.Parent => ScopeInfo.Parent;

string IBaseElementNavigator<ITypedElement>.Name => ScopeInfo.Name;

// TODO:
// Als wij een BackboneElement zijn, dan is onze naam niet this.TypeName maar "BackboneElement" of
// "Element", afhankelijk van waar hij in de .net inheritance hierarchie zit.
// HEt moet "code" zijn als dit een "Code<T>" is. Dat zijn geloof ik de afwijkingen.
// Wellioht is er ook nog iets met de directe properties "Extension.url" en "Element.id" die van een
// system type zijn ipv een FHIR type.
string? IBaseElementNavigator<ITypedElement>.InstanceType =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can implement InstanceType based on the available (reflection) type information in .NET, to avoid referencing ModelInspector (that's a hack,really).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to know how, let's discuss.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the comments above the property I gave a hint: it's basically base.TypeName with some exceptions for Code<T> and backbone elements. You can also look at the IStructureDefinitionSummary implementation on ClassMapping, since that actually bridges between .NET types and the ITYpedElement definition stuff.

((IStructureDefinitionSummary)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not need a ModelInspector anymore.

ModelInspector
.ForType(this.GetType())
.FindOrImportClassMapping(this.GetType())!
).TypeName;

private object? _value;
private object? _lastCachedValue;


internal object? ToITypedElementValue()
{
try
{
return this switch
{
Instant { Value: { } ins } => P.DateTime.FromDateTimeOffset(ins),
Time { Value: { } time } => P.Time.Parse(time),
Date { Value: { } dt } => P.Date.Parse(dt),
FhirDateTime { Value: { } fdt } => P.DateTime.Parse(fdt),
Integer fint => fint.Value,
Integer64 fint64 => fint64.Value,
PositiveInt pint => pint.Value,
UnsignedInt unsint => unsint.Value,
Base64Binary { Value: { } b64 } => PrimitiveTypeConverter.ConvertTo<string>(b64),
PrimitiveType prim => prim.ObjectValue,
_ => null
};
}
catch (FormatException)
{
// If it fails, just return the unparsed contents
return (this as PrimitiveType)?.ObjectValue;
}
}

object? IBaseElementNavigator<ITypedElement>.Value
{
get
{
if (this is not PrimitiveType { ObjectValue: { } ov }) return null;
if (ov == _lastCachedValue) return _value;
_value = ToITypedElementValue();
_lastCachedValue = ov;

return _value;
}
}

string ITypedElement.Location =>
(ScopeInfo.Index, ScopeInfo.Parent) switch
{
// if we have an index, write it
({} idx, {} parent) => $"{parent.Location}.{ScopeInfo.Name}[{idx}]",
// if we do not, write 0 as idx
(_, {} parent) => $"{parent.Location}.{ScopeInfo.Name}[0]",
// if we have neither, we are the root.
_ => $"{ScopeInfo.Name}"
};

IElementDefinitionSummary? ITypedElement.Definition => null;

string IShortPathGenerator.ShortPath =>
(ScopeInfo.Index, ScopeInfo.Parent) switch
{
// if we have an index, we have a parent.
({ } idx, {} parent) => $"{parent.ShortPath}.{ScopeInfo.Name}[{idx}]",
// Note that we omit indices here.
(_, { } parent) => $"{parent.ShortPath}.{ScopeInfo.Name}",
// if we have neither, we are the root. Note that we omit indices here.
_ => ScopeInfo.Name
};

public Base FhirValue => this;

string? IResourceTypeSupplier.ResourceType =>
this is Resource
#pragma warning disable CS0618 // Type or member is obsolete
? ((IBaseElementNavigator<ITypedElement>)this).InstanceType
#pragma warning restore CS0618 // Type or member is obsolete
: null;
}

#endif
14 changes: 12 additions & 2 deletions src/Hl7.Fhir.Base/Model/Base.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ POSSIBILITY OF SUCH DAMAGE.

*/

using Hl7.Fhir.Introspection;
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Utility;
using System;
using System.Collections;
Expand All @@ -51,7 +51,17 @@ public abstract partial class Base : IDeepCopyable, IDeepComparable,

private AnnotationList annotations => LazyInitializer.EnsureInitialized(ref _annotations, () => new());

public IEnumerable<object> Annotations(Type type) => annotations.OfType(type);
public IEnumerable<object> Annotations(Type type)
{
if (type == typeof(ITypedElement) || type == typeof(IShortPathGenerator))
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
return new[] { this };
else if (type == typeof(IFhirValueProvider))
return new[] { this };
else if (type == typeof(IResourceTypeSupplier))
return new[] { this };
else
return annotations.OfType(type);
}

public void AddAnnotation(object annotation) => annotations.AddAnnotation(annotation);

Expand Down
4 changes: 2 additions & 2 deletions src/Hl7.Fhir.Base/Rest/HttpContentParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public static async Task<Binary> ReadBinaryDataFromMessage(this HttpResponseMess
return ContentType.GetResourceFormatFromContentType(contentType) switch
{
ResourceFormat.Xml when bodyText is not null && SerializationUtil.ProbeIsXml(bodyText) =>
ser.DeserializeFromXml(bodyText),
ser.DeserializeFromXml(bodyText) as Resource,
ResourceFormat.Json when bodyText is not null && SerializationUtil.ProbeIsJson(bodyText) =>
ser.DeserializeFromJson(bodyText),
ResourceFormat.Xml or ResourceFormat.Json =>
Expand Down Expand Up @@ -275,7 +275,7 @@ public static async Task<Binary> ReadBinaryDataFromMessage(this HttpResponseMess
return ContentType.GetResourceFormatFromContentType(contentType) switch
{
ResourceFormat.Xml when bodyText is not null && SerializationUtil.ProbeIsFhirXml(bodyText) =>
ser.DeserializeFromXml(bodyText),
ser.DeserializeFromXml(bodyText) as Resource,
ResourceFormat.Json when bodyText is not null && SerializationUtil.ProbeIsFhirJson(bodyText) =>
ser.DeserializeFromJson(bodyText),
_ => default
Expand Down
5 changes: 3 additions & 2 deletions src/Hl7.Fhir.Base/Serialization/BaseFhirXmlPocoSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#nullable enable

using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Introspection;
using Hl7.Fhir.Model;
using Hl7.Fhir.Specification;
Expand Down Expand Up @@ -48,12 +49,12 @@ public void Serialize(IReadOnlyDictionary<string, object> members, XmlWriter wri
{
writer.WriteStartDocument();

var simulateRoot = members is not Resource;
var simulateRoot = ((IScopedNode)members).Parent is not null || members is not Resource;
if (simulateRoot)
{
// Serialization in XML of non-resources is problematic, since there's no root.
// It's a common usecase though, so "invent" a root that's the name of the element's type.
var rootElementName = members is Base b ? b.TypeName : members.GetType().Name;
var rootElementName = members is Base b ? ((ITypedElement)b).Name : members.GetType().Name;
writer.WriteStartElement(rootElementName, XmlNs.FHIR);
}

Expand Down
Loading