Created
May 17, 2022 07:41
-
-
Save brianpos/ae7d9d8a085d7ad1242a064591e850dc to your computer and use it in GitHub Desktop.
A more complete version of the subset of QuestionnaireResponse Validations in dotnet
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
public class QuestionnaireResponse_Validator | |
{ | |
public QuestionnaireResponse_Validator(ValidationSettings settings = null) | |
{ | |
_settings = settings ?? new ValidationSettings() | |
{ | |
TerminologyServerAddress = "https://sqlonfhir-r4.azurewebsites.net/fhir", | |
TerminologyServerFhirClientSettings = new FhirClientSettings() | |
{ | |
VerifyFhirVersion = false, | |
PreferCompressedResponses = true | |
} | |
}; | |
} | |
ValidationSettings _settings; | |
List<Task> AsyncValidations = new List<System.Threading.Tasks.Task>(); | |
ConcurrentQueue<OperationOutcome.IssueComponent> outcomeIssues = new ConcurrentQueue<OperationOutcome.IssueComponent>(); | |
private int DateCompare(string date1, string date2) | |
{ | |
if (string.IsNullOrEmpty(date1) || string.IsNullOrEmpty(date2)) return 0; | |
int minPrecision = Math.Min(date1.Length, date2.Length); | |
return String.Compare(date1.Substring(0, minPrecision), date2.Substring(0, minPrecision)); | |
} | |
public async Task<OperationOutcome> Validate(QuestionnaireResponse qr, Questionnaire q) | |
{ | |
if (q == null) | |
{ | |
ReportValidationMessage(ValidationResult.questionnaireNotFound, null, new[] { "QuestionnaireResponse.questionnaire" }, qr.Status ?? QuestionnaireResponse.QuestionnaireResponseStatus.Completed, null, null, null, questionaireCanonicalUrl: qr.Questionnaire); | |
} | |
else | |
{ | |
// Check that the form definition was active/effective dates | |
if (q.EffectivePeriod != null && !string.IsNullOrEmpty(qr.Authored) && (DateCompare(q.EffectivePeriod.Start, qr.Authored) > 0 || DateCompare(q.EffectivePeriod.End, qr.Authored) < 0)) | |
{ | |
ReportValidationMessage(ValidationResult.questionnaireInactive, null, new[] { "QuestionnaireResponse.authored" }, qr.Status ?? QuestionnaireResponse.QuestionnaireResponseStatus.Completed, null, null, null); | |
} | |
// check if the questionnaire definition is in draft | |
if (q.Status == PublicationStatus.Draft) | |
{ | |
ReportValidationMessage(ValidationResult.questionnaireDraft, null, new[] { "QuestionnaireResponse.questionnaire" }, qr.Status ?? QuestionnaireResponse.QuestionnaireResponseStatus.Completed, null, null, null); | |
} | |
// check if the questionnaire definition is in draft | |
if (q.Status == PublicationStatus.Retired) | |
{ | |
ReportValidationMessage(ValidationResult.questionnaireRetired, null, new[] { "QuestionnaireResponse.questionnaire" }, qr.Status ?? QuestionnaireResponse.QuestionnaireResponseStatus.Completed, null, null, null); | |
} | |
// Check that the structure matches | |
var symbolTable = new Hl7.FhirPath.Expressions.SymbolTable(Hl7.FhirPath.FhirPathCompiler.DefaultSymbolTable); | |
symbolTable.AddVar("questionnaire", q.ToTypedElement()); | |
foreach (var variableExpression in q.variables()) | |
{ | |
// var values = EvaluateFhirPath(symbolTable, variableExpression, outcome, "variable"); | |
// Questionnaire_PrePopulate_Observation.AddVariable(symbolTable, variableExpression.Name, values); | |
} | |
ValidateItems(qr, q, symbolTable, "QuestionnaireResponse.item", qr.Item, q.Item, qr.Status ?? QuestionnaireResponse.QuestionnaireResponseStatus.Completed); | |
// check that all the top level invariants/extensions all pass | |
ValidateInvariants(qr, q, symbolTable, null, null, new[] { "QuestionnaireResponse" }, qr.Status ?? QuestionnaireResponse.QuestionnaireResponseStatus.Completed); | |
// await for any background tasks to complete | |
if (AsyncValidations.Any()) | |
{ | |
await Task.WhenAll(AsyncValidations); | |
} | |
} | |
// 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(QuestionnaireResponse QR, Questionnaire Q, SymbolTable symbolTable, string pathExpression, List<QuestionnaireResponse.ItemComponent> items, IEnumerable<Questionnaire.ItemComponent> itemDefinitions, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
List<QuestionnaireResponse.ItemComponent> itemsRemaining = items.ToList(); | |
foreach (var itemDef in itemDefinitions) | |
{ | |
var itemsForItemDefinition = itemsRemaining.Where(i => i.LinkId == itemDef.LinkId).ToArray(); | |
foreach (var i in itemsForItemDefinition) | |
itemsRemaining.Remove(i); | |
foreach (var item in itemsForItemDefinition) | |
{ | |
ValidateItem(QR, Q, symbolTable, $"{pathExpression}[{items.IndexOf(item)}]", item, itemDef, status); | |
} | |
if (!itemsForItemDefinition.Any()) | |
{ | |
// Check if the definition was a required field... (fake an item, but can't use it's path as it isn't real) | |
var fakeItem = new FakeItem() { LinkId = itemDef.LinkId }; | |
ValidateItem(QR, Q, symbolTable, $"{pathExpression.Substring(0, pathExpression.Length - 5)}", fakeItem, 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}[{items.IndexOf(item)}]" }, status, 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) | |
} | |
/// <summary> | |
/// This FakeItem class is just used to be a marker | |
/// when validating a group that has no children | |
/// so that mandatory fields are checked consistently | |
/// </summary> | |
private class FakeItem : QuestionnaireResponse.ItemComponent | |
{ | |
} | |
private void ValidateItem(QuestionnaireResponse QR, Questionnaire Q, SymbolTable symbolTable, string pathExpression, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// check repeats/mandatory/min count/max count | |
if (item.Answer.Count > 1 && itemDef.Repeats != true) | |
{ | |
// too many responses (for non repeating item) | |
ReportValidationMessage(ValidationResult.repeats, itemDef, new[] { pathExpression }, status, item, null, null); | |
} | |
if (item.Answer.Count == 0 && itemDef.Required == true) | |
{ | |
// Mandatory | |
ReportValidationMessage(ValidationResult.required, itemDef, new[] { pathExpression }, status, item, null, null); | |
} | |
var minOccurs = itemDef.minOccurs(); | |
if (minOccurs.HasValue && item.Answer.Count < minOccurs.Value) | |
{ | |
// not enough answers | |
ReportValidationMessage(ValidationResult.minCount, itemDef, new[] { pathExpression }, status, item, null, null); | |
} | |
// May need to move the invariant processing up to here | |
// This was a Fake Item introduced to check for Mandatory/min count child items | |
// so bail any further testing | |
if (item is FakeItem) return; | |
var maxOccurs = itemDef.maxOccurs(); | |
if (maxOccurs.HasValue && item.Answer.Count > maxOccurs.Value) | |
{ | |
// too many answers | |
ReportValidationMessage(ValidationResult.maxCount, itemDef, new[] { pathExpression }, status, item, null, null); | |
} | |
if (itemDef.Type == Questionnaire.QuestionnaireItemType.Display && item.Answer.Count > 0) | |
{ | |
// Display Items should't have answers | |
ReportValidationMessage(ValidationResult.displayAnswer, itemDef, new[] { pathExpression }, status, item, null, null); | |
} | |
if (itemDef.Type == Questionnaire.QuestionnaireItemType.Question) | |
{ | |
// "Question" Items should't be used | |
ReportValidationMessage(ValidationResult.invalidType, itemDef, new[] { pathExpression }, status, item, null, null); | |
} | |
if (itemDef.Type == Questionnaire.QuestionnaireItemType.Group) | |
{ | |
// Check for children | |
if (item.Answer.Any()) | |
{ | |
ReportValidationMessage(ValidationResult.groupShouldNotHaveAnswers, itemDef, new[] { $"{pathExpression}.answer" }, status, item, null, null); | |
} | |
if (itemDef.Item?.Any() == true) | |
{ | |
ValidateItems(QR, Q, symbolTable, $"{pathExpression}.item", item.Item, itemDef.Item, status); | |
} | |
} | |
else | |
{ | |
if (item.Item.Any()) | |
{ | |
// This is meant to be that there is no group intended to be found here... | |
ReportValidationMessage(ValidationResult.invalidType, itemDef, new[] { pathExpression }, status, item, null, null); | |
} | |
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, status); | |
// Check for children | |
if (itemDef.Item?.Any() == true) | |
{ | |
ValidateItems(QR, Q, symbolTable, $"{pathExpression}.answer[{answerIndex}].item", answer.Item, itemDef.Item, status); | |
} | |
answerIndex++; | |
} | |
} | |
// check that all the invariants/extensions all pass | |
ValidateInvariants(QR, Q, symbolTable, item, itemDef, new[] { pathExpression }, status); | |
} | |
private void ValidateItemTypeData(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
switch (itemDef.Type) | |
{ | |
case Questionnaire.QuestionnaireItemType.Group: | |
// Nothing to see here - no spec defined rules applicable | |
break; | |
case Questionnaire.QuestionnaireItemType.Display: | |
// Nothing to see here - handled above | |
break; | |
case Questionnaire.QuestionnaireItemType.Question: | |
// Nothing to see here - handled above | |
break; | |
case Questionnaire.QuestionnaireItemType.Boolean: | |
if (item.Answer[answerIndex].Value is FhirBoolean fb) | |
{ | |
// There are no Boolean specific validation rules | |
// ValidateBooleanValue(false, item, itemDef, answerIndex, answerItemPathExpression, fb); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Decimal: | |
if (item.Answer[answerIndex].Value is FhirDecimal fd) | |
{ | |
ValidateDecimalValue(false, item, itemDef, answerIndex, answerItemPathExpression, fd, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Integer: | |
if (item.Answer[answerIndex].Value is Integer fi) | |
{ | |
ValidateIntegerValue(false, item, itemDef, answerIndex, answerItemPathExpression, fi, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Date: | |
if (item.Answer[answerIndex].Value is Date fdate) | |
{ | |
ValidateDateValue(false, item, itemDef, answerIndex, answerItemPathExpression, fdate, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.DateTime: | |
if (item.Answer[answerIndex].Value is FhirDateTime fdateTime) | |
{ | |
ValidateDateTimeValue(false, item, itemDef, answerIndex, answerItemPathExpression, fdateTime, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Time: | |
if (item.Answer[answerIndex].Value is Time ft) | |
{ | |
ValidateTimeValue(false, item, itemDef, answerIndex, answerItemPathExpression, ft, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.String: | |
if (item.Answer[answerIndex].Value is FhirString str) | |
{ | |
ValidateStringValue(false, item, itemDef, answerIndex, answerItemPathExpression, str, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Text: | |
if (item.Answer[answerIndex].Value is FhirString strText) | |
{ | |
ValidateStringValue(true, item, itemDef, answerIndex, answerItemPathExpression, strText, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Url: | |
if (item.Answer[answerIndex].Value is FhirUri furl) | |
{ | |
ValidateUrlValue(false, item, itemDef, answerIndex, answerItemPathExpression, furl, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Choice: | |
if (item.Answer[answerIndex].Value is Coding coding) | |
{ | |
// validate the coding | |
ValidateCodingValue(item, itemDef, answerIndex, answerItemPathExpression, coding, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, 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, status); | |
} | |
else if (item.Answer[answerIndex].Value is FhirString strOpen) | |
{ | |
// String values not TEXT values (don't have newlines) | |
ValidateStringValue(false, item, itemDef, answerIndex, answerItemPathExpression, strOpen, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Attachment: | |
if (item.Answer[answerIndex].Value is Attachment att) | |
{ | |
// validate the attahcment | |
ValidateAttachmentValue(item, itemDef, answerIndex, answerItemPathExpression, att, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Reference: | |
if (item.Answer[answerIndex].Value is ResourceReference resref) | |
{ | |
ValidateReferenceValue(false, item, itemDef, answerIndex, answerItemPathExpression, resref, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
case Questionnaire.QuestionnaireItemType.Quantity: | |
if (item.Answer[answerIndex].Value is Quantity fq) | |
{ | |
ValidateQuantityValue(false, item, itemDef, answerIndex, answerItemPathExpression, fq, status); | |
} | |
else | |
ReportValidationMessage(ValidationResult.invalidAnswerType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
break; | |
} | |
} | |
private void ValidateAnswerOption<T>(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, T value, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
where T : DataType | |
{ | |
// Check if constrained by AnswerOptions (closed in R4 - option in R5) | |
var typeOptions = itemDef.AnswerOption.Where(ao => ao.Value is T && ao.Value.Matches(value)); | |
if (itemDef.AnswerOption.Any()) | |
{ | |
if (!typeOptions.Any()) // Check as a "pattern" | |
{ | |
ReportValidationMessage(ValidationResult.invalidAnswerOption, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
// Check option Exclusive http://hl7.org/fhir/StructureDefinition/questionnaire-optionExclusive | |
if (typeOptions.Any(to => to.optionExclusive() == true)) | |
{ | |
if (item.Answer.Count > 1) | |
{ | |
ReportValidationMessage(ValidationResult.exclusiveAnswerOption, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
} | |
} | |
} | |
private void ValidateCodingValue(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Coding coding, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// Check if constrained by AnswerOptions (closed in R4 - option in R5) | |
ValidateAnswerOption(item, itemDef, answerIndex, answerItemPathExpression, coding, status); | |
// Check against answerValueSet | |
ValidateCodingValueAgainstValueSet(item, itemDef, answerIndex, answerItemPathExpression, coding, status); | |
// TODO: ??? Check ordinal Value too (and populate if it not provided -- requires a lookup, not validate-code) | |
} | |
private void ValidateCodingValueAgainstValueSet(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Coding coding, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
if (!string.IsNullOrEmpty(itemDef.AnswerValueSet)) | |
{ | |
// TODO: Check for the preferrred terminology server extension | |
FhirClient ts; | |
if (_settings.TerminologyServerMessageHandler != null) | |
ts = new FhirClient(_settings.TerminologyServerAddress, _settings.TerminologyServerFhirClientSettings, _settings.TerminologyServerMessageHandler); | |
else | |
ts = new FhirClient(_settings.TerminologyServerAddress, _settings.TerminologyServerFhirClientSettings); | |
// split the AnswerValueSet value into canonical and version. | |
var canonical = new CanonicalUrl(itemDef.AnswerValueSet); | |
Task validateCode = ts.ValidateCodeAsync(url: canonical.Url, version: canonical.Version, coding: coding) | |
.ContinueWith((result) => | |
{ | |
Console.WriteLine(ts.LastBodyAsText); | |
if (result.IsFaulted) | |
{ | |
result.Exception?.Handle(ex => | |
{ | |
Console.WriteLine(ex.ToString()); | |
ReportValidationMessage(ValidationResult.tsError, itemDef, answerItemPathExpression, status, item, answerIndex, null, ex); | |
return true; | |
}); | |
return; | |
} | |
if (result.Result.Result.Value.Value != true) | |
{ | |
ReportValidationMessage(ValidationResult.invalidCoding, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
}); | |
this.AsyncValidations.Add(validateCode); | |
} | |
// Check against answerExpression? (assuming have access to all data - maybe this becomes a warning only) | |
//if (itemDef.answerExpression.any()) | |
{ | |
// just log an info message that this isn't supported yet | |
} | |
// External Dependencies: (data access incl. permissions) | |
// * launch context | |
// * source queries | |
// * answerValueSet | |
// * fhirpath/cql expressions & resolve() | |
// * x-fhirquery expressions | |
} | |
private void ValidateQuantityUnitValueAgainstValueSet(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Coding coding, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
if (!string.IsNullOrEmpty(itemDef.unitValueSet())) | |
{ | |
// if there are no computable units (code) specified, then this needs to be reported as an error | |
if (string.IsNullOrEmpty(coding.System) || string.IsNullOrEmpty(coding.Code)) | |
{ | |
// this value isn't testable, so bail here. | |
ReportValidationMessage(ValidationResult.invalidUnitValueSet, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
return; | |
} | |
// TODO: Check for the preferrred terminology server extension | |
FhirClient ts; | |
if (_settings.TerminologyServerMessageHandler != null) | |
ts = new FhirClient(_settings.TerminologyServerAddress, _settings.TerminologyServerFhirClientSettings, _settings.TerminologyServerMessageHandler); | |
else | |
ts = new FhirClient(_settings.TerminologyServerAddress, _settings.TerminologyServerFhirClientSettings); | |
// split the AnswerValueSet value into canonical and version. | |
var canonical = new CanonicalUrl(itemDef.unitValueSet()); | |
Task validateCode = ts.ValidateCodeAsync(url: canonical.Url, version: canonical.Version, coding: coding) | |
.ContinueWith((result) => | |
{ | |
Console.WriteLine(ts.LastBodyAsText); | |
if (result.IsFaulted) | |
{ | |
result.Exception?.Handle(ex => | |
{ | |
Console.WriteLine(ex.ToString()); | |
ReportValidationMessage(ValidationResult.tsError, itemDef, answerItemPathExpression, status, item, answerIndex, null, ex); | |
return true; | |
}); | |
return; | |
} | |
if (result.Result.Result.Value.Value != true) | |
{ | |
ReportValidationMessage(ValidationResult.invalidUnitValueSet, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
}); | |
this.AsyncValidations.Add(validateCode); | |
} | |
} | |
private void ValidateQuantityValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Quantity value, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// Check for units http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption | |
var unitOptions = itemDef.unitOptions().ToList(); | |
if (unitOptions.Any()) | |
{ | |
if (!unitOptions.Any(uo => | |
{ | |
if (!string.IsNullOrEmpty(uo.System) && uo.System != value.System) return false; | |
if (!string.IsNullOrEmpty(uo.Code) && uo.Code != value.Code) return false; | |
// Not sure on this one, as if there are translations or other designations then this could legitimately be different | |
// if (!string.IsNullOrEmpty(uo.Display) && uo.Display != value.Unit) return false; | |
return true; | |
})) | |
{ | |
ReportValidationMessage(ValidationResult.invalidUnit, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
} | |
// and also from here http://hl7.org/fhir/StructureDefinition/questionnaire-unitValueSet | |
// which can be done as a ValidateCode call (just like coded items) | |
ValidateQuantityUnitValueAgainstValueSet(item, itemDef, answerIndex, answerItemPathExpression, new Coding(value.System, value.Code, value.Unit), status); | |
// Min value | |
if (itemDef.minQuantity() != null) | |
{ | |
if (!CanConvertUnits(itemDef.minQuantity(), value)) | |
ReportValidationMessage(ValidationResult.minValueIncompatUnits, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
else if (CompareQuantity(itemDef.minQuantity(), value) > 0) | |
ReportValidationMessage(ValidationResult.minValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
// max value | |
if (itemDef.maxQuantity() != null) | |
{ | |
if (!CanConvertUnits(itemDef.maxQuantity(), value)) | |
ReportValidationMessage(ValidationResult.maxValueIncompatUnits, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
else if (CompareQuantity(itemDef.maxQuantity(), value) < 0) | |
ReportValidationMessage(ValidationResult.maxValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
// Referenced content? e.g. reference ranges? | |
} | |
private static Fhir.Metrics.SystemOfUnits _system; | |
bool CanConvertUnits(Quantity l, Quantity r) | |
{ | |
// this implementation only supports converting UCUM units (but will default to ucum if not specified) | |
var lSys = l.System ?? Hl7.Fhir.ElementModel.Types.Quantity.UCUM; | |
var rSys = r.System ?? Hl7.Fhir.ElementModel.Types.Quantity.UCUM; | |
if (rSys != lSys) return false; | |
if (!string.IsNullOrEmpty(r.Code) && r.Code == l.Code) return true; | |
if (!string.IsNullOrEmpty(r.Unit) && r.Unit == l.Unit) return true; | |
if (rSys != Hl7.Fhir.ElementModel.Types.Quantity.UCUM) return false; | |
// Check by converting to the canonical types (lazy load the ucum values) | |
if (_system == null) _system = Fhir.Metrics.UCUM.Load(); | |
try | |
{ | |
// new Fhir.Metrics.SystemOfUnits().Conversions. | |
var lv = _system.Quantity(l.Value.ToString(), l.Code ?? l.Unit); | |
var lc = _system.Canonical(lv); | |
var rv = _system.Quantity(r.Value.ToString(), r.Code ?? r.Unit); | |
var rc = _system.Canonical(rv); | |
if (rc.Metric.Symbols != lc.Metric.Symbols) return false; | |
} | |
catch (ArgumentException) | |
{ | |
// unable to read the value/codes in the converter (therefore can't convert them) | |
return false; | |
} | |
return true; | |
} | |
int CompareQuantity(Quantity l, Quantity r) | |
{ | |
if (!string.IsNullOrEmpty(r.Code) && r.Code == l.Code || !string.IsNullOrEmpty(r.Unit) && r.Unit == l.Unit) | |
return Decimal.Compare(l.Value.Value, r.Value.Value); | |
// Perform the ucum conversion (_system was lazy initialized during the CanConvertUnits routine) | |
var lv = _system.Quantity(l.Value.ToString(), l.Code ?? l.Unit); | |
var lc = _system.Canonical(lv); | |
var rv = _system.Quantity(r.Value.ToString(), r.Code ?? r.Unit); | |
var rc = _system.Canonical(rv); | |
return Decimal.Compare(lc.Value.ToDecimal(), rc.Value.ToDecimal()); | |
} | |
private void ValidateReferenceValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, ResourceReference resref, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// Check the format (option for RI checks?) | |
if (!string.IsNullOrEmpty(resref.Reference)) | |
{ | |
// Check that the URL is a well formed URL | |
if (!Uri.IsWellFormedUriString(resref.Reference, UriKind.RelativeOrAbsolute)) | |
{ | |
ReportValidationMessage(ValidationResult.invalidRefValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
else | |
{ | |
var ri = new ResourceIdentity(resref.Reference); | |
var schemes = new[] { "http", "https" }; | |
if (ri.IsAbsoluteUri && !schemes.Contains(ri.Scheme)) | |
{ | |
ReportValidationMessage(ValidationResult.invalidRefValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
else | |
{ | |
if (!ModelInfo.IsKnownResource(ri.ResourceType)) | |
{ | |
ReportValidationMessage(ValidationResult.invalidRefResourceType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
// http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource | |
var resourceTypes = itemDef.referenceResource().ToList(); | |
if (!string.IsNullOrEmpty(ri.ResourceType) && resourceTypes.Any()) | |
{ | |
if (!resourceTypes.Contains(ri.ResourceType)) | |
{ | |
ReportValidationMessage(ValidationResult.invalidRefResourceTypeRestriction, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
} | |
// If we can resolve the target of the instance, then we should be able to validate that this profile is correct | |
// http://hl7.org/fhir/StructureDefinition/questionnaire-referenceProfile | |
// TODO: Enable an async hook to go check these in the setting object | |
} | |
// Can't do any validations on this one http://hl7.org/fhir/StructureDefinition/questionnaire-referenceFilter | |
} | |
} | |
// Check if constrained by AnswerOptions (closed in R4 - option in R5) | |
ValidateAnswerOption(item, itemDef, answerIndex, answerItemPathExpression, resref, status); | |
} | |
private void ValidateUrlValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, FhirUri furl, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// Validate the format (uuid) | |
if (furl.Value.StartsWith("urn:uuid:")) | |
{ | |
if (!Guid.TryParse(furl.Value.Substring("urn:uuid:".Length), out Guid valueGuid)) | |
{ | |
ReportValidationMessage(ValidationResult.invalidUrlValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
return; | |
} | |
// Validate the format | |
if (!Uri.IsWellFormedUriString(furl.Value, UriKind.RelativeOrAbsolute)) | |
{ | |
ReportValidationMessage(ValidationResult.invalidUrlValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
} | |
private void ValidateTimeValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Time value, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// Check if constrained by AnswerOptions (closed in R4 - option in R5) | |
ValidateAnswerOption(item, itemDef, answerIndex, answerItemPathExpression, value, status); | |
} | |
private void ValidateDateTimeValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, FhirDateTime value, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// min value | |
var min = itemDef.minValue() as FhirDateTime; | |
if (min != null && DateCompare(min.Value, value.Value) > 0) | |
ReportValidationMessage(ValidationResult.minValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// max value | |
var max = itemDef.maxValue() as FhirDateTime; | |
if (max != null && DateCompare(max.Value, value.Value) < 0) | |
ReportValidationMessage(ValidationResult.maxValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
private void ValidateDateValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Date value, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// min value | |
var min = itemDef.minValue() as Date; | |
if (min != null && DateCompare(min.Value, value.Value) > 0) | |
ReportValidationMessage(ValidationResult.minValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// max value | |
var max = itemDef.maxValue() as Date; | |
if (max != null && DateCompare(max.Value, value.Value) < 0) | |
ReportValidationMessage(ValidationResult.maxValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// Check if constrained by AnswerOptions (closed in R4 - option in R5) | |
ValidateAnswerOption(item, itemDef, answerIndex, answerItemPathExpression, value, status); | |
} | |
private void ValidateIntegerValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Integer fi, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// min value | |
var min = itemDef.minValue() as Integer; | |
if (min != null && min.Value > fi.Value) | |
ReportValidationMessage(ValidationResult.minValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// max value | |
var max = itemDef.maxValue() as Integer; | |
if (max != null && max.Value < fi.Value) | |
ReportValidationMessage(ValidationResult.maxValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// Check if constrained by AnswerOptions (closed in R4 - option in R5) | |
ValidateAnswerOption(item, itemDef, answerIndex, answerItemPathExpression, fi, status); | |
} | |
private void ValidateDecimalValue(bool v, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, FhirDecimal fd, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// min value | |
var min = itemDef.minValue() as FhirDecimal; | |
if (min != null && min.Value > fd.Value) | |
ReportValidationMessage(ValidationResult.minValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// max value | |
var max = itemDef.maxValue() as FhirDecimal; | |
if (max != null && max.Value < fd.Value) | |
ReportValidationMessage(ValidationResult.maxValue, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// max decimal places | |
var maxDecPlaces = itemDef.maxDecimalPlaces(); | |
if (fd.Value.HasValue && maxDecPlaces.HasValue && CountDecimalDigits(fd.Value.Value) > maxDecPlaces.Value) | |
{ | |
ReportValidationMessage(ValidationResult.maxDecimalPlaces, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
// TODO: https://build.fhir.org/extension-quantity-precision.html | |
} | |
int CountDecimalDigits(decimal n) | |
{ | |
return n.ToString(System.Globalization.CultureInfo.InvariantCulture) | |
.SkipWhile(c => c != '.') | |
.Skip(1) | |
.Count(); | |
} | |
private void ValidateAttachmentValue(QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, Attachment att, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// Size of attachment inconsistent | |
if (att.Size.HasValue && att.Data?.Length != att.Size) | |
{ | |
ReportValidationMessage(ValidationResult.attachmentSizeInconsistent, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
// Max File Size (bytes) | |
if (att.Data?.Length > itemDef.maxSize()) | |
{ | |
ReportValidationMessage(ValidationResult.maxAttachmentSize, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
// Supported Types | |
var mimeTypes = itemDef.mimeTypes(); | |
if (mimeTypes?.Any() == true && (!mimeTypes.Contains(att.ContentType) || string.IsNullOrEmpty(att.ContentType))) | |
{ | |
ReportValidationMessage(ValidationResult.invalidAttachmentType, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
} | |
private void ValidateStringValue(bool PermitNewLines, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, int answerIndex, string[] answerItemPathExpression, FhirString strOpen, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
// Newlines are only valid for TEXT not STRING | |
if (!PermitNewLines && strOpen.Value?.IndexOfAny(new[] { '\r', '\n' }) > -1) | |
ReportValidationMessage(ValidationResult.invalidNewLine, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// Min Length | |
if (strOpen.Value?.Length < itemDef.minLength()) | |
ReportValidationMessage(ValidationResult.minLength, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// Max length | |
if (strOpen.Value?.Length > itemDef.MaxLength) | |
ReportValidationMessage(ValidationResult.maxLength, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
// Check the Regex rules http://hl7.org/fhir/StructureDefinition/regex | |
// Suggest including the entryFormat extension to guide the use of the regex violation (placeholder text) | |
var regexValue = itemDef.regex(); | |
if (!string.IsNullOrEmpty(regexValue)) | |
{ | |
// https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices | |
try | |
{ | |
// By using the static IsMatch method .net will cache the expression compilation in memory | |
// (defaults to 15 instances, if more are needed, update the System.Text.RegularExpressions.Regex.CacheSize value) | |
var resultRegex = System.Text.RegularExpressions.Regex.IsMatch(strOpen.Value, regexValue, System.Text.RegularExpressions.RegexOptions.None, _settings.RegexTimeout); | |
if (!resultRegex) | |
{ | |
ReportValidationMessage(ValidationResult.regex, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
} | |
catch (System.Text.RegularExpressions.RegexMatchTimeoutException ex) | |
{ | |
// TODO: probably something to specifically log (should the checked data be in this message?) | |
_settings.Logger?.Log(LogLevel.Error, $"Timeout attempting to evaluate regex expression: {regexValue} on string {strOpen.Value}"); | |
// Regex validation processing timed out | |
ReportValidationMessage(ValidationResult.regexTimeout, itemDef, answerItemPathExpression, status, item, answerIndex, null); | |
} | |
} | |
// Check if constrained by AnswerOptions (closed in R4 - option in R5) | |
ValidateAnswerOption(item, itemDef, answerIndex, answerItemPathExpression, strOpen, status); | |
// Check if constrained by AnswerValueSet... CODE value in the codings :( | |
if (itemDef.Type == Questionnaire.QuestionnaireItemType.String) // TEXT doesn't use it | |
{ | |
// This is prevented by que-5 - Brian to log this for R5 to fix. | |
// https://build.fhir.org/terminologies.html#strings | |
// https://build.fhir.org/extension-originaltext.html | |
ValidateCodingValueAgainstValueSet(item, itemDef, answerIndex, answerItemPathExpression, new Coding(null, strOpen.Value), status); | |
} | |
} | |
void ValidateInvariants(QuestionnaireResponse QR, Questionnaire Q, SymbolTable symbolTable, QuestionnaireResponse.ItemComponent item, Questionnaire.ItemComponent itemDef, string[] answerItemPathExpression, QuestionnaireResponse.QuestionnaireResponseStatus status) | |
{ | |
IEnumerable<QuestionnaireInvariant> invariants; | |
if (itemDef != null) | |
{ | |
//foreach (var variableExpression in itemDef.variables()) | |
//{ | |
// var values = EvaluateFhirPath(symbolTable, variableExpression, outcome, "variable"); | |
// Questionnaire_PrePopulate_Observation.AddVariable(symbolTable, variableExpression.Name, values); | |
//} | |
invariants = itemDef.constraints(); | |
} | |
else | |
invariants = Q.constraints(); | |
if (invariants != null && invariants.Any()) | |
{ | |
FhirEvaluationContext ctxt; | |
ctxt = new FhirEvaluationContext(QR.ToTypedElement()); | |
foreach (var invariant in invariants) | |
{ | |
try | |
{ | |
var itemSpecificTable = new SymbolTable(symbolTable); | |
if (itemDef != null) | |
{ | |
itemSpecificTable.AddVar("qitem", itemDef.ToTypedElement()); | |
} | |
var compiler = new FhirPathCompiler(itemSpecificTable); | |
var expr = compiler.Compile(invariant.expression); | |
IEnumerable<ITypedElement> result; | |
if (itemDef != null) | |
result = expr(item.ToTypedElement(), ctxt); | |
else | |
result = expr(QR.ToTypedElement(), ctxt); | |
if (result.Count() != 1 || !(bool)result.First().Value) | |
{ | |
// TODO: Need to re-evaluate the location paths (if specified) | |
if (invariant.location.Any()) | |
{ | |
} | |
ReportValidationMessage(ValidationResult.invariant, itemDef, answerItemPathExpression, status, item, null, invariant, questionaireCanonicalUrl: QR.Questionnaire ?? new CanonicalUrl(Q.Url) { Version = Q.VersionElement }.Value); | |
} | |
} | |
catch (Exception ex) | |
{ | |
ReportValidationMessage(ValidationResult.invariantExecution, itemDef, answerItemPathExpression, status, item, null, invariant, ex); | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment