Skip to content

Instantly share code, notes, and snippets.

@brianpos
Last active November 13, 2024 08:39
Show Gist options
  • Save brianpos/80587755f0f432f411918801e4f75aa3 to your computer and use it in GitHub Desktop.
Save brianpos/80587755f0f432f411918801e4f75aa3 to your computer and use it in GitHub Desktop.
Demonstration implementation (WIP) for the definition based FHIR Questionniare data extraction
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