Skip to content

Commit

Permalink
Assign date or dateTime that contains only year
Browse files Browse the repository at this point in the history
A FHIR date or dateTime may be 4-digit year. The FSH parser will assign
this value a NUMBER token type. When assigning to a date or dateTime
element, if a type mismatch occurs, and the original value is a number,
try to assign the raw value. If it is a valid 4-digit year, it will be
assigned successfully. Otherwise, the original error will be thrown and
logged.
  • Loading branch information
mint-thompson committed Sep 16, 2024
1 parent f6827cc commit 9397183
Show file tree
Hide file tree
Showing 8 changed files with 444 additions and 6 deletions.
39 changes: 37 additions & 2 deletions src/export/CodeSystemExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { logger } from '../utils/FSHLogger';
import { MasterFisher, assembleFSHPath, resolveSoftIndexing } from '../utils';
import { InstanceExporter, Package } from '.';
import { CannotResolvePathError, MismatchedTypeError } from '../errors';
import { isEqual } from 'lodash';
import { cloneDeep, isEqual } from 'lodash';

export class CodeSystemExporter {
constructor(
Expand Down Expand Up @@ -157,11 +157,46 @@ export class CodeSystemExporter {
}
return replacedRule;
} catch (originalErr) {
// if the value is a number, it may have been a four-digit number
// that we tried to assign to a date or dateTime.
// a four-digit number could be a valid year, so see if it can be assigned.
if (
originalErr instanceof MismatchedTypeError &&
['date', 'dateTime'].includes(originalErr.elementType) &&
['number', 'bigint'].includes(typeof rule.value)
) {
try {
const retryRule = cloneDeep(rule);
const { pathParts } = codeSystemSD.validateValueAtPath(
path,
retryRule.rawValue,
this.fisher
);
if (pathParts.some(part => isExtension(part.base))) {
ruleMap.set(assembleFSHPath(pathParts).replace(/\[0+\]/g, ''), { pathParts });
}
retryRule.value = retryRule.rawValue;
return retryRule;
} catch (retryErr) {
if (retryErr instanceof MismatchedTypeError) {
logger.error(originalErr.message, rule.sourceInfo);
if (originalErr.stack) {
logger.debug(originalErr.stack);
}
} else {
logger.error(retryErr.message, rule.sourceInfo);
if (retryErr.stack) {
logger.debug(retryErr.stack);
}
}
return null;
}
}
// if an Instance has an id that looks like a number, bigint, or boolean,
// we may have tried to assign that value instead of an Instance.
// try to fish up an Instance with the rule's raw value.
// if we find one, try assigning that instead.
if (
else if (
originalErr instanceof MismatchedTypeError &&
['number', 'bigint', 'boolean'].includes(typeof rule.value)
) {
Expand Down
27 changes: 26 additions & 1 deletion src/export/InstanceExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,36 @@ export class InstanceExporter implements Fishable {
}
doRuleValidation(rule instanceof AssignmentRule ? rule.value : null);
} catch (originalErr) {
// if the value is a number, it may have been a four-digit number
// that we tried to assign to a date or dateTime.
// a four-digit number could be a valid year, so see if it can be assigned.
if (
rule instanceof AssignmentRule &&
originalErr instanceof MismatchedTypeError &&
['date', 'dateTime'].includes(originalErr.elementType) &&
['number', 'bigint'].includes(typeof rule.value)
) {
try {
doRuleValidation(rule.rawValue);
} catch (retryErr) {
if (retryErr instanceof MismatchedTypeError) {
logger.error(originalErr.message, rule.sourceInfo);
if (originalErr.stack) {
logger.debug(originalErr.stack);
}
} else {
logger.error(retryErr.message, rule.sourceInfo);
if (retryErr.stack) {
logger.debug(retryErr.stack);
}
}
}
}
// if an Instance has an id that looks like a number, bigint, or boolean,
// we may have tried to validate with that value instead of an Instance.
// try to fish up an Instance with the rule's raw value.
// if we find one, try validating with that instead.
if (
else if (
rule instanceof AssignmentRule &&
originalErr instanceof MismatchedTypeError &&
['number', 'bigint', 'boolean'].includes(typeof rule.value)
Expand Down
60 changes: 58 additions & 2 deletions src/export/StructureDefinitionExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,15 @@ export class StructureDefinitionExporter implements Fishable {
}
const replacedRule = replaceReferences(rule, this.tank, this);
try {
element.assignValue(replacedRule.value, replacedRule.exactly, this);
// since we have the element already, we can check for the "date parsed as number" special case now instead of after an exception
if (
['date', 'dateTime'].includes(element.type[0].code) &&
['number', 'bigint'].includes(typeof replacedRule.value)
) {
element.assignValue(replacedRule.rawValue, replacedRule.exactly, this);
} else {
element.assignValue(replacedRule.value, replacedRule.exactly, this);
}
} catch (originalErr) {
// if an Instance has an id that looks like a number, bigint, or boolean,
// we may have tried to assign that value instead of an Instance.
Expand Down Expand Up @@ -911,7 +919,34 @@ export class StructureDefinitionExporter implements Fishable {
} else if (rule instanceof CaretValueRule) {
const replacedRule = replaceReferences(rule, this.tank, this);
if (replacedRule.path !== '') {
element.setInstancePropertyByPath(replacedRule.caretPath, replacedRule.value, this);
try {
element.setInstancePropertyByPath(replacedRule.caretPath, replacedRule.value, this);
} catch (originalErr) {
// if the value is a number, it may have been a four-digit number
// that we tried to assign to a date or dateTime.
// a four-digit number could be a valid year, so see if it can be assigned.
if (
originalErr instanceof MismatchedTypeError &&
['date', 'dateTime'].includes(originalErr.elementType) &&
['number', 'bigint'].includes(typeof replacedRule.value)
) {
try {
element.setInstancePropertyByPath(
replacedRule.caretPath,
replacedRule.rawValue,
this
);
} catch (retryErr) {
if (retryErr instanceof MismatchedTypeError) {
throw originalErr;
} else {
throw retryErr;
}
}
} else {
throw originalErr;
}
}
} else {
if (replacedRule.isInstance) {
if (this.deferredCaretRules.has(structDef)) {
Expand All @@ -927,7 +962,28 @@ export class StructureDefinitionExporter implements Fishable {
this
);
} catch (originalErr) {
// if the value is a number, it may have been a four-digit number
// that we tried to assign to a date or dateTime.
// a four-digit number could be a valid year, so see if it can be assigned.
if (
originalErr instanceof MismatchedTypeError &&
['date', 'dateTime'].includes(originalErr.elementType) &&
['number', 'bigint'].includes(typeof replacedRule.value)
) {
try {
structDef.setInstancePropertyByPath(
replacedRule.caretPath,
replacedRule.rawValue,
this
);
} catch (retryErr) {
if (retryErr instanceof MismatchedTypeError) {
throw originalErr;
} else {
throw retryErr;
}
}
} else if (
originalErr instanceof MismatchedTypeError &&
['number', 'bigint', 'boolean'].includes(typeof rule.value)
) {
Expand Down
35 changes: 34 additions & 1 deletion src/export/ValueSetExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
setImpliedPropertiesOnInstance
} from '../fhirtypes/common';
import { isUri } from 'valid-url';
import { flatMap, partition, xor } from 'lodash';
import { cloneDeep, flatMap, partition, xor } from 'lodash';

export class ValueSetExporter {
constructor(
Expand Down Expand Up @@ -240,6 +240,39 @@ export class ValueSetExporter {
ruleMap.set(assembleFSHPath(pathParts).replace(/\[0+\]/g, ''), { pathParts });
return rule;
} catch (originalErr) {
// if the value is a number, it may have been a four-digit number
// that we tried to assign to a date or dateTime.
// a four-digit number could be a valid year, so see if it can be assigned.
if (
originalErr instanceof MismatchedTypeError &&
['date', 'dateTime'].includes(originalErr.elementType) &&
['number', 'bigint'].includes(typeof rule.value)
) {
try {
const retryRule = cloneDeep(rule);
const { pathParts } = valueSetSD.validateValueAtPath(
rule.caretPath,
retryRule.rawValue,
this.fisher
);
ruleMap.set(assembleFSHPath(pathParts).replace(/\[0+\]/g, ''), { pathParts });
retryRule.value = retryRule.rawValue;
return retryRule;
} catch (retryErr) {
if (retryErr instanceof MismatchedTypeError) {
logger.error(originalErr.message, rule.sourceInfo);
if (originalErr.stack) {
logger.debug(originalErr.stack);
}
} else {
logger.error(retryErr.message, rule.sourceInfo);
if (retryErr.stack) {
logger.debug(retryErr.stack);
}
}
return null;
}
}
// if an Instance has an id that looks like a number, bigint, or boolean,
// we may have tried to assign that value instead of an Instance.
// try to fish up an Instance with the rule's raw value.
Expand Down
86 changes: 86 additions & 0 deletions test/export/CodeSystemExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,92 @@ describe('CodeSystemExporter', () => {
);
});

it('should assign a date that was parsed as a number', () => {
// CodeSystem: CaretCodeSystem
// * #someCode "Some Code"
// * ^extension[0].url = "http://example.org/SomeExt"
// * ^extension[0].valueDate = 2023
const codeSystem = new FshCodeSystem('CaretCodeSystem');
const someCode = new ConceptRule('someCode', 'Some Code');
const extensionUrl = new CaretValueRule('');
extensionUrl.caretPath = 'extension[0].url';
extensionUrl.value = 'http://example.org/SomeExt';
const extensionValue = new CaretValueRule('');
extensionValue.caretPath = 'extension[0].valueDate';
extensionValue.value = BigInt(2023);
extensionValue.rawValue = '2023';
codeSystem.rules.push(someCode, extensionUrl, extensionValue);
doc.codeSystems.set(codeSystem.name, codeSystem);

const exported = exporter.export().codeSystems;
expect(exported.length).toBe(1);
expect(exported[0]).toEqual({
resourceType: 'CodeSystem',
id: 'CaretCodeSystem',
name: 'CaretCodeSystem',
content: 'complete',
url: 'http://hl7.org/fhir/us/minimal/CodeSystem/CaretCodeSystem',
count: 1,
status: 'draft',
extension: [
{
url: 'http://example.org/SomeExt',
valueDate: '2023'
}
],
concept: [
{
code: 'someCode',
display: 'Some Code'
}
]
});
});

it('should assign a dateTime that was parsed as a number', () => {
// CodeSystem: CaretCodeSystem
// * #someCode "Some Code"
// * #someCode ^property[0].code = #standard
// * #someCode ^property[0].valueDateTime = 0081
const codeSystem = new FshCodeSystem('CaretCodeSystem');
const someCode = new ConceptRule('someCode', 'Some Code');
const propertyCode = new CaretValueRule('');
propertyCode.pathArray = ['#someCode'];
propertyCode.caretPath = 'property[0].code';
propertyCode.value = new FshCode('standard');
const propertyValue = new CaretValueRule('');
propertyValue.pathArray = ['#someCode'];
propertyValue.caretPath = 'property[0].valueDateTime';
propertyValue.value = BigInt(81);
propertyValue.rawValue = '0081';
codeSystem.rules.push(someCode, propertyCode, propertyValue);
doc.codeSystems.set(codeSystem.name, codeSystem);

const exported = exporter.export().codeSystems;
expect(exported.length).toBe(1);
expect(exported[0]).toEqual({
resourceType: 'CodeSystem',
id: 'CaretCodeSystem',
name: 'CaretCodeSystem',
content: 'complete',
url: 'http://hl7.org/fhir/us/minimal/CodeSystem/CaretCodeSystem',
count: 1,
status: 'draft',
concept: [
{
code: 'someCode',
display: 'Some Code',
property: [
{
code: 'standard',
valueDateTime: '0081'
}
]
}
]
});
});

describe('#insertRules', () => {
let cs: FshCodeSystem;
let ruleSet: RuleSet;
Expand Down
31 changes: 31 additions & 0 deletions test/export/InstanceExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10117,6 +10117,37 @@ describe('InstanceExporter', () => {
});
});

it('should assign a date that was parsed as a number', () => {
// Instance: ExampleObs
// InstanceOf: Observation
// * extension[0].url = "http://example.org/SomeExt"
// * extension[0].valueDate = 2055
const observation = new Instance('ExampleObs');
observation.instanceOf = 'Observation';
const extensionUrl = new AssignmentRule('extension[0].url');
extensionUrl.value = 'http://example.org/SomeExt';
const extensionValue = new AssignmentRule('extension[0].valueDate');
extensionValue.value = BigInt(2055);
extensionValue.rawValue = '2055';
observation.rules.push(extensionUrl, extensionValue);
const exportedInstance = exportInstance(observation);
expect(exportedInstance.extension[0].valueDate).toEqual('2055');
});

it('should assign a dateTime that was parsed as a number', () => {
// Instance: ExampleObs
// InstanceOf: Observation
// * valueDateTime = 0895
const observation = new Instance('ExampleObs');
observation.instanceOf = 'Observation';
const assignmentRule = new AssignmentRule('valueDateTime');
assignmentRule.value = BigInt(895);
assignmentRule.rawValue = '0895';
observation.rules.push(assignmentRule);
const exportedInstance = exportInstance(observation);
expect(exportedInstance.valueDateTime).toEqual('0895');
});

describe('#TimeTravelingResources', () => {
it('should export a R5 ActorDefinition in a R4 IG', () => {
// Instance: AD1
Expand Down
Loading

0 comments on commit 9397183

Please sign in to comment.