Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/Firely.Fhir.Validation/Impl/AllValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace Firely.Fhir.Validation
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class AllValidator : IGroupValidatable
public class AllValidator : IGroupValidatable, IAssertionContainer
{
/// <summary>
/// The member assertions the instance should be validated against.
Expand Down Expand Up @@ -77,6 +77,13 @@ public AllValidator(bool shortcircuitEvaluation, params IAssertion[] members) :
{
}

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
var members = Members.TryRewriteMembers(AssertionStep.Member, rewrite);
return members is null ? this : new AllValidator(members, ShortcircuitEvaluation);
}

/// <inheritdoc cref="IGroupValidatable.Validate(IEnumerable{PocoNode}, ValidationSettings, ValidationState)"/>
ResultReport IGroupValidatable.Validate(
IEnumerable<PocoNode> input,
Expand Down
11 changes: 10 additions & 1 deletion src/Firely.Fhir.Validation/Impl/AnyValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace Firely.Fhir.Validation
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class AnyValidator : IGroupValidatable
public class AnyValidator : IGroupValidatable, IAssertionContainer
{
/// <summary>
/// The member assertions of which at least one should hold.
Expand Down Expand Up @@ -59,6 +59,15 @@ public AnyValidator(params IAssertion[] members) : this(members.AsEnumerable(),
{
}

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
/// <remarks>The <see cref="SummaryError"/> is not visited: it is the error reported when all members
/// fail, not an assertion this validator validates against.</remarks>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
var members = Members.TryRewriteMembers(AssertionStep.Member, rewrite);
return members is null ? this : new AnyValidator(members, SummaryError);
}

/// <inheritdoc cref="IGroupValidatable.Validate(IEnumerable{PocoNode}, ValidationSettings, ValidationState)"/>
ResultReport IGroupValidatable.Validate(
IEnumerable<PocoNode> input,
Expand Down
22 changes: 21 additions & 1 deletion src/Firely.Fhir.Validation/Impl/ChildrenValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ namespace Firely.Fhir.Validation
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class ChildrenValidator : IValidatable, IReadOnlyDictionary<string, IAssertion>
public class ChildrenValidator : IValidatable, IReadOnlyDictionary<string, IAssertion>, IAssertionContainer
{
private readonly Dictionary<string, IAssertion> _childList = new();

Expand Down Expand Up @@ -82,6 +82,26 @@ public ChildrenValidator(IEnumerable<(string name, IAssertion assertion)> childL
public IAssertion? Lookup(string name) =>
ChildList.TryGetValue(name, out var child) ? child : null;

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
Dictionary<string, IAssertion>? updated = null;

foreach (var (name, child) in _childList)
{
var rewritten = rewrite(AssertionStep.Child(name), child);

if (!ReferenceEquals(rewritten, child))
{
// Only start copying once we actually have a change to record.
updated ??= new Dictionary<string, IAssertion>(_childList);
updated[name] = rewritten;
}
}

return updated is null ? this : new ChildrenValidator(updated, AllowAdditionalChildren);
}

/// <inheritdoc />
public JToken ToJson() =>
new JProperty("children", new JObject() { ChildList.Select(child =>
Expand Down
30 changes: 29 additions & 1 deletion src/Firely.Fhir.Validation/Impl/DefinitionsAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace Firely.Fhir.Validation
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class DefinitionsAssertion : IAssertion
public class DefinitionsAssertion : IAssertion, IAssertionContainer
{
/// <summary>
/// The list of subschemas.
Expand Down Expand Up @@ -61,6 +61,34 @@ public DefinitionsAssertion(IEnumerable<ElementSchema> schemas)
public ElementSchema? FindFirstByAnchor(string anchor) =>
Schemas.FirstOrDefault(s => s.Id == "#" + anchor);

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
/// <remarks>Since the subschemas are found by anchor, a rewrite must return an
/// <see cref="ElementSchema"/> that kept its id - copying a schema through
/// <see cref="ElementSchema.WithMembers(IEnumerable{IAssertion})"/> does so.</remarks>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
ElementSchema[]? updated = null;

for (var index = 0; index < Schemas.Count; index++)
{
var schema = Schemas[index];
var anchor = ((string)schema.Id).TrimStart('#');
var rewritten = rewrite(AssertionStep.Subschema(anchor), schema);

if (ReferenceEquals(rewritten, schema)) continue;

if (rewritten is not ElementSchema rewrittenSchema)
throw new InvalidOperationException(
$"A rewrite of subschema '{schema.Id}' must return an {nameof(ElementSchema)}, but it returned a {rewritten.GetType().Name}.");
Comment on lines +80 to +82

// Only start copying once we actually have a change to record.
updated ??= [.. Schemas];
updated[index] = rewrittenSchema;
}

return updated is null ? this : new DefinitionsAssertion(updated);
}

/// <inheritdoc cref="IJsonSerializable.ToJson"/>
public JToken ToJson() =>
new JProperty("definitions", new JArray(
Expand Down
11 changes: 10 additions & 1 deletion src/Firely.Fhir.Validation/Impl/ElementSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Firely.Fhir.Validation
/// schema to be succesful.
/// </summary>
[DataContract]
public class ElementSchema : IGroupValidatable
public class ElementSchema : IGroupValidatable, IAssertionContainer
{
/// <summary>
/// The unique id for this schema.
Expand Down Expand Up @@ -75,6 +75,15 @@ private static IReadOnlyCollection<IAssertion> extractShortcutMembers(IEnumerabl
internal virtual ElementSchema WithMembers(IEnumerable<IAssertion> members)
=> new(Id, members);

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
/// <remarks>Rewriting the members goes through <see cref="WithMembers(IEnumerable{IAssertion})"/>, so
/// the copy is of the same concrete schema type and its shortcut members are recalculated.</remarks>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
var members = Members.TryRewriteMembers(AssertionStep.Member, rewrite);
return members is null ? this : WithMembers(members);
Comment on lines +83 to +84
}

internal virtual ResultReport ValidateInternal(
IEnumerable<PocoNode> input,
ValidationSettings vc,
Expand Down
12 changes: 11 additions & 1 deletion src/Firely.Fhir.Validation/Impl/KeyedObjectValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace Firely.Fhir.Validation
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class KeyedObjectValidator : IValidatable
public class KeyedObjectValidator : IValidatable, IAssertionContainer
{
/// <summary>
/// The assertion each entry (JSON property value) of the keyed object is validated against.
Expand Down Expand Up @@ -72,6 +72,16 @@ public KeyedObjectValidator(IAssertion entryAssertion, int? min = null, int? max
Max = max;
}

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
var entryAssertion = rewrite(AssertionStep.Member, EntryAssertion);

return ReferenceEquals(entryAssertion, EntryAssertion)
? this
: new KeyedObjectValidator(entryAssertion, Min, Max);
}

/// <inheritdoc />
public JToken ToJson() =>
new JProperty("keyed-object", new JObject(
Expand Down
12 changes: 11 additions & 1 deletion src/Firely.Fhir.Validation/Impl/PathSelectorValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Hl7.FhirPath;
using Hl7.FhirPath.Expressions;
using Newtonsoft.Json.Linq;
using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;
Expand All @@ -30,7 +31,7 @@ namespace Firely.Fhir.Validation
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class PathSelectorValidator : IValidatable
public class PathSelectorValidator : IValidatable, IAssertionContainer
{
/// <summary>
/// The FhirPath statement used to select a value to validate.
Expand All @@ -53,6 +54,15 @@ public PathSelectorValidator(string path, IAssertion other)
Other = other;
}

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
/// <remarks>In practice a rewrite never reaches this validator, since it only occurs inside the
/// discriminators of a <see cref="SliceValidator"/>, which are not visited.</remarks>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
var other = rewrite(AssertionStep.Member, Other);
return ReferenceEquals(other, Other) ? this : new PathSelectorValidator(Path, other);
}

/// <inheritdoc/>
/// <remarks>Note that this validator is only used internally to represent the checks for
/// the path-based discriminated cases in a <see cref="SliceValidator" />, so this validator
Expand Down
41 changes: 40 additions & 1 deletion src/Firely.Fhir.Validation/Impl/ReferencedInstanceValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public enum ReferenceChecks
#else
[Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class ReferencedInstanceValidator : IValidatable
public class ReferencedInstanceValidator : IValidatable, IAssertionContainer
{
/// <summary>
/// The schema to validate the target of a reference against, for targets of a given type:
Expand Down Expand Up @@ -186,6 +186,45 @@ public ReferencedInstanceValidator(IEnumerable<TargetCase> targetCases,

private readonly TargetCase? _catchAllCase;

/// <summary>
/// Copies <paramref name="original"/>, replacing the schema(s) the target is validated against.
/// </summary>
/// <remarks>Used to rewrite the target schemas without having to reproduce the original's
/// configuration through one of the public constructors, which do not all carry every setting.</remarks>
private ReferencedInstanceValidator(ReferencedInstanceValidator original, IAssertion? schema, IReadOnlyList<TargetCase>? targetCases)
{
Schema = schema;
TargetCases = targetCases;
AggregationRules = original.AggregationRules;
VersioningRules = original.VersioningRules;
Checks = original.Checks;
_catchAllCase = targetCases is [{ Type: "Resource" } single] ? single : null;
}

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
var schema = Schema is null ? null : rewrite(AssertionStep.ReferenceTarget(null), Schema);
TargetCase[]? updatedCases = null;

for (var index = 0; index < TargetCases?.Count; index++)
{
var targetCase = TargetCases[index];
var rewritten = rewrite(AssertionStep.ReferenceTarget(targetCase.Type), targetCase.Schema);

if (!ReferenceEquals(rewritten, targetCase.Schema))
{
// Only start copying once we actually have a change to record.
updatedCases ??= [.. TargetCases];
updatedCases[index] = new TargetCase(targetCase.Type, rewritten);
}
}

return ReferenceEquals(schema, Schema) && updatedCases is null
? this
: new ReferencedInstanceValidator(this, schema, updatedCases ?? TargetCases);
}

/// <summary>
/// Whether any <see cref="AggregationRules"/> have been specified on the constructor.
/// </summary>
Expand Down
30 changes: 29 additions & 1 deletion src/Firely.Fhir.Validation/Impl/SliceValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace Firely.Fhir.Validation
#else
[System.Obsolete("This function is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.")]
#endif
public class SliceValidator : IGroupValidatable
public class SliceValidator : IGroupValidatable, IAssertionContainer
{
/// <summary>
/// Represents a named, conditional assertion on a set of elements.
Expand Down Expand Up @@ -139,6 +139,34 @@ public SliceValidator(bool ordered, bool defaultAtEnd, IAssertion @default, IEnu
Slices = slices.ToArray() ?? throw new ArgumentNullException(nameof(slices));
}

/// <inheritdoc cref="IAssertionContainer.WithChildren(Func{AssertionStep, IAssertion, IAssertion})"/>
/// <remarks>The discriminators (<see cref="SliceCase.Condition"/>) are deliberately not visited:
/// they decide which slice an instance belongs to, and rewriting them would change the slicing
/// itself rather than the validation performed on a slice.</remarks>
IAssertion IAssertionContainer.WithChildren(Func<AssertionStep, IAssertion, IAssertion> rewrite)
{
SliceCase[]? updated = null;

for (var index = 0; index < Slices.Count; index++)
{
var slice = Slices[index];
var rewritten = rewrite(AssertionStep.Slice(slice.Name), slice.Assertion);

if (!ReferenceEquals(rewritten, slice.Assertion))
{
// Only start copying once we actually have a change to record.
updated ??= [.. Slices];
updated[index] = new SliceCase(slice.Name, slice.Condition, rewritten, slice.Required);
}
}

var @default = rewrite(AssertionStep.Member, Default);

return updated is null && ReferenceEquals(@default, Default)
? this
: new SliceValidator(Ordered, DefaultAtEnd, @default, updated ?? Slices, MultiCase);
}

/// <inheritdoc/>
ResultReport IValidatable.Validate(PocoNode input, ValidationSettings vc, ValidationState state) => ((IGroupValidatable)this).Validate(input, vc, state);

Expand Down
Loading