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

Fixed location property on TypedElementOnSourceNode.cs #2747

Merged
merged 12 commits into from
Mar 27, 2024
Merged
8 changes: 8 additions & 0 deletions src/Hl7.Fhir.Base/ElementModel/SourceNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ public static string GetResourceTypeIndicator(this ISourceNode node) =>
/// <seealso cref="IAnnotated"/>
public static IEnumerable<object> Annotations(this ISourceNode node, Type type) =>
node is IAnnotated ann ? ann.Annotations(type) : Enumerable.Empty<object>();

/// <summary>
/// Gets specific annotations from the list of annotations on the node.
/// </summary>
/// <returns>All of the annotations of the given type, or an empty list if none were found.</returns>
/// <seealso cref="IAnnotated"/>
public static IEnumerable<T> Annotations<T>(this ISourceNode node) =>
(node is IAnnotated ann ? ann.Annotations<T>() : []);

/// <summary>
/// Gets a specific annotation from the list of annotations on the node.
Expand Down
67 changes: 36 additions & 31 deletions src/Hl7.Fhir.Base/ElementModel/TypedElementOnSourceNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@
using System.Threading;
using P = Hl7.Fhir.ElementModel.Types;

#nullable enable

namespace Hl7.Fhir.ElementModel
{
internal class TypedElementOnSourceNode : ITypedElement, IAnnotated, IExceptionSource, IShortPathGenerator
{
private const string XHTML_INSTANCETYPE = "xhtml";
private const string XHTML_DIV_TAG_NAME = "div";

public TypedElementOnSourceNode(ISourceNode source, string type, IStructureDefinitionSummaryProvider provider, TypedElementSettings settings = null)
public TypedElementOnSourceNode(ISourceNode source, string? type, IStructureDefinitionSummaryProvider provider, TypedElementSettings? settings = null)
{
if (source == null) throw Error.ArgumentNull(nameof(source));

Expand All @@ -31,12 +33,13 @@ public TypedElementOnSourceNode(ISourceNode source, string type, IStructureDefin
if (source is IExceptionSource ies && ies.ExceptionHandler == null)
ies.ExceptionHandler = (o, a) => ExceptionHandler.NotifyOrThrow(o, a);

Location = source.Name;
ShortPath = source.Name;
_source = source;
(InstanceType, Definition) = buildRootPosition(type);
}

private (string instanceType, IElementDefinitionSummary definition) buildRootPosition(string type)
private (string instanceType, IElementDefinitionSummary? definition) buildRootPosition(string? type)
{
var rootType = type ?? _source.GetResourceTypeIndicator();
if (rootType == null)
Expand All @@ -45,7 +48,7 @@ public TypedElementOnSourceNode(ISourceNode source, string type, IStructureDefin
throw Error.Format(nameof(type), $"Cannot determine the type of the root element at '{_source.Location}', " +
$"please supply a type argument.");
else
return (rootType, null);
return ("Base", null);
}

var elementType = Provider.Provide(rootType);
Expand All @@ -66,7 +69,7 @@ public TypedElementOnSourceNode(ISourceNode source, string type, IStructureDefin
}


private TypedElementOnSourceNode(TypedElementOnSourceNode parent, ISourceNode source, IElementDefinitionSummary definition, string instanceType, string prettyPath)
private TypedElementOnSourceNode(TypedElementOnSourceNode parent, ISourceNode source, IElementDefinitionSummary? definition, string instanceType, string prettyPath, string location)
{
_source = source;
ShortPath = prettyPath;
Expand All @@ -75,11 +78,12 @@ private TypedElementOnSourceNode(TypedElementOnSourceNode parent, ISourceNode so
Definition = definition;
InstanceType = instanceType;
_settings = parent._settings;
Location = location;
}

public ExceptionNotificationHandler ExceptionHandler { get; set; }
public ExceptionNotificationHandler? ExceptionHandler { get; set; }

private void raiseTypeError(string message, object source, bool warning = false, string location = null)
private void raiseTypeError(string message, object source, bool warning = false, string? location = null)
{
var exMessage = $"Type checking the data: {message}";
if (!string.IsNullOrEmpty(location))
Expand All @@ -101,7 +105,7 @@ private void raiseTypeError(string message, object source, bool warning = false,

private readonly TypedElementSettings _settings;

public IElementDefinitionSummary Definition { get; private set; }
public IElementDefinitionSummary? Definition { get; private set; }

public string Name => Definition?.ElementName ?? _source.Name;

Expand All @@ -120,7 +124,7 @@ private void raiseTypeError(string message, object source, bool warning = false,
// R3 and R4, these value (and url and id elements by the way) will indicate which type
// of system types there are, implicitly specifying the mapping between primitive
// FHIR types and system types.
private static Type tryMapFhirPrimitiveTypeToSystemType(string fhirType)
private static Type? tryMapFhirPrimitiveTypeToSystemType(string fhirType)
{
switch (fhirType)
{
Expand Down Expand Up @@ -158,7 +162,7 @@ private static Type tryMapFhirPrimitiveTypeToSystemType(string fhirType)
}
}

private object valueFactory()
private object? valueFactory()
{
string sourceText = _source.Text;

Expand Down Expand Up @@ -209,7 +213,7 @@ private object valueFactory()
if (P.Any.TryParse(sourceText, typeof(P.DateTime), out var dateTimeVal))
{
// TruncateToDate converts 1991-02-03T11:22:33Z to 1991-02-03+00:00 which is not a valid date!
var date = (dateTimeVal as P.DateTime).TruncateToDate();
var date = (dateTimeVal as P.DateTime)!.TruncateToDate();
// so we cut off timezone by converting it to timeoffset and cast back to date.
return P.Date.FromDateTimeOffset(date.ToDateTimeOffset(0, 0, 0, TimeSpan.Zero));
}
Expand All @@ -220,13 +224,13 @@ private object valueFactory()
}
}

private object _value;
private object? _value;
private bool _valueInitialized = false;
private static object _initializationLock = new();

public object Value => LazyInitializer.EnsureInitialized(ref _value, ref _valueInitialized, ref _initializationLock, valueFactory);
public object Value => LazyInitializer.EnsureInitialized(ref _value, ref _valueInitialized, ref _initializationLock, valueFactory)!;

private string deriveInstanceType(ISourceNode current, IElementDefinitionSummary info)
private string? deriveInstanceType(ISourceNode current, IElementDefinitionSummary info)
{
var resourceTypeIndicator = current.GetResourceTypeIndicator();

Expand Down Expand Up @@ -338,7 +342,7 @@ private string typeFromLogicalModelCanonical(ITypeSerializationInfo info)
return pos > -1 ? type.Substring(pos + 1) : type;
}

private bool tryGetBySuffixedName(Dictionary<string, IElementDefinitionSummary> dis, string name, out IElementDefinitionSummary info)
private bool tryGetBySuffixedName(Dictionary<string, IElementDefinitionSummary> dis, string name, out IElementDefinitionSummary? info)
{
// Simplest case, one on one match between name and element name
if (dis.TryGetValue(name, out info))
Expand All @@ -361,7 +365,7 @@ private bool tryGetBySuffixedName(Dictionary<string, IElementDefinitionSummary>
}
}

private IEnumerable<TypedElementOnSourceNode> enumerateElements(Dictionary<string, IElementDefinitionSummary> dis, ISourceNode parent, string name)
private IEnumerable<TypedElementOnSourceNode> enumerateElements(Dictionary<string, IElementDefinitionSummary> dis, ISourceNode parent, string? name)
{
IEnumerable<ISourceNode> childSet;

Expand All @@ -372,17 +376,17 @@ private IEnumerable<TypedElementOnSourceNode> enumerateElements(Dictionary<strin
{
var hit = dis.TryGetValue(name, out var info);
childSet = hit
? (info.IsChoiceElement ? parent.Children(name + "*") : parent.Children(name))
? (info!.IsChoiceElement ? parent.Children(name + "*") : parent.Children(name))
: Enumerable.Empty<ISourceNode>();
}

string lastName = null;
string? lastName = null;
int _nameIndex = 0;

foreach (var scan in childSet)
{
var hit = tryGetBySuffixedName(dis, scan.Name, out var info);
string instanceType = info == null ? null :
string? instanceType = info == null ? null :
deriveInstanceType(scan, info);

// If we have definitions for the children, but we didn't find definitions for this
Expand All @@ -406,28 +410,31 @@ private IEnumerable<TypedElementOnSourceNode> enumerateElements(Dictionary<strin
}

var prettyPath =
hit && !info.IsCollection ? $"{ShortPath}.{info.ElementName}" : $"{ShortPath}.{scan.Name}[{_nameIndex}]";
hit && !info!.IsCollection ? $"{ShortPath}.{info.ElementName}" : $"{ShortPath}.{scan.Name}[{_nameIndex}]";

var location =
hit ? $"{Location}.{info!.ElementName}[{_nameIndex}]" : $"{Location}.{scan.Name}[{_nameIndex}]";

// Special condition for ccda.
// If we encounter a xhtml node in a ccda document we will flatten all childnodes
// and use their content to build up the xml.
// The xml will be put in this node and children will be ignored.
if (instanceType == XHTML_INSTANCETYPE && info.Representation == XmlRepresentation.CdaText)
if (instanceType == XHTML_INSTANCETYPE && info!.Representation == XmlRepresentation.CdaText)
{
#pragma warning disable CS0618 // Type or member is obsolete
var xmls = scan.Children().Select(c => c.Annotation<ICdaInfoSupplier>()?.XHtmlText);
#pragma warning restore CS0618 // Type or member is obsolete

var source = SourceNode.Valued(scan.Name, string.Join(string.Empty, xmls));
yield return new TypedElementOnSourceNode(this, source, info, instanceType, prettyPath);
yield return new TypedElementOnSourceNode(this, source, info, instanceType, prettyPath, location);
continue;
}

yield return new TypedElementOnSourceNode(this, scan, info, instanceType, prettyPath);
yield return new TypedElementOnSourceNode(this, scan, info, instanceType!, prettyPath, location);
}
}

public IEnumerable<ITypedElement> Children(string name = null)
public IEnumerable<ITypedElement> Children(string? name = null)
{
// If we have an xhtml typed node and there is not a div tag around the content
// then we will not enumerate through the children of this node, since there will be no types
Expand Down Expand Up @@ -461,13 +468,13 @@ public IEnumerable<ITypedElement> Children(string name = null)
private IEnumerable<ITypedElement> runAdditionalRules(IEnumerable<ITypedElement> children)
{
#pragma warning disable 612, 618
var additionalRules = _source.Annotations(typeof(AdditionalStructuralRule));
var additionalRules = _source.Annotations<AdditionalStructuralRule>().ToArray();
var stateBag = new Dictionary<AdditionalStructuralRule, object>();
foreach (var child in children)
{
foreach (var rule in additionalRules.Cast<AdditionalStructuralRule>())
foreach (var rule in additionalRules)
{
stateBag.TryGetValue(rule, out object state);
stateBag.TryGetValue(rule, out object? state);
state = rule(child, this, state);
if (state != null) stateBag[rule] = state;
}
Expand All @@ -477,25 +484,23 @@ private IEnumerable<ITypedElement> runAdditionalRules(IEnumerable<ITypedElement>
#pragma warning restore 612, 618
}

public string Location => _source.Location;
public string Location { get; init; }

public string ShortPath { get; private set; }

public override string ToString() =>
$"{(InstanceType != null ? ($"[{InstanceType}] ") : "")}{_source}";
$"{(($"[{InstanceType}] "))}{_source}";

public IEnumerable<object> Annotations(Type type)
{
#pragma warning disable IDE0046 // Convert to conditional expression
if (type == typeof(TypedElementOnSourceNode) || type == typeof(ITypedElement) || type == typeof(IShortPathGenerator))
#pragma warning restore IDE0046 // Convert to conditional expression
return new[] { this };
else
return _source.Annotations(type);
}
}

[Obsolete("This class is used for internal purposes and is subject to change without notice. Don't use.")]
public delegate object AdditionalStructuralRule(ITypedElement node, IExceptionSource ies, object state);
public delegate object? AdditionalStructuralRule(ITypedElement node, IExceptionSource ies, object? state);
}

2 changes: 1 addition & 1 deletion src/Hl7.Fhir.ElementModel.Shared.Tests/ScopedNodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ static bool CCDATypeNameMapper(string typeName, out string canonical)
Assert.AreEqual(1, assertXHtml.Count());
Assert.AreEqual("text", assertXHtml.First().Name);
Assert.AreEqual("xhtml", assertXHtml.First().InstanceType);
Assert.AreEqual("text", assertXHtml.First().Location);
Assert.AreEqual("Section.text[0]", assertXHtml.First().Location);
Assert.IsNotNull(assertXHtml.First().Value);


Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using Hl7.Fhir.Model;
using System;
using System.Linq;
using System.Threading.Tasks;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Specification;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using Task = System.Threading.Tasks.Task;

namespace Hl7.Fhir.ElementModel.Tests
{
Expand All @@ -22,5 +24,27 @@ public async Task TestExceptionComplexTypeValue()

var _ = typedBundle.Children("entry").First().Value;
}

private SourceNode testPatient => SourceNode.Node("Patient",
SourceNode.Resource("contained", "Observation", SourceNode.Valued("valueBoolean", "true")),
SourceNode.Valued("active", "true",
SourceNode.Valued("id", "myId2"),
SourceNode.Node("extension",
SourceNode.Valued("url", "http://example.org/ext"),
SourceNode.Valued("valueString", "world!"))));

private TypedElementOnSourceNode getTestPatient => (TypedElementOnSourceNode)testPatient.ToTypedElement(ModelInfo.ModelInspector, "Patient");

[TestMethod]
public void KnowsPath()
{
var tp = getTestPatient;
Assert.AreEqual("Patient", getTestPatient.Location);
Assert.AreEqual("Patient.contained[0].value[0]", getTestPatient.Children("contained").First().Children("value").First().Location);
Assert.AreEqual("Patient.active[0]", getTestPatient.Children("active").First().Location);
Assert.AreEqual("Patient.active[0].id[0]", getTestPatient.Children("active").First().Children("id").First().Location);
Assert.AreEqual("Patient.active[0].extension[0].url[0]", getTestPatient.Children("active").First().Children("extension").First().Children("url").First().Location);
Assert.AreEqual("Patient.active[0].extension[0].value[0]", getTestPatient.Children("active").First().Children("extension").First().Children("value").First().Location);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ public string RoundTripXml(string original) =>
engine.SerializeToXml(
engine.DeserializeFromJson(
engine.SerializeToJson(
engine.DeserializeFromXml(original))));
engine.DeserializeFromXml(original)!))!);
public string RoundTripJson(string original) =>
engine.SerializeToJson(
engine.DeserializeFromXml(
engine.SerializeToXml(
engine.DeserializeFromJson(original))));
engine.DeserializeFromJson(original)!))!);
}

internal class TypedElementBasedRoundtripper(IStructureDefinitionSummaryProvider provider) : IRoundTripper
Expand Down Expand Up @@ -249,7 +249,7 @@ public void TestMatchAndExactly(ZipArchiveEntry entry)
? NEW_POCO_ENGINE.DeserializeFromXml(input)
: NEW_POCO_ENGINE.DeserializeFromJson(input);

var r2 = (Resource)resource.DeepCopy();
var r2 = (Resource)resource!.DeepCopy();
Assert.IsTrue(resource.Matches(r2),
"Serialization of " + name + " did not match output - Matches test");
Assert.IsTrue(resource.IsExactly(r2),
Expand Down
Loading