Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn when temporal primitive is not formatted correctly #256

Merged
merged 5 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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('.')}`,
jafeltra marked this conversation as resolved.
Show resolved Hide resolved
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
Loading