Skip to content

Instantly share code, notes, and snippets.

@brianpos
Created March 26, 2022 03:55
Show Gist options
  • Save brianpos/28f7b7fc15a5a1b7a8803b0c18e4a22a to your computer and use it in GitHub Desktop.
Save brianpos/28f7b7fc15a5a1b7a8803b0c18e4a22a to your computer and use it in GitHub Desktop.
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using Hl7.FhirPath;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Task = System.Threading.Tasks.Task;
// using QuestionnaireSDC_Extensions;
namespace QForms.Helpers
{
public class QuestionnaireResponse_Validator
{
public QuestionnaireResponse_Validator()
{
}
List<Task> AsyncValidations = new List<System.Threading.Tasks.Task>();
ConcurrentQueue<OperationOutcome.IssueComponent> outcomeIssues = new ConcurrentQueue<OperationOutcome.IssueComponent>();
CancellationToken _token;
public async Task<OperationOutcome> Validate(QuestionnaireResponse qr, Questionnaire q, CancellationToken token)
{
_token = token;
// Check that the structure matches
ValidateItems("QuestionnaireResponse.item", qr.Item, q.Item, qr.Status);
// await for any background tasks to complete
if (AsyncValidations.Any())
{
Console.WriteLine("Waiting for asyc processes to complete..");
await Task.WhenAll(AsyncValidations).ConfigureAwait(false);
Console.WriteLine("Async processing completed");
}
// append all the outcomes into the output results
var outcome = new OperationOutcome();
outcome.Issue.AddRange(outcomeIssues);
// and clear out the data
AsyncValidations.Clear();
outcomeIssues.Clear();
return outcome;
}
private void ValidateItems(string pathExpression, List<QuestionnaireResponse.ItemComponent> items, IEnumerable<Questionnaire.ItemComponent> itemDefinitions, QuestionnaireResponse.QuestionnaireResponseStatus? status)
{
IEnumerable<QuestionnaireResponse.ItemComponent> itemsRemaining = items;
foreach (var itemDef in itemDefinitions)
{
var itemsForItemDefinition = itemsRemaining.Where(i => i.LinkId == itemDef.LinkId);
itemsRemaining = itemsRemaining.Except(itemsForItemDefinition);
foreach (var item in itemsForItemDefinition)
{
ValidateItem($"{pathExpression}[{items.IndexOf(item)}]", item, itemDef, status);
}
}
// Check if there are any items left that did not have a definition (as these are in error)
foreach (var item in itemsRemaining)
{
ReportValidationMessage(ValidationResult.invalidLinkId, null, new[] { pathExpression }, null, item, null, null);
}
// Should we be checking the order of the items in the collection(s) too?
// Lloyd, yes we should be - that can help with the performance too (don't need to split/join items)
}
private void ValidateItem(string pathExpression, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, QuestionnaireResponse.QuestionnaireResponseStatus? status)
{
int answerIndex = 0;
foreach (var answer in item.Answer)
{
var answerItemPathExpression = new[] { $"{pathExpression}.answer[{answerIndex}]" };
// check that the datatypes for all the answers match the definition
ValidateItemTypeData(item, itemDef, answerIndex, answerItemPathExpression);
// Check for children of answers
if (answer.Item?.Any() == true)
{
ValidateItems($"{pathExpression}.answer[{answerIndex}].item", answer.Item, itemDef.Item, status);
}
answerIndex++;
}
// check that all the invariants/extensions all pass
// ValidateInvartiants(item, itemDef);
// Check for children
if (itemDef.Item?.Any() == true)
{
ValidateItems($"{pathExpression}.item", item.Item, itemDef.Item, status);
}
}
private void ValidateItemTypeData(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression)
{
switch (itemDef.Type)
{
case Questionnaire.QuestionnaireItemType.Choice:
if (item.Answer[answerIndex].Value is Coding coding)
{
// validate the coding
ValidateCodingValue(item, itemDef, answerIndex, answerItemPathExpression, coding);
}
else
ReportValidationMessage(ValidationResult.invalidType, itemDef, answerItemPathExpression, null, item, answerIndex, null);
break;
case Questionnaire.QuestionnaireItemType.OpenChoice:
if (item.Answer[answerIndex].Value is Coding codingOpen)
{
// validate the coding
ValidateCodingValue(item, itemDef, answerIndex, answerItemPathExpression, codingOpen);
}
else if (item.Answer[answerIndex].Value is FhirString strOpen)
{
ValidateStringValue(false, item, itemDef, answerIndex, answerItemPathExpression, strOpen);
}
else
ReportValidationMessage(ValidationResult.invalidType, itemDef, answerItemPathExpression, null, item, answerIndex, null);
break;
}
}
private void ValidateCodingValue(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Coding coding)
{
// Check if the value is in the actual valueset
// check cache, check in progress cache - check cache has a task for the lookup, so hook onto it too, include request id in the cache too
// call terminology server
// update cache
// return result
var ts = new FhirClient("https://sqlonfhir-r4.azurewebsites.net/fhir");
Task validateCode = ts.ValidateCodeAsync(url: new FhirUri("http://hl7.org/fhir/ValueSet/jurisdiction"), version: new FhirString("4.0.1"), coding: coding)
.ContinueWith((result) =>
{
// Thread.Sleep(10000);
Console.WriteLine(ts.LastBodyAsText);
if (result.IsFaulted)
{
Console.WriteLine(result.Exception.ToString());
ReportValidationMessage(ValidationResult.unknown, itemDef, answerItemPathExpression, null, item, answerIndex, null);
return;
}
if (result.Result.Result.Value.Value != true)
{
ReportValidationMessage(ValidationResult.minValue, itemDef, answerItemPathExpression, null, item, answerIndex, null);
}
});
this.AsyncValidations.Add(validateCode);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment