Skip to content

Commit

Permalink
Warn when temporal primitive is not formatted correctly (#256)
Browse files Browse the repository at this point in the history
* Warn when temporal primitive is not formatted correctly

The FHIR types dateTime, date, time, and instant all have required
formats. If a value of one of these types does not match the format,
emit a warning. The rule to set this value is still created.

* Include name of FHIR entity in temporal format warning

* Improve warning messages

Warning message for concept caret rules includes the concept path.
Warning message for values on Instances uses the target id instead of
name.

* Add instance type to warning message

Update test for caret rule on profile to more closely resemble realistic
output.
  • Loading branch information
mint-thompson authored Mar 29, 2024
1 parent 30f9434 commit 52914b3
Show file tree
Hide file tree
Showing 17 changed files with 605 additions and 47 deletions.
32 changes: 30 additions & 2 deletions src/extractor/AssignmentRuleExtractor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { fhirtypes, fshtypes } from 'fsh-sushi';
import { ProcessableElementDefinition } from '../processor';
import { ExportableAssignmentRule } from '../exportable';
import { getPath } from '../utils';
import { dateRegex, dateTimeRegex, getPath, instantRegex, timeRegex, logger } from '../utils';
import { fshifyString } from '../exportable/common';

export class AssignmentRuleExtractor {
static process(input: ProcessableElementDefinition): ExportableAssignmentRule[] {
static process(
input: ProcessableElementDefinition,
entityName: string
): ExportableAssignmentRule[] {
// check for fixedSomething or patternSomething
// pattern and fixed are mutually exclusive
// these are on one-type elements, so if our SD has value[x],
Expand All @@ -28,6 +31,31 @@ export class AssignmentRuleExtractor {
assignmentRule.value = BigInt(matchingValue);
} else {
assignmentRule.value = matchingValue;
if (matchingKey.endsWith('DateTime')) {
if (!dateTimeRegex.test(matchingValue)) {
logger.warn(
`Value ${matchingValue} on ${entityName} element ${assignmentRule.path} is not a valid FHIR dateTime`
);
}
} else if (matchingKey.endsWith('Date')) {
if (!dateRegex.test(matchingValue)) {
logger.warn(
`Value ${matchingValue} on ${entityName} element ${assignmentRule.path} is not a valid FHIR date`
);
}
} else if (matchingKey.endsWith('Time')) {
if (!timeRegex.test(matchingValue)) {
logger.warn(
`Value ${matchingValue} on ${entityName} element ${assignmentRule.path} is not a valid FHIR time`
);
}
} else if (matchingKey.endsWith('Instant')) {
if (!instantRegex.test(matchingValue)) {
logger.warn(
`Value ${matchingValue} on ${entityName} element ${assignmentRule.path} is not a valid FHIR instant`
);
}
}
}
} else {
assignmentRule.value = matchingValue;
Expand Down
27 changes: 23 additions & 4 deletions src/extractor/CaretValueRuleExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ export class CaretValueRuleExtractor {
const remainingFlatElementArray = flatElementArray.filter(
([key]) => !input.processedPaths.includes(key)
);
const entityPath = path === '' ? structDef.name : `${structDef.name}.${path}`;
remainingFlatElementArray.forEach(([key], i) => {
const caretValueRule = new ExportableCaretValueRule(path);
caretValueRule.caretPath = key;
caretValueRule.value = getFSHValue(i, remainingFlatElementArray, 'ElementDefinition', fisher);
caretValueRule.value = getFSHValue(
i,
remainingFlatElementArray,
'ElementDefinition',
entityPath,
fisher
);
// If the value is empty, we can't use it. Log an error and give up on trying to use this key.
if (isFSHValueEmpty(caretValueRule.value)) {
logger.error(
Expand Down Expand Up @@ -207,7 +214,7 @@ export class CaretValueRuleExtractor {
}
const caretValueRule = new ExportableCaretValueRule('');
caretValueRule.caretPath = key;
caretValueRule.value = getFSHValue(i, flatArray, 'StructureDefinition', fisher);
caretValueRule.value = getFSHValue(i, flatArray, 'StructureDefinition', input.name, fisher);
if (isFSHValueEmpty(caretValueRule.value)) {
logger.error(
`Value in StructureDefinition ${input.name} for element ${key} is empty. No caret value rule will be created.`
Expand Down Expand Up @@ -241,7 +248,13 @@ export class CaretValueRuleExtractor {
}
const caretValueRule = new ExportableCaretValueRule('');
caretValueRule.caretPath = key;
caretValueRule.value = getFSHValue(i, flatArray, resourceType, fisher);
caretValueRule.value = getFSHValue(
i,
flatArray,
resourceType,
input.name ?? input.id,
fisher
);
if (isFSHValueEmpty(caretValueRule.value)) {
logger.error(
`Value in ${resourceType} ${
Expand Down Expand Up @@ -272,7 +285,13 @@ export class CaretValueRuleExtractor {
flatArray.forEach(([key], i) => {
const caretValueRule = new ExportableCaretValueRule('');
caretValueRule.caretPath = key;
caretValueRule.value = getFSHValue(i, flatArray, 'Concept', fisher);
caretValueRule.value = getFSHValue(
i,
flatArray,
'Concept',
`${entityName} ${pathArray.join('.')}`,
fisher
);
caretValueRule.isCodeCaretRule = true;
caretValueRule.pathArray = [...pathArray];
if (isFSHValueEmpty(caretValueRule.value)) {
Expand Down
12 changes: 8 additions & 4 deletions src/extractor/InvariantExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ProcessableStructureDefinition,
switchQuantityRules
} from '../processor';
import { getFSHValue, getPathValuePairs, isFSHValueEmpty } from '../utils';
import { getFSHValue, getPath, getPathValuePairs, isFSHValueEmpty } from '../utils';
import { ExportableAssignmentRule } from '../exportable';

export class InvariantExtractor {
Expand Down Expand Up @@ -55,21 +55,25 @@ export class InvariantExtractor {
// so that we can get the FSH value correctly.
// but, we want the original path for the rule itself.
const flatPropertyArray = toPairs(
getPathValuePairs(workingConstraint, x => `constraint.${x}`)
getPathValuePairs(workingConstraint, x => `constraint[${i}].${x}`)
);
const elementPath = getPath(input);
const entityPath =
elementPath === '.' ? structDef.name : `${structDef.name}.${elementPath}`;
flatPropertyArray.forEach(([path], propertyIdx) => {
const originalPath = path.replace('constraint.', '');
const originalPath = path.replace(`constraint[${i}].`, '');
const assignmentRule = new ExportableAssignmentRule(originalPath);
assignmentRule.value = getFSHValue(
propertyIdx,
flatPropertyArray,
'ElementDefinition',
entityPath,
fisher
);
if (!isFSHValueEmpty(assignmentRule.value)) {
invariant.rules.push(assignmentRule);
}
constraintPaths.push(`constraint[${i}].${originalPath}`);
constraintPaths.push(path);
});
switchQuantityRules(invariant.rules);

Expand Down
8 changes: 7 additions & 1 deletion src/processor/InstanceProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ export class InstanceProcessor {
const flatInstanceArray = toPairs(getPathValuePairs(inputJSON));
flatInstanceArray.forEach(([path], i) => {
const assignmentRule = new ExportableAssignmentRule(path);
assignmentRule.value = getFSHValue(i, flatInstanceArray, instanceOfJSON.type, fisher);
assignmentRule.value = getFSHValue(
i,
flatInstanceArray,
instanceOfJSON.type,
`${target.instanceOf} ${target.id}`,
fisher
);
// if the value is empty, we can't use that
if (isFSHValueEmpty(assignmentRule.value)) {
logger.error(
Expand Down
6 changes: 3 additions & 3 deletions src/processor/StructureDefinitionProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,21 +225,21 @@ export class StructureDefinitionProcessor {
newRules.push(
BindingRuleExtractor.process(element),
ObeysRuleExtractor.process(element),
...AssignmentRuleExtractor.process(element)
...AssignmentRuleExtractor.process(element, target.name)
);
} else if (isNewSlice) {
newRules.push(
ContainsRuleExtractor.process(element, input, fisher),
OnlyRuleExtractor.process(element),
...AssignmentRuleExtractor.process(element),
...AssignmentRuleExtractor.process(element, target.name),
BindingRuleExtractor.process(element),
ObeysRuleExtractor.process(element)
);
} else {
newRules.push(
CardRuleExtractor.process(element, input, fisher),
OnlyRuleExtractor.process(element),
...AssignmentRuleExtractor.process(element),
...AssignmentRuleExtractor.process(element, target.name),
FlagRuleExtractor.process(element),
BindingRuleExtractor.process(element),
ObeysRuleExtractor.process(element)
Expand Down
80 changes: 77 additions & 3 deletions src/utils/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import { flatten } from 'flat';
import { flatMap, flatten as flat, isObject, isEmpty, isNil } from 'lodash';
import { fhirtypes, fshtypes, utils } from 'fsh-sushi';
import { ProcessableStructureDefinition, ProcessableElementDefinition } from '../processor';
import { logger } from './GoFSHLogger';

// See https://hl7.org/fhir/R5/datatypes.html#dateTime
export const dateTimeRegex = /^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?$/;
// See https://hl7.org/fhir/R5/datatypes.html#date
export const dateRegex = /^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$/;
// See https://hl7.org/fhir/R5/datatypes.html#time
export const timeRegex = /^([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]{1,9})?$/;
// See https://hl7.org/fhir/R5/datatypes.html#instant
export const instantRegex = /^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]{1,9})?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))$/;

// This function depends on the id of an element to construct the path.
// Per the specification https://www.hl7.org/fhir/elementdefinition.html#id, we should
Expand Down Expand Up @@ -76,6 +86,7 @@ export function getFSHValue(
index: number,
flatArray: [string, string | number | boolean][],
resourceType: string,
resourceName: string,
fisher: utils.Fishable
): number | boolean | string | fshtypes.FshCode | bigint {
const [key, value] = flatArray[index];
Expand All @@ -91,6 +102,35 @@ export function getFSHValue(
return new fshtypes.FshCode(value.toString());
} else if (type === 'integer64') {
return BigInt(value);
} else if (type === 'dateTime') {
if (!dateTimeRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR dateTime`
);
}
return value;
} else if (type === 'date') {
if (!dateRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR date`
);
}
return value;
} else if (type === 'time') {
if (!timeRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR time`
);
}
return value;
} else if (type === 'instant') {
typeCache.get(resourceType).set(pathWithoutIndex, 'instant');
if (!instantRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR instant`
);
}
return value;
} else if (type) {
return value;
}
Expand Down Expand Up @@ -119,9 +159,10 @@ export function getFSHValue(
]) as [string, string | number | boolean][];
const containedResourceType = subArray.find(([key]) => key === 'resourceType')?.[1] as string;
const newIndex = subArray.findIndex(([key]) => key === newKey);
const containedResourceName = `${resourceName}.${baseKey}`;

// Get the FSH value based on the contained resource type. Use paths relative to the contained resource.
return getFSHValue(newIndex, subArray, containedResourceType, fisher);
return getFSHValue(newIndex, subArray, containedResourceType, containedResourceName, fisher);
}
if (!typeCache.has(resourceType)) {
typeCache.set(resourceType, new Map());
Expand All @@ -136,9 +177,42 @@ export function getFSHValue(
} else if (element?.type?.[0]?.code === 'integer64') {
typeCache.get(resourceType).set(pathWithoutIndex, 'integer64');
return BigInt(value);
} else if (element?.type?.[0]?.code === 'dateTime') {
typeCache.get(resourceType).set(pathWithoutIndex, 'dateTime');
if (!dateTimeRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR dateTime`
);
}
return value;
} else if (element?.type?.[0]?.code === 'date') {
typeCache.get(resourceType).set(pathWithoutIndex, 'date');
if (!dateRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR date`
);
}
return value;
} else if (element?.type?.[0]?.code === 'time') {
typeCache.get(resourceType).set(pathWithoutIndex, 'time');
if (!timeRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR time`
);
}
return value;
} else if (element?.type?.[0]?.code === 'instant') {
typeCache.get(resourceType).set(pathWithoutIndex, 'instant');
if (!instantRegex.test(value.toString())) {
logger.warn(
`Value ${value.toString()} on ${resourceName} element ${key} is not a valid FHIR instant`
);
}
return value;
} else {
typeCache.get(resourceType).set(pathWithoutIndex, typeof value);
return value;
}
typeCache.get(resourceType).set(pathWithoutIndex, typeof value);
return value;
}

// Typical empty FSH values are: [], {}, null, undefined
Expand Down
Loading

0 comments on commit 52914b3

Please sign in to comment.