Skip to content

Instantly share code, notes, and snippets.

@brianpos
Last active June 7, 2022 18:30
Show Gist options
  • Save brianpos/51ab10627f456385129ebee452ec1ff4 to your computer and use it in GitHub Desktop.
Save brianpos/51ab10627f456385129ebee452ec1ff4 to your computer and use it in GitHub Desktop.
Sample function for a custom operation to evaluate the results of a fhirpath operation for testing
if (operation == "fhirpath")
{
Resource resource = operationParameters.GetResource("resource");
string resourceId = operationParameters.GetString("resource");
string terminologyServerUrl = operationParameters.GetString("terminologyserver");
if (resource == null && !string.IsNullOrEmpty(resourceId))
{
// load the resource from another server
ResourceIdentity ri = new ResourceIdentity(resourceId);
if (!string.IsNullOrEmpty(ri.BaseUri?.OriginalString))
{
try
{
var remoteServer = new FhirClient(ri.BaseUri, new FhirClientSettings() { VerifyFhirVersion = false });
resource = remoteServer.Get(ri);
}
catch (FhirOperationException fex)
{
OperationOutcome outcome = new OperationOutcome();
outcome.SetAnnotation(HttpStatusCode.BadRequest);
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.NotFound,
Details = new CodeableConcept() { Text = $"Unable to retrieve resource {resourceId}" },
Diagnostics = resourceId
});
outcome.SetAnnotation(new AnnotationSourceResource() { ValidatingResource = operationParameters });
return Task<Resource>.FromResult(outcome as Resource);
}
}
}
return Task<Resource>.FromResult(EvaluateFhirPathTesterExpression(resourceId, resource, operationParameters.GetString("context"), operationParameters.GetString("expression"), terminologyServerUrl) as Resource);
}
const string exturlJsonValue = "http://fhir.forms-lab.com/StructureDefinition/json-value";
public static Resource EvaluateFhirPathTesterExpression(string resourceId, Resource resource, string context, string expression, string terminologyServerUrl)
{
Hl7.Fhir.FhirPath.ElementNavFhirExtensions.PrepareFhirSymbolTableFunctions();
var result = new Parameters() { Id = "fhirpath" };
var configParameters = new Parameters.ParameterComponent() { Name = "parameters" };
result.Parameter.Add(configParameters);
if (!string.IsNullOrEmpty(context))
configParameters.Part.Add(new Parameters.ParameterComponent() { Name = "context", Value = new FhirString(context) });
configParameters.Part.Add(new Parameters.ParameterComponent() { Name = "expression", Value = new FhirString(expression) });
if (!string.IsNullOrEmpty(resourceId))
configParameters.Part.Add(new Parameters.ParameterComponent() { Name = "resource", Value = new FhirString(resourceId) });
else if (resource != null)
configParameters.Part.Add(new Parameters.ParameterComponent() { Name = "resource", Resource = resource });
if (!string.IsNullOrEmpty(terminologyServerUrl))
configParameters.Part.Add(new Parameters.ParameterComponent() { Name = "terminologyServerUrl", Value = new FhirString(terminologyServerUrl) });
// op outcome just in case we get really bad issues
OperationOutcome outcome = new OperationOutcome();
outcome.SetAnnotation(HttpStatusCode.BadRequest);
outcome.SetAnnotation(new AnnotationSourceResource() { ValidatingResource = result });
ScopedNode inputNav;
FhirEvaluationContext evalContext;
if (resource != null)
{
// result.Parameter.Add(new Parameters.ParameterComponent() { Name = "input", Resource = resource });
inputNav = new ScopedNode(TypedSerialization.ToTypedElement(resource));
evalContext = new FhirEvaluationContext(inputNav);
}
else
{
inputNav = null;
evalContext = new FhirEvaluationContext();
}
SymbolTable symbolTable = new SymbolTable(FhirPathCompiler.DefaultSymbolTable);
var te = new FhirPathTerminologies() { TerminologyServerUrl = terminologyServerUrl ?? "https://sqlonfhir-r4.azurewebsites.net/fhir" };
symbolTable.AddVar("terminologies", te);
symbolTable.Add("expand", (FhirPathTerminologies e, string can, string p) =>
{
var result = te.Expand(can, p);
if (result != null)
return TypedSerialization.ToTypedElement(result);
return null;
});
symbolTable.Add("expand", (FhirPathTerminologies e, string can) =>
{
var result = te.Expand(can, "");
if (result != null)
return TypedSerialization.ToTypedElement(result);
return null;
});
symbolTable.Add("lookup", (ITypedElement a, ITypedElement b, ITypedElement c) => te.Lookup(a, b, c));
symbolTable.Add("lookup", (ITypedElement a, ITypedElement b) => te.Lookup(a, b));
symbolTable.Add("lookup", (ITypedElement a) => te.Lookup(a));
// Register the tracer in the eval Context
List<KeyValuePair<string, IEnumerable<ITypedElement>>> traceList = new List<KeyValuePair<string, IEnumerable<ITypedElement>>>();
evalContext.Tracer = (name, values) =>
{
traceList.Add(new KeyValuePair<string, IEnumerable<ITypedElement>>(name, values));
};
Dictionary<string, ITypedElement> resolvedItems = new Dictionary<string, ITypedElement>();
evalContext.ElementResolver = (referenceValue) =>
{
if (resolvedItems.ContainsKey(referenceValue)) return resolvedItems[referenceValue];
if (referenceValue?.StartsWith("http") == true)
{
try
{
WebResolver wr = new WebResolver();
var t = wr.ResolveByUri(referenceValue);
if (t != null)
{
var tv = new ScopedNode(TypedSerialization.ToTypedElement(t));
resolvedItems.Add(referenceValue, tv);
return tv;
}
}
catch (FhirOperationException fex)
{
result.Parameter.Add(new Parameters.ParameterComponent() { Name = "error", Value = new FhirString($"Resource '{referenceValue}' unble to be resolved:\r\n{fex.Message}") });
return null;
}
}
if (referenceValue?.StartsWith("#") == true && resource is DomainResource dr)
{
// locate the contained resource
var cr = dr.Contained?.FirstOrDefault(r => "#" + r.Id == referenceValue);
if (cr != null)
{
var tv = new ScopedNode(TypedSerialization.ToTypedElement(cr));
resolvedItems.Add(referenceValue, tv);
return tv;
}
}
return null;
};
// compile the expression
CompiledExpression xps = null;
var compiler = new FhirPathCompiler(symbolTable);
try
{
xps = compiler.Compile(expression);
}
catch (Exception ex)
{
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.Exception,
Details = new CodeableConcept() { Text = $"Invalid expression: {ex.Message}" },
Diagnostics = expression
});
return outcome;
}
IEnumerable<ITypedElement> outputValues = null;
if (xps != null)
{
Dictionary<string, ITypedElement> contextList = new Dictionary<string, ITypedElement>();
// before we execute the expression, if there is a property context to run from, navigate to that one fisrt
if (!string.IsNullOrEmpty(context))
{
// inputNav = NavigateToContextProperty(evalContext, inputNav, context);
CompiledExpression cexpr = null;
try
{
cexpr = compiler.Compile(context);
foreach (var val in cexpr(inputNav, evalContext))
{
contextList.Add(val.Location, val);
}
}
catch (NullReferenceException ex)
{
if (inputNav == null)
{
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.Value,
Details = new CodeableConcept() { Text = $"Context expression requires a resource" },
Diagnostics = context
});
}
else
{
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.Exception,
Details = new CodeableConcept() { Text = $"Invalid context expression: {ex.Message}" },
Diagnostics = context
});
}
result.SetAnnotation<HttpStatusCode>(HttpStatusCode.BadRequest);
result.Parameter.Add(new Parameters.ParameterComponent() { Name = "error", Value = new FhirString("Context expression compilation error:\r\n" + ex.Message) });
return outcome;
}
catch (Exception ex)
{
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.Exception,
Details = new CodeableConcept() { Text = $"Invalid context expression: {ex.Message}" },
Diagnostics = context
});
result.SetAnnotation<HttpStatusCode>(HttpStatusCode.BadRequest);
result.Parameter.Add(new Parameters.ParameterComponent() { Name = "error", Value = new FhirString("Context expression compilation error:\r\n" + ex.Message) });
return outcome;
}
}
else
{
contextList.Add("", inputNav);
}
// Execute expression
foreach (var ctExpr in contextList)
{
try
{
traceList.Clear();
outputValues = xps(ctExpr.Value, evalContext).ToList();
}
catch (NullReferenceException ex)
{
if (inputNav == null)
{
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.Value,
Details = new CodeableConcept() { Text = $"Expression requires a resource {ctExpr.Key}" },
Diagnostics = ex.Message
});
}
else
{
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.Exception,
Details = new CodeableConcept() { Text = $"Expression evaluation error: {ex.Message}" },
Diagnostics = context
});
}
result.SetAnnotation<HttpStatusCode>(HttpStatusCode.BadRequest);
result.Parameter.Add(new Parameters.ParameterComponent() { Name = "error", Value = new FhirString("Expression evaluation error:\r\n" + ex.Message) });
return outcome;
}
catch (Exception ex)
{
outcome.Issue.Add(new OperationOutcome.IssueComponent()
{
Severity = OperationOutcome.IssueSeverity.Error,
Code = OperationOutcome.IssueType.Exception,
Details = new CodeableConcept() { Text = $"Invalid expression: {ex.Message}" },
Diagnostics = context
});
result.SetAnnotation<HttpStatusCode>(HttpStatusCode.BadRequest);
result.Parameter.Add(new Parameters.ParameterComponent() { Name = "error", Value = new FhirString("Expression evaluation error:\r\n" + ex.Message) });
return outcome;
}
try
{
var partContext = new Parameters.ParameterComponent();
partContext.Name = "result";
if (!string.IsNullOrEmpty(ctExpr.Key))
partContext.Value = new FhirString(ctExpr.Key);
result.Parameter.Add(partContext);
if (outputValues.Any())
{
foreach (var item in outputValues.ToFhirValues())
{
var resultPart = new Parameters.ParameterComponent() { Name = item.TypeName };
partContext.Part.Add(resultPart);
if (item is DataType dt)
resultPart.Value = dt;
else if (item is Resource fr)
resultPart.Resource = fr;
else
resultPart.SetStringExtension(exturlJsonValue, _jsFormatter.SerializeToString(item));
}
}
// Append Trace Results
if (traceList.Any())
{
foreach (var ti in traceList)
{
var traceParam = new Parameters.ParameterComponent() { Name = "trace", Value = new FhirString(ti.Key) };
partContext.Part.Add(traceParam);
foreach (var val in ti.Value.ToFhirValues())
{
if (val is DataType dt)
traceParam.Part.Add(new Parameters.ParameterComponent() { Name = dt.TypeName, Value = dt });
else if (val is Resource fr)
traceParam.Part.Add(new Parameters.ParameterComponent() { Name = fr.TypeName, Resource = fr });
else
{
var jsonPart = new Parameters.ParameterComponent() { Name = val.TypeName };
traceParam.Part.Add(jsonPart);
jsonPart.SetStringExtension(exturlJsonValue, _jsFormatter.SerializeToString(val));
}
}
}
traceList.Clear();
}
}
catch (Exception ex)
{
result.SetAnnotation<HttpStatusCode>(HttpStatusCode.BadRequest);
result.Parameter.Add(new Parameters.ParameterComponent() { Name = "error", Value = new FhirString($"Processing results error: ({ctExpr.Key})\r\n{ex.Message}") });
return result;
}
}
}
return result;
}
static readonly FhirJsonSerializer _jsFormatter = new FhirJsonSerializer(new SerializerSettings()
{
Pretty = true,
AppendNewLine = true,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment