Last active
November 13, 2024 08:39
-
-
Save brianpos/80587755f0f432f411918801e4f75aa3 to your computer and use it in GitHub Desktop.
Demonstration implementation (WIP) for the definition based FHIR Questionniare data extraction
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Hl7.Fhir.Model; | |
using Hl7.Fhir.Specification; | |
using Hl7.Fhir.Specification.Navigation; | |
using Hl7.Fhir.Specification.Snapshot; | |
using Hl7.Fhir.Specification.Source; | |
using Hl7.Fhir.Utility; | |
using Hl7.Fhir.WebApi; | |
using Hl7.FhirPath; | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reflection; | |
using System.Threading.Tasks; | |
using Hl7.Fhir.FhirPath; | |
using Hl7.Fhir.ElementModel; | |
using Hl7.Fhir.Introspection; | |
namespace Hl7.Fhir.StructuredDataCapture | |
{ | |
public class QuestionnaireResponse_Extract_Definition | |
{ | |
public QuestionnaireResponse_Extract_Definition(IResourceResolver source) | |
{ | |
Source = source; | |
} | |
public IResourceResolver Source { get; private set; } | |
private static ModelInspector _inspector = ModelInfo.ModelInspector; | |
public async Task<IEnumerable<Bundle.EntryComponent>> Extract(Questionnaire q, QuestionnaireResponse qr, OperationOutcome outcome) | |
{ | |
// execute the Definition based extraction | |
try | |
{ | |
List<Bundle.EntryComponent> entries = new List<Bundle.EntryComponent>(); | |
VariableDictionary extractEnvironment = new VariableDictionary(); | |
extractEnvironment.Add("questionnaire", [q.ToTypedElement()]); | |
foreach (var allocateId in q.allocateId()) | |
{ | |
extractEnvironment.Add(allocateId, ElementNode.CreateList(Guid.NewGuid().ToFhirUrnUuid())); | |
} | |
var entryDetails = q.BundleEntryExtractDetails(); | |
var evalContext = qr.ToTypedElement(); | |
// Check at the root level if we have top level resources to create | |
foreach (var resourceProfile in q.itemExtractionContext().Union(entryDetails.Select(ed => ed.Definition)).Distinct().Where(resourceProfile => !string.IsNullOrEmpty(resourceProfile))) | |
{ | |
// this is a canonical URL for a profile | |
var resource = ExtractResourceAtRoot(q, qr, resourceProfile, extractEnvironment, outcome); | |
var entry = AddResourceToTransactionBundle(entries, resource); | |
var ed = entryDetails.FirstOrDefault(ed => ed.Definition == resourceProfile); | |
if (ed != null && entry != null) | |
{ | |
entry.FullUrl = EvaluateFhirPathAsString(evalContext, ed.fullUrl, extractEnvironment, outcome); | |
entry.Request.IfNoneMatch = EvaluateFhirPathAsString(evalContext, ed.ifNoneMatch, extractEnvironment, outcome); | |
entry.Request.IfMatch = EvaluateFhirPathAsString(evalContext, ed.ifMatch, extractEnvironment, outcome); | |
entry.Request.IfModifiedSinceElement = EvaluateFhirPathAsInstant(evalContext, ed.ifModifiedSince, extractEnvironment, outcome); | |
entry.Request.IfNoneExist = EvaluateFhirPathAsString(evalContext, ed.ifNoneExist, extractEnvironment, outcome); | |
} | |
} | |
// iterate all the items in the questionnaire | |
ExtractResourcesForItems(q, q, qr, q.Item, qr.Item, extractEnvironment, outcome, entries); | |
return entries.AsEnumerable(); | |
} | |
catch (Exception ex) | |
{ | |
System.Diagnostics.Trace.WriteLine(ex.Message); | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Definition based extraction error: {ex.Message}") | |
}); | |
return null; | |
} | |
} | |
private static Bundle.EntryComponent AddResourceToTransactionBundle(List<Bundle.EntryComponent> entries, Resource resource) | |
{ | |
Bundle.EntryComponent result = null; | |
if (resource != null) | |
{ | |
result = new Bundle.EntryComponent() | |
{ | |
Resource = resource, | |
Request = new Bundle.RequestComponent | |
{ | |
Method = Bundle.HTTPVerb.POST, | |
Url = resource.TypeName | |
} | |
}; | |
if (!String.IsNullOrEmpty(resource.Id)) | |
{ | |
result.Request.Method = Bundle.HTTPVerb.PUT; | |
result.Request.Url = $"{resource.TypeName}/{resource.Id}"; | |
} | |
entries.Add(result); | |
} | |
return result; | |
} | |
private void ExtractResourcesForItems(Base context, Questionnaire q, QuestionnaireResponse qr, List<Questionnaire.ItemComponent> qItems, List<QuestionnaireResponse.ItemComponent> qrItems, VariableDictionary extractEnvironment, OperationOutcome outcome, List<Bundle.EntryComponent> entries) | |
{ | |
foreach (var itemDef in qItems) | |
{ | |
var entryDetails = itemDef.BundleEntryExtractDetails(); | |
var itemExtractionContexts = itemDef.itemExtractionContext().Union(entryDetails.Select(ed => ed.Definition)).Distinct().Where(resourceProfile => !string.IsNullOrEmpty(resourceProfile)); | |
var childItems = qrItems.Where(i => i.LinkId == itemDef.LinkId); | |
foreach (var child in childItems) | |
{ | |
// find the corresponding items in the questionnaire response | |
if (itemExtractionContexts.Any()) | |
{ | |
foreach (var resourceProfile in itemExtractionContexts) | |
{ | |
var envForItem = extractEnvironment; | |
var allocIdsForItem = itemDef.allocateId(); | |
string fullUrl = null; | |
if (allocIdsForItem.Any()) | |
{ | |
envForItem = new VariableDictionary(extractEnvironment); | |
foreach (var allocateId in allocIdsForItem) | |
{ | |
if (!envForItem.ContainsKey(allocateId)) | |
envForItem.Add(allocateId, ElementNode.CreateList(Guid.NewGuid().ToFhirUrnUuid())); | |
else | |
{ | |
fullUrl = envForItem[allocateId].FirstOrDefault()?.Value?.ToString(); | |
} | |
} | |
} | |
// this is a canonical URL for a profile | |
var resource = ExtractResourceForItem(itemDef, q, qr, resourceProfile, itemDef, child, extractEnvironment, outcome); | |
var entry = AddResourceToTransactionBundle(entries, resource); | |
var ed = entryDetails.FirstOrDefault(ed => ed.Definition == resourceProfile); | |
if (ed != null) | |
{ | |
var evalContext = child.ToTypedElement(); | |
entry.FullUrl = EvaluateFhirPathAsString(evalContext, ed.fullUrl, envForItem, outcome); | |
entry.Request.IfNoneMatch = EvaluateFhirPathAsString(evalContext, ed.ifNoneMatch, envForItem, outcome); | |
entry.Request.IfMatch = EvaluateFhirPathAsString(evalContext, ed.ifMatch, envForItem, outcome); | |
entry.Request.IfModifiedSinceElement = EvaluateFhirPathAsInstant(evalContext, ed.ifModifiedSince, envForItem, outcome); | |
entry.Request.IfNoneExist = EvaluateFhirPathAsString(evalContext, ed.ifNoneExist, envForItem, outcome); | |
} | |
if (fullUrl != null) | |
entry.FullUrl = fullUrl; | |
} | |
} | |
// And nest into the children | |
ExtractResourcesForItems(itemDef, q, qr, itemDef.Item, child.Item, extractEnvironment, outcome, entries); | |
} | |
} | |
} | |
public static string EvaluateFhirPathAsString(ITypedElement element, string expression, VariableDictionary envForItem, OperationOutcome outcome) | |
{ | |
if (string.IsNullOrEmpty(expression)) | |
return null; | |
try | |
{ | |
FhirPathCompiler compiler = new(); | |
var expr = compiler.Compile(expression); | |
var result = expr.Scalar(element, new FhirEvaluationContext(element, envForItem)); | |
return result?.ToString(); | |
} | |
catch (Exception ex) | |
{ | |
// Most likely issue here is a type mismatch, or casting/precision issue | |
System.Diagnostics.Trace.WriteLine(ex.Message); | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Unable to evaluate {expression}: {ex.Message}") | |
}); | |
} | |
return null; | |
} | |
public static Instant EvaluateFhirPathAsInstant(ITypedElement element, string expression, VariableDictionary envForItem, OperationOutcome outcome) | |
{ | |
if (string.IsNullOrEmpty(expression)) | |
return null; | |
try | |
{ | |
FhirPathCompiler compiler = new(); | |
var expr = compiler.Compile(expression); | |
var result = expr(element, new FhirEvaluationContext(element, envForItem)).ToFhirValues().FirstOrDefault(); | |
if (result is Instant i) | |
return i; | |
if (result is FhirDateTime fdt) | |
return new Instant(fdt.ToDateTimeOffsetForFacade()); | |
} | |
catch (Exception ex) | |
{ | |
// Most likely issue here is a type mismatch, or casting/precision issue | |
System.Diagnostics.Trace.WriteLine(ex.Message); | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Unable to evaluate {expression}: {ex.Message}") | |
}); | |
} | |
return null; | |
} | |
private Resource ExtractResourceSkeleton(Questionnaire q, string resourceProfile, OperationOutcome outcome, out StructureDefinition sd, out StructureDefinitionWalker walker, out ClassMapping cm) | |
{ | |
sd = Source.ResolveByCanonicalUri(resourceProfile) as StructureDefinition; | |
if (sd == null) | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.NotFound, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Resource profile {resourceProfile} not found") | |
}); | |
sd = null; | |
walker = null; | |
cm = null; | |
return null; | |
} | |
if (sd.Kind != StructureDefinition.StructureDefinitionKind.Resource) | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.NotFound, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Resource profile {resourceProfile} is not a resource profile and cannot be used with $extract") | |
}); | |
sd = null; | |
walker = null; | |
cm = null; | |
return null; | |
} | |
if (!sd.HasSnapshot) | |
{ | |
// Generate the snapshot! | |
try | |
{ | |
SnapshotGenerator generator = new SnapshotGenerator(Source); | |
generator.Update(sd); | |
} | |
catch (Exception ex) | |
{ | |
throw new FhirServerException(System.Net.HttpStatusCode.BadRequest, $"Error Generating snapshot or {sd.Url}: " + ex.Message); | |
} | |
if (!sd.HasSnapshot) | |
{ | |
// We weren't able to generate the snapshot | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.NotFound, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Unable to generate the snapshot for {sd.Url}") | |
}); | |
sd = null; | |
walker = null; | |
cm = null; | |
return null; | |
} | |
} | |
walker = new StructureDefinitionWalker(sd, Source); | |
cm = _inspector.FindClassMapping(sd.Type); | |
Resource result = Activator.CreateInstance(cm.NativeType) as Resource; | |
var extractedResource = new ExtractedValue(walker, cm, result); | |
// add in the meta of the profile that we created from (provided it's more than just a core resource profile) | |
if (resourceProfile != ModelInfo.CanonicalUriForFhirCoreType(result.TypeName)) | |
{ | |
if (result.Meta == null) result.Meta = new(); | |
if (!result.Meta.Profile.Contains(resourceProfile)) | |
result.Meta.Profile = result.Meta.Profile.Union([resourceProfile]); | |
} | |
// set any fixed/pattern values at the root | |
SetMandatoryFixedAndPatternProperties(extractedResource, outcome); | |
return result; | |
} | |
private Resource ExtractResourceAtRoot(Questionnaire q, QuestionnaireResponse qr, string resourceProfile, VariableDictionary extractEnvironment, OperationOutcome outcome) | |
{ | |
Resource result = ExtractResourceSkeleton(q, resourceProfile, outcome, out var sd, out var walker, out var cm); | |
if (result == null) | |
return null; | |
var extractedResource = new ExtractedValue(walker, cm, result); | |
// Check for any definition fixed/dynamic values (at root of resource) | |
foreach (var definitionValue in q.DefinitionExtracts()) | |
{ | |
ExtractDynamicAndFixedDefinitionValues(q, qr, sd, extractedResource, extractEnvironment, outcome, null, null, definitionValue); | |
} | |
foreach (var responseItem in qr.Item) | |
{ | |
var itemDef = q.Item.FirstOrDefault(i => i.LinkId == responseItem.LinkId); | |
ExtractProperties(q, qr, itemDef, responseItem, sd, walker, extractedResource, extractEnvironment, outcome); | |
} | |
return result; | |
} | |
private Resource ExtractResourceForItem(Questionnaire.ItemComponent itemContext, Questionnaire q, QuestionnaireResponse qr, string resourceProfile, Questionnaire.ItemComponent itemDef, QuestionnaireResponse.ItemComponent responseItem, VariableDictionary extractEnvironment, OperationOutcome outcome) | |
{ | |
Resource result = ExtractResourceSkeleton(q, resourceProfile, outcome, out var sd, out var walker, out var cm); | |
if (result == null) | |
return null; | |
var extractedResource = new ExtractedValue(walker, cm, result); | |
// Check for any definition fixed/dynamic values (at root of resource) | |
foreach (var definitionValue in itemContext.DefinitionExtracts()) | |
{ | |
ExtractDynamicAndFixedDefinitionValues(q, qr, sd, extractedResource, extractEnvironment, outcome, itemContext, null, definitionValue); | |
} | |
ExtractProperties(q, qr, itemDef, responseItem, sd, walker, extractedResource, extractEnvironment, outcome); | |
return result; | |
} | |
record ExtractedValue | |
{ | |
public ExtractedValue(StructureDefinitionWalker walker, ExtractedValue parent, ClassMapping cm, Base value) | |
{ | |
if (cm == null) throw new ArgumentNullException(nameof(cm)); | |
this.walker = walker; | |
this.Parent = parent; | |
this.Path = parent == null ? $"{walker.Current.PathName}" : $"{parent.Path}.{walker.Current.PathName}"; | |
this.cm = cm; | |
this.Value = value; | |
} | |
public ExtractedValue(StructureDefinitionWalker walker, ClassMapping cm, Base value) | |
{ | |
if (cm == null) throw new ArgumentNullException(nameof(cm)); | |
this.walker = walker; | |
this.Path = walker.Current.PathName; | |
this.cm = cm; | |
this.Value = value; | |
} | |
public StructureDefinitionWalker walker; | |
public ExtractedValue Parent; | |
public string Path; | |
public ClassMapping cm; | |
public Base Value; | |
} | |
private void ExtractProperties(Questionnaire q, QuestionnaireResponse qr, Questionnaire.ItemComponent itemDef, QuestionnaireResponse.ItemComponent respItem, StructureDefinition sd, StructureDefinitionWalker walker, ExtractedValue extractedValue, VariableDictionary extractEnvironment, OperationOutcome outcome) | |
{ | |
if (itemDef.Definition?.StartsWith(sd.Url + "#") == true) | |
{ | |
var propertyPath = itemDef.Definition.Substring(sd.Url.Length + 1); | |
if (!string.IsNullOrEmpty(walker.Current.Current.SliceName)) | |
{ | |
var prefix = $"{walker.Current.Path}:{walker.Current.Current.SliceName}"; | |
if (propertyPath.StartsWith(prefix)) | |
propertyPath = propertyPath.Substring(prefix.Length + 1); | |
} | |
if (propertyPath.StartsWith(walker.Current.Path) && propertyPath != walker.Current.Path) | |
propertyPath = propertyPath.Substring(walker.Current.Path.Length + 1); | |
if (propertyPath.StartsWith(extractedValue.Path) && propertyPath != extractedValue.Path) | |
propertyPath = propertyPath.Substring(extractedValue.Path.Length + 1); | |
foreach (var answer in respItem.Answer) | |
{ | |
var extractedAnswerValue = SetValueAtPropertyPath(extractedValue, outcome, itemDef.LinkId, propertyPath, answer.Value, itemDef.Definition, itemDef); | |
ExtractDynamicAndFixedDefinitionValues(q, qr, sd, extractedAnswerValue, extractEnvironment, outcome, itemDef, respItem); | |
} | |
// handle group level answers | |
if (itemDef.Type == Questionnaire.QuestionnaireItemType.Group) | |
{ | |
// SetValueAtPropertyPath(cm, result, outcome, itemDef, propertyPath, answer.Value); | |
var props = propertyPath.Split('.'); | |
if (propertyPath == walker.Current.Path) | |
props = []; | |
var itemWalker = walker; | |
var propCm = extractedValue.cm; | |
Base val = extractedValue.Value; | |
var extractedGroupItemValue = extractedValue; | |
// walk down the children listed in the props to the final node | |
while (props.Any()) | |
{ | |
// move to the child item | |
var propName = props.First(); | |
var cd = ChildDefinitions(itemWalker, propName).ToArray(); | |
if (!cd.Any()) | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Property `{propName}` does not exist on `{itemWalker.Current.Path}` while extracting for linkId: {itemDef.LinkId} Definition: {itemDef.Definition}") | |
}); | |
break; | |
} | |
itemWalker = new StructureDefinitionWalker(cd.First(), Source); // itemWalker.Child(props.First()); | |
// create the new property value | |
var pm = extractedValue.cm.PropertyMappings.FirstOrDefault(pm => pm.Name == itemWalker.Current.PathName); | |
// Do we need to do any casting here? | |
Base propValue = Activator.CreateInstance(pm.PropertyTypeMapping.NativeType) as Base; | |
extractedGroupItemValue = new ExtractedValue(itemWalker, extractedGroupItemValue, pm.PropertyTypeMapping, propValue); | |
ExtractDynamicAndFixedDefinitionValues(q, qr, sd, extractedGroupItemValue, extractEnvironment, outcome, itemDef, respItem); | |
// Set the value | |
SetValue(val, pm, propValue, outcome); | |
if (itemWalker.Current.Current.IsChoice()) | |
{ | |
var localWalker = itemWalker.Walk($"ofType({propValue.TypeName})").FirstOrDefault(); | |
if (localWalker == null) | |
{ | |
// report the issue | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Warning, | |
Details = new CodeableConcept(null, null, $"Unable to step into type `{propValue.TypeName}` for property: {itemWalker.Current.CanonicalPath()}") | |
}); | |
break; | |
} | |
itemWalker = localWalker; | |
} | |
SetMandatoryFixedAndPatternProperties(extractedGroupItemValue, outcome); | |
// move to the next item | |
val = propValue; | |
propCm = pm.PropertyTypeMapping; | |
props = props.Skip(1).ToArray(); | |
} | |
// And set the values into the item | |
if (itemDef.Item.Any() && respItem.Item.Any()) | |
{ | |
foreach (var childItem in respItem.Item) | |
{ | |
var childItemDef = itemDef.Item.FirstOrDefault(i => i.LinkId == childItem.LinkId); | |
ExtractProperties(q, qr, childItemDef, childItem, sd, itemWalker, extractedGroupItemValue, extractEnvironment, outcome); | |
} | |
} | |
} | |
} | |
else | |
{ | |
// walk into children | |
if (itemDef.Item.Any() && respItem.Item.Any()) | |
{ | |
foreach (var childItem in respItem.Item) | |
{ | |
var childItemDef = itemDef.Item.FirstOrDefault(i => i.LinkId == childItem.LinkId); | |
ExtractProperties(q, qr, childItemDef, childItem, sd, walker, extractedValue, extractEnvironment, outcome); | |
} | |
} | |
} | |
} | |
private void ExtractDynamicAndFixedDefinitionValues(Questionnaire q, QuestionnaireResponse qr, StructureDefinition sd, ExtractedValue extractedValue, VariableDictionary extractEnvironment, OperationOutcome outcome, Questionnaire.ItemComponent itemDef, QuestionnaireResponse.ItemComponent respItem) | |
{ | |
// Check for any definition fixed/dynamic values | |
foreach (var definitionValue in itemDef.DefinitionExtracts()) | |
{ | |
ExtractDynamicAndFixedDefinitionValues(q, qr, sd, extractedValue, extractEnvironment, outcome, itemDef, respItem, definitionValue); | |
} | |
} | |
private void ExtractDynamicAndFixedDefinitionValues(Questionnaire q, QuestionnaireResponse qr, StructureDefinition sd, ExtractedValue extractedValue, VariableDictionary extractEnvironment, OperationOutcome outcome, Questionnaire.ItemComponent itemDef, QuestionnaireResponse.ItemComponent respItem, DefinitionExtract definitionValue) | |
{ | |
var propertyPathDefinedValues = definitionValue.Definition.Substring(sd.Url.Length + 1); | |
// locate the common ancestor | |
var commonAncestor = extractedValue; | |
while (commonAncestor.Parent != null && !propertyPathDefinedValues.StartsWith(commonAncestor.Path)) | |
{ | |
commonAncestor = commonAncestor.Parent; | |
} | |
var fvWalker = new StructureDefinitionWalker(commonAncestor.walker); | |
if (!string.IsNullOrEmpty(fvWalker.Current.Current.SliceName)) | |
{ | |
var prefix = $"{fvWalker.Current.Path}:{fvWalker.Current.Current.SliceName}"; | |
if (propertyPathDefinedValues.StartsWith(prefix)) | |
propertyPathDefinedValues = propertyPathDefinedValues.Substring(prefix.Length + 1); | |
} | |
if (propertyPathDefinedValues.StartsWith(fvWalker.Current.Path) && propertyPathDefinedValues != fvWalker.Current.Path) | |
propertyPathDefinedValues = propertyPathDefinedValues.Substring(fvWalker.Current.Path.Length + 1); | |
if (propertyPathDefinedValues.StartsWith(commonAncestor.Path)) | |
propertyPathDefinedValues = propertyPathDefinedValues.Substring(commonAncestor.Path.Length + 1); | |
if (definitionValue.Expression != null) | |
{ | |
// Process the expression | |
if (definitionValue.Expression.Language == "text/fhirpath") | |
{ | |
// Validate this fhirpath expression | |
FhirPathCompiler fpc = new FhirPathCompiler(); | |
try | |
{ | |
var cexpr = fpc.Compile(definitionValue.Expression.Expression_); | |
// set environment variables | |
var env = new VariableDictionary(extractEnvironment); | |
if (itemDef != null) | |
env.Add("qitem", [itemDef.ToTypedElement()]); | |
var qrTypedElement = qr.ToTypedElement(); | |
var itemContextTypedElement = respItem != null ? respItem.ToTypedElement() : qrTypedElement; | |
FhirEvaluationContext ctx = new FhirEvaluationContext(qrTypedElement, env); | |
var exprValues = cexpr(itemContextTypedElement, ctx).ToFhirValues(); | |
foreach (var value in exprValues) | |
{ | |
if (value != null) | |
SetValueAtPropertyPath(commonAncestor, outcome, itemDef?.LinkId, propertyPathDefinedValues, value, definitionValue.Definition, itemDef); | |
} | |
} | |
catch (Exception ex) | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Error evaluating fhirpath expression: {ex.Message}"), | |
Diagnostics = definitionValue.Expression.Expression_ | |
}); | |
} | |
} | |
else | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.NotSupported, | |
Severity = OperationOutcome.IssueSeverity.Warning, | |
Details = new CodeableConcept(null, null, $"Only the fhirpath language is supported for extraction ({definitionValue.Expression.Language} is unsupported)") | |
}); | |
} | |
} | |
else | |
{ | |
var value = definitionValue.FixedValue; | |
if (value != null) | |
SetValueAtPropertyPath(commonAncestor, outcome, itemDef?.LinkId, propertyPathDefinedValues, value, definitionValue.Definition, itemDef); | |
} | |
} | |
static private Dictionary<string, string> FhirToFhirPathDataTypeMappings = new Dictionary<string, string>(){ | |
{ "boolean", "http://hl7.org/fhirpath/System.Boolean" }, | |
{ "uri", "string" }, | |
{ "code", "string" }, | |
{ "oid", "string" }, | |
{ "id", "string" }, | |
{ "uuid", "string" }, | |
{ "markdown", "string" }, | |
{ "base64Binary", "string" }, | |
{ "unsignedInt", "integer" }, | |
{ "positiveInt", "integer" }, | |
{ "integer64", "http://hl7.org/fhirpath/System.Long" }, | |
{ "date", "dateTime" }, | |
{ "dateTime", "dateTime" }, | |
}; | |
private ExtractedValue SetValueAtPropertyPath(ExtractedValue extractedValue, OperationOutcome outcome, string linkId, string propertyPath, Base value, string definition, Questionnaire.ItemComponent itemDef) | |
{ | |
var props = propertyPath.Split('.'); | |
if (propertyPath == extractedValue.walker.Current.Path) | |
props = []; | |
var itemWalker = extractedValue.walker; | |
var propCm = extractedValue.cm; | |
Base val = extractedValue.Value; | |
// start with the value passed in | |
// ExtractedValue extractedValue = new ExtractedValue() { cm = cm, Path = propertyPath, Value = result }; | |
string messagePrefix = linkId != null ? $"linkId: {linkId}" : String.Empty; | |
// walk down the children listed in the props to the final node | |
while (props.Any()) | |
{ | |
// move to the child item | |
var propName = props.First(); | |
props = props.Skip(1).ToArray(); | |
var cd = ChildDefinitions(itemWalker, propName); | |
if (!cd.Any()) | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Property `{propName}` does not exist on `{itemWalker.Current.Path}` while extracting {messagePrefix} Definition: {definition}") | |
}); | |
break; | |
} | |
itemWalker = new StructureDefinitionWalker(cd.First(), Source); // itemWalker.Child(props.First()); | |
string sliceName = null; | |
if (propName.Contains(':')) | |
{ | |
// Remove slice from name | |
sliceName = propName.Substring(propName.IndexOf(':') + 1); | |
propName = propName.Substring(0, propName.IndexOf(':')).Replace("[x]", ""); | |
if (itemWalker.Current.Current.SliceName == null && sliceName.StartsWith(propName)) | |
{ | |
var typeName = sliceName.Substring(propName.Length); | |
var typeDef = itemWalker.Current.Current.Type.FirstOrDefault(t => t.Code.Equals(typeName, StringComparison.OrdinalIgnoreCase)); | |
itemWalker = itemWalker.Walk($"ofType({typeDef.Code})").First(); | |
} | |
} | |
else if (itemWalker.Current.Current.IsChoice() && itemWalker.Current.PathName.Replace("[x]", "") != propName.Replace("[x]", "")) | |
{ | |
// Walk into the implicit type slice | |
if (itemWalker.Current.Current.SliceName == null) | |
{ | |
var implicitType = propName.Substring(itemWalker.Current.PathName.Length - 3); | |
sliceName = propName; | |
propName = itemWalker.Current.PathName.Replace("[x]", ""); | |
string typeName = itemWalker.Current.Current.Type.FirstOrDefault(t => String.Equals(t.Code, implicitType, StringComparison.OrdinalIgnoreCase))?.Code; | |
if (typeName != null) | |
itemWalker = itemWalker.Walk($"ofType({typeName})").First(); | |
} | |
} | |
// create the new property value | |
var pm = propCm.PropertyMappings.FirstOrDefault(pm => pm.Name == propName || pm.Name == propName.Replace("[x]", "")); | |
propCm = pm.PropertyTypeMapping; | |
Base propValue; | |
if (props.Length == 0) | |
{ | |
// Do we need to do any casting here? | |
propValue = GetPropertyValueCastingIfNeeded(outcome, value, definition, itemDef, val, messagePrefix, propName, pm); | |
} | |
else | |
{ | |
var existingValue = pm.GetValue(val); | |
if (existingValue != null && existingValue is Base) //existingValue?.GetType() == pm.PropertyTypeMapping.NativeType) | |
{ | |
propValue = existingValue as Base; | |
if (existingValue.GetType() != pm.PropertyTypeMapping.NativeType) | |
{ | |
// This is a choice type, so we can grab the class mapping for this specific type | |
propCm = _inspector.FindClassMapping(propValue.TypeName); | |
} | |
} | |
else | |
{ | |
if (pm.Name != propName) | |
{ | |
// This is choice type, so locate the type from the attributes | |
var typeName = propName.Substring(pm.Name.Length); | |
// use the itemWalker to get the types | |
var typeDef = itemWalker.Current.Current.Type.FirstOrDefault(t => t.Code.Equals(typeName, StringComparison.OrdinalIgnoreCase)); | |
if (itemWalker.Current.Current.IsChoice() && typeDef != null) | |
{ | |
itemWalker = itemWalker.Walk($"ofType({typeDef.Code})").FirstOrDefault(); | |
propCm = _inspector.FindClassMapping(typeDef.Code); | |
var propType = Hl7.Fhir.Model.ModelInfo.ModelInspector.GetTypeForFhirType(typeDef.Code); | |
if (existingValue?.GetType() == propType) | |
propValue = existingValue as Base; | |
else | |
propValue = Activator.CreateInstance(propType) as Base; | |
} | |
else | |
{ | |
// in the [AllowedTypes(typeof(Hl7.Fhir.Model.Quantity),typeof(Hl7.Fhir.Model.CodeableConcept),typeof(Hl7.Fhir.Model.FhirString),typeof(Hl7.Fhir.Model.FhirBoolean),typeof(Hl7.Fhir.Model.Integer),typeof(Hl7.Fhir.Model.Range),typeof(Hl7.Fhir.Model.Ratio),typeof(Hl7.Fhir.Model.SampledData),typeof(Hl7.Fhir.Model.Time),typeof(Hl7.Fhir.Model.FhirDateTime),typeof(Hl7.Fhir.Model.Period))] | |
var typeProperty = pm.NativeProperty.GetCustomAttribute<Hl7.Fhir.Validation.AllowedTypesAttribute>()?.Types.FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)); | |
if (typeProperty != null) | |
{ | |
propValue = Activator.CreateInstance(typeProperty) as Base; | |
itemWalker = itemWalker.Walk($"ofType({propValue.TypeName})").FirstOrDefault(); | |
propCm = _inspector.FindClassMapping(propValue.TypeName); | |
} | |
else | |
{ | |
// cannot create an instance of ... | |
propValue = null; | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Warning, | |
Details = new CodeableConcept(null, null, $"Unable to create instance of {pm.Name} while extracting {messagePrefix} Definition: {definition}") | |
}); | |
} | |
} | |
} | |
else | |
{ | |
if (sliceName != null) | |
{ | |
propCm = _inspector.FindClassMapping(itemWalker.Current.PathName) ?? pm.PropertyTypeMapping; | |
if (propCm == null) | |
propCm = pm.PropertyTypeMapping; | |
propValue = Activator.CreateInstance(propCm.NativeType) as Base; | |
} | |
else | |
propValue = Activator.CreateInstance(pm.PropertyTypeMapping.NativeType) as Base; | |
} | |
} | |
} | |
extractedValue = new ExtractedValue(itemWalker, extractedValue, propCm, propValue); | |
if (propValue != null) | |
{ | |
if (itemWalker.Current.Current.IsChoice()) | |
{ | |
var localWalker = itemWalker.Walk($"ofType({propValue.TypeName})").FirstOrDefault(); | |
if (localWalker == null) | |
{ | |
// report the issue | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Warning, | |
Details = new CodeableConcept(null, null, $"Unable to step into type `{propValue.TypeName}` for property: {itemWalker.Current.CanonicalPath()}") | |
}); | |
break; | |
} | |
itemWalker = localWalker; | |
// re-set the walker on the extracted value (this isn't another parent level) | |
extractedValue = new ExtractedValue(itemWalker, extractedValue.Parent, propCm, propValue); | |
} | |
SetMandatoryFixedAndPatternProperties(extractedValue, outcome); | |
} | |
// Set the value | |
SetValue(val, pm, propValue, outcome); | |
// move to the next item | |
val = propValue; | |
} | |
return extractedValue; | |
} | |
internal static Base GetPropertyValueCastingIfNeeded(OperationOutcome outcome, Base value, string definition, Questionnaire.ItemComponent itemDef, Base val, string messagePrefix, string propName, PropertyMapping pm) | |
{ | |
Base propValue; | |
if (value != null && value.GetType() != pm.PropertyTypeMapping.NativeType) | |
{ | |
if (pm.PropertyTypeMapping.NativeType.IsAbstract) | |
{ | |
// Check if the type is one of the supported types | |
if (pm.FhirType.Contains(value.GetType())) | |
propValue = value; | |
else | |
{ | |
// need to cast it? | |
var pt = value as PrimitiveType; | |
if (pt == null && pm.DeclaringClass.Name != "Extension") | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Informational, | |
Severity = OperationOutcome.IssueSeverity.Information, | |
Details = new CodeableConcept(null, null, $"Casting required for {value.GetType().Name} to {pm.PropertyTypeMapping.NativeType.Name} while extracting {messagePrefix} Definition: {definition}") | |
}); | |
} | |
// Just use the value we have (and hope for the best) | |
var existingValue = pm.GetValue(val) as Base; | |
if (existingValue == null) | |
propValue = value; | |
else if (pt.TypeName == existingValue?.TypeName && existingValue is PrimitiveType ePT) | |
{ | |
ePT.ObjectValue = pt.ObjectValue; | |
propValue = ePT; | |
} | |
else | |
{ | |
// copy the value in? | |
propValue = value; | |
} | |
} | |
} | |
else | |
{ | |
propValue = Activator.CreateInstance(pm.PropertyTypeMapping.NativeType) as Base; | |
if (propValue is PrimitiveType pt && value is PrimitiveType ptA) | |
{ | |
if (pt is Instant ptI && ptA is FhirDateTime ptD) | |
{ | |
ptI.Value = ptD.ToDateTimeOffsetForFacade(); | |
} | |
else if (FhirToFhirPathDataTypeMappings.ContainsKey(pt.TypeName) && FhirToFhirPathDataTypeMappings[pt.TypeName] == ptA.TypeName) | |
{ | |
pt.ObjectValue = ptA.ObjectValue; | |
if (pt is Date fdate && fdate.Value?.Length > 10) | |
{ | |
// Truncate the date if it was too long (might happens when allocating from a datetime) | |
fdate.Value = fdate.Value.Substring(0, 10); | |
} | |
} | |
else if (pt.TypeName != ptA.TypeName) | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Informational, | |
Severity = OperationOutcome.IssueSeverity.Information, | |
Details = new CodeableConcept(null, null, $"Invalid item type {value.TypeName} to populate into {propValue.TypeName} for {value.GetType().Name} to {pm.PropertyTypeMapping.NativeType.Name} while extracting {messagePrefix} Definition: {definition}") | |
}); | |
} | |
else | |
pt.ObjectValue = ptA.ObjectValue; | |
} | |
else if (propValue is PrimitiveType ptV && value is Coding codingA) | |
ptV.ObjectValue = codingA.Code; | |
else if (pm.PropertyTypeMapping.NativeType == typeof(Hl7.Fhir.ElementModel.Types.String) && value is FhirString fs) | |
propValue = fs; | |
else if (propValue is FhirDecimal fd && value is Quantity q) | |
{ | |
fd.Value = q.Value; | |
if (itemDef?.Type == Questionnaire.QuestionnaireItemType.Quantity) | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Informational, | |
Severity = OperationOutcome.IssueSeverity.Information, | |
Details = new CodeableConcept(null, null, $"Used `Quantity.value` to populate a {propValue.TypeName} property `{propName}` while extracting {messagePrefix}, consider removing the `.value` so that the other quantity properties are extracted, or change the item type to `decimal`. Definition: {definition}") | |
}); | |
} | |
else | |
{ | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Value, | |
Severity = OperationOutcome.IssueSeverity.Warning, | |
Details = new CodeableConcept(null, null, $"Casting required for {value.GetType().Name} to {pm.PropertyTypeMapping.NativeType.Name} while extracting {messagePrefix} Definition: {definition}") | |
}); | |
} | |
} | |
} | |
else | |
{ | |
propValue = value; | |
} | |
return propValue; | |
} | |
private void SetMandatoryFixedAndPatternProperties(ExtractedValue extractedValue, OperationOutcome outcome) | |
{ | |
if (!extractedValue.walker.Current.HasChildren) | |
{ | |
// Only Nothing to do if there are no child properties | |
// return; | |
} | |
var localWalker = extractedValue.walker; | |
if (extractedValue.walker.Current.Current.IsChoice()) | |
{ | |
localWalker = extractedValue.walker.Walk($"ofType({extractedValue.Value.TypeName})").FirstOrDefault(); | |
if (localWalker == null) | |
{ | |
// report the issue | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Warning, | |
Details = new CodeableConcept(null, null, $"Unable to step into type `{extractedValue.Value.TypeName}` for property: {extractedValue.walker.Current.CanonicalPath()}") | |
}); | |
return; | |
} | |
} | |
foreach (var pm in extractedValue.cm.PropertyMappings) | |
{ | |
var cd = ChildDefinitions(localWalker, pm.Name); | |
foreach (var elementOrSlice in cd) | |
{ | |
var child = new StructureDefinitionWalker(elementOrSlice, Source); | |
if (child != null) // && child.Current.Current.Min > 0) | |
{ | |
if (child.Current.Current.Fixed != null) | |
{ | |
// fill in this property | |
SetValue(extractedValue.Value, pm, child.Current.Current.Fixed, outcome); | |
} | |
if (child.Current.Current.Pattern != null) | |
{ | |
// fill in this property | |
SetValue(extractedValue.Value, pm, child.Current.Current.Pattern, outcome); | |
} | |
} | |
} | |
} | |
} | |
internal static void SetValue(Base context, Introspection.PropertyMapping pm, Base value, OperationOutcome outcome) | |
{ | |
if (pm.IsCollection) | |
{ | |
var list = pm.GetValue(context) as IList; | |
list.Add(value); | |
} | |
else | |
{ | |
try | |
{ | |
if (pm.ImplementingType.Name == "Code`1" && value is PrimitiveType ptCode) | |
{ | |
PrimitiveType newValue = Activator.CreateInstance(pm.ImplementingType) as PrimitiveType; | |
newValue.ObjectValue = ptCode.ObjectValue; | |
pm.SetValue(context, newValue); | |
} | |
else if (pm.ImplementingType == typeof(string) && value is IValue<string> pt) | |
{ | |
pm.SetValue(context, pt.Value); | |
} | |
else | |
{ | |
pm.SetValue(context, value); | |
} | |
} | |
catch (Exception) | |
{ | |
System.Diagnostics.Trace.WriteLine($"Error setting {pm.Name} with {value}"); | |
outcome.Issue.Add(new OperationOutcome.IssueComponent() | |
{ | |
Code = OperationOutcome.IssueType.Exception, | |
Severity = OperationOutcome.IssueSeverity.Error, | |
Details = new CodeableConcept(null, null, $"Error setting {pm.Name} with {value}, casting may be required") | |
}); | |
} | |
} | |
} | |
private IEnumerable<ElementDefinitionNavigator> ChildDefinitions(StructureDefinitionWalker walker, string? childName = null) | |
{ | |
string sliceName = null; | |
if (childName.Contains(':')) | |
{ | |
var parts = childName.Split(':'); | |
sliceName = parts[1]; | |
childName = parts[0]; | |
} | |
var canonicals = walker.Current.Current.Type.Select(t => t.GetTypeProfile()).Distinct().ToArray(); | |
if (canonicals.Length > 1) | |
throw new StructureDefinitionWalkerException($"Cannot determine which child to select, since there are multiple paths leading from here ('{walker.Current.CanonicalPath()}'), use 'ofType()' to disambiguate"); | |
// Since the element ID, resource ID and extension URL use fhirpath primitives, we should not walk into those | |
// e.g. type http://hl7.org/fhir/StructureDefinition/http://hl7.org/fhirpath/System.String is returned by t.GetTypeProfile() | |
if (canonicals.Length == 1 && canonicals[0].Contains("http://hl7.org/fhirpath")) | |
yield break; | |
// Take First(), since we have determined above that there's just one distinct result to expect. | |
// (this will be the case when Type=R | |
var expanded = walker.Expand().Single(); | |
var nav = expanded.Current.ShallowCopy(); | |
if (!nav.MoveToFirstChild()) yield break; | |
do | |
{ | |
if (nav.Current.IsPrimitiveValueConstraint()) continue; // ignore value attribute | |
if (childName != null && nav.Current.MatchesName(childName)) | |
{ | |
if (sliceName == null || sliceName != null && nav.Current.SliceName == sliceName) | |
yield return nav.ShallowCopy(); | |
} | |
// Also check the name as a type constraint e.g. valueQuantity | |
if (nav.Current.IsChoice()) | |
{ | |
string namePart = GetNameFromPath(nav.Current.Path); | |
foreach (var type in nav.Current.Type) | |
{ | |
if (childName.Equals(namePart.Replace("[x]", type.Code), StringComparison.OrdinalIgnoreCase)) | |
{ | |
if (sliceName == null || sliceName != null && nav.Current.SliceName == sliceName) | |
yield return nav.ShallowCopy(); | |
} | |
// special case for this Questionnaire item definition based validation routine | |
if (childName == namePart && sliceName.Equals(namePart.Replace("[x]", type.Code), StringComparison.OrdinalIgnoreCase)) | |
{ | |
yield return nav.ShallowCopy(); | |
} | |
} | |
} | |
} | |
while (nav.MoveToNext()); | |
} | |
/// <summary> | |
/// Returns the last part of the element's path. | |
/// </summary> | |
private static string GetNameFromPath(string path) | |
{ | |
var pos = path.LastIndexOf("."); | |
return pos != -1 ? path.Substring(pos + 1) : path; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment