Skip to content

Commit

Permalink
Merge pull request #2747 from FirelyTeam/feature/location-itypedelement
Browse files Browse the repository at this point in the history
Fixed location property on TypedElementOnSourceNode.cs
  • Loading branch information
ewoutkramer authored Mar 27, 2024
2 parents e0c5f31 + 8717149 commit 6731010
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 195 deletions.
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

0 comments on commit 6731010

Please sign in to comment.