Skip to content

Instantly share code, notes, and snippets.

@brianpos
Created May 17, 2022 07:41
Show Gist options
  • Save brianpos/ae7d9d8a085d7ad1242a064591e850dc to your computer and use it in GitHub Desktop.
Save brianpos/ae7d9d8a085d7ad1242a064591e850dc to your computer and use it in GitHub Desktop.
A more complete version of the subset of QuestionnaireResponse Validations in dotnet
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