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

Support Is-a filter in local ValueSet expansion #2733

Merged
merged 21 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b9ab5c6
add filter extensions
mmsmits Mar 11, 2024
5c6f307
freature: added is-a filter to expand valuesets
mmsmits Mar 11, 2024
78aab37
added tests
mmsmits Mar 11, 2024
9390e07
improved Tests
mmsmits Mar 11, 2024
2264ce1
document and clean up
mmsmits Mar 12, 2024
3fa0eef
remove some unused code
mmsmits Mar 12, 2024
021143a
throw an error when trying to filter codes from LOINC or SNOMED CT
mmsmits Mar 12, 2024
560da4a
(this commit shows better than review words what I meant)
ewoutkramer Mar 13, 2024
49f2dfb
moved valueset expansion extensions to shims.base
mmsmits Mar 15, 2024
800f044
minor improvements
mmsmits Mar 15, 2024
97933f2
added test to check for a positive result
mmsmits Mar 15, 2024
5db1f19
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
mmsmits Mar 15, 2024
2ad9b62
improvement: first flatten codes and then use a lookup to find all de…
mmsmits Mar 15, 2024
f19783a
improvement of checking for correct filter is moved to switch statement
mmsmits Mar 15, 2024
931d41e
also copies the properties of CodeSystem concepts to ValueSet contains
mmsmits Mar 15, 2024
5e3621d
make list of CodeSystems to complex to filter a setting
mmsmits Mar 15, 2024
3ceabd7
handles filtering on incomplete codesystems
mmsmits Mar 18, 2024
b82ee16
fix test
mmsmits Mar 18, 2024
9f2c78d
remove specific check for certain codesystems. Now check if the CodeS…
mmsmits Mar 19, 2024
fbae182
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
mmsmits Mar 19, 2024
9ec0acd
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
ewoutkramer Mar 19, 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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE
*/

#nullable enable

using Hl7.Fhir.Specification.Source;
using Hl7.Fhir.Utility;
using System;
Expand All @@ -23,7 +25,7 @@ public class ValueSetExpanderSettings
/// to another valueset is encountered.
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
public ISyncOrAsyncResourceResolver ValueSetSource { get; set; }
public ISyncOrAsyncResourceResolver? ValueSetSource { get; set; }
#pragma warning restore CS0618 // Type or member is obsolete

/// <summary>
Expand Down Expand Up @@ -67,3 +69,5 @@ public void CopyTo(ValueSetExpanderSettings other)

}
}

#nullable restore
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Conformance/Model/ValueSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public bool CodeInExpansion(String code, string? system = null)
return null;
}

public ValueSet.ContainsComponent FindInExpansion(String code, string? system = null)
public ValueSet.ContainsComponent? FindInExpansion(String code, string? system = null)
{
ensureExpansion();

Expand Down
53 changes: 0 additions & 53 deletions src/Hl7.Fhir.Conformance/Model/ValueSetExpansionExtensions.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Hl7.Fhir.STU3/Model/ValueSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public bool CodeInExpansion(String code, string? system = null)
return null;
}

