Skip to content
Open
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
13 changes: 11 additions & 2 deletions src/fhirtypes/StructureDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,12 @@ export class StructureDefinition {
unfoldedElements = matchingElements[0].unfoldChoiceElementTypes(fisher);
newMatchingElements = unfoldedElements.filter(e => e.path.startsWith(fhirPathString));
}
} else if (matchingElements[0].id.endsWith('[x]')) {
} else if (
matchingElements[0].id.endsWith('[x]') ||
(matchingElements[0].path.endsWith('[x]') && matchingElements[0].sliceName != null)
) {
// The element is a choice element or a slice of a choice element with multiple types.
// Find the common ancestor of all types to navigate into sub-paths (e.g., .extension).
unfoldedElements = matchingElements[0].unfoldChoiceElementTypes(fisher);
newMatchingElements = unfoldedElements.filter(e => e.path.startsWith(fhirPathString));
}
Expand Down Expand Up @@ -931,13 +936,17 @@ export class StructureDefinition {
findObsoleteChoices(baseElement: ElementDefinition, oldTypes: ElementDefinitionType[]): string[] {
// first, find all the elements representing choices for the same choice element
const parentSlice = baseElement.parent()?.sliceName;
const baseSliceName = baseElement.sliceName;
const choiceElements = this.elements.filter(e => {
const eParentSlice = e.parent()?.sliceName;
return (
e.path === baseElement.path &&
(parentSlice == null ||
parentSlice === eParentSlice ||
eParentSlice?.startsWith(`${parentSlice}/`))
eParentSlice?.startsWith(`${parentSlice}/`)) &&
// When baseElement is itself a named slice, only consider its sub-slices,
// not sibling slices at the same level
(baseSliceName == null || e.sliceName?.startsWith(`${baseSliceName}/`))
);
});
const matchedThings: ElementDefinition[] = [];
Expand Down
54 changes: 54 additions & 0 deletions test/export/StructureDefinitionExporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5442,6 +5442,60 @@ describe('StructureDefinitionExporter R4', () => {
expect(loggerSpy.getAllLogs()).toHaveLength(0);
});

it('should not log an error when a rule path has a subpath of a sliced choice element', () => {
loggerSpy.reset();
const profile = new Profile('ConstrainedObservation');
profile.parent = 'Observation';
// * value[x] ^slicing.discriminator[0].type = #type
// * value[x] ^slicing.discriminator[0].path = "$this"
// * value[x] ^slicing.rules = #open
// * value[x] contains valueString 0..1 and valueOther 0..1
// * value[x][valueString] only string
// * value[x][valueOther] only Quantity or CodeableConcept
// * value[x][valueOther].extension 1..1
const slicingType = new CaretValueRule('value[x]');
slicingType.caretPath = 'slicing.discriminator[0].type';
slicingType.value = new FshCode('type');
const slicingPath = new CaretValueRule('value[x]');
slicingPath.caretPath = 'slicing.discriminator[0].path';
slicingPath.value = '$this';
const slicingRules = new CaretValueRule('value[x]');
slicingRules.caretPath = 'slicing.rules';
slicingRules.value = new FshCode('open');
const valueXSlices = new ContainsRule('value[x]');
valueXSlices.items = [{ name: 'valueString' }, { name: 'valueOther' }];
const stringCard = new CardRule('value[x][valueString]');
stringCard.min = 0;
stringCard.max = '1';
const otherCard = new CardRule('value[x][valueOther]');
otherCard.min = 0;
otherCard.max = '1';
const stringType = new OnlyRule('value[x][valueString]');
stringType.types = [{ type: 'string' }];
const otherType = new OnlyRule('value[x][valueOther]');
otherType.types = [{ type: 'Quantity' }, { type: 'CodeableConcept' }];
const otherExtensionCard = new CardRule('value[x][valueOther].extension');
otherExtensionCard.min = 1;
otherExtensionCard.max = '1';

profile.rules.push(
slicingType,
slicingPath,
slicingRules,
valueXSlices,
stringCard,
otherCard,
stringType,
otherType,
otherExtensionCard
);

exporter.exportStructDef(profile);
const sd = pkg.profiles[0];
expect(sd).toBeTruthy();
expect(loggerSpy.getAllLogs()).toHaveLength(0);
});

it('should log an error when extension is constrained with a modifier extension', () => {
const extension = new Extension('StrangeExtension');
const modifier = new FlagRule('.');
Expand Down
26 changes: 26 additions & 0 deletions test/import/FSHImporter.SDRules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1728,6 +1728,32 @@ describe('FSHImporter', () => {
undefined
);
});

it('should parse contains rule on a choice with two items', () => {
const input = leftAlign(`
Profile: ObservationProfile
Parent: Observation
* value[x] contains valueString 0..1 and valueOther 0..1
* value[x][valueString] only string
* value[x][valueOther] only Quantity or CodeableConcept
* value[x][valueOther].extension 1..1
`);

const result = importSingleText(input);
const profile = result.profiles.get('ObservationProfile');
expect(profile.rules).toHaveLength(6);
assertContainsRule(profile.rules[0], 'value[x]', 'valueString', 'valueOther');
assertCardRule(profile.rules[1], 'value[x][valueString]', 0, 1);
assertCardRule(profile.rules[2], 'value[x][valueOther]', 0, 1);
assertOnlyRule(profile.rules[3], 'value[x][valueString]', { type: 'string' });
assertOnlyRule(
profile.rules[4],
'value[x][valueOther]',
{ type: 'Quantity' },
{ type: 'CodeableConcept' }
);
assertCardRule(profile.rules[5], 'value[x][valueOther].extension', 1, 1);
});
});

describe('#caretValueRule', () => {
Expand Down
6 changes: 4 additions & 2 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"extends": "../tsconfig",
"extends": "../tsconfig.json",
"compilerOptions": {
"strictNullChecks": false
"strictNullChecks": false,
"noEmit": true,
"types": ["jest", "node"]
},
"include": [
"./**/*.ts"
Expand Down
Loading