diff --git a/firely-validator-api-tests.props b/firely-validator-api-tests.props index 532db30b..645d2df6 100644 --- a/firely-validator-api-tests.props +++ b/firely-validator-api-tests.props @@ -10,7 +10,7 @@ - 5.4.1-20231211.2 + 5.4.1-20231212.3 5.1.0 diff --git a/firely-validator-api.props b/firely-validator-api.props index d503347c..4b960aec 100644 --- a/firely-validator-api.props +++ b/firely-validator-api.props @@ -19,7 +19,7 @@ - 5.4.1-20231211.2 + 5.4.1-20231212.3 diff --git a/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs b/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs index 25742747..713f107d 100644 --- a/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/FhirPathValidator.cs @@ -100,14 +100,12 @@ protected override (bool, ResultReport?) RunInvariant(IScopedNode input, Validat { try { -#pragma warning disable CS0618 // Type or member is obsolete - var node = input as ScopedNode ?? new ScopedNode(input.AsTypedElement()); -#pragma warning restore CS0618 // Type or member is obsolete + ScopedNode node = input.ToScopedNode(); var context = new FhirEvaluationContext(node.ResourceContext) { TerminologyService = new ValidateCodeServiceToTerminologyServiceAdapter(vc.ValidateCodeService) }; - return (predicate(input, context, vc), null); + return (predicate(node, context, vc), null); } catch (Exception e) { @@ -153,14 +151,12 @@ private CompiledExpression getDefaultCompiledExpression(FhirPathCompiler compile } } - private bool predicate(IScopedNode input, EvaluationContext context, ValidationContext vc) + private bool predicate(ScopedNode input, EvaluationContext context, ValidationContext vc) { var compiler = vc?.FhirPathCompiler ?? DefaultCompiler; var compiledExpression = getDefaultCompiledExpression(compiler); -#pragma warning disable CS0618 // Type or member is obsolete - return compiledExpression.IsTrue(input.AsTypedElement(), context); -#pragma warning restore CS0618 // Type or member is obsolete + return compiledExpression.IsTrue(input, context); } /// diff --git a/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs b/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs index 6cf9bc99..6aed5833 100644 --- a/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs +++ b/src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs @@ -115,11 +115,8 @@ private record ResolutionResult(ITypedElement? ReferencedResource, AggregationMo ResolutionResult resolution = new(null, null, null); List evidence = new(); - if (input is not ScopedNode instance) - throw new InvalidOperationException($"Cannot validate because input is not of type {nameof(ScopedNode)}."); - // First, try to resolve within this instance (in contained, Bundle.entry) - evidence.Add(resolveLocally(instance, reference, s, out resolution)); + evidence.Add(resolveLocally(input.ToScopedNode(), reference, s, out resolution)); // Now that we have tried to fetch the reference locally, we have also determined the kind of // reference we are dealing with, so check it for aggregation and versioning rules. diff --git a/src/Firely.Fhir.Validation/Schema/ValueElementNode.cs b/src/Firely.Fhir.Validation/Schema/ValueElementNode.cs index 1e056d67..cda1b951 100644 --- a/src/Firely.Fhir.Validation/Schema/ValueElementNode.cs +++ b/src/Firely.Fhir.Validation/Schema/ValueElementNode.cs @@ -4,7 +4,6 @@ * via any medium is strictly prohibited. */ -using Hl7.Fhir.ElementModel; using Hl7.Fhir.Language; using System.Collections.Generic; using System.Linq; @@ -20,8 +19,6 @@ public ValueElementNode(IScopedNode wrapped) _wrapped = wrapped; } - public IScopedNode? Parent => _wrapped.Parent; - public string Name => "value"; public string InstanceType => TypeSpecifier.ForNativeType(_wrapped.Value.GetType()).FullName; diff --git a/src/Firely.Fhir.Validation/Support/IScopedNode.cs b/src/Firely.Fhir.Validation/Support/IScopedNode.cs new file mode 100644 index 00000000..c53512bb --- /dev/null +++ b/src/Firely.Fhir.Validation/Support/IScopedNode.cs @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + +#nullable enable + +using Hl7.Fhir.ElementModel; + +namespace Firely.Fhir.Validation +{ + /// + /// An element within a tree of typed FHIR data with also a parent element. + /// + /// + /// 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 + /// +#pragma warning disable CS0618 // Type or member is obsolete + internal interface IScopedNode : IBaseElementNavigator +#pragma warning restore CS0618 // Type or member is obsolete + { + /* + * + /// + /// The parent node of this node, or null if this is the root node. + /// + IScopedNode? Parent { get; } // We don't need this probably. + */ + } +} + +#nullable restore \ No newline at end of file diff --git a/src/Firely.Fhir.Validation/Support/IScopedNodeExtensions.cs b/src/Firely.Fhir.Validation/Support/IScopedNodeExtensions.cs new file mode 100644 index 00000000..b610368e --- /dev/null +++ b/src/Firely.Fhir.Validation/Support/IScopedNodeExtensions.cs @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + +#nullable enable + + +using Hl7.Fhir.ElementModel; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Firely.Fhir.Validation +{ + internal static class IScopedNodeExtensions + { + /// + /// Converts a to a . + /// + /// An node + /// An implementation of + /// Be careful when using this method, the returned does not implement + /// the methods and . + /// + [Obsolete("WARNING! For internal API use only. Turning an IScopedNode into an ITypedElement will cause problems for" + + "Location and Definitions. Those properties are not implemented using this method and can cause problems " + + "elsewhere. Please don't use this method unless you know what you are doing.")] + public static ITypedElement AsTypedElement(this IScopedNode node) => node switch + { + TypedElementToIScopedNodeToAdapter adapter => adapter.ScopedNode, + ITypedElement ite => ite, + _ => new ScopedNodeToTypedElementAdapter(node) + }; + //node is ITypedElement ite ? ite : new ScopedNodeToTypedElementAdapter(node); + + public static ScopedNode ToScopedNode(this IScopedNode node) => node switch + { + TypedElementToIScopedNodeToAdapter adapter => adapter.ScopedNode, + _ => throw new ArgumentException("The node is not a TypedElementToIScopedNodeToAdapter") + }; + + /// + /// Returns the parent resource of this node, or null if this node is not part of a resource. + /// + /// + /// + /// + public static IEnumerable Children(this IEnumerable nodes, string? name = null) => + nodes.SelectMany(n => n.Children(name)); + } +} + +#nullable restore \ No newline at end of file diff --git a/src/Firely.Fhir.Validation/Support/ScopedNodeExtensions.cs b/src/Firely.Fhir.Validation/Support/ScopedNodeExtensions.cs index c4a578bb..dfc767c6 100644 --- a/src/Firely.Fhir.Validation/Support/ScopedNodeExtensions.cs +++ b/src/Firely.Fhir.Validation/Support/ScopedNodeExtensions.cs @@ -14,6 +14,15 @@ namespace Firely.Fhir.Validation /// internal static class ScopedNodeExtensions { + + /// + /// Convert a to a . + /// + /// An + /// + public static IScopedNode AsScopedNode(this ITypedElement node) + => new TypedElementToIScopedNodeToAdapter(node.ToScopedNode()); + /// /// /// @@ -25,6 +34,53 @@ public static IScopedNode ToScopedNode(this IReadOnlyDictionary inspector ??= ModelInspector.ForAssembly(node.GetType().Assembly); return new ScopedNodeOnDictionary(inspector, node.GetType().Name, node); } + + internal static Quantity ParseQuantity(this IScopedNode instance) +#pragma warning disable CS0618 // Type or member is obsolete + => instance.ParseQuantityInternal(); +#pragma warning restore CS0618 // Type or member is obsolete + + internal static T ParsePrimitive(this IScopedNode instance) where T : PrimitiveType, new() +#pragma warning disable CS0618 // Type or member is obsolete + => instance.ParsePrimitiveInternal(); +#pragma warning restore CS0618 // Type or member is obsolete + + internal static Coding ParseCoding(this IScopedNode instance) +#pragma warning disable CS0618 // Type or member is obsolete + => instance.ParseCodingInternal(); +#pragma warning restore CS0618 // Type or member is obsolete + + internal static ResourceReference ParseResourceReference(this IScopedNode instance) +#pragma warning disable CS0618 // Type or member is obsolete + => instance.ParseResourceReferenceInternal(); +#pragma warning restore CS0618 // Type or member is obsolete + + internal static CodeableConcept ParseCodeableConcept(this IScopedNode instance) +#pragma warning disable CS0618 // Type or member is obsolete + => instance.ParseCodeableConceptInternal(); +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// Parses a bindeable type (code, Coding, CodeableConcept, Quantity, string, uri) into a FHIR coded datatype. + /// Extensions will be parsed from the 'value' of the (simple) extension. + /// + /// + /// An Element of a coded type (code, Coding or CodeableConcept) dependin on the instance type, + /// or null if no bindable instance data was found + /// The instance type is mapped to a codable type as follows: + /// 'code' => code + /// 'Coding' => Coding + /// 'CodeableConcept' => CodeableConcept + /// 'Quantity' => Coding + /// 'Extension' => depends on value[x] + /// 'string' => code + /// 'uri' => code + /// + internal static Element ParseBindable(this IScopedNode instance) +#pragma warning disable CS0618 // Type or member is obsolete + => instance.ParseBindableInternal(); +#pragma warning restore CS0618 // Type or member is obsolete + } internal class ScopedNodeOnDictionary : IScopedNode diff --git a/src/Firely.Fhir.Validation/Support/ScopedNodeToTypedElementAdapter.cs b/src/Firely.Fhir.Validation/Support/ScopedNodeToTypedElementAdapter.cs new file mode 100644 index 00000000..08dccf17 --- /dev/null +++ b/src/Firely.Fhir.Validation/Support/ScopedNodeToTypedElementAdapter.cs @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + +#nullable enable + +using Hl7.Fhir.ElementModel; +using Hl7.Fhir.Specification; +using System.Collections.Generic; +using System.Linq; + +namespace Firely.Fhir.Validation +{ + /// + /// An adapter from to . + /// + /// Be careful, this adapter does not implement the and + /// property. + /// + internal class ScopedNodeToTypedElementAdapter : ITypedElement + { + private readonly IScopedNode _adaptee; + + public ScopedNodeToTypedElementAdapter(IScopedNode adaptee) + { + _adaptee = adaptee; + } + + public string Location => throw new System.NotImplementedException(); + + public IElementDefinitionSummary Definition => throw new System.NotImplementedException(); + + public string Name => _adaptee.Name; + + public string InstanceType => _adaptee.InstanceType; + + public object Value => _adaptee.Value; + + public IEnumerable Children(string? name = null) => + _adaptee.Children(name).Select(n => new ScopedNodeToTypedElementAdapter(n)); + } +} + +#nullable restore \ No newline at end of file diff --git a/src/Firely.Fhir.Validation/Support/TypedElementToIScopedNodeToAdapter.cs b/src/Firely.Fhir.Validation/Support/TypedElementToIScopedNodeToAdapter.cs new file mode 100644 index 00000000..f2502f84 --- /dev/null +++ b/src/Firely.Fhir.Validation/Support/TypedElementToIScopedNodeToAdapter.cs @@ -0,0 +1,31 @@ +using Hl7.Fhir.ElementModel; +using System.Collections.Generic; +using System.Linq; + +namespace Firely.Fhir.Validation +{ + internal class TypedElementToIScopedNodeToAdapter : IScopedNode + { + private readonly ITypedElement _adaptee; + + public TypedElementToIScopedNodeToAdapter(ScopedNode adaptee) : this(adaptee as ITypedElement) + { + } + + private TypedElementToIScopedNodeToAdapter(ITypedElement adaptee) + { + _adaptee = adaptee; + } + + public ScopedNode ScopedNode => (ScopedNode)_adaptee; // we know that this is always a ScopedNode + + public string Name => _adaptee.Name; + + public string InstanceType => _adaptee.InstanceType; + + public object Value => _adaptee.Value; + + IEnumerable IBaseElementNavigator.Children(string? name) => + _adaptee.Children(name).Select(n => new TypedElementToIScopedNodeToAdapter(n)); + } +} diff --git a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs index b460e3dc..f38aebdf 100644 --- a/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs +++ b/test/Firely.Fhir.Validation.Compilation.Tests.Shared/ResourceSchemaValidationTests.cs @@ -87,7 +87,7 @@ public void AvoidsRedoingProfileValidation() vc.ResolveExternalReference = resolveTestData; var validationState = new ValidationState(); - var result = schemaElement!.Validate(new ScopedNode(all.ToTypedElement()), vc, validationState); + var result = schemaElement!.Validate(new ScopedNode(all.ToTypedElement()).AsScopedNode(), vc, validationState); result.Result.Should().Be(ValidationResult.Failure); var issues = result.Evidence.OfType().ToList(); issues.Count.Should().Be(1); // Bundle.entry[2].resource[0] is validated twice against different profiles. diff --git a/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs b/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs index 1a3a1df8..63c48120 100644 --- a/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs +++ b/test/Firely.Fhir.Validation.Tests/Impl/ReferencedInstanceValidatorTests.cs @@ -101,7 +101,7 @@ public void ValidateInstance(object instance, object testeeo, bool success, stri static ResultReport test(object instance, IAssertion testee, ValidationContext vc) { - var te = new ScopedNode(instance.ToTypedElement()); + var te = instance.ToTypedElement().AsScopedNode(); var asserter = te.Children("entry").First().Children("resource").Children("asserter").Single(); return testee.Validate(asserter, vc); }