public ValueSet.ContainsComponent FindInExpansion(String code, string? system = null)
public ValueSet.ContainsComponent? FindInExpansion(String code, string? system = null)
{
ensureExpansion();

Expand Down
53 changes: 0 additions & 53 deletions src/Hl7.Fhir.STU3/Model/ValueSetExpansionExtensions.cs

This file was deleted.

2 changes: 2 additions & 0 deletions src/Hl7.Fhir.Shims.Base/Hl7.Fhir.Shims.Base.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
<Import_RootNamespace>Hl7.Fhir.Shims.Base</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Model\ValueSetExpansionExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Source\SnapshotSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\CodeSystemFilterProcessor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\ExpandParameters.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\ExternalTerminologyService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Specification\Terminology\FallbackTerminologyService.cs" />
Expand Down
84 changes: 84 additions & 0 deletions src/Hl7.Fhir.Shims.Base/Model/ValueSetExpansionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CSDC = Hl7.Fhir.Model.CodeSystem.ConceptDefinitionComponent;

#nullable enable

namespace Hl7.Fhir.Model
{
public static class ValueSetExpansionExtensions
{
public static ValueSet.ContainsComponent? FindCode(this IEnumerable<ValueSet.ContainsComponent> cnt, string code, string? system = null)
{
foreach (var contains in cnt)
{
var result = contains.FindCode(code, system);
if (result != null) return result;
}

return null;
}
public static ValueSet.ContainsComponent? FindCode(this ValueSet.ContainsComponent contains, string code, string? system = null)
{
// Direct hit
if (code == contains.Code && (system == null || system == contains.System))
return contains;

// Not in this node, but this node may have child nodes to check
if (contains.Contains?.Any() == true)
return contains.Contains.FindCode(code, system);
else
return null;
}

internal static CSDC? FindCode(this IEnumerable<CSDC> concepts, string code)
{
return concepts.findCodeByPredicate(c => c.Code == code);
}

private static CSDC? findCodeByPredicate(this IEnumerable<CSDC> concepts, Predicate<CSDC> predicate)
{
foreach (var concept in concepts)
{
var result = concept.findCodeByPredicate(predicate);
if (result != null) return result;
}
return null;
}

private static CSDC? findCodeByPredicate(this CSDC concept, Predicate<CSDC> predicate)
{
// Direct hit
if (predicate(concept))
return concept;

// Not in this node, but this node may have child nodes to check
if (concept.Concept?.Any() == true)
return concept.Concept.findCodeByPredicate(predicate);
else
return null;
}

/// <summary>
/// Loops through all concepts and descendants and returns a flat list of concepts, without a nested hierarchy
/// </summary>
/// <param name="concepts">List of code system concepts</param>
/// <returns></returns>
internal static List<CSDC> Flatten(this IEnumerable<CSDC> concepts)
{
var flatList = new List<CSDC>();

foreach (var concept in concepts)
{
if (concept.Concept?.Any() == true)
{
flatList.AddRange(concept.Concept.Flatten());
concept.Concept.Clear();
}
flatList.Add(concept);
}
return flatList;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using Hl7.Fhir.Model;
using Hl7.Fhir.Specification.Source;
using Hl7.Fhir.Utility;
using System.Collections.Generic;
using System.Linq;
using T = System.Threading.Tasks;

#nullable enable

namespace Hl7.Fhir.Specification.Terminology
{

internal static class CodeSystemFilterProcessor
{
private const string SUBSUMEDBYCODE = "subsumedBy";

/// <summary>
/// Retrieve codes from a CodeSystem resource bases on one or multiple filters.
/// </summary>
/// <param name="codeSystemUri">Uri of the CodeSystem</param>
/// <param name="filters">Filters to be applied</param>
/// <param name="settings">ValueSetExpanderSettings </param>
/// <returns></returns>
/// <exception cref="ValueSetExpansionTooComplexException">Thrown when a filter is applied that is not supported (yet)</exception>
/// <exception cref="CodeSystemUnknownException">Thrown when no resource resolver was set in ValueSetExpanderSettings.ValueSetSource</exception>
/// <exception cref="CodeSystemUnknownException">Thrown when the requested CodeSystem can not be found by the resource resolver in ValueSetExpanderSettings</exception>
internal async static T.Task<IEnumerable<ValueSet.ContainsComponent>> FilterConceptsFromCodeSystem(string codeSystemUri, List<ValueSet.FilterComponent> filters, ValueSetExpanderSettings settings)
{
if (settings.ValueSetSource == null)
throw Error.InvalidOperation($"No valueset resolver available to resolve codesystem '{codeSystemUri}', so the expansion cannot be completed.");

var codeSystem = await settings.ValueSetSource.AsAsync().FindCodeSystemAsync(codeSystemUri).ConfigureAwait(false)
?? throw new CodeSystemUnknownException($"Cannot find codesystem '{codeSystemUri}', so the defined filter(s) cannot be applied.");

if (codeSystem.Content.GetLiteral() != "complete")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (codeSystem.Content.GetLiteral() != "complete")
if (codeSystem.Content != CodeSystemContentMode.Complete)

throw new CodeSystemIncompleteException($"CodeSystem {codeSystemUri} is marked incomplete, so the defines filter(s) cannot be applied.");


var result = applyFilters(filters, codeSystem);

return result.Select(c => c.ToContainsComponent(codeSystem, settings));
}

private static List<CodeSystem.ConceptDefinitionComponent> applyFilters(List<ValueSet.FilterComponent> filters, CodeSystem codeSystem)
{
var result = codeSystem.Concept;
var properties = codeSystem.Property;

ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
foreach (var filter in filters)
{
result = applyFilter(result, properties, filter).ToList();
}

return result;
}

private static IEnumerable<CodeSystem.ConceptDefinitionComponent> applyFilter(List<CodeSystem.ConceptDefinitionComponent> concepts, List<CodeSystem.PropertyComponent> properties, ValueSet.FilterComponent filter)
{
return filter.Op switch
{
FilterOperator.IsA => applyIsAFilter(concepts, properties, filter),
_ => throw new ValueSetExpansionTooComplexException($"ConceptSets with a filter {filter.Op} are not yet supported.")
};
}

private static IEnumerable<CodeSystem.ConceptDefinitionComponent> applyIsAFilter(List<CodeSystem.ConceptDefinitionComponent> concepts, List<CodeSystem.PropertyComponent> properties, ValueSet.FilterComponent filter)
{
var result = new List<CodeSystem.ConceptDefinitionComponent>();

//find descendants based on subsumedBy
if (properties.Any(p => p.Code == SUBSUMEDBYCODE))
{
//first find the parent itself (if it's in the CodeSystem)
if (concepts.FindCode(filter.Value) is { } concept)
result.Add(concept);

//Create a lookup which lists children by parent.
var flattened = concepts.Flatten();
var childrenLookup = CreateSubsumedByLookup(flattened);

//find descendants based on that lookup
var descendants = applySubsumedBy(childrenLookup, filter);
result.AddRange(descendants);
}
else
{
//SubsumedBy is not used, we should only check for a nested hierarchy, and include the code and it's descendants
if (concepts.FindCode(filter.Value) is { } concept)
ewoutkramer marked this conversation as resolved.
Show resolved Hide resolved
result.Add(concept);
}
return result;
}

private static ILookup<string, CodeSystem.ConceptDefinitionComponent> CreateSubsumedByLookup(List<CodeSystem.ConceptDefinitionComponent> flattenedConcepts)
{
return flattenedConcepts
.SelectMany(concept => concept.Property
.Where(p => p.Code == SUBSUMEDBYCODE && p.Value is Code && ((Code)p.Value).Value is not null)
.Select(p => new { SubsumedByValue = ((Code)p.Value).Value, Concept = concept }))
.ToLookup(x => x.SubsumedByValue, x => x.Concept);
}

private static List<CodeSystem.ConceptDefinitionComponent> applySubsumedBy(ILookup<string, CodeSystem.ConceptDefinitionComponent> lookup, ValueSet.FilterComponent filter)
{
var result = new List<CodeSystem.ConceptDefinitionComponent>();
var root = filter.Value;
if (root != null)
{
addDescendants(lookup, root, result);
}
return result;
}

//recursively loop through all the children to eventually find all descendants.
private static void addDescendants(ILookup<string, CodeSystem.ConceptDefinitionComponent> lookup, string parent, List<CodeSystem.ConceptDefinitionComponent> result)
{
if (lookup[parent] is { } children)
{
foreach (var child in children)
{
result.Add(child);
parent = child.Code;
addDescendants(lookup, parent, result);
}
}
}

}
}

#nullable restore
Loading