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 1 commit
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
27 changes: 26 additions & 1 deletion src/extractor/AssignmentRuleExtractor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 {
Expand All @@ -28,6 +28,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 element ${assignmentRule.path} is not a valid FHIR dateTime`
);
}
} else if (matchingKey.endsWith('Date')) {
if (!dateRegex.test(matchingValue)) {
logger.warn(
`Value ${matchingValue} on element ${assignmentRule.path} is not a valid FHIR date`
);
}
} else if (matchingKey.endsWith('Time')) {
if (!timeRegex.test(matchingValue)) {
logger.warn(
`Value ${matchingValue} on element ${assignmentRule.path} is not a valid FHIR time`
);
}
} else if (matchingKey.endsWith('Instant')) {
if (!instantRegex.test(matchingValue)) {
logger.warn(
`Value ${matchingValue} on element ${assignmentRule.path} is not a valid FHIR instant`
);
}
}
}
} else {
assignmentRule.value = matchingValue;
Expand Down
60 changes: 58 additions & 2 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 @@ -91,6 +101,27 @@ 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 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 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 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 element ${key} is not a valid FHIR instant`);
}
return value;
} else if (type) {
return value;
}
Expand Down Expand Up @@ -136,9 +167,34 @@ 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 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 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 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 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
139 changes: 139 additions & 0 deletions test/extractor/AssignmentRuleExtractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { fhirtypes, fshtypes } from 'fsh-sushi';
import { AssignmentRuleExtractor } from '../../src/extractor';
import { ExportableAssignmentRule } from '../../src/exportable';
import { ProcessableElementDefinition } from '../../src/processor';
import { loggerSpy } from '../helpers/loggerSpy';

describe('AssignmentRuleExtractor', () => {
beforeEach(() => {
loggerSpy.reset();
});

describe('#simple-values', () => {
let looseSD: any;

Expand Down Expand Up @@ -119,6 +124,140 @@ describe('AssignmentRuleExtractor', () => {
});
});

describe('#simple-date-and-time-values', () => {
let looseSD: any;

beforeAll(() => {
looseSD = JSON.parse(
fs
.readFileSync(
path.join(__dirname, 'fixtures', 'temporal-assigned-value-profile.json'),
'utf-8'
)
.trim()
);
});

it('should extract an assigned value rule with an instant value', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[0]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('effectiveInstant');
expectedRule.value = '2020-07-24T09:31:23.745-04:00';
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('patternInstant');
expect(loggerSpy.getAllMessages()).toHaveLength(0);
});

it('should extract an assigned value with a dateTime value', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[1]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('valueDateTime');
expectedRule.value = '2013-01-01T00:00:00.000Z';
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('patternDateTime');
expect(loggerSpy.getAllMessages()).toHaveLength(0);
});

it('should extract an assigned value with a time value', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[2]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('valueTime');
expectedRule.value = '15:45:00';
expectedRule.exactly = true;
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('fixedTime');
expect(loggerSpy.getAllMessages()).toHaveLength(0);
});

it('should extract an assigned value with a date value', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[3]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('extension.valueDate');
expectedRule.value = '2023-09-21';
expectedRule.exactly = true;
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('fixedDate');
expect(loggerSpy.getAllMessages()).toHaveLength(0);
});
});

describe('#simple-date-and-time-warnings', () => {
let looseSD: any;

beforeAll(() => {
looseSD = JSON.parse(
fs
.readFileSync(
path.join(__dirname, 'fixtures', 'temporal-warning-value-profile.json'),
'utf-8'
)
.trim()
);
});

it('should extract an assigned value rule with an incorrectly formatted instant value and log a warning', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[0]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('effectiveInstant');
expectedRule.value = '2020-07-24 9:31:23.745-04:00';
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('patternInstant');
expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
expect(loggerSpy.getLastMessage('warn')).toMatch(
/Value 2020-07-24 9:31:23\.745-04:00 on element effectiveInstant is not a valid FHIR instant/s
);
});

it('should extract an assigned value with an incorrectly formatted dateTime value and log a warning', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[1]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('valueDateTime');
expectedRule.value = '2013-01-01 00:00:00.000';
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('patternDateTime');
expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
expect(loggerSpy.getLastMessage('warn')).toMatch(
/Value 2013-01-01 00:00:00\.000 on element valueDateTime is not a valid FHIR dateTime/s
);
});

it('should extract an assigned value with an incorrectly formatted time value and log a warning', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[2]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('valueTime');
expectedRule.value = '15:45';
expectedRule.exactly = true;
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('fixedTime');
expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
expect(loggerSpy.getLastMessage('warn')).toMatch(
/Value 15:45 on element valueTime is not a valid FHIR time/s
);
});

it('should extract an assigned value with an incorrectly formatted date value and log a warning', () => {
const element = ProcessableElementDefinition.fromJSON(looseSD.differential.element[3]);
const assignmentRules = AssignmentRuleExtractor.process(element);
const expectedRule = new ExportableAssignmentRule('extension.valueDate');
expectedRule.value = '2023/09/21';
expectedRule.exactly = true;
expect(assignmentRules).toHaveLength(1);
expect(assignmentRules[0]).toEqual<ExportableAssignmentRule>(expectedRule);
expect(element.processedPaths).toContain('fixedDate');
expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
expect(loggerSpy.getLastMessage('warn')).toMatch(
/Value 2023\/09\/21 on element extension\.valueDate is not a valid FHIR date/s
);
});
});

describe('#complex-values', () => {
let looseSD: any;

Expand Down
26 changes: 26 additions & 0 deletions test/extractor/fixtures/temporal-assigned-value-profile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"resourceType": "StructureDefinition",
"kind": "resource",
"type": "Observation",
"name": "TemporalAssignedValueObservation",
"differential": {
"element": [
{
"id": "Observation.effectiveInstant",
"patternInstant": "2020-07-24T09:31:23.745-04:00"
},
{
"id": "Observation.valueDateTime",
"patternDateTime": "2013-01-01T00:00:00.000Z"
},
{
"id": "Observation.valueTime",
"fixedTime": "15:45:00"
},
{
"id": "Observation.extension.valueDate",
"fixedDate": "2023-09-21"
}
]
}
}
26 changes: 26 additions & 0 deletions test/extractor/fixtures/temporal-warning-value-profile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"resourceType": "StructureDefinition",
"kind": "resource",
"type": "Observation",
"name": "TemporalWarningValueObservation",
"differential": {
"element": [
{
"id": "Observation.effectiveInstant",
"patternInstant": "2020-07-24 9:31:23.745-04:00"
},
{
"id": "Observation.valueDateTime",
"patternDateTime": "2013-01-01 00:00:00.000"
},
{
"id": "Observation.valueTime",
"fixedTime": "15:45"
},
{
"id": "Observation.extension.valueDate",
"fixedDate": "2023/09/21"
}
]
}
}
Loading
Loading